@kontourai/flow-agents 3.0.0 → 3.2.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 (158) hide show
  1. package/.github/workflows/ci.yml +12 -0
  2. package/CHANGELOG.md +32 -0
  3. package/CONTEXT.md +67 -1
  4. package/README.md +4 -0
  5. package/build/src/cli/assignment-provider.d.ts +188 -0
  6. package/build/src/cli/assignment-provider.js +193 -51
  7. package/build/src/cli/workflow-sidecar.d.ts +16 -6
  8. package/build/src/cli/workflow-sidecar.js +492 -45
  9. package/build/src/lib/flow-resolver.d.ts +12 -6
  10. package/build/src/lib/flow-resolver.js +30 -14
  11. package/build/src/tools/validate-source-tree.js +2 -1
  12. package/context/contracts/assignment-provider-contract.md +10 -1
  13. package/context/contracts/execution-contract.md +78 -0
  14. package/context/contracts/probe-docs-write-contract.md +187 -0
  15. package/context/scripts/hooks/config-protection.js +24 -4
  16. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  17. package/context/scripts/hooks/stop-goal-fit.js +262 -5
  18. package/context/scripts/hooks/workflow-steering.js +42 -0
  19. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  20. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  21. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  22. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  23. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  24. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  25. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  26. package/docs/adr/0007-skill-audit.md +2 -0
  27. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  28. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  29. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  30. package/docs/adr/0011-mcp-posture.md +2 -0
  31. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  32. package/docs/adr/0013-context-lifecycle.md +2 -0
  33. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  34. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  35. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  36. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  37. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  38. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  39. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  40. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  41. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +113 -0
  42. package/docs/adr/README.md +49 -0
  43. package/docs/adr/index.md +34 -0
  44. package/docs/decisions/agent-coordination.md +20 -0
  45. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  46. package/docs/decisions/context-lifecycle.md +18 -0
  47. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  48. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  49. package/docs/decisions/flow-kit.md +20 -0
  50. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  51. package/docs/decisions/graph-knowledge-provider.md +63 -0
  52. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  53. package/docs/decisions/index.md +19 -0
  54. package/docs/decisions/kit-dependency-ownership.md +18 -0
  55. package/docs/decisions/kit-operation-boundary.md +18 -0
  56. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  57. package/docs/decisions/kontour-resource-contract.md +18 -0
  58. package/docs/decisions/mcp-posture.md +18 -0
  59. package/docs/decisions/three-hard-boundary-model.md +18 -0
  60. package/docs/decisions/trust-reconcile.md +20 -0
  61. package/docs/decisions/typescript-source-policy.md +48 -0
  62. package/docs/decisions/workflow-enforcement.md +18 -0
  63. package/docs/decisions/workflow-trust-state.md +20 -0
  64. package/docs/fixture-ownership.md +1 -1
  65. package/docs/workflow-usage-guide.md +1 -1
  66. package/evals/ci/run-baseline.sh +6 -0
  67. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  68. package/evals/integration/test_checkpoint_signing.sh +4 -3
  69. package/evals/integration/test_current_json_per_actor.sh +516 -0
  70. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  71. package/evals/integration/test_gate_lockdown.sh +46 -0
  72. package/evals/integration/test_model_routing_escalation.sh +145 -0
  73. package/evals/integration/test_publish_delivery.sh +14 -6
  74. package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
  75. package/evals/integration/test_stop_hook_release.sh +552 -0
  76. package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
  77. package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
  78. package/evals/run.sh +10 -0
  79. package/evals/static/test_knowledge_providers.sh +13 -4
  80. package/evals/static/test_model_routing_hints.sh +107 -0
  81. package/evals/static/test_workflow_skills.sh +15 -2
  82. package/kits/builder/skills/builder-shape/SKILL.md +10 -0
  83. package/kits/builder/skills/deliver/SKILL.md +69 -11
  84. package/kits/builder/skills/design-probe/SKILL.md +47 -0
  85. package/kits/builder/skills/execute-plan/SKILL.md +13 -0
  86. package/kits/builder/skills/fix-bug/SKILL.md +17 -0
  87. package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
  88. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  89. package/kits/builder/skills/plan-work/SKILL.md +9 -0
  90. package/kits/builder/skills/pull-work/SKILL.md +10 -0
  91. package/kits/builder/skills/review-work/SKILL.md +11 -0
  92. package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
  93. package/kits/builder/skills/verify-work/SKILL.md +11 -0
  94. package/kits/knowledge/adapters/default-store/index.js +147 -18
  95. package/kits/knowledge/adapters/flow-runner/index.js +912 -16
  96. package/kits/knowledge/adapters/obsidian-store/index.js +97 -14
  97. package/kits/knowledge/adapters/shared/codec.js +265 -0
  98. package/kits/knowledge/docs/README.md +121 -2
  99. package/kits/knowledge/docs/store-contract.md +517 -7
  100. package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
  101. package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
  102. package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
  103. package/kits/knowledge/evals/contract-suite/suite.test.js +212 -0
  104. package/kits/knowledge/evals/freshness/suite.test.js +339 -0
  105. package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
  106. package/kits/knowledge/evals/retirement/suite.test.js +1 -1
  107. package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
  108. package/kits/knowledge/flows/promote.flow.json +84 -0
  109. package/kits/knowledge/kit.json +15 -0
  110. package/kits/knowledge/promote/distill.js +96 -0
  111. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  112. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  113. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  114. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  115. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  116. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  117. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  118. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  119. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  120. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  121. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  122. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  123. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  124. package/kits/knowledge/promote/health.js +137 -0
  125. package/kits/knowledge/promote/index.js +176 -0
  126. package/kits/knowledge/promote/ingest.js +103 -0
  127. package/kits/knowledge/promote/lib.js +132 -0
  128. package/kits/knowledge/promote/link.js +84 -0
  129. package/kits/knowledge/promote/promote.test.js +174 -0
  130. package/kits/knowledge/providers/conformance/suite.test.js +18 -0
  131. package/kits/knowledge/providers/index.js +1 -0
  132. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  133. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  134. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  135. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  136. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  137. package/kits/knowledge/providers/neo4j/index.js +280 -0
  138. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  139. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  140. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  141. package/package.json +4 -2
  142. package/schemas/workflow-handoff.schema.json +6 -0
  143. package/scripts/README.md +1 -0
  144. package/scripts/ci/mint-attestation.js +33 -6
  145. package/scripts/ci/trust-reconcile.js +144 -26
  146. package/scripts/freeze-adrs.mjs +364 -0
  147. package/scripts/hooks/config-protection.js +24 -4
  148. package/scripts/hooks/evidence-capture.js +4 -1
  149. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  150. package/scripts/hooks/lib/current-pointer.js +123 -0
  151. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  152. package/scripts/hooks/stop-goal-fit.js +262 -5
  153. package/scripts/hooks/workflow-steering.js +42 -0
  154. package/scripts/statusline/flow-agents-statusline.js +3 -1
  155. package/src/cli/assignment-provider.ts +239 -59
  156. package/src/cli/workflow-sidecar.ts +529 -43
  157. package/src/lib/flow-resolver.ts +35 -14
  158. package/src/tools/validate-source-tree.ts +2 -1
@@ -26,10 +26,15 @@ The store holds three record types. Every record, regardless of type, shares a c
26
26
  | `body` | string | yes | Primary content. Format is type-specific (see below). |
27
27
  | `category` | string | yes | Dot-separated hierarchical category string, e.g. `"engineering.api"`. Must be non-empty. |
28
28
  | `tags` | string[] | no | Flat list of tag strings for secondary classification. |
29
+ | `aliases` | string[] | no | Human-readable, category-scoped slug aliases resolvable via `get`/`getLinks` (Addendum H). Append-only; never changes `id`. |
29
30
  | `links` | Link[] | no | Outbound links from this record (see §2). |
30
31
  | `provenance` | Provenance | yes | Immutable creation provenance (see §4). |
31
32
  | `created_at` | ISO-8601 string | yes | Creation timestamp in UTC. |
32
33
  | `updated_at` | ISO-8601 string | yes | Last mutation timestamp in UTC. |
34
+ | `expires_at` | ISO-8601 string | no | Optional record-carried expiry (Addendum J). When now is at or past it, the record derives to `stale`. Never silently mutates the record. |
35
+ | `ttl_seconds` | number | no | Optional relative expiry in seconds from `created_at` (Addendum J). `expires_at`, when present, takes precedence. |
36
+
37
+ Records **without** `expires_at` / `ttl_seconds` behave exactly as before this addendum — they are never derived `stale` via the expiry path.
33
38
 
34
39
  ### 1.2 `raw` Record
35
40
 
@@ -342,11 +347,14 @@ The adapter MUST implement:
342
347
 
343
348
  | Method | Signature | Description |
344
349
  |---|---|---|
345
- | `get` | `(id: string) => Record \| null` | Retrieve record by id. Returns null if not found. |
346
- | `getLinks` | `(id: string) => { forward: Link[], reverse: Link[] }` | Return all links for a record from the graph index. |
350
+ | `get` | `(idOrHandle: string) => Record \| null` | Retrieve a record. `idOrHandle` may be a full id, a slug alias, or an unambiguous id prefix (Addendum H). Returns null if it resolves to nothing; throws `AMBIGUOUS_ID` on an ambiguous prefix. |
351
+ | `getLinks` | `(idOrHandle: string) => { forward: Link[], reverse: Link[] }` | Return all links for a record from the graph index. Accepts the same handle forms as `get`; an unresolved handle yields empty arrays, an ambiguous prefix throws `AMBIGUOUS_ID`. |
347
352
  | `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. |
348
353
  | `listByType` | `(type: RecordType) => Record[]` | List all records of a given type. |
349
354
 
355
+ > **Identity resolution** (`get`/`getLinks`) is specified in full in **Addendum H**. The `prefix`
356
+ > option on `listByCategory` is unrelated — it filters by *category* prefix, not by id.
357
+
350
358
  ---
351
359
 
352
360
  ## 8. Adapter Contract Summary
@@ -374,6 +382,15 @@ missing. The thrown error MUST have a `code` property set to `"MISSING_EVIDENCE"
374
382
  human-readable `message`. This enables the contract suite to distinguish enforcement failures
375
383
  from unexpected errors.
376
384
 
385
+ ### 8.1 Error Codes
386
+
387
+ | `code` | Raised by | Meaning |
388
+ |---|---|---|
389
+ | `MISSING_EVIDENCE` | any mutation op; `create`/`update` when an `aliases` entry is malformed | A required evidence/provenance field is missing, or supplied input is invalid. |
390
+ | `NOT_FOUND` | `link` (unknown `target_id`) and other ops that require an existing target | A referenced record id does not exist. |
391
+ | `AMBIGUOUS_ID` | `get`, `getLinks` | An id prefix matched more than one record (Addendum H). The error carries a `matches` array of the colliding ids. MUST NOT be swallowed into a `null` return. |
392
+ | `SLUG_CONFLICT` | `create`, `update` | A supplied slug alias is already assigned to a *different* record. |
393
+
377
394
  ---
378
395
 
379
396
  ## 9. YAML Frontmatter Convention (Default Adapter)
@@ -387,6 +404,7 @@ type: <raw|compiled|concept>
387
404
  title: <title>
388
405
  category: <category>
389
406
  tags: [<tag>, ...]
407
+ aliases: [<slug>, ...] # optional — human-readable, category-scoped (Addendum H)
390
408
  created_at: <ISO-8601>
391
409
  updated_at: <ISO-8601>
392
410
  provenance:
@@ -409,8 +427,9 @@ mutation_log:
409
427
  <body text, may include [[wikilinks]]>
410
428
  ```
411
429
 
412
- Files are stored as `<store_root>/records/<id>.md`. The graph index lives at
413
- `<store_root>/graph-index.json`.
430
+ Files are stored as `<store_root>/records/<id>.md` keyed by the full `id`, unchanged by this
431
+ addendum. The graph index lives at `<store_root>/graph-index.json`; the slug alias map (Addendum H)
432
+ lives at `<store_root>/alias-index.json`.
414
433
 
415
434
  ---
416
435
 
@@ -832,9 +851,17 @@ interface FreshnessFlag {
832
851
  ```
833
852
 
834
853
  `auditFreshness` returns `{ audited, skipped, flags, telemetryEvents }` where `audited` counts
835
- records that had a resolvable threshold, `skipped` counts opt-out categories, and `flags` lists the
836
- stale records. Gate telemetry is emitted at `collect-gate` and `flag-gate`
837
- (`knowledge.audit-freshness`).
854
+ records that had a resolvable threshold **or a record-carried expiry**, `skipped` counts opt-out
855
+ categories (no threshold *and* no expiry), and `flags` lists the stale records. Gate telemetry is
856
+ emitted at `collect-gate` and `flag-gate` (`knowledge.audit-freshness`).
857
+
858
+ **Extension (#341, Addendum J).** A record that carries its own `expires_at` / `ttl_seconds` is
859
+ flagged when now is at or past that expiry — **without** any caller-supplied threshold. Such an
860
+ expiry flag cites the expiry it fired on: it carries `reason: "expiry"`, `expiresAt` (the effective
861
+ expiry, ISO-8601), `matchedThresholdKey: "expires_at"`, and `thresholdDays: null` (an expiry cites a
862
+ timestamp, not a day-count). Threshold-driven flags (Addendum D above) carry `reason: "threshold"`
863
+ and are otherwise unchanged. The expiry path takes precedence over the threshold path for a record
864
+ that is past both.
838
865
 
839
866
  ## Addendum E — Category Canonicalization (Hygiene #4, #106)
840
867
 
@@ -1034,3 +1061,486 @@ interface ContradictionFlag {
1034
1061
  counts the in-scope compiled records, `compared` counts the similar pairs the contradiction fn
1035
1062
  judged, and `flags` lists the conflicting pairs. Gate telemetry is emitted at `collect-gate` and
1036
1063
  `flag-gate` (`knowledge.detect-contradictions`).
1064
+
1065
+ ## Addendum H — Record Identity Resolution (short-id prefix + slug aliases, #339)
1066
+
1067
+ ### H.1 Motivation
1068
+
1069
+ On-disk identity is a full `id` (§1.1) plus a file path (§9). Prose that cites a record — a
1070
+ curated doc, a commit message, an issue comment — cannot practically carry a full 36-char UUID, so
1071
+ the field partner cites **8-char short ids** (`12cc5573`, `f5df7b4c`) and would prefer
1072
+ **human-readable slugs**. Neither resolves against exact-id `get`, and a store restructure that
1073
+ re-files a record silently severs every inbound short-id/slug reference.
1074
+
1075
+ This addendum adds a **resolution layer** over the unchanged on-disk identity. It changes nothing
1076
+ about how records are stored (H.6) — it only widens what `get`/`getLinks` accept as input.
1077
+
1078
+ ### H.2 Resolvable Handle Forms
1079
+
1080
+ `get(handle)` and `getLinks(handle)` MUST resolve, in this precedence order:
1081
+
1082
+ 1. **Exact full id** — `handle` equals a stored record `id`. (Unchanged §7 behavior; the O(1)
1083
+ hot path.)
1084
+ 2. **Slug alias** — `handle` is a registered alias in the alias map (H.4) whose target still exists.
1085
+ 3. **Unambiguous id prefix** — `handle` is at least `MIN_ID_PREFIX` (**8**) characters and is a
1086
+ prefix of **exactly one** record `id`.
1087
+
1088
+ The first form that matches wins. A `handle` that matches none of the three resolves to *nothing*.
1089
+
1090
+ ### H.3 Resolution Outcomes
1091
+
1092
+ | Situation | `get` | `getLinks` |
1093
+ |---|---|---|
1094
+ | Resolves to exactly one record | returns that record | returns that record's `{ forward, reverse }` |
1095
+ | Resolves to nothing (no exact id, no alias, no ≥8-char single-match prefix) | returns `null` | returns `{ forward: [], reverse: [] }` |
1096
+ | Prefix (≥8 chars) matches **more than one** record | **throws `AMBIGUOUS_ID`** | **throws `AMBIGUOUS_ID`** |
1097
+
1098
+ An `AMBIGUOUS_ID` error MUST have `code === "AMBIGUOUS_ID"`, a human-readable `message`, and a
1099
+ `matches` array of the colliding full ids. An ambiguous prefix MUST NOT be silently resolved to one
1100
+ arbitrary record, nor collapsed to `null` — ambiguity is a distinct, surfaced condition.
1101
+
1102
+ A `handle` **shorter than `MIN_ID_PREFIX`** is never treated as a prefix (it can still match an
1103
+ exact id or a slug). This keeps a stray short token from matching a large, surprising set.
1104
+
1105
+ ### H.4 Slug Aliases
1106
+
1107
+ - A slug is a lowercase, category-scoped, human-readable handle, e.g.
1108
+ `decision.strategy/2026-07-03-gtm-direction`. It MUST match
1109
+ `^[a-z0-9]([a-z0-9._/-]*[a-z0-9])?$` (≤ 200 chars). Malformed slugs are rejected at
1110
+ `create`/`update` with `MISSING_EVIDENCE`.
1111
+ - Slugs are **caller-supplied** via the optional `aliases: string[]` field on `create` and
1112
+ `update`. The store does **not** auto-derive slugs (auto-slugging is a future concern) — the
1113
+ date-stamped examples cannot be derived from title alone.
1114
+ - Aliases are **append-only**: `update` with an `aliases` field UNIONs the supplied slugs with the
1115
+ record's existing aliases; it never drops a previously issued slug. This is what guarantees H.5.
1116
+ - A slug already assigned to a *different* record is a `SLUG_CONFLICT` (a slug identifies at most
1117
+ one record). Re-supplying a record's own slug is an idempotent no-op.
1118
+
1119
+ ### H.5 Restructure Survival (the alias map)
1120
+
1121
+ The alias map is a store-level structure `{ slug → id }`. It is keyed by **full id** — never by
1122
+ category or file path. Therefore:
1123
+
1124
+ - Recategorizing a record via `update` (which, in the Obsidian adapter, **relocates the note's
1125
+ file**) leaves both the `id` and the `slug → id` mapping untouched.
1126
+ - The id still resolves (it is the record's stable key); the short-id prefix still resolves (it is
1127
+ a prefix of that unchanged id); the slug still resolves (the map still points it at that id).
1128
+
1129
+ So a previously issued short-id prefix or slug MUST still resolve to the same record after a
1130
+ restructure. Note the slug is a **stable alias**, not a value recomputed from the current category:
1131
+ a slug minted under `decision.strategy/…` keeps resolving even after the record moves to
1132
+ `decision.gtm`.
1133
+
1134
+ Like the graph index (§5.2), the alias map is a **derived cache** — each record's own `aliases`
1135
+ array is the source of truth, so an adapter that persists the map SHOULD rebuild it on `reindex()`.
1136
+ The default adapter persists it at `<store_root>/alias-index.json`
1137
+ (`{ schema_version, by_slug: { <slug>: <id> } }`).
1138
+
1139
+ ### H.6 On-Disk Identity Is Unchanged (non-goals)
1140
+
1141
+ - The `id` in the §1.1 envelope and the `records/<id>.md` file naming (§9) are **unchanged**.
1142
+ Aliases are a resolution layer, not a rename.
1143
+ - Exact-id `get`/`getLinks` behavior is byte-for-byte unchanged.
1144
+ - This addendum does **not** migrate existing stores or repair already-broken citations — an
1145
+ adapter/repo may seed its own alias map for legacy short ids, but that is out of scope here.
1146
+ - Full-text/semantic search is out of scope.
1147
+
1148
+ ### H.7 Resolution Constants
1149
+
1150
+ | Constant | Value | Meaning |
1151
+ |---|---|---|
1152
+ | `MIN_ID_PREFIX` | `8` | Minimum handle length eligible for prefix resolution. |
1153
+ | slug pattern | `^[a-z0-9]([a-z0-9._/-]*[a-z0-9])?$` | Valid slug alias shape (≤ 200 chars). |
1154
+
1155
+ An independent adapter conforms to this addendum when the `identity: *` sections of the contract
1156
+ suite (`evals/contract-suite/suite.test.js`, §15–§17) pass against it.
1157
+
1158
+ ---
1159
+
1160
+ ## Addendum I — Inbound-Reference Integrity (doc→store citations, #340)
1161
+
1162
+ ### I.1 Motivation
1163
+
1164
+ The store's graph (§2, §5) and the hygiene audits (Addenda D–G) cover record→record links and
1165
+ survey **records**. Nothing resolves the references a human actually reads: the record ids that
1166
+ curated docs — `NOW.md`, `strategy/*.md`, a roadmap — cite **into** the store. A store restructure
1167
+ that re-files or retires a record silently severs every inbound doc citation, and every existing
1168
+ gate still reports PASS. Field evidence (design partner `kontourai/ops`): `verify-ops` stayed green
1169
+ for weeks while `NOW.md` and `strategy/vision.md` cited unresolvable short-ids (`12cc5573`,
1170
+ `f5df7b4c`). This addendum makes that rot **fail closed**.
1171
+
1172
+ The check is a **read-only** flow (`KnowledgeFlowRunner.checkInboundReferences`) in the style of the
1173
+ Addenda D–G audits: it mutates no record, appends no mutation-log entry, and forks no mutation path.
1174
+ It resolves citations through the unchanged Addendum H identity path — it adds no new query surface.
1175
+
1176
+ ### I.2 Options
1177
+
1178
+ | Option | Type | Default | Meaning |
1179
+ |---|---|---|---|
1180
+ | `docGlobs` | string[] | `[]` | Globs (relative to `docsRoot`) whose matching docs are scanned. **Empty = opt-in no-op pass** (Addenda D–G convention). Supports `*` (within a segment), `**` (across segments), `?`. |
1181
+ | `docsRoot` | string | `workspace`, then `cwd` | Root the globs resolve against (the docs live outside the store). |
1182
+ | `markers` | string[] | `["rec:", "record:"]` | Citation-marker prefixes that opt a following short-id/slug into **definite** citation status (I.3). |
1183
+
1184
+ ### I.3 Citation Forms — the commit-SHA precision problem
1185
+
1186
+ A bare 8-hex token is inherently ambiguous: `12cc5573` is equally a record short-id and an
1187
+ abbreviated git commit SHA. Failing on every non-resolving 8-hex token would flag every SHA in prose
1188
+ (the gate cries wolf and gets muted); never failing on them re-opens the exact rot this closes. The
1189
+ check resolves the tension with **two tiers**:
1190
+
1191
+ **Definite citations** — unmistakably a record reference, so resolved regardless of whether they
1192
+ currently resolve, and an unresolved one **FAILS** (fail closed):
1193
+
1194
+ | Form | Example | Why it is SHA-safe |
1195
+ |---|---|---|
1196
+ | Full UUID (8-4-4-4-12 hex) | `aaaaaaaa-1111-4111-8111-111111111111` | The hyphenated shape never collides with a git SHA (40/abbrev hex, no hyphens) or prose. |
1197
+ | Marker + token | `rec:12cc5573`, `record:decision.strategy/gtm` | The marker is explicit intent; the token may be a short-id or slug. |
1198
+ | Wikilink | `[[decision.strategy/gtm]]` | The kit's own link syntax. |
1199
+
1200
+ **Candidate bare short-ids** — a standalone ≥8-hex token with **no** citation form is included in
1201
+ the index **only when it already resolves** to a record. A non-resolving bare hex is indistinguishable
1202
+ from a commit SHA, so it is **ignored** — never indexed, never failed.
1203
+
1204
+ > **Deliberate, documented miss.** A *broken* bare-hex short-id (no marker/wikilink) is invisible to
1205
+ > the check — by construction, since it does not resolve. This is a precision-over-recall choice:
1206
+ > **zero false positives on commit hashes**, at the cost of not catching bare-hex short-id rot until
1207
+ > the doc adopts a citation form. To get fail-closed protection for a short-id or slug, cite it in a
1208
+ > marker or wikilink form; full-UUID citations are protected with zero configuration. Migrating a
1209
+ > consuming repo's house style to a marker is an ops-repo change (a non-goal here).
1210
+
1211
+ Every extracted token is resolved via `get(token)` (Addendum H: exact id → slug alias → unambiguous
1212
+ ≥8-char prefix). `null` → unresolved; an `AMBIGUOUS_ID` throw is caught and reported as an unresolved
1213
+ citation with `reason: "ambiguous"` (fail closed — an ambiguous citation does not uniquely resolve).
1214
+
1215
+ ### I.4 Fail-Closed Semantics
1216
+
1217
+ The result's `ok` is `true` **iff** `unresolved` is empty — i.e. every definite citation resolved. A
1218
+ caller (e.g. ops `verify-ops`) treats `ok === false` as a gate failure. Each `unresolved` entry names
1219
+ `doc`, `line`, `column`, `token`, `form`, and `reason` so the break is actionable. An empty scan with
1220
+ configured globs that cite nothing is a pass; an empty scan from **no** globs is the explicit opt-in
1221
+ no-op — neither is an "empty success" that masks an unresolved definite citation.
1222
+
1223
+ ### I.5 Citation Index (result shape)
1224
+
1225
+ The result exposes the full citation index so downstream flows (supersede/retire propagation) can
1226
+ enumerate citers:
1227
+
1228
+ ```json
1229
+ {
1230
+ "ok": true,
1231
+ "scanned": ["NOW.md", "strategy/vision.md"],
1232
+ "citations": [
1233
+ { "doc": "NOW.md", "line": 3, "column": 17, "token": "aaaaaaaa-1111-4111-8111-111111111111",
1234
+ "form": "uuid", "resolved": true, "recordId": "aaaaaaaa-1111-4111-8111-111111111111" }
1235
+ ],
1236
+ "unresolved": [
1237
+ { "doc": "NOW.md", "line": 5, "column": 9, "token": "deadbeef-0000-4000-8000-000000000000",
1238
+ "form": "uuid", "reason": "not-found" }
1239
+ ],
1240
+ "byDoc": { "NOW.md": [ { "token": "…", "form": "uuid", "resolved": true, "recordId": "…", "line": 3, "column": 17 } ] },
1241
+ "byRecord": { "aaaaaaaa-1111-4111-8111-111111111111": [ { "doc": "NOW.md", "line": 3, "column": 17, "token": "…", "form": "uuid" } ] }
1242
+ }
1243
+ ```
1244
+
1245
+ - `citations` — every recorded citation (definite ones, plus bare candidates that resolved).
1246
+ - `byDoc` — doc → its cited record ids (every scanned doc appears, even if it cites nothing).
1247
+ - `byRecord` — record id → its citers (the doc→record edge; a natural typed edge under #317).
1248
+
1249
+ ### I.6 Conformance
1250
+
1251
+ An adapter conforms to this addendum when the inbound-reference suite
1252
+ (`evals/inbound-references/suite.test.js`) passes against it — AC1 (three forms extracted +
1253
+ resolved), AC2 (fail closed on a nonexistent id; no-op pass with no globs), AC3 (read-only), and the
1254
+ commit-SHA-safety case. The suite is parameterized by `KNOWLEDGE_ADAPTER`, so conformance is required
1255
+ of every adapter (AC4). Because the check reuses `get`, any Addendum-H-conforming adapter satisfies it
1256
+ without new storage.
1257
+
1258
+ ---
1259
+
1260
+ ## Addendum J — Record Freshness & Status Semantics (Hachure alignment, #341)
1261
+
1262
+ ### J.1 Motivation — no silent rot for our own knowledge
1263
+
1264
+ The store is strong at *filing* and, since Addendum D, at *auditing* staleness — but staleness lived
1265
+ entirely in the auditor's call: thresholds are supplied per invocation, so freshness intent lived in
1266
+ whoever remembered to pass the right options, not on the record. A record could not say "I go stale
1267
+ on 2026-09-01"; the only freshness mechanism for a long-lived store was a human remembering to run an
1268
+ audit with the right numbers (field evidence: the ops design partner's radar section carried a
1269
+ `prune monthly` *comment* as its sole freshness mechanism). Knowledge rots silently between audits.
1270
+
1271
+ Meanwhile Kontour already owns a trust vocabulary for exactly this — **Hachure**: `expiresAt` /
1272
+ `ttlSeconds`, statuses `unknown / proposed / verified / stale / superseded`, and the "no silent rot"
1273
+ principle. This addendum makes the knowledge store **eat that vocabulary**: records carry their own
1274
+ expiry and go *visibly* stale under the same semantics our trust bundles use. It **aligns** with
1275
+ Hachure's vocabulary and semantics — it does **not** import Hachure's schema or take a format
1276
+ dependency (a non-goal). Ephemeris integration and automatic re-verification / auto-retirement are
1277
+ also non-goals: this addendum **flags**; the operator routes a flag through `knowledge.retire` or a
1278
+ fresh capture/compile, exactly as Addendum D established.
1279
+
1280
+ ### J.2 Record-carried freshness fields
1281
+
1282
+ Two **optional** envelope fields (§1.1), round-tripped by every adapter through the shared codec:
1283
+
1284
+ | Field | Type | Meaning |
1285
+ |---|---|---|
1286
+ | `expires_at` | ISO-8601 string | Absolute instant the record expires. |
1287
+ | `ttl_seconds` | number (> 0) | Relative expiry: `created_at + ttl_seconds`. |
1288
+
1289
+ - Both are settable at `create` and via `update` (they are mutable fields; `update` records them in
1290
+ its mutation-log `fields` evidence like any other). Supplying either as `null` / `""` on `update`
1291
+ **clears** it.
1292
+ - A malformed value (non-ISO `expires_at`, non-positive `ttl_seconds`) is rejected at the mutation
1293
+ boundary with `error.code === "MISSING_EVIDENCE"` — bad freshness never reaches disk.
1294
+ - **Effective expiry** = `expires_at` when present, else `created_at + ttl_seconds`. An explicit
1295
+ `expires_at` therefore **takes precedence** over `ttl_seconds`.
1296
+ - **Backward compatibility (R5):** a record carrying neither field has `null` effective expiry and is
1297
+ never derived `stale` via the expiry path — it behaves exactly as before this addendum.
1298
+
1299
+ ### J.3 Derived states — `stale` and `superseded`
1300
+
1301
+ `stale` and `superseded` are **derived**, never stored. They are computed on read/query and **never**
1302
+ mutate the record's `status` (Addendum B) or any other field. "No silent rot" means a record becomes
1303
+ *visibly* stale under query — not that the store silently rewrites it.
1304
+
1305
+ - **`stale`** — the record's effective expiry exists and now is **at or past** it. The boundary is
1306
+ **inclusive**: a record `expires_at: T` is stale from `T` onward (`now >= T`). A record may also be
1307
+ surfaced as stale by `auditFreshness` (Addendum D + J.5). Derivation never changes the record.
1308
+ - **`superseded`** — the record is the target of an Addendum A.5 `supersede` relationship: it carries
1309
+ a `superseded-by` mutation-log entry, equivalently an incoming `supersedes` edge in the graph
1310
+ (`getLinks(id).reverse`). This is the cross-adapter query surface for supersession — it works
1311
+ identically whether the adapter keeps superseded records in place (default) or archives them
1312
+ (obsidian), because both maintain the graph. Supersession is a **derived state**, not a `status`
1313
+ value; `supersede` remains the only op that establishes it.
1314
+
1315
+ ### J.4 Hachure vocabulary alignment
1316
+
1317
+ The kit keeps **two orthogonal axes**, and aligns each to Hachure's vocabulary without forking a
1318
+ second lifecycle:
1319
+
1320
+ 1. **Lifecycle status** (stored; mutated only by `retire`, Addendum B): `active` / `implemented` /
1321
+ `retired`.
1322
+ 2. **Freshness/derivation state** (derived; never stored): `fresh` / `stale` / `superseded`.
1323
+
1324
+ | Hachure term | Kit representation | Axis | Derived from |
1325
+ |---|---|---|---|
1326
+ | `verified` | `active` and not past expiry ("fresh") | lifecycle + freshness | `status === "active"` **and** not stale |
1327
+ | `stale` | `stale` | freshness (derived) | now ≥ effective expiry (`expires_at`, or `created_at + ttl_seconds`), or an `auditFreshness` flag |
1328
+ | `superseded` | `superseded` | freshness (derived) | incoming `supersedes` edge / `superseded-by` log entry (Addendum A.5) |
1329
+ | `proposed` | open proposal | flow state | a pending `proposes` link (Addenda B.7 / D) — not a record status |
1330
+ | `unknown` | *(not modelled)* | — | the kit has no "unknown" state; every record is at least `active` |
1331
+
1332
+ `retired` (kit) has no Hachure freshness equivalent — it is a **terminal lifecycle decision**,
1333
+ orthogonal to freshness. Retired records are excluded from the default working set (Addendum B.3) and
1334
+ so from stale-filtered listings, but remain fully queryable via `includeRetired` and `get`.
1335
+
1336
+ ### J.5 Query surface
1337
+
1338
+ - **`listByType(type, { stale: true, now? })`** and **`listByCategory(category, { stale: true, now?, prefix?, includeRetired? })`** return only records whose effective expiry is at or past `now` (default `Date.now()`; `now` is injectable for deterministic tests). Absent `stale` → unchanged
1339
+ behaviour. The staleness filter composes with the existing status filter (retired still excluded by
1340
+ default).
1341
+ - **`auditFreshness`** (Addendum D.2/D.3) additionally flags a record past its **own** `expires_at`
1342
+ with **no** caller-supplied thresholds, each such flag citing the expiry as the threshold that
1343
+ fired (`reason: "expiry"`, `expiresAt`, `matchedThresholdKey: "expires_at"`, `thresholdDays: null`).
1344
+ Record-carried expiry thus **complements** Addendum D's caller-supplied thresholds rather than
1345
+ replacing them.
1346
+ - **`superseded`** is queried via the existing `getLinks(id).reverse` surface (J.3) — no new storage
1347
+ or list method; it works uniformly across adapters.
1348
+
1349
+ ### J.6 Conformance
1350
+
1351
+ An adapter conforms to this addendum when the freshness suite
1352
+ (`evals/freshness/suite.test.js`) passes against it: `expires_at` / `ttl_seconds` round-trip through
1353
+ `get`; expiry transitions (past → stale, future → not, inclusive boundary, no-freshness-fields →
1354
+ never stale) hold; `{ stale: true }` listing returns exactly the expired records; and a superseded
1355
+ record is queryable as superseded. The suite is parameterized by `KNOWLEDGE_ADAPTER`, so conformance
1356
+ is required of every bundled adapter. Backward compatibility (R5) is covered by the unmodified
1357
+ `contract-suite`, `audit-freshness`, and `retirement` suites continuing to pass.
1358
+
1359
+ ---
1360
+
1361
+ ## Addendum K — Supersede/Retire Citer Propagation (flag inbound citers, #342)
1362
+
1363
+ ### K.1 Motivation
1364
+
1365
+ The `supersede` op (Addendum A.5) and the `retire` op with `supersededByRef` (Addendum B.4) preserve
1366
+ the affected record (supersede-not-delete / retire-not-delete) and make **store-internal** citers
1367
+ discoverable via the reverse-link index (§5, `getLinks(id).reverse`). But nothing enumerated or
1368
+ flagged the **docs** citing a superseded record — so a superseded decision could leave every citer
1369
+ silently pointing at dead or outdated authority.
1370
+
1371
+ Field evidence (design partner `kontourai/ops`, knowledge record `0e439c57`): the 6/17 "sell the
1372
+ combination" decision became unresolvable after a store restructure and **four docs** (`NOW.md` ×2,
1373
+ `strategy/vision.md`, `strategy/2026-06-18-market-fork-decision.md`) cited it invisibly for weeks —
1374
+ the citers kept presenting a dead record as live authority. This addendum closes that loop: on a
1375
+ superseded/retired record it enumerates first-degree inbound citers from **both** indexes and emits a
1376
+ supersession-aware flag per citer.
1377
+
1378
+ This is a **read-only** enumeration in the hygiene-flow style (Addenda D–G): the existing gated
1379
+ `supersede` and `retire` ops are unchanged, and no new mutation path is forked. It does **not**
1380
+ auto-edit citing docs, does **not** cascade status changes, and does **not** follow citers-of-citers
1381
+ (first-degree only). Fixing a citation is the operator's or a downstream flow's gated action.
1382
+
1383
+ ### K.2 `flagSupersededCiters` Flow-Runner Operation
1384
+
1385
+ `KnowledgeFlowRunner.flagSupersededCiters(recordId, options)` (also the module-level
1386
+ `flagSupersededCiters(recordId, { store, ... })`):
1387
+
1388
+ **Options:**
1389
+
1390
+ | Option | Type | Default | Description |
1391
+ |---|---|---|---|
1392
+ | `docGlobs` | `string[]` | `[]` | Globs to scan for doc citers via the #340 citation index. Opt-in: `[]` = store citers only, no doc scan. |
1393
+ | `docsRoot` | `string` | workspace, then cwd | Root the globs resolve against. |
1394
+ | `markers` | `string[]` | `DEFAULT_CITATION_MARKERS` | Citation marker prefixes forwarded to the inbound-reference scan (Addendum I). |
1395
+ | `agent` | `string` | runner agent | Agent recorded on the flow telemetry. |
1396
+
1397
+ `recordId` accepts a full id or any Addendum-H handle (short-id prefix / slug alias); the returned
1398
+ `recordId` is the resolved full id. A record that does not exist throws (`MISSING_EVIDENCE`) — never a
1399
+ silent empty pass.
1400
+
1401
+ **Supersession evidence** (at least one is required for any flag to be emitted):
1402
+ - **supersede op** (A.5): a reverse link of kind `"supersedes"` from the superseding record, mirrored
1403
+ by a `"superseded-by"` mutation-log entry carrying `new_id`. Both sources are read; their union is
1404
+ `supersededByIds`.
1405
+ - **retire-with-supersededByRef** (B.4): a `"retire"` mutation-log entry whose
1406
+ `evidence.supersededByRef` names the superseding artifact; surfaced as `supersededByRef` (and
1407
+ `retiredStatus: "retired"`).
1408
+
1409
+ **Citer enumeration** (first-degree only):
1410
+ - **store records** — `getLinks(id).reverse`, EXCLUDING the `"supersedes"` link itself and any link
1411
+ originating from a superseding record (the replacement authority is not a silent dead citer). Every
1412
+ other inbound link is a citer, tagged with its `linkKind`.
1413
+ - **docs** — `checkInboundReferences({ docGlobs, ... }).byRecord[id]` (Addendum I). The superseded
1414
+ record is preserved, so its citations still resolve. Opt-in via `docGlobs`.
1415
+
1416
+ ### K.3 Flag Evidence Guarantee
1417
+
1418
+ Every flag carries the superseding context that produced it — the flag-gate refuses to emit a flag
1419
+ without it. Consequently a record with **no** supersession evidence yields `superseded: false` and an
1420
+ **empty** flag list (no false flags, fail closed); a superseded record with no citers likewise yields
1421
+ an empty flag list. The enumerated citer counts (`storeCiters` / `docCiters`) are reported regardless
1422
+ of gating, so a caller can see the inbound edges independent of supersession state.
1423
+
1424
+ ```ts
1425
+ interface SupersededCiterFlag {
1426
+ citerRef: string; // record id, or "doc:line:column"
1427
+ citerKind: "record" | "doc";
1428
+ citedId: string; // full id of the superseded / retired record
1429
+ supersededByIds: string[]; // superseding record id(s) from the supersede op
1430
+ supersededByRef: string | null; // the retire supersededByRef, when present
1431
+ // record citer:
1432
+ linkKind?: string; // the reverse link kind that made it a citer
1433
+ // doc citer (from the #340 citation index):
1434
+ doc?: string; line?: number; column?: number; token?: string; form?: string;
1435
+ }
1436
+ ```
1437
+
1438
+ `flagSupersededCiters` returns:
1439
+
1440
+ ```ts
1441
+ {
1442
+ recordId: string; // resolved full id
1443
+ superseded: boolean; // whether supersession evidence exists
1444
+ supersededByIds: string[];
1445
+ supersededByRef: string | null;
1446
+ retiredStatus: "retired" | null;
1447
+ storeCiters: number; // count from the reverse-link index
1448
+ docCiters: number; // count from the citation index (0 when no globs)
1449
+ flags: SupersededCiterFlag[]; // gated by the evidence guarantee
1450
+ telemetryEvents: object[];
1451
+ }
1452
+ ```
1453
+
1454
+ Gate telemetry is emitted at `collect-gate` and `flag-gate` (`knowledge.flag-superseded-citers`);
1455
+ when doc globs are configured, the folded `knowledge.check-inbound-references` gate events (Addendum
1456
+ I) also appear in the trail.
1457
+
1458
+ ### K.4 Read-Only Invariant
1459
+
1460
+ `flagSupersededCiters` reads the store's query surface (`get`, `getLinks`) and — when doc globs are
1461
+ configured — the #340 inbound-reference scan (doc files + `get`). It mutates no record and appends no
1462
+ mutation-log entry. The store tree is byte-identical before and after the call.
1463
+
1464
+ ### K.5 Conformance
1465
+
1466
+ An adapter conforms to this addendum when the supersede-propagation suite
1467
+ (`evals/supersede-propagation/suite.test.js`) passes against it — AC1 (store-record + doc citer
1468
+ enumerated from the two indexes), AC2 (every flag carries citer ref + cited id + superseding context;
1469
+ superseded-with-no-citers and non-superseded both yield zero flags), AC3 (read-only), and the
1470
+ retire-with-supersededByRef path. The suite is parameterized by `KNOWLEDGE_ADAPTER`, so conformance is
1471
+ required of every adapter (AC4). Because it reuses `get`/`getLinks` and the Addendum-I check, any
1472
+ adapter conforming to Addenda A, B, H, and I satisfies it without new storage.
1473
+
1474
+ ---
1475
+
1476
+ ## Addendum L — Incremental Consolidation (append mode, #343)
1477
+
1478
+ ### L.1 Problem: whole-body consolidate is heavy enough that sessions skip it
1479
+
1480
+ The `knowledge.consolidate` flow (Addendum A) originally accepted only a **whole-body** input:
1481
+ the caller authored the *entire* updated snapshot body (`options.proposedBody`) and the runner
1482
+ replaced the snapshot with it. To add one decision, the caller had to re-read and re-emit every
1483
+ prior entry. Field evidence from the ops design partner (record `0e439c57`) showed this cost is
1484
+ high enough that real sessions **skip consolidation entirely** — the living decision snapshot
1485
+ silently stops living, defeating the flow.
1486
+
1487
+ The snapshot already links its contributing compiled records with kind `"source"` and carries
1488
+ `provenance.source_ids` (Addendum A.3), so the body is *derivable from records*. Append mode uses
1489
+ that: the caller supplies only the new contribution and the runner regenerates the body.
1490
+
1491
+ ### L.2 Append-mode input
1492
+
1493
+ `KnowledgeFlowRunner.consolidate(snapshotIdOrTopic, options)` accepts, as an alternative to
1494
+ `options.proposedBody`, an **appended entry** identifying the new compiled record:
1495
+
1496
+ | Option | Type | Description |
1497
+ |---|---|---|
1498
+ | `appendEntryRecordId` | `string` | Id of the new **compiled** record to append (the appended entry). `appendEntry: { recordId }` is an accepted equivalent shape. |
1499
+ | `header` | `string` (opt) | Body header prepended before the rendered entries. |
1500
+ | `entryRenderer` | `(record) => string` (opt) | Pluggable per-entry renderer. Default renders a decision-log section: `## <title>\n\n<body>`. |
1501
+
1502
+ `proposedBody` (whole-body mode) and the append options are **mutually exclusive**; supplying
1503
+ both throws `MISSING_EVIDENCE`. The appended record MUST exist and be type `"compiled"`
1504
+ (otherwise `MISSING_EVIDENCE`).
1505
+
1506
+ ### L.3 Body regeneration
1507
+
1508
+ On an append-mode call the runner:
1509
+
1510
+ 1. Resolves the **current** (non-superseded) snapshot for the topic (or the given snapshot id).
1511
+ 2. Reads that snapshot's contributing records: `provenance.source_ids` first, falling back to its
1512
+ kind `"source"` links (Addendum A.3).
1513
+ 3. Forms the new source set = *prior sources* ++ *appended entry*, de-duplicated with order
1514
+ preserved (re-appending the same record is idempotent — no duplicate source id).
1515
+ 4. Regenerates the body by rendering **every** source record in order (`entryRenderer`, joined by a
1516
+ `---` rule, optional `header`). The caller never supplies prior content.
1517
+
1518
+ The regenerated body then flows through the *unchanged* consolidate machinery
1519
+ (propose → evidence-gate → apply-or-reject, Addendum A.5): a new snapshot is created with the
1520
+ regenerated body, `provenance.source_ids` and kind `"source"` links covering every contributing
1521
+ record **including the new one** (R3), and the prior snapshot(s) are superseded, not deleted.
1522
+
1523
+ ### L.4 Back-compat
1524
+
1525
+ Whole-body mode is unchanged: existing callers passing `options.proposedBody` work exactly as
1526
+ before, and `evals/consolidation/suite.test.js` passes unmodified. Append mode is purely additive
1527
+ — it changes how the effective body and source cluster are *computed*, not the gate shape, the
1528
+ supersede-not-delete invariant (A.5), or the telemetry gate points.
1529
+
1530
+ ### L.5 Sequential-session safety (no lost update)
1531
+
1532
+ Because the body is regenerated from the **current live snapshot's** records on every call, two
1533
+ sequential consolidations from different sessions/agents, each appending one distinct entry, yield
1534
+ a snapshot containing **both** entries — the second session resolves the first session's snapshot,
1535
+ reads its (now-larger) source set, and appends to it rather than overwriting a stale whole body.
1536
+
1537
+ ### L.6 Conformance
1538
+
1539
+ An adapter conforms when the incremental-consolidation suite
1540
+ (`evals/consolidate-incremental/suite.test.js`) passes against it. The suite is parameterized by
1541
+ `KNOWLEDGE_ADAPTER`; the no-lost-update case (R4) and the provenance/source-link completeness case
1542
+ (R3, asserted via the portable `get()` + reverse-`supersedes`-link guarantees of A.7) are required
1543
+ of every adapter. The default-store's additional guarantee that `listByType("snapshot")` still
1544
+ returns superseded snapshots is covered for that adapter by `evals/consolidation/suite.test.js`
1545
+ (the Obsidian adapter archives superseded snapshots out of the working set — both keep them
1546
+ reachable via `get()`).