Flow Projection Generator + Fidelity Report — Contract (Phase 7A, Step 7A-11a)
Status: Frozen contract — step 7A-11a (Thinking). This document fixes the projection generator
contract, the rendered-artifact shape, staleness/drift detection, the fidelity.dropped_fields
report, the flow project wire shapes (CLI = MCP = Hub REST), the anti-drift demo acceptance
criteria, and the seven-tier test matrix before any mechanical implementation. 7A-11b (Auto)
implements lib/flow/projection-generator.mjs, the flow project surfaces, the OpenAPI block, and
the seven tiers to this contract — it does not redesign it.
Authored on branch feat/flow-projection-pilot (the same branch as 7A-10b).
Related:
docs/FLOW-V0-SPEC.md— the canonical spec this contract implements: §4 Projection generator (dogfood target, anti-drift demo, fidelity report, staleness, drift), §1.7knowtation.flow_projection/v0, §1.1 theHarnessenum, §6 security checklist (gate 8: projections derived/read-only), §9 seven-tier plan.docs/FLOW-STORE-CONTRACT-7A-10.md— the read surface already shipped: the generator reads canonical Flows through the same store (getFlow) and reuses its scope-resolution and parity model function-for-function.scooling/docs/FLOW-PLATFORM-ARCHITECTURE.md— the consumer architecture: § Tenet point 5 (harness projection, not ownership), theProjectiondefinition, theFlowProjectionAdapterboundary (project/listProjections/projectionStaleness), and the forward-scan "projection fidelity loss" risk this report answers.lib/flow/flow-store.mjs— the canonical source the generator reads (getFlow,flowDefinitionForClient,parseSemver/compareSemver, id/version regexes).lib/flow/flow-handlers.mjs/lib/flow/flow-scope.mjs— the CLI = MCP = Hub parity and deny-by-default scope machinery theflow projectsurface reuses unchanged.lib/calendar/event-store.mjs— the Option A parity reference the store already mirrors; the generator stays a pure, local, no-network module in the same spirit.
Scope fence (7A-11a): Generator contract + rendered shape + staleness/drift + fidelity report +
flow project read/derive wire shapes + OpenAPI shapes (to land with the routes in 11b) + the
anti-drift acceptance criteria + the seven-tier test matrix — contract only. No
lib/flow/projection-generator.mjs implementation, no flow project CLI/MCP/Hub wiring, no
OpenAPI edit, no generated artifact written into any repo, no Scooling changes, no
posture-constant flips (FLOW_LIVE_READ_AUTHORIZED stays false in Scooling). flow run, live
authoring, hosted projection, and external-agent bundles remain separate later gates
(7A-L2/L3/L5).
Simple summary
A Flow is the one true copy of a procedure, kept in Knowtation. A projection is a printout
of that procedure in the exact format one tool expects — a Cursor rule file, an AGENTS.md, a
.cursorrules. The projection generator is the printer: you point it at a canonical Flow and a
target format, and it prints the matching file. The printout is read-only and disposable — if
you lose it, you reprint it from the canonical Flow and nothing is lost; if someone scribbles on the
printout by hand, that is drift, and the system notices and refuses to treat the scribble as the
new truth.
Two honest promises make this trustworthy. First, the fidelity report: some formats simply cannot say everything a Flow can (a plain rule file may have no place for "when not to run a step"). When the generator has to leave something out, it writes down exactly what it dropped, so you are never silently misled. Second, staleness: every printout remembers which version of the Flow it came from, so when the Flow changes, the system can tell you "this printout is out of date — reprint it." The proof that it all works (the anti-drift demo): change the canonical Flow, reprint, and the only difference in the file is exactly the change you made — no hand-drift, no surprises.
Technical summary
A pure, local, no-network generator module (lib/flow/projection-generator.mjs) renders a canonical
knowtation.flow/v0 + its knowtation.flow_step/v0 records (read via the 7A-10b store, never a
second source) into a knowtation.flow_projection/v0 artifact for a target harness. Rendering is
deterministic — same canonical input + same PROJECTION_GENERATOR_VERSION ⇒ byte-identical
rendered text — which is what makes the anti-drift diff meaningful. Projections are
generated_from_canonical: true, editable: false, and carry a fidelity.dropped_fields[] listing
every step-anatomy field the target harness could not express. Staleness is a pure semver
comparison (projection.flow_version vs the latest visible flow.version); drift is a
content-hash comparison between an on-disk artifact and a fresh render of the same canonical version.
The generator is exposed read/derive-only over the three identical surfaces — CLI
(knowtation flow project), MCP (flow_project), Hub REST (GET /api/v1/flows/{id}/projection) —
returning byte-identical knowtation.flow_project/v0 envelopes for the same authorized request,
reusing the 7A-10b deny-by-default scope resolution unchanged. v0 active harnesses are cursor_rule
and cli_runbook (the dogfood targets .cursor/rules/*.mdc, AGENTS.md, .cursorrules);
cursor_skill and mcp_prompt are reserved/parse-valid; agent_bundle is inert (external-agent
gate 7A-L2). No secrets ever appear in any rendered artifact, hash, or response; scope is enforced
server-side, deny-by-default, with no existence leak.
1. Generator module shape — lib/flow/projection-generator.mjs (contract, no impl)
A pure, local, file-system-free, network-free module — the same posture as flow-store.mjs.
It reads canonical Flows only through the store (getFlow from flow-store.mjs); it never owns
a second source, never writes the index, and never routes a proposal. Surfaces (route/CLI/MCP) call
it; it never calls them. The function table below pins names, signatures, and return shapes for
7A-11b.
1.1 Constants
| Constant | Value | Purpose |
|---|---|---|
PROJECTION_GENERATOR_VERSION |
'1' (string, bumped only on a deliberate rendering change) |
Stamped into every envelope; a render is reproducible only for a fixed generator version. A bump is itself a staleness/regeneration trigger. |
ACTIVE_HARNESSES |
Set(['cursor_rule', 'cli_runbook']) |
The v0 dogfood targets that render real artifacts |
RESERVED_HARNESSES |
Set(['cursor_skill', 'mcp_prompt']) |
Parse-valid in the Harness enum; rendering deferred (return FLOW_HARNESS_UNSUPPORTED until their slice) |
INERT_HARNESSES |
Set(['agent_bundle']) |
Inert in v0 — external-agent gate 7A-L2; always FLOW_HARNESS_UNSUPPORTED |
MAX_RENDERED_BYTES |
65536 |
Hard cap on a rendered artifact; over-cap ⇒ truncated note in fidelity + bounded output (no unbounded payloads) |
GENERATED_MARKER_PREFIX |
'GENERATED FROM CANONICAL FLOW' |
The leading line of every rendered artifact (harness-commented); the anchor drift detection and humans both read |
Harness enum values come from FLOW-V0-SPEC.md §1.1 verbatim:
cursor_rule | cursor_skill | mcp_prompt | cli_runbook | agent_bundle.
1.2 Core generator functions
| Function | Signature | Returns / behaviour |
|---|---|---|
isHarnessActive |
(harness) → boolean |
ACTIVE_HARNESSES.has(harness); the single gate every surface checks first |
projectFlow |
(flow, steps, { harness, generatedAt? }) → FlowProjection |
Pure render of canonical flow + ordered steps into a knowtation.flow_projection/v0 (§3). Deterministic for a fixed (flow, steps, harness, PROJECTION_GENERATOR_VERSION). generatedAt is injectable for test determinism; it lives only in the response envelope, never in the canonical projection object. Throws no I/O; returns a value object only. |
computeFidelity |
(harness, flow, steps) → { dropped_fields: string[], notes?: string } |
The per-harness fidelity report (§5). Pure; deterministic; sorted dropped_fields. |
renderedContentHash |
(rendered) → string |
Stable hash (e.g. sha256 hex) of the rendered string only, excluding the volatile envelope. Drift/regeneration key (§4.2). Never hashes secrets — rendered carries none by contract. |
isProjectionStale |
(projectionFlowVersion, latestFlowVersion) → boolean |
Pure semver compare via parseSemver/compareSemver: stale ⇔ projectionFlowVersion < latestFlowVersion, or either side unparseable (fail toward "stale", never silently "fresh"). |
detectDrift |
(onDiskRendered, freshRendered) → { drift: boolean, reason: 'clean' \| 'edited' \| 'missing_marker' \| 'absent' } |
Compares a hand-held on-disk artifact against a fresh render of the same canonical version. drift:true when content differs after marker-normalization, when the generated marker is absent (hand-authored), or when the file is absent where one is expected. Read-only; never mutates. |
flowProjectionForClient |
(projection) → object |
The client projection of the §3 object — emits only the §1.7 spec fields; asserts no extra/secret-bearing key (tested). |
Determinism rule (the anti-drift keystone).
projectFlowMUST be a pure function of(flow, steps, harness, PROJECTION_GENERATOR_VERSION). No clock, no randomness, no host paths, no environment in therenderedbytes. The only time-like value (generated_at) is confined to the response envelope (§3.2). This is what makes "edit canonical → regenerate →diffshows exactly the canonical change and nothing else" a true statement.
1.3 What the generator does not do (v0)
- It does not read or write the filesystem itself. The CLI surface (§6.1) is the only thing that
may write a rendered artifact to a local
--outpath, and that is a derived local file, never a canonical write and never a proposal. - It does not accept a hand-edited artifact as input-to-canonical. Drift is detected and reported, never reconciled by the generator (reconciliation, if ever, is a review-before-write proposal at a later gate).
- It does not render
agent_bundle(inert, 7A-L2) orcursor_skill/mcp_prompt(reserved); requests for those fail closed withFLOW_HARNESS_UNSUPPORTED. - It does not advance runs, mutate the index, or touch any scope decision — scope is resolved by
the unchanged 7A-10b
flow-scope.mjsbefore the generator is ever called.
2. Harness targets (v0 dogfood: our own repo guidance)
The v0 dogfood proves anti-drift on a real multi-harness problem we already have: generate our own repo guidance from canonical Flows. Two harnesses are active; they cover all three dogfood targets.
| Harness | v0 status | Dogfood target file(s) | Rendered shape (summary) |
|---|---|---|---|
cursor_rule |
active | .cursor/rules/<flow-slug>.mdc |
MDC frontmatter (description, globs, alwaysApply) derived from flow.summary/tags, then the generated marker, then an ordered, headed step body (owned job, trigger, anti-trigger, boundaries, output shape, verification). |
cli_runbook |
active | AGENTS.md, .cursorrules |
Plain Markdown: a title from flow.title, the generated marker, a one-line summary, then numbered steps with the full step anatomy as labelled lines. No frontmatter. |
cursor_skill |
reserved | .cursor/skills/<slug>/SKILL.md |
Specified later; FLOW_HARNESS_UNSUPPORTED in v0. |
mcp_prompt |
reserved | MCP prompt registration text | Specified later; FLOW_HARNESS_UNSUPPORTED in v0. |
agent_bundle |
inert | external-agent instruction bundle | Gated 7A-L2; always FLOW_HARNESS_UNSUPPORTED. |
2.1 Rendered-artifact invariants (both active harnesses)
- First content line is the generated marker. Harness-appropriate comment carrying
GENERATED FROM CANONICAL FLOW <flow_id>@<flow_version> — DO NOT EDIT; regenerate viaknowtation flow project``. It encodes the sourceflow_id+flow_version+ thePROJECTION_GENERATOR_VERSION, so a reader anddetectDriftboth know provenance and staleness at a glance. (cursor_rule: inside the.mdcafter frontmatter, as an HTML comment;cli_runbook: an HTML comment at the top of the Markdown.) - Ordered exactly by
step.ordinal. Step order in the artifact equals the canonical order; no reordering, no de-duplication, no editorializing. - Verbatim, inert step text.
instruction,owned_job,trigger,when_not_to_run,boundaries,output_shape, andskill_refsare rendered as data — escaped/quoted for the harness format, never interpreted, never executed, and incapable of widening scope from inside the artifact. - No secrets, ever.
requires/skill_refsrender as handles/opaque ids only;provenance/evidence_ref(run-side) are never projected; notoken/oauth/refresh_token/ raw note body / prompt / completion can appear. The security tier asserts this by scanning the rendered bytes. - Bounded. Rendered output never exceeds
MAX_RENDERED_BYTES; over-cap is truncated at a step boundary and recorded infidelity.notes.
3. Rendered projection object — knowtation.flow_projection/v0
3.1 Canonical projection object (spec §1.7 — unchanged, no new fields on the canonical shape)
projectFlow returns exactly the spec §1.7 fields — nothing more on the canonical object:
{
"schema": "knowtation.flow_projection/v0",
"flow_id": "flow_overseer_handover",
"flow_version": "0.1.0", // the version this artifact was generated from (staleness key)
"harness": "cursor_rule", // Harness enum
"rendered": "…harness-specific text, marker-first, ordered, secret-free…",
"generated_from_canonical": true, // const true
"editable": false, // const false — a hand-edited projection is drift, not a source
"fidelity": { "dropped_fields": ["when_not_to_run"], "notes": "cursor_rule has no anti-trigger slot" }
}
3.2 Response envelope — knowtation.flow_project/v0 (mirrors how flow_get/v0 wraps flow)
Staleness and drift/regeneration bookkeeping live in the envelope, never on the canonical
projection object (keeping §1.7 pure, exactly as FlowGetResponse wraps Flow):
{
"schema": "knowtation.flow_project/v0",
"vault_id": "default",
"projection": { /* knowtation.flow_projection/v0 — §3.1 */ },
"staleness": {
"stale": false,
"projection_version": "0.1.0",
"latest_version": "0.1.0"
},
"generator": {
"generator_version": "1", // PROJECTION_GENERATOR_VERSION
"content_hash": "sha256:…", // renderedContentHash(projection.rendered)
"generated_at": "2026-06-20T00:00:00Z" // envelope-only; never in projection.rendered
}
}
- The
schemadiscriminators (knowtation.flow_projection/v0on the object,knowtation.flow_project/v0on the envelope) are stamped by the generator/handler so every surface emits them identically. content_hashlets a caller (CLI--check, a CI gate, the ScoolingFlowProjectionAdapter) detect drift without re-shipping the whole artifact.generated_atis the only non-deterministic value and is excluded fromcontent_hashand fromprojection.rendered, so parity and anti-drift comparisons stay byte-stable.
4. Staleness and drift
4.1 Staleness — a pure version lag
- A projection is stale when
projection.flow_versionis less than the latest visibleflow.versionfor thatflow_id(within the caller's scope), perparseSemver/compareSemver. - Either version unparseable ⇒ stale (fail toward "regenerate", never silently "fresh").
- A
PROJECTION_GENERATOR_VERSIONbump is also a regeneration trigger: an artifact whose marker carries an older generator version is treated as needing regeneration even at the same flow version. - Surfaced in the envelope
stalenessblock and consumed by the ScoolingFlowProjectionAdapter.projectionStaleness(flowId, harness)(no Scooling change in 11a; this is the contract it will read).
4.2 Drift — a hand-edited artifact is never a source
- Drift = an on-disk rendered artifact whose content (after normalizing the volatile marker
fields) differs from a fresh deterministic render of the same canonical
flow_version, or whose generated marker is missing entirely (hand-authored), or which is absent where the canonical Flow expects one. detectDriftreports drift; it never reconciles it. Becauseeditable: false, a drifted artifact is never read back as a write source (spec §6 gate 8). The remedy is always regenerate (overwrite the derived artifact), never "promote the edit".- Drift detection is read-only and pure:
(onDiskRendered, freshRendered) → verdict. The CLI--checkmode (§6.1) surfaces it with a non-zero exit; the HubGET …/projectionreports it in the payload (a GET never writes). Any future "reconcile a hand edit" path is a separate review-before-write proposal gate, out of scope here.
5. Fidelity report — fidelity.dropped_fields
When a target harness cannot express a step-anatomy field, the projection records exactly what it dropped, so the user is never silently misled (the spec §4 fidelity requirement and the architecture "projection fidelity loss" risk).
5.1 Contract
fidelity.dropped_fieldsis a sorted, de-duplicated array of canonical field names that the harness format could not represent (e.g.when_not_to_run,requires,skill_refs,outputs,inputs,verification.evidence_required).- A field is "dropped" only when its canonical value was present and non-empty but has no slot in the rendered artifact — never list a field that was empty/absent in the source.
fidelity.notes(optional) is a short human string explaining the loss or any truncation (MAX_RENDERED_BYTES).- The report is per-(flow, harness) and deterministic — same input ⇒ same report.
- A dropped field is a transparency note, not a license to leak. Dropping a field never means rendering it "somewhere unsafe"; it means it is absent from the artifact and named in the report.
5.2 v0 per-harness fidelity baseline (representative, frozen for 11b)
| Canonical field | cursor_rule |
cli_runbook |
|---|---|---|
owned_job, instruction, trigger, boundaries, output_shape, verification.kind/description |
expressed | expressed |
when_not_to_run (anti-trigger) |
dropped (no MDC slot) → listed | expressed (labelled line) |
requires (handles) |
dropped (kept out of rule prose) → listed when present | expressed as a handles list |
skill_refs (opaque ids) |
expressed as a reference list | expressed as a reference list |
inputs / outputs |
dropped → listed when present | expressed |
verification.evidence_required |
expressed (inline) | expressed (inline) |
The exact matrix is finalized in 11b implementation, but the rule is frozen: every
present-but-unexpressible field MUST appear in dropped_fields; nothing expressed may also be listed.
6. flow project wire shapes (read/derive) — CLI = MCP = Hub REST
flow project is read/derive-only in v0: it reads canonical Flows (scope-checked) and derives an
artifact. It performs no canonical write and no proposal. The defining invariant from 7A-10
holds: the same authorized request returns byte-identical knowtation.flow_project/v0 JSON on all
three surfaces (modulo the envelope-only generated_at, which callers exclude from equality just as
the generator excludes it from content_hash). Scope resolution reuses flow-scope.mjs unchanged
(deny-by-default; scope narrows only; no existence leak).
The OpenAPI shapes in §7 are specified here and land in
docs/openapi.yamlin the same change as the routes (7A-11b) — never a docs-only PR tomain.
6.1 CLI
knowtation flow project <flow_id> --harness <harness> [--version <semver>] [--out <path>] [--check] [--json]
--harness(required) — one of the active harnesses; reserved/inert ⇒FLOW_HARNESS_UNSUPPORTED.--version(optional) — pin aSEMVER_REsource version; default = latest visible.--json— print the exactknowtation.flow_project/v0envelope; without it, printrenderedplus a human staleness/fidelity summary.--out <path>— writeprojection.renderedto a local derived file (the dogfood artifact). This is the only writeflow projectmay perform; it is a derived projection, not a canonical write or proposal. Absent--out, output goes to stdout only.--check— read-only drift/staleness gate: regenerate, compare against the on-disk artifact at--out/default path viadetectDrift+isProjectionStale, print the verdict, and exit non-zero on drift or staleness (the CI-friendly anti-drift gate).--checknever writes.- Scope resolves exactly as Hub does (local identity → authorized scopes →
visibleScopes); no flag can grant a scope the caller lacks. Error codes match the Hub (BAD_REQUEST,FLOW_HARNESS_UNSUPPORTED,FLOW_SCOPE_DENIED,FLOW_SCOPE_AMBIGUOUS,unknown_flow).
6.2 MCP tool
| Tool | Inputs | v0 | Result |
|---|---|---|---|
flow_project |
flow_id (req), harness (req, enum), version? (semver), vault_id? |
active (read/derive) | knowtation.flow_project/v0 envelope — same JSON as Hub/CLI. The MCP tool never writes a file (no --out analogue); it returns the rendered artifact and bookkeeping only. |
It delegates to the same shared handler as CLI/Hub (parity with flow_list/flow_get in
mcp/tools/flow.mjs); no surface re-shapes the payload.
6.3 Hub REST
| Method + path | Purpose | v0 |
|---|---|---|
GET /api/v1/flows/{id}/projection?harness=&version= |
Derived projection + staleness + generator bookkeeping | active (read/derive) |
BearerAuth + X-Vault-Id + requireRole('viewer'…), parity with GET /api/v1/flows/{id}. A GET is
read-only — it reports staleness/drift in the payload and never writes an artifact server-side.
6.4 Shared handler (parity, mirrors flow-handlers.mjs)
7A-11b adds handleFlowProjectRequest(input) to lib/flow/flow-handlers.mjs returning the same
{ ok, payload } | { ok:false, status, error, code } shape as the list/get handlers. It:
- validates
flow_id(FLOW_ID_RE) andversion(SEMVER_REwhen present) →BAD_REQUEST; - resolves
visibleScopesvia the unchangedresolveHandlerVisibleScopes(ambiguous ⇒FLOW_SCOPE_AMBIGUOUS); - rejects a non-active
harness→FLOW_HARNESS_UNSUPPORTED(400); - loads canonical via
getFlow(missing/scope-invisible ⇒404 unknown_flow— no existence leak); - calls
projectFlow+computeFidelity+ staleness/hash helpers and returns theknowtation.flow_project/v0envelope.
6.5 Error codes
| Code | Status | When |
|---|---|---|
BAD_REQUEST |
400 | malformed flow_id, bad version, missing/unknown harness value |
FLOW_HARNESS_UNSUPPORTED |
400 | a syntactically valid but reserved/inert harness (cursor_skill, mcp_prompt, agent_bundle) in v0 |
FLOW_SCOPE_AMBIGUOUS |
400 | scope resolution ambiguous (fails closed) |
FLOW_SCOPE_DENIED |
403 | scope query asks for a tier the caller lacks |
unknown_flow |
404 | flow missing or scope-invisible (indistinguishable) |
Reuses the existing Error schema ({ error, code }) — no new error shape.
7. OpenAPI wire shapes (to land in docs/openapi.yaml with the routes, 7A-11b)
New path under the existing Flows tag; BearerAuth + X-Vault-Id + requireRole(viewer…).
7.1 GET /api/v1/flows/{id}/projection
/api/v1/flows/{id}/projection:
get:
tags: [Flows]
summary: Derive a read-only harness projection of a canonical flow
description: >
Renders the canonical flow (latest visible, or pinned ?version) into the requested
harness as knowtation.flow_project/v0. Derived and read-only — generated_from_canonical
is always true and editable is always false. No secrets appear in rendered text.
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
- { name: harness, in: query, required: true, schema: { type: string, enum: [cursor_rule, cursor_skill, mcp_prompt, cli_runbook, agent_bundle] } }
- { name: version, in: query, schema: { type: string } }
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/FlowProjectResponse' } } } }
'400': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } # BAD_REQUEST | FLOW_HARNESS_UNSUPPORTED | FLOW_SCOPE_AMBIGUOUS
'401': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'403': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } # FLOW_SCOPE_DENIED
'404': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } # unknown_flow (missing OR scope-invisible)
7.2 Component schemas
components:
schemas:
FlowProjectResponse:
type: object
required: [schema, vault_id, projection, staleness, generator]
properties:
schema: { type: string, enum: [knowtation.flow_project/v0] }
vault_id: { type: string }
projection: { $ref: '#/components/schemas/FlowProjection' }
staleness: { $ref: '#/components/schemas/FlowProjectionStaleness' }
generator: { $ref: '#/components/schemas/FlowProjectionGenerator' }
FlowProjection: # knowtation.flow_projection/v0 — spec §1.7 (no extra fields)
type: object
required: [schema, flow_id, flow_version, harness, rendered, generated_from_canonical, editable]
properties:
schema: { type: string, enum: [knowtation.flow_projection/v0] }
flow_id: { type: string }
flow_version: { type: string }
harness: { type: string, enum: [cursor_rule, cursor_skill, mcp_prompt, cli_runbook, agent_bundle] }
rendered: { type: string, maxLength: 65536 } # MAX_RENDERED_BYTES; no secrets
generated_from_canonical: { type: boolean, enum: [true] }
editable: { type: boolean, enum: [false] }
fidelity:
type: object
required: [dropped_fields]
properties:
dropped_fields: { type: array, items: { type: string } }
notes: { type: string }
FlowProjectionStaleness:
type: object
required: [stale, projection_version, latest_version]
properties:
stale: { type: boolean }
projection_version: { type: string }
latest_version: { type: string }
FlowProjectionGenerator:
type: object
required: [generator_version, content_hash, generated_at]
properties:
generator_version: { type: string }
content_hash: { type: string } # hash of rendered only; generated_at excluded
generated_at: { type: string }
The
Errorschema already exists; projection routes reuse it with the §6.5codestrings.
8. Fail-closed rules (generator + all three surfaces)
- Derived, read-only, regenerable.
generated_from_canonical: true,editable: false. A hand-edited projection is drift, detected and never used as a write source (spec §6 gate 8). - Deterministic render.
projectFlowis pure over(flow, steps, harness, PROJECTION_GENERATOR_VERSION);generated_atis envelope-only and excluded fromcontent_hash. Anti-driftdiffis therefore meaningful. - No secrets in any artifact, hash, or response.
requires/skill_refsrender as handles/opaque ids only; notoken/oauth/refresh_token/raw note body/prompt/completion; the security tier scansrenderedand the full envelope. - Untrusted step text. Step anatomy fields render as inert data (escaped/quoted), never interpreted, never able to widen scope or escalate from inside the artifact.
- Scope is authorization, deny-by-default, no existence leak. Reuses 7A-10b
flow-scope.mjs/resolveHandlerVisibleScopesunchanged;scopenarrows only; missing and scope-invisible both ⇒404 unknown_flow. - Harness fail-closed. Only
ACTIVE_HARNESSESrender; reserved/inert ⇒FLOW_HARNESS_UNSUPPORTED. No partial/guessed render of an unsupported harness. - Honest fidelity. Every present-but-unexpressible field appears in
dropped_fields; nothing expressed is also listed; dropping never means leaking elsewhere. - Bounded + ordered.
rendered≤MAX_RENDERED_BYTES(truncated at a step boundary, noted in fidelity); steps always inordinalorder; no unbounded payloads, no quadratic scans. - No new write path.
flow projectperforms no canonical write and no proposal. The only write anywhere is the CLI--outlocal derived artifact;--checkand the Hub GET are read-only.
9. Seven-tier test matrix (what each tier proves — representative cases, not code)
Per RULE #0, 7A-11b ships all seven tiers. Files follow the repo .test.mjs convention
(test/flow-projection-generator-*.test.mjs, test/flow-project-parity-*.test.mjs). No network in
unit tests. Fixtures reuse the six flows/starter/ bundles (notably flow_overseer_handover,
flow_multi_repo_change — the two project-scoped dogfood Flows), a malicious-step fixture, a
multi-version fixture (e.g. 0.1.0 + 0.2.0), and a hand-edited-artifact fixture for drift.
1. unit — test/flow-projection-generator-unit.test.mjs
isHarnessActivetrue only forcursor_rule/cli_runbook; reserved/inert are false.projectFlowreturns a §1.7-shaped object withgenerated_from_canonical:true,editable:false, the marker as the first content line, steps inordinalorder, no extra keys (flowProjectionForClientparity).- Determinism: two
projectFlowcalls on identical input produce byte-identicalrenderedand identicalrenderedContentHash;generated_atdifferences never affect the hash. computeFidelitylists exactly the present-but-unexpressible fields (e.g.when_not_to_runforcursor_rule), sorted/de-duped, and never lists an expressed or absent field.isProjectionStale:0.1.0<0.2.0⇒ stale; equal ⇒ fresh; unparseable ⇒ stale (fail-closed).renderedContentHashstable across runs; differs whenrendereddiffers by one byte.
2. integration — test/flow-project-parity-integration.test.mjs
- Parity: CLI
flow project --json, MCPflow_projectresult, and the Hub route handler payload are deep-equal (excluding envelopegenerated_at) for the same authorized request. - Scope filter applied identically across all three; a pinned
--versionresolves identically. FLOW_HARNESS_UNSUPPORTEDreturned identically on all three foragent_bundle/cursor_skill.- The generator reads canonical via the same
getFlowthe read surface uses (one source).
3. e2e — test/flow-projection-generator-e2e.test.mjs
- Empty vault → first read seeds starters →
flow project flow_overseer_handover --harness cli_runbookrenders six ordered steps withhuman_review/artifact_existsverifications intact, marker first, no secrets →--outwrites the artifact → re-running--checkreports clean (no drift). flow project … --harness cursor_ruleemits valid MDC frontmatter + marker + ordered body and afidelity.dropped_fieldscontainingwhen_not_to_run.
4. stress — test/flow-projection-generator-stress.test.mjs
- Project a
MAX_STEPS_PER_FLOW-step flow and many flows back-to-back;renderedstays ≤MAX_RENDERED_BYTES(truncation noted in fidelity), ordering stays correct, render time bounded; no quadratic blowup over step count.
5. data-integrity — test/flow-projection-generator-data-integrity.test.mjs
- Round-trip fidelity: every step-anatomy field is either present in
renderedor named indropped_fields— never silently missing from both. - Anti-drift (the acceptance proof): render → bump canonical (e.g. tighten a step's
verification, bumpflow.version) → re-render → thediffof the tworenderedartifacts contains only the canonical change (no manual/incidental drift). Deleting a rendered artifact and regenerating reproduces it byte-for-byte (lossless). flow_versionin the projection equals the canonical version it was generated from.
6. performance — test/flow-projection-generator-performance.test.mjs
projectFlow+computeFidelity+renderedContentHashcomplete within a p95 budget on the large fixture; staleness/drift checks are O(content size); no quadratic scans in fidelity computation.
7. security — test/flow-projection-generator-security.test.mjs
- No secrets:
JSON.stringifyof the full envelope and the rawrenderedbytes contain notoken/oauth/refresh_token/raw-content markers;requires/skill_refsrender as handles only. - Injection inert: a malicious
instruction/boundaries/skill_reffixture renders as escaped, inert data — it cannot break out of the harness format, cannot widen scope, and is never executed. - Scope denial / no existence leak: a
personalcaller projecting aprojectflow gets404 unknown_flowidentical to a missing id;scope=orgunderpersonalauthorization is denied (FLOW_SCOPE_DENIED), never widened. - Drift never promoted: a hand-edited artifact is flagged by
detectDriftand is never read back as canonical;editable:falseholds; the only remedy is regenerate. - Harness fail-closed:
agent_bundle/cursor_skill/mcp_promptnever render a partial artifact.
10. Anti-drift demo — acceptance criteria for the pilot (7A-11 / 7A-12)
The convincing dogfood proof, frozen here as the acceptance bar the 11b implementation and the 7A-12 demo must hit:
- Generate.
knowtation flow project <project-scoped dogfood flow> --harness cursor_rule --out .cursor/rules/<slug>.mdc(and--harness cli_runbook --out AGENTS.md) produces a marker-first, ordered, secret-free artifact from the canonical Flow. - Edit canonical → regenerate → clean diff. Make one canonical change (e.g. add an optional step
or tighten a
verification), bumpflow.version, regenerate; the artifactdiffshows exactly that change and no manual drift. - Delete loses nothing. Delete the rendered artifact, regenerate; it is reproduced byte-for-byte from canonical.
- Hand-edit is caught. Hand-edit the artifact, run
flow project … --check;detectDriftreportsdrift:trueand the command exits non-zero — the hand edit is never promoted to canonical. - Staleness surfaces. With canonical ahead of a generated artifact,
--check/GET …/projectionreportsstale:truewith the lagging vs latest versions. - Fidelity is honest.
cursor_rulefor a flow withwhen_not_to_runlists it indropped_fields;cli_runbookexpresses it and does not list it.
11. Acceptance (7A-11a)
- The generator module shape (
lib/flow/projection-generator.mjsfunction table, constants, determinism rule), the rendered-artifact invariants and per-harness shapes, theknowtation.flow_projection/v0object (spec §1.7, unchanged) +knowtation.flow_project/v0envelope, staleness + drift detection, thefidelity.dropped_fieldscontract, theflow projectCLI/MCP/Hub read-derive wire shapes + OpenAPI shapes, the fail-closed rules, the anti-drift demo acceptance criteria, and the seven-tier test matrix are frozen here — contract only, no implementation. - Ratified against
FLOW-V0-SPEC.md§4/§1.7/§1.1/§6/§9 andscooling/docs/FLOW-PLATFORM-ARCHITECTURE.md(tenet point 5,Projectiondefinition,FlowProjectionAdapterproject/listProjections/projectionStaleness, projection-fidelity-loss risk). Reuses 7A-10b scope/parity machinery unchanged. - Muse-committed on
feat/flow-projection-pilot; handover regenerated to point at 7A-11b (Auto: generator +flow projectsurfaces + OpenAPI + seven-tier impl to this contract).
Non-goals (7A-11)
- No generator implementation, no
flow projectwiring, no OpenAPI edit (all 7A-11b). - No
cursor_skill/mcp_promptrendering (reserved); noagent_bundle(inert — 7A-L2). - No reconciliation of a hand-edited projection back to canonical (any such path is a later review-before-write gate).
- No
flow runadvancement (gated 7A-L3); no live capture (inert — 7A-L4); no hosted projection or MuseHub enrichment (7A-L5). - No Scooling changes and no posture-constant flips;
FLOW_LIVE_READ_AUTHORIZEDstaysfalsein Scooling.
Handoff notes (for the next step, 7A-11b — Auto)
- Branch is
feat/flow-projection-pilot; 7A-11a (this contract) is Muse-committed. Always target Knowtation withmuse -C ~/knowtation …. - 7A-11b (Auto) — implement
lib/flow/projection-generator.mjsto §1 (pure, deterministic, reads canonical viagetFlow), addhandleFlowProjectRequesttolib/flow/flow-handlers.mjs(§6.4), wireknowtation flow project(§6.1), theflow_projectMCP tool (§6.2), andGET /api/v1/flows/{id}/projection(§6.3), land the OpenAPI block (§7) with the routes, and ship all seven tiers (§9). Hit the anti-drift acceptance bar (§10). - Keep
generated_atenvelope-only and out ofcontent_hash/rendered; that is what keeps parity and anti-drift comparisons byte-stable. - Scope/parity machinery from 7A-10b (
flow-scope.mjs,resolveHandlerVisibleScopes) is reused unchanged — do not fork it. - 7A-12 runs the anti-drift diff demo end-to-end on our own repo guidance; 7A-13 is the Scooling loopback read wire (separate, gated). No Scooling posture flip rides 7A-11b.