@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.
- package/.github/workflows/ci.yml +12 -0
- package/CHANGELOG.md +32 -0
- package/CONTEXT.md +67 -1
- package/README.md +4 -0
- package/build/src/cli/assignment-provider.d.ts +188 -0
- package/build/src/cli/assignment-provider.js +193 -51
- package/build/src/cli/workflow-sidecar.d.ts +16 -6
- package/build/src/cli/workflow-sidecar.js +492 -45
- package/build/src/lib/flow-resolver.d.ts +12 -6
- package/build/src/lib/flow-resolver.js +30 -14
- package/build/src/tools/validate-source-tree.js +2 -1
- package/context/contracts/assignment-provider-contract.md +10 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/contracts/probe-docs-write-contract.md +187 -0
- package/context/scripts/hooks/config-protection.js +24 -4
- package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/context/scripts/hooks/stop-goal-fit.js +262 -5
- package/context/scripts/hooks/workflow-steering.js +42 -0
- package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
- package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
- package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0006-typescript-first-source-policy.md +2 -0
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0007-skill-audit.md +2 -0
- package/docs/adr/0008-kit-operation-boundary.md +2 -0
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
- package/docs/adr/0011-mcp-posture.md +2 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
- package/docs/adr/0013-context-lifecycle.md +2 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
- package/docs/adr/0016-three-hard-boundary-model.md +2 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
- package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
- package/docs/adr/0019-kit-dependency-ownership.md +2 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +113 -0
- package/docs/adr/README.md +49 -0
- package/docs/adr/index.md +34 -0
- package/docs/decisions/agent-coordination.md +20 -0
- package/docs/decisions/anti-gaming-trust-security.md +20 -0
- package/docs/decisions/context-lifecycle.md +18 -0
- package/docs/decisions/core-domain-kit-boundary.md +18 -0
- package/docs/decisions/flow-flow-agents-boundary.md +18 -0
- package/docs/decisions/flow-kit.md +20 -0
- package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
- package/docs/decisions/graph-knowledge-provider.md +63 -0
- package/docs/decisions/hook-core-kit-boundary.md +18 -0
- package/docs/decisions/index.md +19 -0
- package/docs/decisions/kit-dependency-ownership.md +18 -0
- package/docs/decisions/kit-operation-boundary.md +18 -0
- package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
- package/docs/decisions/kontour-resource-contract.md +18 -0
- package/docs/decisions/mcp-posture.md +18 -0
- package/docs/decisions/three-hard-boundary-model.md +18 -0
- package/docs/decisions/trust-reconcile.md +20 -0
- package/docs/decisions/typescript-source-policy.md +48 -0
- package/docs/decisions/workflow-enforcement.md +18 -0
- package/docs/decisions/workflow-trust-state.md +20 -0
- package/docs/fixture-ownership.md +1 -1
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
- package/evals/integration/test_checkpoint_signing.sh +4 -3
- package/evals/integration/test_current_json_per_actor.sh +516 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
- package/evals/integration/test_gate_lockdown.sh +46 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +14 -6
- package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
- package/evals/run.sh +10 -0
- package/evals/static/test_knowledge_providers.sh +13 -4
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/evals/static/test_workflow_skills.sh +15 -2
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/deliver/SKILL.md +69 -11
- package/kits/builder/skills/design-probe/SKILL.md +47 -0
- package/kits/builder/skills/execute-plan/SKILL.md +13 -0
- package/kits/builder/skills/fix-bug/SKILL.md +17 -0
- package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
- package/kits/builder/skills/plan-work/SKILL.md +9 -0
- package/kits/builder/skills/pull-work/SKILL.md +10 -0
- package/kits/builder/skills/review-work/SKILL.md +11 -0
- package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
- package/kits/builder/skills/verify-work/SKILL.md +11 -0
- package/kits/knowledge/adapters/default-store/index.js +147 -18
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +97 -14
- package/kits/knowledge/adapters/shared/codec.js +265 -0
- package/kits/knowledge/docs/README.md +121 -2
- package/kits/knowledge/docs/store-contract.md +517 -7
- package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
- package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
- package/kits/knowledge/evals/contract-suite/suite.test.js +212 -0
- package/kits/knowledge/evals/freshness/suite.test.js +339 -0
- package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1 -1
- package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
- package/kits/knowledge/flows/promote.flow.json +84 -0
- package/kits/knowledge/kit.json +15 -0
- package/kits/knowledge/promote/distill.js +96 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
- package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
- package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
- package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
- package/kits/knowledge/promote/fixtures/session/state.json +19 -0
- package/kits/knowledge/promote/health.js +137 -0
- package/kits/knowledge/promote/index.js +176 -0
- package/kits/knowledge/promote/ingest.js +103 -0
- package/kits/knowledge/promote/lib.js +132 -0
- package/kits/knowledge/promote/link.js +84 -0
- package/kits/knowledge/promote/promote.test.js +174 -0
- package/kits/knowledge/providers/conformance/suite.test.js +18 -0
- package/kits/knowledge/providers/index.js +1 -0
- package/kits/knowledge/providers/neo4j/connection.js +121 -0
- package/kits/knowledge/providers/neo4j/cypher.js +190 -0
- package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
- package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
- package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
- package/kits/knowledge/providers/neo4j/index.js +280 -0
- package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
- package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
- package/kits/knowledge/providers/neo4j/sync.js +235 -0
- package/package.json +4 -2
- package/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/README.md +1 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +144 -26
- package/scripts/freeze-adrs.mjs +364 -0
- package/scripts/hooks/config-protection.js +24 -4
- package/scripts/hooks/evidence-capture.js +4 -1
- package/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/scripts/hooks/lib/current-pointer.js +123 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
- package/scripts/hooks/stop-goal-fit.js +262 -5
- package/scripts/hooks/workflow-steering.js +42 -0
- package/scripts/statusline/flow-agents-statusline.js +3 -1
- package/src/cli/assignment-provider.ts +239 -59
- package/src/cli/workflow-sidecar.ts +529 -43
- package/src/lib/flow-resolver.ts +35 -14
- 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` | `(
|
|
346
|
-
| `getLinks` | `(
|
|
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
|
|
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
|
|
836
|
-
|
|
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()`).
|