Flow Store + List/Get Parity — Contract (Phase 7A, Step 7A-10a)
Status: Implemented — step 7A-10b (Auto). Store module, Hub routes, CLI/MCP wiring, OpenAPI block,
and seven-tier tests ship on branch feat/flow-projection-pilot. This document remains the
frozen contract reference; do not redesign it in follow-on steps.
Authored on branch feat/flow-projection-pilot (branched from feat/flow-v0-spec).
Related:
docs/FLOW-V0-SPEC.md— the canonical spec this contract implements (§1 schemas, §2 Storage Option A, §3 API surfaces, §6 security checklist, §9 seven-tier plan).lib/calendar/event-store.mjs— the Option A parity reference (local file-backed, per-vault, atomic write,*ForClientprojection, deny-by-default query filter). The Flow store mirrors its shape function-for-function where the concern is the same.flows/starter/— the six canonical seed Flows / 24 steps authored in 7A-3 (flow_*.jsonbundles) that the store seeds on first read.docs/openapi.yaml— where the Hub wire shapes in §6 land in the same change as the routes (7A-10b), per the no-docs-only-PR rule.scooling/docs/FLOW-ADAPTERS-CONTRACT-7A-5.md— the consumer read shape Scooling expects (FlowSummary,FlowDefinitionView, content-minimization, scope-denial codes). This contract is ratified against it so the Hub JSON theFlowAdapterwill read at 7A-10b matches field-for-field.
Scope fence (7A-10a): Store API + list/get read contract + OpenAPI wire shapes + seven-tier test
matrix only. No lib/flow/ implementation, no Hub routes, no CLI wiring, no
MCP tool wiring, no Scooling changes, no posture-constant flips. v0 read surface is
list/get only — propose / project / run are named here for parity completeness but are
not in 7A-10's read scope (propose/project = 7A-10b+ / 7A-11; run = gated 7A-L3).
Simple summary
A Flow is a saved procedure (an ordered checklist with proof-of-done). Knowtation is the one place Flows live. This document is the blueprint for the filing cabinet that holds them (the store) and the two read doors every surface uses to look inside: list (give me the titles of the Flows I'm allowed to see) and get (give me one whole Flow with its steps). The blueprint guarantees three things. First, the command line, the MCP plug-in, and the Hub website all return the exact same answer — change a Flow once and every door shows the change, with no copying. Second, you only ever see Flows you're allowed to see; the server decides that, never the client, and the default is "see nothing extra." Third, the list view is trimmed (titles and counts, never the full step text), and no secret, token, or private content is ever in any answer. The store is modeled on the calendar store that already works this way, so it inherits a proven, safe shape.
Technical summary
A local file-backed, per-vault Flow index store (lib/flow/flow-store.mjs) that mirrors
lib/calendar/event-store.mjs Option A: structured records in a JSON index (primary, query/scope
source of truth) plus an optional vault Markdown mirror (type: flow, derived from the index,
never the reverse). v0 exposes read-only listFlows (content-minimized summaries, scope/tag
filtered) and getFlow (full definition + ordered steps) over three identical surfaces — CLI
(knowtation flow list / flow get), MCP (flow_list / flow_get), and Hub REST
(GET /api/v1/flows, GET /api/v1/flows/{id}) — returning byte-identical JSON for the same
authorized request. Scope is authorization, enforced server-side, deny-by-default
(WorkspaceScopeAdapter + retrieval policy); the client never supplies its own visible tier. Step
text is untrusted input; list views are truncated; evidence/provenance and requires /
skill_refs are pointers/handles only — no secrets, tokens, or raw content in any response. The
store seeds the six 7A-3 starter bundles from flows/starter/ on first read (idempotent). All seven
test tiers are specified for the store and for list/get parity.
1. Store module shape — lib/flow/flow-store.mjs (Option A, calendar parity)
The Flow store is a pure, local, file-backed module with no network and no Express coupling —
exactly like event-store.mjs. Routes/CLI/MCP call it; it never calls them. The function table
below pins names, parameters, and return shapes for 7A-10b.
1.1 Constants and on-disk layout
| Constant | Value | Parity note |
|---|---|---|
FLOW_STORE_FILENAME |
'hub_flow_store.json' |
mirrors STORE_FILENAME = 'hub_calendar_store.json' |
STARTER_FLOWS_DIRNAME |
'flows/starter' (repo-relative; resolvable override for tests) |
seed source (7A-3 bundles) |
MAX_FLOW_SUMMARIES |
200 |
list cap; sets truncated when exceeded |
MAX_STEPS_PER_FLOW |
100 |
get cap; rejects/marks over-long step lists |
On-disk shape (one file under data_dir, keyed by vault, mirroring the calendar store):
{
"vaults": {
"<vault_id>": {
"flows": [ /* knowtation.flow/v0 records */ ],
"steps": [ /* knowtation.flow_step/v0 records */ ],
"runs": [ /* knowtation.flow_run/v0 records (read-only in v0) */ ],
"candidates": [ /* knowtation.flow_candidate/v0 records (inert in v0) */ ],
"projections": [ /* knowtation.flow_projection/v0 records (derived; 7A-11) */ ]
}
}
}
The index is the source of truth. The optional vault Markdown mirror (spec §2.2) is a projection of the index, reconciled as a proposal if hand-edited — never silently promoted.
1.2 Persistence primitives (mirror event-store.mjs exactly)
| Function | Signature | Behaviour (parity target) |
|---|---|---|
getFlowStorePath |
(dataDir) → string |
path.join(dataDir, FLOW_STORE_FILENAME) |
loadFlowStore |
(dataDir) → FlowStoreFile |
read+parse; never throws — malformed/missing ⇒ { vaults: {} } |
saveFlowStore |
(dataDir, store) → void |
mkdir -p, write to *.<pid>.<uuid>.tmp, renameSync (atomic), 2-space JSON |
getVaultFlowStore |
(dataDir, vaultId) → VaultFlowStore |
lazily creates an empty {flows,steps,runs,candidates,projections} |
1.3 Id + version helpers (pin §1.2 of the spec)
| Export | Value / signature | Source |
|---|---|---|
FLOW_ID_RE |
/^flow_[a-z0-9_]{1,64}$/ |
spec §1.2 |
FLOW_STEP_ID_RE |
/^flow_[a-z0-9_]{1,64}#[1-9][0-9]*$/ |
<flow_id>#<ordinal> |
FLOW_RUN_ID_RE |
/^run_[a-z0-9_]{1,48}$/ |
spec §1.2 |
SEMVER_RE |
strict MAJOR.MINOR.PATCH |
flow.version |
buildFlowStepId |
(flowId, ordinal) → string |
`${flowId}#${ordinal}` (parity with buildEventId) |
1.4 Seeding from flows/starter/ (idempotent)
| Function | Signature | Behaviour |
|---|---|---|
seedStarterFlows |
(dataDir, vaultId, { starterDir? }) → { seeded: number, skipped: number } |
Load each flows/starter/flow_*.json bundle ({ flow, steps }), validate against §1 schema, and upsert by (flow_id, version). Idempotent: re-running seeds nothing new (skipped counts already-present). A bundle that fails schema validation is rejected and logged, never partially written. |
Seeding is read-time lazy (first listFlows/getFlow on an empty vault triggers it) and is the
only write the v0 store performs — it materializes canonical, user-owned seeds, not user data,
and routes no proposal (the seeds ship with the repo). All later durable writes route through
proposals (review-before-write); the store exposes no public create/update/delete in v0.
1.5 Read operations (the v0 surface)
| Function | Signature | Returns | Rules |
|---|---|---|---|
listFlows |
(dataDir, vaultId, { visibleScopes, scope?, tags?, limit? }) → FlowListResult |
{ flows: FlowSummary[], effective_scope, truncated } |
content-minimized; deny-by-default scope filter (§4); limit capped at MAX_FLOW_SUMMARIES; truncated:true when capped or when more than limit match |
getFlow |
(dataDir, vaultId, flowId, { visibleScopes, version? }) → FlowDefinitionResult \| null |
{ flow: Flow, steps: FlowStep[] } or null when not found/not visible |
flowId must match FLOW_ID_RE; resolve latest version within visible scope unless version pins one; steps returned in ascending ordinal order; capped at MAX_STEPS_PER_FLOW |
visibleScopes is a server-resolved Set<Scope> passed in by the route/CLI/MCP after scope
resolution (§4). The store never trusts a client-supplied tier; an absent visibleScopes
resolves to { 'personal' } (lowest). getFlow returns null (mapped to 404 unknown_flow) for
both genuinely-missing and scope-invisible flows so existence is never leaked across scope.
1.6 Client projections (*ForClient — calendar parity)
| Function | Signature | Drops / keeps |
|---|---|---|
flowSummaryForClient |
(flow) → FlowSummary |
keeps flow_id, title, version, scope, summary, tags, step_count, updated, truncated; drops the full steps[], inputs, vault_mirror_path (content-minimization) |
flowDefinitionForClient |
(flow, steps) → { flow, steps } |
full definition + ordered steps; strips any field not in §1.3/§1.4 of the spec; never emits oauth_ref-style or secret-bearing keys (none exist in the schema, asserted in tests) |
2. Read operations contract (list + get)
2.1 flow list — content-minimized, scope/tag filtered
- Input: optional
scope(narrow within authorized scopes only — never widen), optionaltag(single tag membership), optionallimit(1..MAX_FLOW_SUMMARIES, defaultMAX_FLOW_SUMMARIES). - Output: an array of
FlowSummary(one line per visible flow, latest version perflow_idwithin scope), the resolvedeffective_scope, andtruncated. - Content-minimization: summaries carry
step_count(an integer), never step bodies, instructions, boundaries, requires, or skill refs. - Ordering: stable — by
updateddescending, thenflow_idascending (deterministic for tests).
2.2 flow get — full definition + ordered steps
- Input:
flow_id(required,FLOW_ID_RE), optionalversion(SEMVER_RE; default = latest visible). - Output: the full
knowtation.flow/v0record plus itsknowtation.flow_step/v0records in ascendingordinalorder. - Untrusted: every step's
instruction,owned_job,trigger,when_not_to_run,boundaries,output_shape, andskill_refsare data, not commands — returned verbatim, never interpreted, and they cannot widen scope or escalate permission from inside. - Not found / not visible: both ⇒
404 unknown_flow(no existence leak across scope).
3. CLI commands — knowtation flow … (per FLOW-V0-SPEC §3.1)
A new flow subcommand namespace (the CLI dispatches on subcommand === 'flow', then the next
arg). v0 wires only the two read commands; the rest are reserved per the spec and print a
"gated/not-in-v0" notice.
knowtation flow list [--scope personal|project|org] [--tag <t>] [--limit <n>] [--json]
knowtation flow get <flow_id> [--version <semver>] [--json]
# reserved (not wired in v0): flow propose | flow export | flow import | flow project | flow run
--jsonprints the exact Hub JSON (§6) to stdout; without it, a human-readable table (flow list) or summary (flow get).- The CLI resolves scope the same way the Hub does (local config identity → authorized scopes);
it passes
visibleScopesinto the store. No CLI flag can grant a scope the caller lacks. - Errors map to non-zero exits with the same code strings as the Hub (
BAD_REQUEST,NOT_FOUND,FLOW_SCOPE_DENIED).
CLI dispatch parity: today the CLI uses flat verbs (list-notes, get-note). The flow
namespace is the §3.1-specified shape; 7A-10b adds the flow branch in cli/index.mjs delegating
straight to lib/flow/flow-store.mjs (no duplicated logic).
4. Scope enforcement (server-side, deny-by-default)
| Rule | Contract |
|---|---|
| Authorization, not a filter | The caller's visible scope set is resolved server-side from verified identity + WorkspaceScopeAdapter + Knowtation retrieval policy. The client's scope query param can only narrow within that set. |
| Deny by default | Absent/ambiguous resolution ⇒ { 'personal' } (lowest). A personal context never receives project/org flows. |
| No existence leak | A flow outside the visible scope is indistinguishable from a missing one (getFlow ⇒ null ⇒ 404 unknown_flow). |
| Error codes | unauthorized tier ⇒ 403 FLOW_SCOPE_DENIED; ambiguous scope ⇒ 400 FLOW_SCOPE_AMBIGUOUS (mirrors the consumer's flow_scope_denied / flow_scope_ambiguous). |
| Vault binding | X-Vault-Id + role required on the Hub (requireVaultAccess + requireRole('viewer'…)), parity with the calendar routes; the CLI binds the locally-configured vault. |
The starter set spans scopes on purpose: four personal Flows + two project Flows
(flow_multi_repo_change, flow_overseer_handover). The scope test fixtures use exactly this set
so a personal context proves it sees four and never the two project Flows.
5. Starter Flow seed (from flows/starter/)
- The six 7A-3 bundles (
flow_capture_to_note,flow_research_brief,flow_reviewed_writeback,flow_session_to_flow,flow_multi_repo_change,flow_overseer_handover) are the canonical seed definitions.seedStarterFlowsvalidates each against §1 of the spec and upserts by(flow_id, version). - Seeding is idempotent and read-time lazy; it writes only canonical seeds, never user data, and routes no proposal.
- A bundle failing schema validation (e.g. a step missing
trigger/verification— the "anatomy-completeness" rule) is rejected wholesale and logged; the store is never left partially seeded.
6. OpenAPI wire shapes (to land in docs/openapi.yaml with the routes in 7A-10b)
Specified here, added to docs/openapi.yaml in the same change as the routes (no docs-only PR
to main). New tag: Flows. Both routes are BearerAuth + X-Vault-Id + requireRole(viewer…).
6.1 GET /api/v1/flows — list (scope/tag filtered, content-minimized)
Query params: scope (enum personal|project|org, optional — narrows only), tag (string,
optional), limit (integer 1..200, optional).
/api/v1/flows:
get:
tags: [Flows]
summary: List scope-visible flows (content-minimized)
parameters:
- { name: scope, in: query, schema: { type: string, enum: [personal, project, org] } }
- { name: tag, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200 } }
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/FlowListResponse' } } } }
'400': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } # BAD_REQUEST | FLOW_SCOPE_AMBIGUOUS
'401': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'403': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } # FLOW_SCOPE_DENIED
6.2 GET /api/v1/flows/{id} — full definition + ordered steps
Path param id (FLOW_ID_RE); query param version (SEMVER_RE, optional — default latest).
/api/v1/flows/{id}:
get:
tags: [Flows]
summary: Get one flow definition + ordered steps
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
- { name: version, in: query, schema: { type: string } }
responses:
'200': { content: { application/json: { schema: { $ref: '#/components/schemas/FlowGetResponse' } } } }
'400': { content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
'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)
6.3 Component schemas
components:
schemas:
FlowListResponse:
type: object
required: [schema, vault_id, effective_scope, flows, truncated]
properties:
schema: { type: string, enum: [knowtation.flow_list/v0] }
vault_id: { type: string }
effective_scope: { type: string, enum: [personal, project, org] }
flows:
type: array
maxItems: 200
items: { $ref: '#/components/schemas/FlowSummary' }
truncated: { type: boolean }
FlowSummary: # content-minimized — NO step bodies
type: object
required: [schema, flow_id, title, version, scope, summary, tags, step_count, updated, truncated]
properties:
schema: { type: string, enum: [knowtation.flow/v0] }
flow_id: { type: string }
title: { type: string }
version: { type: string }
scope: { type: string, enum: [personal, project, org] }
summary: { type: string }
tags: { type: array, maxItems: 32, items: { type: string } }
step_count: { type: integer, minimum: 0 }
updated: { type: string }
truncated: { type: boolean }
FlowGetResponse:
type: object
required: [schema, vault_id, flow, steps]
properties:
schema: { type: string, enum: [knowtation.flow_get/v0] }
vault_id: { type: string }
flow: { $ref: '#/components/schemas/Flow' }
steps:
type: array
maxItems: 100
items: { $ref: '#/components/schemas/FlowStep' }
Flow: # knowtation.flow/v0 — full (spec §1.3)
type: object
required: [schema, flow_id, title, version, scope, summary, steps, updated, truncated]
properties:
schema: { type: string, enum: [knowtation.flow/v0] }
flow_id: { type: string }
title: { type: string }
version: { type: string }
scope: { type: string, enum: [personal, project, org] }
summary: { type: string }
tags: { type: array, maxItems: 32, items: { type: string } }
steps: { type: array, maxItems: 100, items: { type: string } } # ordered step ids
inputs:
type: array
items:
type: object
required: [name, type, required]
properties:
name: { type: string }
type: { type: string }
required: { type: boolean }
vault_mirror_path: { type: string, nullable: true }
updated: { type: string }
truncated: { type: boolean }
FlowStep: # knowtation.flow_step/v0 — full (spec §1.4); all text is UNTRUSTED
type: object
required: [schema, step_id, flow_id, ordinal, owned_job, instruction, trigger, when_not_to_run, boundaries, output_shape, verification, automatable]
properties:
schema: { type: string, enum: [knowtation.flow_step/v0] }
step_id: { type: string }
flow_id: { type: string }
ordinal: { type: integer, minimum: 1 }
owned_job: { type: string }
instruction: { type: string }
trigger: { type: string }
when_not_to_run:{ type: string }
requires:
type: array
items:
type: object
required: [kind, id]
properties:
kind: { type: string, enum: [vault_scope, tool, file, artifact] }
id: { type: string } # handle/pointer only — never an inline secret
boundaries: { type: array, items: { type: string } }
skill_refs:
type: array
items:
type: object
required: [kind, id]
properties:
kind: { type: string, enum: [mcp_prompt, skill_pack, cli, external_tool] }
id: { type: string } # opaque id only
inputs:
type: array
items:
type: object
required: [name, from]
properties: { name: { type: string }, from: { type: string } }
outputs:
type: array
items:
type: object
required: [name, type]
properties: { name: { type: string }, type: { type: string } }
output_shape: { type: string }
verification:
type: object
required: [kind, evidence_required, description]
properties:
kind: { type: string, enum: [human_review, artifact_exists, value_match, test_pass, agent_check] }
evidence_required: { type: boolean }
description: { type: string }
automatable: { type: string, enum: [manual, agent_assisted, automatable] }
The
Errorschema ({ error, code }) already exists indocs/openapi.yaml; Flow routes reuse it with thecodestrings above (BAD_REQUEST,NOT_FOUND/unknown_flow,FLOW_SCOPE_DENIED,FLOW_SCOPE_AMBIGUOUS), matching the calendar route convention.
7. Triple-surface parity contract (CLI = MCP = Hub REST)
The defining invariant of 7A-10: the same authorized request returns byte-identical JSON on all three surfaces.
| Surface | list | get | Source of the JSON |
|---|---|---|---|
| CLI | knowtation flow list --json |
knowtation flow get <id> --json |
lib/flow/flow-store.mjs |
| MCP | flow_list tool result |
flow_get tool result |
same module |
| Hub REST | GET /api/v1/flows |
GET /api/v1/flows/{id} |
same module |
- All three call the same
listFlows/getFlowand the same*ForClientprojections — no surface re-shapes the payload. Parity is proven by deep-equality in the integration tier (§8.2). - Scope resolution differs in input (HTTP header/JWT vs local config) but converges on the
same
visibleScopesset fed to the store; the JSON out is identical. - The
schemadiscriminator (knowtation.flow_list/v0/knowtation.flow_get/v0) is stamped by the store, so every surface emits it identically.
8. Fail-closed rules (apply to the store and all three surfaces)
- Scope is authorization, deny-by-default. Client never supplies its own visible tier; absent
⇒
personal; apersonalcontext never receivesproject/orgflows; ambiguous ⇒FLOW_SCOPE_AMBIGUOUS; unauthorized ⇒FLOW_SCOPE_DENIED. (§4) - No existence leak. Missing and scope-invisible both return
404 unknown_flow. - Step text + skill refs are untrusted.
instruction/boundaries/skill_refs/output_shapeare returned verbatim as data; they can never widen scope or escalate permission, and the store never executes or interpolates them. - No secrets in any response.
requires/skill_refscarry handles/opaque ids only;provenance(on runs) carries hashed actor + harness label only; notoken,oauth,refresh_token, raw note body, prompt, or completion ever appears in any flow/step/list object. The security tier asserts this by scanning serialized output. - Content-minimized, truncated list views.
flow listreturns summaries (counts + metadata), never step bodies; results carrytruncatedand bounded arrays (MAX_FLOW_SUMMARIES,MAX_STEPS_PER_FLOW). - Read-only in v0. The store exposes no public create/update/delete. The only write is the
idempotent canonical seed; all durable user changes route through proposals later
(review-before-write).
flow runadvancement stays gated (7A-L3). - Robust load.
loadFlowStorenever throws on malformed/missing files (returns empty), and writes are atomic (tmp+rename) — a crashed write never corrupts the index. - Deterministic, bounded ordering. List =
updateddesc thenflow_idasc; steps =ordinalasc; both capped — no unbounded payloads, no quadratic scans.
9. Seven-tier test matrix (what each tier proves — representative cases, not code)
Per RULE #0, 7A-10b ships all seven tiers. Files follow the repo .test.mjs convention
(test/flow-store-*.test.mjs, test/flow-list-get-parity-*.test.mjs). No network in unit
tests. Fixtures use the six flows/starter/ bundles plus a malicious-step fixture and an empty
vault.
1. unit — test/flow-store-unit.test.mjs
loadFlowStorereturns{ vaults: {} }for missing and malformed files (never throws).saveFlowStorewrites atomically (tmp + rename) and round-trips a vault store.- Id/version regexes accept canonical ids and reject malformed (
FLOW_ID_RE,FLOW_STEP_ID_RE,FLOW_RUN_ID_RE,SEMVER_RE);buildFlowStepIdcomposes<flow_id>#<ordinal>. flowSummaryForClientdrops step bodies/inputs/mirror path and keeps the nine summary fields;flowDefinitionForClientemits only spec-§1 fields.seedStarterFlowsvalidates a good bundle and rejects an anatomy-incomplete one (missingtrigger/verification) without partial write.listFlows/getFlowstamp theschemadiscriminator;getFloworders steps byordinal.
2. integration — test/flow-list-get-parity-integration.test.mjs
- Parity: CLI
flow list/flow getJSON, MCPflow_list/flow_getresult, and the Hub route handler payload are deep-equal for the same authorized request (samevisibleScopes). - Scope filter applied identically across all three for
listandget. getFlowstep ids match theflow.steps[]order;step_countin the summary equalssteps.lengthfromget.- Seeding is idempotent across repeated
listFlowscalls (no duplicateflow_id/version).
3. e2e — test/flow-store-e2e.test.mjs
- Empty vault → first
flow listseeds the six starters →flow get flow_overseer_handoverreturns six ordered steps withhuman_review+artifact_existsverification kinds intact →flow list --scope personalreturns exactly the four personal flows. - A pinned
--versionreturns that version; absent returns latest.
4. stress — test/flow-store-stress.test.mjs
- Seed/load a vault with up to
MAX_FLOW_SUMMARIESflows andMAX_STEPS_PER_FLOW-step flows;list/getstay correct and settruncatedwhen capped. Deepstep_stateson read-only runs do not slowget.
5. data-integrity — test/flow-store-data-integrity.test.mjs
- Round-trip a starter bundle through
seed → getFlowwith no field loss/mutation: steps keepordinalorder;requires/skill_refs/verification/scope/versionsurvive byte-for-byte. done+evidence_requiredrun step states never appear withverified:false(read invariant).- The optional vault mirror, when present, is never treated as the source (index wins).
6. performance — test/flow-store-performance.test.mjs
listFlows(scope-filtered) andgetFlowcomplete within a p95 budget on the large fixture; no quadratic blowups in scope/tag filtering or step ordering; load is O(file size).
7. security — test/flow-store-security.test.mjs
- Scope denial: a
personalvisibleScopesnever returns the twoprojectflows; ambiguous scope fails closed (FLOW_SCOPE_AMBIGUOUS); unauthorized tier ⇒FLOW_SCOPE_DENIED. - No existence leak:
getFlowfor aprojectflow under apersonalscope returnsnull⇒404 unknown_flow, identical to a truly-missing id. - Injection: a malicious
instruction/boundariesfixture is returned verbatim as inert data and never alters scope/permission or gets interpreted. - No secrets:
JSON.stringifyof everylist/getresult contains notoken/oauth/refresh_token/raw-content markers;requires/skill_refsare handles only. - Client cannot widen scope: a
scope=orgquery param underpersonalauthorization does not return org flows (param narrows only).
10. Acceptance (7A-10a)
- Store module shape (
lib/flow/flow-store.mjs), read-op contract (listFlows/getFlow), CLI command shape, Hub route shapes, OpenAPI wire schemas, scope-enforcement model, starter-seed behaviour, fail-closed rules, the triple-surface parity invariant, and the seven-tier test matrix are all frozen here — contract only, no implementation. - Ratified against
FLOW-V0-SPEC.md§1–§3/§6/§9 and against the consumer shape inscooling/docs/FLOW-ADAPTERS-CONTRACT-7A-5.md(FlowSummary / FlowDefinitionView / content-minimization / scope-denial codes). - Muse-committed on
feat/flow-projection-pilot; handover regenerated to point at 7A-10b (Auto: store + routes + CLI + OpenAPI + seven-tier impl to this contract).
Non-goals (7A-10)
- No
propose/export/import/projectread or write paths (named for parity; impl later). - No run advancement (
flow run) — gated 7A-L3. - No live capture / candidate detection — inert (7A-L4).
- No Scooling changes and no posture-constant flips;
FLOW_LIVE_READ_AUTHORIZEDstaysfalsein Scooling until the 7A-10b HubGET /flowsships and is wired.
Handoff notes (for the next step, 7A-11 — Thinking)
- Branch is
feat/flow-projection-pilot; 7A-10b is Muse-committed (store + routes + CLI + MCP + OpenAPI + seven tiers). Always target Knowtation withmuse -C ~/knowtation …. - 7A-11a (Thinking) — design the projection generator + fidelity report contract per
docs/FLOW-V0-SPEC.md§4 (knowtation.flow_projection/v0, harness targets, staleness/drift). - Hub
GET /api/v1/flows+GET /api/v1/flows/{id}are live on self-hosted Hub; ScoolingFLOW_LIVE_READ_AUTHORIZEDflip is a separate, later Scooling step (not part of 7A-10b). - Parity gate:
test/flow-list-get-parity-integration.test.mjs— CLI = MCP = Hub handler deep-equality for the same authorized request.