@kontourai/flow-agents 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.github/workflows/runtime-compat.yml +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/README.md +38 -19
  4. package/build/src/cli/flow-kit.js +9 -4
  5. package/build/src/cli/runtime-adapter.js +9 -5
  6. package/build/src/cli/telemetry-doctor.js +4 -1
  7. package/build/src/runtime-adapters.js +34 -0
  8. package/build/src/tools/build-universal-bundles.js +18 -1
  9. package/console.telemetry.json +115 -20
  10. package/docs/_layouts/default.html +2 -0
  11. package/docs/index.md +8 -0
  12. package/docs/integrations/index.md +4 -0
  13. package/docs/integrations/knowledge-kit-live.md +211 -0
  14. package/docs/kit-authoring-guide.md +169 -0
  15. package/docs/spec/runtime-hook-surface.md +56 -3
  16. package/evals/acceptance/run.sh +10 -1
  17. package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
  18. package/evals/acceptance/test_pi_harness.sh +15 -0
  19. package/evals/integration/test_runtime_adapter_activation.sh +113 -1
  20. package/integrations/strands/examples/knowledge_kit_live.py +461 -0
  21. package/integrations/strands/flow_agents_strands/steering.py +54 -1
  22. package/integrations/strands/tests/test_hooks.py +88 -0
  23. package/integrations/strands-ts/src/hooks.ts +104 -0
  24. package/integrations/strands-ts/test/test-steering.ts +159 -0
  25. package/kits/catalog.json +6 -0
  26. package/kits/knowledge/adapters/default-store/index.js +821 -0
  27. package/kits/knowledge/adapters/flow-runner/index.js +1179 -0
  28. package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
  29. package/kits/knowledge/docs/README.md +135 -0
  30. package/kits/knowledge/docs/store-contract.md +526 -0
  31. package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
  32. package/kits/knowledge/evals/contract-suite/suite.test.js +670 -0
  33. package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
  34. package/kits/knowledge/evals/synthesis/suite.test.js +909 -0
  35. package/kits/knowledge/flows/compile.flow.json +60 -0
  36. package/kits/knowledge/flows/consolidate.flow.json +77 -0
  37. package/kits/knowledge/flows/ingest.flow.json +60 -0
  38. package/kits/knowledge/flows/store-contract.flow.json +48 -0
  39. package/kits/knowledge/flows/synthesize.flow.json +77 -0
  40. package/kits/knowledge/kit.json +78 -0
  41. package/package.json +1 -1
  42. package/src/cli/flow-kit.ts +10 -4
  43. package/src/cli/runtime-adapter.ts +10 -5
  44. package/src/cli/telemetry-doctor.ts +4 -1
  45. package/src/runtime-adapters.ts +35 -0
  46. package/src/tools/build-universal-bundles.ts +18 -1
@@ -0,0 +1,526 @@
1
+ ---
2
+ title: Knowledge Kit Store Contract
3
+ version: "1.0"
4
+ ---
5
+
6
+ # Knowledge Kit Store Contract
7
+
8
+ This document defines the storage contract for the Knowledge Kit. Any store adapter that
9
+ implements this contract can be substituted for the default adapter without changing kit flows.
10
+ The contract is self-contained: a second adapter author should be able to read this document
11
+ alone and produce a conforming implementation.
12
+
13
+ ---
14
+
15
+ ## 1. Record Types
16
+
17
+ The store holds three record types. Every record, regardless of type, shares a common envelope.
18
+
19
+ ### 1.1 Common Record Envelope
20
+
21
+ | Field | Type | Required | Description |
22
+ |---|---|---|---|
23
+ | `id` | string | yes | Stable, opaque identifier. Must be unique within the store. Adapter may generate (e.g. UUID or slug). |
24
+ | `type` | `"raw"` \| `"compiled"` \| `"concept"` | yes | Discriminates the record type. |
25
+ | `title` | string | yes | Human-readable title. |
26
+ | `body` | string | yes | Primary content. Format is type-specific (see below). |
27
+ | `category` | string | yes | Dot-separated hierarchical category string, e.g. `"engineering.api"`. Must be non-empty. |
28
+ | `tags` | string[] | no | Flat list of tag strings for secondary classification. |
29
+ | `links` | Link[] | no | Outbound links from this record (see §2). |
30
+ | `provenance` | Provenance | yes | Immutable creation provenance (see §4). |
31
+ | `created_at` | ISO-8601 string | yes | Creation timestamp in UTC. |
32
+ | `updated_at` | ISO-8601 string | yes | Last mutation timestamp in UTC. |
33
+
34
+ ### 1.2 `raw` Record
35
+
36
+ A raw record holds unprocessed source material exactly as received — a document excerpt,
37
+ a transcript snippet, a URL with notes, or any other unrefined input.
38
+
39
+ - `body`: free-form string, preserved verbatim.
40
+ - No semantic constraints on content.
41
+ - Intended as the input stage before compilation.
42
+
43
+ ### 1.3 `compiled` Record
44
+
45
+ A compiled record is a normalized, editor-reviewed distillation of one or more raw records.
46
+
47
+ - `body`: structured markdown, expected to be human-readable reference prose.
48
+ - Must link (via `links`) to at least one `raw` record as its source, using link kind `"source"`.
49
+ - The `source_ids` provenance field (see §4) records which raw record(s) were compiled.
50
+
51
+ ### 1.4 `concept` Record
52
+
53
+ A concept record defines a named idea, term, or principle that other records can reference.
54
+
55
+ - `body`: definition or explanation string.
56
+ - May link to `compiled` records via link kind `"example"` or `"related"`.
57
+ - Concept ids are used as the `target_id` in wikilink-style link syntax.
58
+
59
+ ---
60
+
61
+ ## 2. Links
62
+
63
+ Links express directed relationships between records. A link is stored as part of the source
64
+ record's `links` array. The graph index (see §5) mirrors all links for efficient traversal.
65
+
66
+ ### 2.1 Link Object
67
+
68
+ | Field | Type | Required | Description |
69
+ |---|---|---|---|
70
+ | `target_id` | string | yes | `id` of the target record in this store. |
71
+ | `kind` | string | yes | Relationship kind (see §2.2). |
72
+ | `label` | string | no | Human display label for the link. |
73
+
74
+ ### 2.2 Link Kinds
75
+
76
+ | Kind | Direction | Meaning |
77
+ |---|---|---|
78
+ | `"source"` | compiled → raw | Compiled record was derived from this raw record. |
79
+ | `"example"` | concept → compiled | This compiled record exemplifies the concept. |
80
+ | `"related"` | any → any | General semantic relation. |
81
+ | `"refines"` | compiled → compiled | Later record refines or supersedes an earlier one. |
82
+ | `"proposes"` | any → concept | Record proposes a change to the concept (used with `propose` mutation). |
83
+
84
+ Adapters MUST store and return link objects with at least `target_id` and `kind`. Unknown
85
+ kinds MUST NOT be rejected — forward compatibility requires tolerating new kinds.
86
+
87
+ ### 2.3 Wikilink Syntax
88
+
89
+ In `body` text, outbound links may also appear as `[[target_id]]` or `[[target_id|label]]`.
90
+ The default adapter indexes these inline wikilinks into the `links` array on write and renders
91
+ them back on read. The contract requires that after a round-trip, links declared in the `links`
92
+ array are queryable via `getLinks(id)`. Inline wikilink parsing is an adapter implementation
93
+ detail; the contract does not mandate a body format.
94
+
95
+ ---
96
+
97
+ ## 3. Categories
98
+
99
+ Categories provide hierarchical classification. Rules:
100
+
101
+ - A category is a dot-separated string of one or more non-empty segments, e.g. `"engineering"`,
102
+ `"engineering.api"`, `"engineering.api.rest"`.
103
+ - Segments must match `[a-z0-9_-]+` (lowercase alphanumeric, hyphens, underscores).
104
+ - The store MUST support querying records by exact category or by prefix (all descendants).
105
+ - Empty string is not a valid category. The adapter MUST reject records with an empty category.
106
+
107
+ ---
108
+
109
+ ## 4. Provenance
110
+
111
+ Every record carries immutable creation provenance. Mutation operations supply evidence that
112
+ is appended to a mutable `mutation_log`. Adapters MUST enforce required provenance fields and
113
+ MUST reject mutations missing them (see §6).
114
+
115
+ ### 4.1 Creation Provenance Object (`provenance`)
116
+
117
+ | Field | Type | Required | Description |
118
+ |---|---|---|---|
119
+ | `source_ids` | string[] | no | IDs of records this record was derived from (for compiled records). |
120
+ | `agent` | string | yes | Identifier of the agent or process that created this record. |
121
+ | `session_id` | string | no | Identifier of the session in which the record was created. |
122
+ | `note` | string | no | Free-form provenance note. |
123
+
124
+ ### 4.2 Mutation Log Entry
125
+
126
+ Each mutation appends one log entry. Log entries are append-only; adapters MUST NOT
127
+ overwrite or delete log entries.
128
+
129
+ | Field | Type | Required | Description |
130
+ |---|---|---|---|
131
+ | `op` | string | yes | Operation name (one of the mutation ops in §6). |
132
+ | `at` | ISO-8601 string | yes | Timestamp of the mutation. |
133
+ | `agent` | string | yes | Agent that performed the mutation. |
134
+ | `note` | string | no | Reason or human note for the mutation. |
135
+ | `evidence` | object | no | Op-specific evidence (see §6 per-op tables). |
136
+
137
+ ---
138
+
139
+ ## 5. Graph Index
140
+
141
+ The store maintains a JSON graph index file (or equivalent in-memory structure) that mirrors
142
+ all link relationships for O(1) lookup by source or target. The index enables:
143
+
144
+ - Forward lookup: given a record id, return all outbound links.
145
+ - Reverse lookup: given a record id, return all inbound links (backlinks).
146
+
147
+ The index MUST be updated atomically with every mutation that changes links. After any
148
+ mutation, a `getLinks(id)` call MUST reflect the current link state.
149
+
150
+ The default adapter stores the index as `graph-index.json` at the store root.
151
+
152
+ ### 5.1 Graph Index Schema
153
+
154
+ ```json
155
+ {
156
+ "schema_version": "1.0",
157
+ "forward": {
158
+ "<source_id>": [
159
+ { "target_id": "<id>", "kind": "<kind>", "label": "<optional>" }
160
+ ]
161
+ },
162
+ "reverse": {
163
+ "<target_id>": [
164
+ { "source_id": "<id>", "kind": "<kind>" }
165
+ ]
166
+ }
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 6. Mutation Operations
173
+
174
+ The store exposes six mutation operations. Each operation specifies:
175
+
176
+ - **Required provenance/evidence fields** that the caller MUST supply.
177
+ - The adapter MUST reject the call with an error if any required field is missing.
178
+ - Optional fields that the adapter records when present.
179
+
180
+ ### 6.1 `create`
181
+
182
+ Create a new record. The adapter assigns `id`, `created_at`, and `updated_at`.
183
+
184
+ **Required fields:**
185
+
186
+ | Field | Location | Description |
187
+ |---|---|---|
188
+ | `type` | top-level | Record type: `"raw"`, `"compiled"`, or `"concept"`. |
189
+ | `title` | top-level | Non-empty title string. |
190
+ | `body` | top-level | Non-empty body string. |
191
+ | `category` | top-level | Non-empty dot-separated category. |
192
+ | `provenance.agent` | provenance | Agent identifier. |
193
+
194
+ **Optional fields:** `tags`, `links`, `provenance.source_ids`, `provenance.session_id`, `provenance.note`.
195
+
196
+ **Rejection conditions:**
197
+ - Missing or empty `type`, `title`, `body`, `category`.
198
+ - `type` not one of `"raw"`, `"compiled"`, `"concept"`.
199
+ - Empty `category` or category with invalid segments.
200
+ - Missing `provenance.agent`.
201
+
202
+ **Post-conditions:** Record is retrievable by `get(id)`. Links in `links` array are indexed.
203
+
204
+ ### 6.2 `update`
205
+
206
+ Update mutable fields of an existing record. Immutable fields (`id`, `type`, `created_at`,
207
+ `provenance`) MUST NOT change.
208
+
209
+ **Required fields:**
210
+
211
+ | Field | Location | Description |
212
+ |---|---|---|
213
+ | `id` | argument | ID of the record to update. |
214
+ | `agent` | evidence | Agent performing the update. |
215
+
216
+ **Optional update fields (at least one must be supplied):** `title`, `body`, `category`, `tags`, `links`.
217
+
218
+ **Rejection conditions:**
219
+ - Record with `id` does not exist.
220
+ - No mutable fields supplied (no-op updates are rejected).
221
+ - Missing `agent` in evidence.
222
+
223
+ **Post-conditions:** `updated_at` is refreshed. Mutation log entry appended. If `links`
224
+ changed, graph index is updated.
225
+
226
+ ### 6.3 `link`
227
+
228
+ Add one or more directed links from a source record to target records. Idempotent: adding
229
+ an already-existing (source, target, kind) triple is not an error; the link is not duplicated.
230
+
231
+ **Required fields:**
232
+
233
+ | Field | Location | Description |
234
+ |---|---|---|
235
+ | `source_id` | argument | ID of the source record. |
236
+ | `links` | argument | Non-empty array of Link objects with `target_id` and `kind`. |
237
+ | `agent` | evidence | Agent performing the link operation. |
238
+
239
+ **Rejection conditions:**
240
+ - `source_id` does not exist.
241
+ - Any `target_id` in `links` does not exist.
242
+ - `links` array is empty.
243
+ - Missing `agent` in evidence.
244
+
245
+ **Post-conditions:** Each new link is present in `getLinks(source_id)`. Graph index updated.
246
+ Mutation log entry appended to source record.
247
+
248
+ ### 6.4 `propose`
249
+
250
+ Record a proposed change to a concept record. The proposal does not modify the concept;
251
+ it creates a link of kind `"proposes"` from the proposing record to the concept, and
252
+ appends a mutation log entry with the proposal text.
253
+
254
+ **Required fields:**
255
+
256
+ | Field | Location | Description |
257
+ |---|---|---|
258
+ | `concept_id` | argument | ID of the concept record to propose a change to. |
259
+ | `proposer_id` | argument | ID of the record making the proposal. |
260
+ | `proposal` | evidence | Non-empty string describing the proposed change. |
261
+ | `agent` | evidence | Agent submitting the proposal. |
262
+
263
+ **Rejection conditions:**
264
+ - `concept_id` does not exist or is not of type `"concept"`.
265
+ - `proposer_id` does not exist.
266
+ - `proposal` string is empty or missing.
267
+ - Missing `agent` in evidence.
268
+
269
+ **Post-conditions:** A link of kind `"proposes"` from `proposer_id` to `concept_id` is recorded
270
+ in both the record and the graph index. Mutation log entry appended to concept record.
271
+
272
+ ### 6.5 `apply`
273
+
274
+ Apply a pending proposal: update the concept body with the proposed change and mark the
275
+ proposal as applied.
276
+
277
+ **Required fields:**
278
+
279
+ | Field | Location | Description |
280
+ |---|---|---|
281
+ | `concept_id` | argument | ID of the concept being updated. |
282
+ | `proposer_id` | argument | ID of the record whose proposal is being applied. |
283
+ | `new_body` | evidence | The replacement body text for the concept. |
284
+ | `agent` | evidence | Agent applying the proposal. |
285
+ | `rationale` | evidence | Non-empty string explaining why the proposal is accepted. |
286
+
287
+ **Rejection conditions:**
288
+ - `concept_id` does not exist or is not of type `"concept"`.
289
+ - `proposer_id` does not exist.
290
+ - No `"proposes"` link from `proposer_id` to `concept_id` exists.
291
+ - `new_body` is missing or empty.
292
+ - `rationale` is missing or empty.
293
+ - Missing `agent` in evidence.
294
+
295
+ **Post-conditions:** Concept `body` is replaced with `new_body`. `updated_at` refreshed.
296
+ Mutation log entry (op=`"apply"`) appended. The `"proposes"` link MAY remain in the
297
+ graph (it is a historical record); adapters SHOULD NOT silently delete it.
298
+
299
+ ### 6.6 `reject`
300
+
301
+ Reject a pending proposal: record that the proposal was reviewed and declined.
302
+ The concept is not modified.
303
+
304
+ **Required fields:**
305
+
306
+ | Field | Location | Description |
307
+ |---|---|---|
308
+ | `concept_id` | argument | ID of the concept. |
309
+ | `proposer_id` | argument | ID of the proposing record. |
310
+ | `agent` | evidence | Agent rejecting the proposal. |
311
+ | `reason` | evidence | Non-empty string explaining the rejection. |
312
+
313
+ **Rejection conditions:**
314
+ - `concept_id` does not exist or is not of type `"concept"`.
315
+ - `proposer_id` does not exist.
316
+ - No `"proposes"` link from `proposer_id` to `concept_id` exists.
317
+ - `reason` is missing or empty.
318
+ - Missing `agent` in evidence.
319
+
320
+ **Post-conditions:** Concept `body` is unchanged. `updated_at` is NOT changed (concept
321
+ itself was not mutated). Mutation log entry (op=`"reject"`) appended to concept record.
322
+
323
+ ---
324
+
325
+ ## 7. Query Interface
326
+
327
+ The adapter MUST implement:
328
+
329
+ | Method | Signature | Description |
330
+ |---|---|---|
331
+ | `get` | `(id: string) => Record \| null` | Retrieve record by id. Returns null if not found. |
332
+ | `getLinks` | `(id: string) => { forward: Link[], reverse: Link[] }` | Return all links for a record from the graph index. |
333
+ | `listByCategory` | `(category: string, options?: { prefix?: boolean }) => Record[]` | List records by exact category match. If `prefix` is true, match all records whose category starts with the given string. |
334
+ | `listByType` | `(type: RecordType) => Record[]` | List all records of a given type. |
335
+
336
+ ---
337
+
338
+ ## 8. Adapter Contract Summary
339
+
340
+ An adapter is a JavaScript module (ESM) exporting a default class or factory function. The
341
+ constructor / factory accepts `{ storeRoot: string }`. The instance exposes:
342
+
343
+ ```ts
344
+ interface KnowledgeStoreAdapter {
345
+ create(record: CreateInput): Promise<string>; // returns new id
346
+ update(id: string, fields: UpdateFields, evidence: UpdateEvidence): Promise<void>;
347
+ link(sourceId: string, links: Link[], evidence: LinkEvidence): Promise<void>;
348
+ propose(conceptId: string, proposerId: string, evidence: ProposeEvidence): Promise<void>;
349
+ apply(conceptId: string, proposerId: string, evidence: ApplyEvidence): Promise<void>;
350
+ reject(conceptId: string, proposerId: string, evidence: RejectEvidence): Promise<void>;
351
+ get(id: string): Promise<Record | null>;
352
+ getLinks(id: string): Promise<{ forward: Link[]; reverse: Link[] }>;
353
+ listByCategory(category: string, options?: { prefix?: boolean }): Promise<Record[]>;
354
+ listByType(type: "raw" | "compiled" | "concept"): Promise<Record[]>;
355
+ }
356
+ ```
357
+
358
+ All mutation methods MUST throw (or return a rejected Promise) when required evidence is
359
+ missing. The thrown error MUST have a `code` property set to `"MISSING_EVIDENCE"` and a
360
+ human-readable `message`. This enables the contract suite to distinguish enforcement failures
361
+ from unexpected errors.
362
+
363
+ ---
364
+
365
+ ## 9. YAML Frontmatter Convention (Default Adapter)
366
+
367
+ The default adapter serializes each record as a markdown file with YAML frontmatter:
368
+
369
+ ```markdown
370
+ ---
371
+ id: <id>
372
+ type: <raw|compiled|concept>
373
+ title: <title>
374
+ category: <category>
375
+ tags: [<tag>, ...]
376
+ created_at: <ISO-8601>
377
+ updated_at: <ISO-8601>
378
+ provenance:
379
+ agent: <agent>
380
+ session_id: <optional>
381
+ source_ids: [<id>, ...]
382
+ note: <optional>
383
+ links:
384
+ - target_id: <id>
385
+ kind: <kind>
386
+ label: <optional>
387
+ mutation_log:
388
+ - op: <op>
389
+ at: <ISO-8601>
390
+ agent: <agent>
391
+ note: <optional>
392
+ evidence: {}
393
+ ---
394
+
395
+ <body text, may include [[wikilinks]]>
396
+ ```
397
+
398
+ Files are stored as `<store_root>/records/<id>.md`. The graph index lives at
399
+ `<store_root>/graph-index.json`.
400
+
401
+ ---
402
+
403
+ ## Addendum A — Snapshot Record Semantics (S6)
404
+
405
+ ### A.1 Snapshot Type Decision
406
+
407
+ A `snapshot` is a **distinct record type** (not a concept subtype). Rationale: snapshots have
408
+ unique semantics that differ from concept records in three ways:
409
+
410
+ 1. **Topic binding** — a snapshot is always bound to a topic (category or explicit topic string),
411
+ whereas a concept is an independent named idea.
412
+ 2. **Supersedes relationship** — snapshots participate in a directed supersedes chain; concepts do
413
+ not. A snapshot "supersedes" its predecessors, preserving them for provenance while establishing
414
+ the current view.
415
+ 3. **Consolidation lifecycle** — snapshots are produced by the `knowledge.consolidate` flow and
416
+ carry evidence of which compiled records contributed to the latest decisions. Concepts carry
417
+ definitions; snapshots carry bounded decision summaries.
418
+
419
+ Adding `snapshot` as a fourth top-level type is the smallest, clearest extension: it extends the
420
+ `type` discriminant, adds one link kind, and adds one mutation op — all self-contained.
421
+
422
+ ### A.2 Extended Type Discriminant
423
+
424
+ The `type` field on the common envelope (§1.1) now accepts four values:
425
+
426
+ | Type | Description |
427
+ |---|---|
428
+ | `"raw"` | Unprocessed source material (§1.2). |
429
+ | `"compiled"` | Normalized, editor-reviewed distillation (§1.3). |
430
+ | `"concept"` | Named idea, term, or principle (§1.4). |
431
+ | `"snapshot"` | Bounded decision summary for a topic (§A.3). |
432
+
433
+ ### A.3 `snapshot` Record
434
+
435
+ A snapshot record holds the current consolidated decisions for a topic. It is produced and
436
+ updated only through the `knowledge.consolidate` flow.
437
+
438
+ - `body`: structured markdown summarising the latest known decisions, open items, and context for
439
+ the topic.
440
+ - `topic` (in `provenance.note` or a dedicated field — default adapter stores it in `tags[0]`
441
+ prefixed `topic:`) — the topic string this snapshot covers. Must be non-empty.
442
+ - Must link (via `links`) to the compiled records that contributed to the latest consolidation
443
+ using link kind `"source"`.
444
+ - May link to superseded predecessor snapshots using link kind `"supersedes"`.
445
+ - Carries `provenance.source_ids` referencing every compiled record that contributed to the
446
+ current body.
447
+
448
+ ### A.4 Extended Link Kinds
449
+
450
+ | Kind | Direction | Meaning |
451
+ |---|---|---|
452
+ | `"source"` | compiled → raw | (existing) |
453
+ | `"example"` | concept → compiled | (existing) |
454
+ | `"related"` | any → any | (existing) |
455
+ | `"refines"` | compiled → compiled | (existing) |
456
+ | `"proposes"` | any → concept | (existing) |
457
+ | `"supersedes"` | snapshot → snapshot | New snapshot supersedes an older snapshot for the same topic. |
458
+
459
+ ### A.5 `supersede` Mutation Operation
460
+
461
+ The `supersede` op marks one or more existing records as superseded by a newer record.
462
+ It NEVER deletes records. Superseded records remain fully queryable with their provenance intact.
463
+
464
+ **Required fields:**
465
+
466
+ | Field | Location | Description |
467
+ |---|---|---|
468
+ | `new_id` | argument | ID of the record that supersedes. |
469
+ | `superseded_ids` | argument | Non-empty array of IDs that are being superseded. |
470
+ | `agent` | evidence | Agent performing the supersede operation. |
471
+ | `rationale` | evidence | Non-empty string explaining why the new record supersedes the old ones. |
472
+
473
+ **Rejection conditions:**
474
+ - `new_id` does not exist.
475
+ - `superseded_ids` is empty.
476
+ - Any id in `superseded_ids` does not exist.
477
+ - Missing `agent` in evidence.
478
+ - Missing or empty `rationale` in evidence.
479
+
480
+ **Post-conditions:**
481
+ - For each id in `superseded_ids`, a link of kind `"supersedes"` from `new_id` to that id is added
482
+ to the graph index.
483
+ - A mutation log entry (op=`"supersede"`) is appended to the record at `new_id`.
484
+ - A mutation log entry (op=`"superseded-by"`) is appended to each record in `superseded_ids`,
485
+ recording which record supersedes them (`new_id`).
486
+ - The superseded records are NOT deleted. Their `body`, `links`, and provenance remain intact.
487
+ - After `supersede`, a `get(superseded_id)` MUST still return the full record.
488
+ - Superseded records can be discovered via `getLinks(superseded_id).reverse` — they will have a
489
+ reverse link of kind `"supersedes"` pointing from `new_id`.
490
+
491
+ **Supersede-not-delete invariant:**
492
+ > Calling `supersede` MUST NOT remove any record. No mutation op in this contract deletes records.
493
+ > This is a hard invariant; adapters MUST enforce it. A retention policy hook for physical archival
494
+ > is a future concern and is explicitly out of scope for this version.
495
+
496
+ ### A.6 Adapter Contract Extension
497
+
498
+ The adapter interface (§8) is extended with one method:
499
+
500
+ ```ts
501
+ interface KnowledgeStoreAdapter {
502
+ // ... existing methods ...
503
+ supersede(newId: string, supersededIds: string[], evidence: SupersedeEvidence): Promise<void>;
504
+ listByType(type: "raw" | "compiled" | "concept" | "snapshot"): Promise<Record[]>;
505
+ }
506
+ ```
507
+
508
+ `SupersedeEvidence`:
509
+ ```ts
510
+ interface SupersedeEvidence {
511
+ agent: string;
512
+ rationale: string;
513
+ note?: string;
514
+ }
515
+ ```
516
+
517
+ ### A.7 Snapshot Queryability Guarantee
518
+
519
+ Superseded snapshots MUST remain queryable:
520
+ - `get(id)` returns the full record (body, links, provenance, mutation_log).
521
+ - `listByType("snapshot")` returns ALL snapshots, including superseded ones.
522
+ - The caller can determine supersession status by inspecting `getLinks(id).reverse` for entries
523
+ with `kind === "supersedes"`.
524
+
525
+ There is no `"archived"` or `"deleted"` status field; the supersession chain is expressed entirely
526
+ through links and mutation log entries.