@kontourai/flow-agents 2.4.0 → 3.1.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/CODEOWNERS +8 -0
- package/.github/workflows/ci.yml +20 -0
- package/.github/workflows/trust-reconcile.yml +62 -4
- package/CHANGELOG.md +38 -0
- package/CONTEXT.md +88 -1
- package/README.md +4 -0
- package/build/src/cli/assignment-provider.d.ts +144 -0
- package/build/src/cli/assignment-provider.js +805 -0
- package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
- package/build/src/cli/effective-assignment-provider-settings.js +125 -0
- package/build/src/cli/validate-workflow-artifacts.js +5 -1
- package/build/src/cli/workflow-sidecar.d.ts +2 -2
- package/build/src/cli/workflow-sidecar.js +549 -145
- package/build/src/cli.js +6 -0
- 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 -0
- package/context/contracts/artifact-contract.md +2 -0
- package/context/contracts/assignment-provider-contract.md +248 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
- package/context/contracts/decision-registry-contract.md +2 -0
- package/context/contracts/delivery-contract.md +2 -0
- package/context/contracts/execution-contract.md +25 -0
- package/context/contracts/governance-adapter-contract.md +2 -0
- package/context/contracts/knowledge-store-contract.md +197 -0
- package/context/contracts/planning-contract.md +2 -0
- package/context/contracts/probe-docs-write-contract.md +187 -0
- package/context/contracts/review-contract.md +2 -0
- package/context/contracts/sandbox-policy.md +2 -0
- package/context/contracts/standing-directives.md +13 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/contracts/work-item-contract.md +2 -0
- package/context/scripts/hooks/config-protection.js +14 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/context/scripts/hooks/stop-goal-fit.js +4 -2
- package/context/scripts/hooks/workflow-steering.js +42 -0
- package/context/settings/assignment-provider-settings.json +33 -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 +182 -0
- package/docs/adr/README.md +49 -0
- package/docs/adr/index.md +34 -0
- package/docs/context-map.md +1 -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 +22 -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/knowledge-store-provider.md +51 -0
- package/docs/decisions/kontour-resource-contract.md +18 -0
- package/docs/decisions/mcp-posture.md +18 -0
- package/docs/decisions/model-routing.md +63 -0
- package/docs/decisions/standing-directives.md +66 -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 -0
- package/docs/workflow-shared-contracts.md +2 -1
- package/docs/workflow-usage-guide.md +7 -1
- package/evals/ci/run-baseline.sh +10 -0
- package/evals/fixtures/assignment-provider/actor-a.json +6 -0
- package/evals/fixtures/assignment-provider/actor-b.json +6 -0
- package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
- package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
- package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
- package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
- package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
- package/evals/integration/test_assignment_provider_github.sh +318 -0
- package/evals/integration/test_assignment_provider_local_file.sh +222 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
- 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_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_publish_delivery.sh +21 -4
- package/evals/integration/test_pull_work_assignment_join.sh +132 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +20 -7
- package/evals/integration/test_reconcile_soundness.sh +33 -9
- package/evals/integration/test_trust_reconcile.sh +9 -8
- package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +91 -3
- package/evals/run.sh +14 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
- package/evals/static/test_knowledge_providers.sh +32 -0
- package/evals/static/test_workflow_skills.sh +15 -2
- package/kits/builder/skills/deliver/SKILL.md +36 -0
- package/kits/builder/skills/design-probe/SKILL.md +37 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
- package/kits/builder/skills/pull-work/SKILL.md +78 -10
- package/kits/knowledge/adapters/default-store/index.js +92 -4
- package/kits/knowledge/adapters/obsidian-store/index.js +69 -4
- package/kits/knowledge/adapters/shared/codec.js +141 -0
- package/kits/knowledge/docs/README.md +121 -2
- package/kits/knowledge/docs/store-contract.md +112 -4
- package/kits/knowledge/evals/contract-suite/suite.test.js +176 -0
- package/kits/knowledge/flows/promote.flow.json +84 -0
- package/kits/knowledge/kit.json +50 -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/fixtures/git-repo/CONTEXT.md +12 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
- package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
- package/kits/knowledge/providers/conformance/suite.test.js +143 -0
- package/kits/knowledge/providers/git-repo/index.js +236 -0
- package/kits/knowledge/providers/health/health-pass.test.js +99 -0
- package/kits/knowledge/providers/health/index.js +153 -0
- package/kits/knowledge/providers/index.js +25 -0
- package/kits/knowledge/providers/lib/model.js +91 -0
- package/kits/knowledge/providers/lib/schema-validate.js +119 -0
- package/kits/knowledge/providers/markdown-vault/index.js +169 -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/kits/knowledge/providers/work-item/index.js +204 -0
- package/package.json +7 -3
- package/schemas/assignment-provider-settings.schema.json +125 -0
- package/schemas/knowledge/edge.schema.json +54 -0
- package/schemas/knowledge/health-report.schema.json +45 -0
- package/schemas/knowledge/node.schema.json +49 -0
- package/schemas/knowledge/proposal.schema.json +53 -0
- package/scripts/README.md +1 -0
- package/scripts/ci/trust-reconcile.js +521 -24
- package/scripts/freeze-adrs.mjs +364 -0
- package/scripts/hooks/config-protection.js +14 -1
- 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 +4 -2
- package/scripts/hooks/workflow-steering.js +42 -0
- package/scripts/statusline/flow-agents-statusline.js +3 -1
- package/src/cli/assignment-provider.ts +927 -0
- package/src/cli/effective-assignment-provider-settings.ts +112 -0
- package/src/cli/validate-workflow-artifacts.ts +5 -1
- package/src/cli/workflow-sidecar.ts +577 -139
- package/src/cli.ts +6 -0
- package/src/lib/flow-resolver.ts +35 -14
- package/src/tools/validate-source-tree.ts +2 -0
|
@@ -26,6 +26,7 @@ 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. |
|
|
@@ -342,11 +343,14 @@ The adapter MUST implement:
|
|
|
342
343
|
|
|
343
344
|
| Method | Signature | Description |
|
|
344
345
|
|---|---|---|
|
|
345
|
-
| `get` | `(
|
|
346
|
-
| `getLinks` | `(
|
|
346
|
+
| `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. |
|
|
347
|
+
| `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
348
|
| `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
349
|
| `listByType` | `(type: RecordType) => Record[]` | List all records of a given type. |
|
|
349
350
|
|
|
351
|
+
> **Identity resolution** (`get`/`getLinks`) is specified in full in **Addendum H**. The `prefix`
|
|
352
|
+
> option on `listByCategory` is unrelated — it filters by *category* prefix, not by id.
|
|
353
|
+
|
|
350
354
|
---
|
|
351
355
|
|
|
352
356
|
## 8. Adapter Contract Summary
|
|
@@ -374,6 +378,15 @@ missing. The thrown error MUST have a `code` property set to `"MISSING_EVIDENCE"
|
|
|
374
378
|
human-readable `message`. This enables the contract suite to distinguish enforcement failures
|
|
375
379
|
from unexpected errors.
|
|
376
380
|
|
|
381
|
+
### 8.1 Error Codes
|
|
382
|
+
|
|
383
|
+
| `code` | Raised by | Meaning |
|
|
384
|
+
|---|---|---|
|
|
385
|
+
| `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. |
|
|
386
|
+
| `NOT_FOUND` | `link` (unknown `target_id`) and other ops that require an existing target | A referenced record id does not exist. |
|
|
387
|
+
| `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. |
|
|
388
|
+
| `SLUG_CONFLICT` | `create`, `update` | A supplied slug alias is already assigned to a *different* record. |
|
|
389
|
+
|
|
377
390
|
---
|
|
378
391
|
|
|
379
392
|
## 9. YAML Frontmatter Convention (Default Adapter)
|
|
@@ -387,6 +400,7 @@ type: <raw|compiled|concept>
|
|
|
387
400
|
title: <title>
|
|
388
401
|
category: <category>
|
|
389
402
|
tags: [<tag>, ...]
|
|
403
|
+
aliases: [<slug>, ...] # optional — human-readable, category-scoped (Addendum H)
|
|
390
404
|
created_at: <ISO-8601>
|
|
391
405
|
updated_at: <ISO-8601>
|
|
392
406
|
provenance:
|
|
@@ -409,8 +423,9 @@ mutation_log:
|
|
|
409
423
|
<body text, may include [[wikilinks]]>
|
|
410
424
|
```
|
|
411
425
|
|
|
412
|
-
Files are stored as `<store_root>/records/<id>.md
|
|
413
|
-
`<store_root>/graph-index.json
|
|
426
|
+
Files are stored as `<store_root>/records/<id>.md` — keyed by the full `id`, unchanged by this
|
|
427
|
+
addendum. The graph index lives at `<store_root>/graph-index.json`; the slug alias map (Addendum H)
|
|
428
|
+
lives at `<store_root>/alias-index.json`.
|
|
414
429
|
|
|
415
430
|
---
|
|
416
431
|
|
|
@@ -1034,3 +1049,96 @@ interface ContradictionFlag {
|
|
|
1034
1049
|
counts the in-scope compiled records, `compared` counts the similar pairs the contradiction fn
|
|
1035
1050
|
judged, and `flags` lists the conflicting pairs. Gate telemetry is emitted at `collect-gate` and
|
|
1036
1051
|
`flag-gate` (`knowledge.detect-contradictions`).
|
|
1052
|
+
|
|
1053
|
+
## Addendum H — Record Identity Resolution (short-id prefix + slug aliases, #339)
|
|
1054
|
+
|
|
1055
|
+
### H.1 Motivation
|
|
1056
|
+
|
|
1057
|
+
On-disk identity is a full `id` (§1.1) plus a file path (§9). Prose that cites a record — a
|
|
1058
|
+
curated doc, a commit message, an issue comment — cannot practically carry a full 36-char UUID, so
|
|
1059
|
+
the field partner cites **8-char short ids** (`12cc5573`, `f5df7b4c`) and would prefer
|
|
1060
|
+
**human-readable slugs**. Neither resolves against exact-id `get`, and a store restructure that
|
|
1061
|
+
re-files a record silently severs every inbound short-id/slug reference.
|
|
1062
|
+
|
|
1063
|
+
This addendum adds a **resolution layer** over the unchanged on-disk identity. It changes nothing
|
|
1064
|
+
about how records are stored (H.6) — it only widens what `get`/`getLinks` accept as input.
|
|
1065
|
+
|
|
1066
|
+
### H.2 Resolvable Handle Forms
|
|
1067
|
+
|
|
1068
|
+
`get(handle)` and `getLinks(handle)` MUST resolve, in this precedence order:
|
|
1069
|
+
|
|
1070
|
+
1. **Exact full id** — `handle` equals a stored record `id`. (Unchanged §7 behavior; the O(1)
|
|
1071
|
+
hot path.)
|
|
1072
|
+
2. **Slug alias** — `handle` is a registered alias in the alias map (H.4) whose target still exists.
|
|
1073
|
+
3. **Unambiguous id prefix** — `handle` is at least `MIN_ID_PREFIX` (**8**) characters and is a
|
|
1074
|
+
prefix of **exactly one** record `id`.
|
|
1075
|
+
|
|
1076
|
+
The first form that matches wins. A `handle` that matches none of the three resolves to *nothing*.
|
|
1077
|
+
|
|
1078
|
+
### H.3 Resolution Outcomes
|
|
1079
|
+
|
|
1080
|
+
| Situation | `get` | `getLinks` |
|
|
1081
|
+
|---|---|---|
|
|
1082
|
+
| Resolves to exactly one record | returns that record | returns that record's `{ forward, reverse }` |
|
|
1083
|
+
| Resolves to nothing (no exact id, no alias, no ≥8-char single-match prefix) | returns `null` | returns `{ forward: [], reverse: [] }` |
|
|
1084
|
+
| Prefix (≥8 chars) matches **more than one** record | **throws `AMBIGUOUS_ID`** | **throws `AMBIGUOUS_ID`** |
|
|
1085
|
+
|
|
1086
|
+
An `AMBIGUOUS_ID` error MUST have `code === "AMBIGUOUS_ID"`, a human-readable `message`, and a
|
|
1087
|
+
`matches` array of the colliding full ids. An ambiguous prefix MUST NOT be silently resolved to one
|
|
1088
|
+
arbitrary record, nor collapsed to `null` — ambiguity is a distinct, surfaced condition.
|
|
1089
|
+
|
|
1090
|
+
A `handle` **shorter than `MIN_ID_PREFIX`** is never treated as a prefix (it can still match an
|
|
1091
|
+
exact id or a slug). This keeps a stray short token from matching a large, surprising set.
|
|
1092
|
+
|
|
1093
|
+
### H.4 Slug Aliases
|
|
1094
|
+
|
|
1095
|
+
- A slug is a lowercase, category-scoped, human-readable handle, e.g.
|
|
1096
|
+
`decision.strategy/2026-07-03-gtm-direction`. It MUST match
|
|
1097
|
+
`^[a-z0-9]([a-z0-9._/-]*[a-z0-9])?$` (≤ 200 chars). Malformed slugs are rejected at
|
|
1098
|
+
`create`/`update` with `MISSING_EVIDENCE`.
|
|
1099
|
+
- Slugs are **caller-supplied** via the optional `aliases: string[]` field on `create` and
|
|
1100
|
+
`update`. The store does **not** auto-derive slugs (auto-slugging is a future concern) — the
|
|
1101
|
+
date-stamped examples cannot be derived from title alone.
|
|
1102
|
+
- Aliases are **append-only**: `update` with an `aliases` field UNIONs the supplied slugs with the
|
|
1103
|
+
record's existing aliases; it never drops a previously issued slug. This is what guarantees H.5.
|
|
1104
|
+
- A slug already assigned to a *different* record is a `SLUG_CONFLICT` (a slug identifies at most
|
|
1105
|
+
one record). Re-supplying a record's own slug is an idempotent no-op.
|
|
1106
|
+
|
|
1107
|
+
### H.5 Restructure Survival (the alias map)
|
|
1108
|
+
|
|
1109
|
+
The alias map is a store-level structure `{ slug → id }`. It is keyed by **full id** — never by
|
|
1110
|
+
category or file path. Therefore:
|
|
1111
|
+
|
|
1112
|
+
- Recategorizing a record via `update` (which, in the Obsidian adapter, **relocates the note's
|
|
1113
|
+
file**) leaves both the `id` and the `slug → id` mapping untouched.
|
|
1114
|
+
- The id still resolves (it is the record's stable key); the short-id prefix still resolves (it is
|
|
1115
|
+
a prefix of that unchanged id); the slug still resolves (the map still points it at that id).
|
|
1116
|
+
|
|
1117
|
+
So a previously issued short-id prefix or slug MUST still resolve to the same record after a
|
|
1118
|
+
restructure. Note the slug is a **stable alias**, not a value recomputed from the current category:
|
|
1119
|
+
a slug minted under `decision.strategy/…` keeps resolving even after the record moves to
|
|
1120
|
+
`decision.gtm`.
|
|
1121
|
+
|
|
1122
|
+
Like the graph index (§5.2), the alias map is a **derived cache** — each record's own `aliases`
|
|
1123
|
+
array is the source of truth, so an adapter that persists the map SHOULD rebuild it on `reindex()`.
|
|
1124
|
+
The default adapter persists it at `<store_root>/alias-index.json`
|
|
1125
|
+
(`{ schema_version, by_slug: { <slug>: <id> } }`).
|
|
1126
|
+
|
|
1127
|
+
### H.6 On-Disk Identity Is Unchanged (non-goals)
|
|
1128
|
+
|
|
1129
|
+
- The `id` in the §1.1 envelope and the `records/<id>.md` file naming (§9) are **unchanged**.
|
|
1130
|
+
Aliases are a resolution layer, not a rename.
|
|
1131
|
+
- Exact-id `get`/`getLinks` behavior is byte-for-byte unchanged.
|
|
1132
|
+
- This addendum does **not** migrate existing stores or repair already-broken citations — an
|
|
1133
|
+
adapter/repo may seed its own alias map for legacy short ids, but that is out of scope here.
|
|
1134
|
+
- Full-text/semantic search is out of scope.
|
|
1135
|
+
|
|
1136
|
+
### H.7 Resolution Constants
|
|
1137
|
+
|
|
1138
|
+
| Constant | Value | Meaning |
|
|
1139
|
+
|---|---|---|
|
|
1140
|
+
| `MIN_ID_PREFIX` | `8` | Minimum handle length eligible for prefix resolution. |
|
|
1141
|
+
| slug pattern | `^[a-z0-9]([a-z0-9._/-]*[a-z0-9])?$` | Valid slug alias shape (≤ 200 chars). |
|
|
1142
|
+
|
|
1143
|
+
An independent adapter conforms to this addendum when the `identity: *` sections of the contract
|
|
1144
|
+
suite (`evals/contract-suite/suite.test.js`, §15–§17) pass against it.
|
|
@@ -873,4 +873,180 @@ describe("Knowledge Kit Store Contract Suite", () => {
|
|
|
873
873
|
assert.ok(typeof record.body === "string", "body is a string");
|
|
874
874
|
});
|
|
875
875
|
});
|
|
876
|
+
|
|
877
|
+
// -----------------------------------------------------------------------
|
|
878
|
+
// §15 identity resolution — short-id prefix (AC1, R1)
|
|
879
|
+
//
|
|
880
|
+
// `get`/`getLinks` accept an unambiguous id prefix (>= 8 chars). An ambiguous
|
|
881
|
+
// prefix throws AMBIGUOUS_ID (a distinct code), never guesses or returns null.
|
|
882
|
+
// On-disk identity is unchanged (R4): records are still keyed by full id.
|
|
883
|
+
// -----------------------------------------------------------------------
|
|
884
|
+
describe("identity: short-id prefix resolution (AC1)", () => {
|
|
885
|
+
let dir, store;
|
|
886
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
887
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
888
|
+
|
|
889
|
+
test("get resolves a unique 8-char id prefix to the record", async () => {
|
|
890
|
+
const id = await store.create({
|
|
891
|
+
type: "concept", title: "Prefix Target", body: "b",
|
|
892
|
+
category: "decision.strategy", provenance: { agent: "tester" },
|
|
893
|
+
});
|
|
894
|
+
const prefix = id.slice(0, 8);
|
|
895
|
+
assert.equal(prefix.length, 8, "sanity: prefix is 8 chars");
|
|
896
|
+
const rec = await store.get(prefix);
|
|
897
|
+
assert.ok(rec, "8-char prefix resolves to a record");
|
|
898
|
+
assert.equal(rec.id, id, "prefix resolves to the correct full id");
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test("getLinks resolves a unique 8-char id prefix", async () => {
|
|
902
|
+
const targetId = await store.create({
|
|
903
|
+
type: "concept", title: "PrefixLinkTarget", body: "t",
|
|
904
|
+
category: "test", provenance: { agent: "tester" },
|
|
905
|
+
});
|
|
906
|
+
const sourceId = await store.create({
|
|
907
|
+
type: "raw", title: "PrefixLinkSource", body: "s", category: "test",
|
|
908
|
+
links: [{ target_id: targetId, kind: "related" }],
|
|
909
|
+
provenance: { agent: "tester" },
|
|
910
|
+
});
|
|
911
|
+
const { forward } = await store.getLinks(sourceId.slice(0, 8));
|
|
912
|
+
assert.ok(forward.some((l) => l.target_id === targetId && l.kind === "related"),
|
|
913
|
+
"getLinks via an 8-char prefix returns the record's forward links");
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("an ambiguous prefix throws AMBIGUOUS_ID (never returns null)", async () => {
|
|
917
|
+
// Two records whose full ids deliberately share an 8-char prefix. create
|
|
918
|
+
// accepts an explicit id (§1.1: adapter MAY generate), so the collision is
|
|
919
|
+
// deterministic without depending on random UUIDs colliding.
|
|
920
|
+
await store.create({
|
|
921
|
+
id: "abcdef12-0000-0000-0000-000000000001",
|
|
922
|
+
type: "raw", title: "Ambiguous One", body: "1", category: "test",
|
|
923
|
+
provenance: { agent: "tester" },
|
|
924
|
+
});
|
|
925
|
+
await store.create({
|
|
926
|
+
id: "abcdef12-0000-0000-0000-000000000002",
|
|
927
|
+
type: "raw", title: "Ambiguous Two", body: "2", category: "test",
|
|
928
|
+
provenance: { agent: "tester" },
|
|
929
|
+
});
|
|
930
|
+
await assert.rejects(
|
|
931
|
+
() => store.get("abcdef12"),
|
|
932
|
+
(err) => {
|
|
933
|
+
assert.equal(err.code, "AMBIGUOUS_ID",
|
|
934
|
+
`expected AMBIGUOUS_ID, got code=${err.code}: ${err.message}`);
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("a prefix shorter than the 8-char minimum does not resolve (null)", async () => {
|
|
941
|
+
await store.create({
|
|
942
|
+
id: "deadbeef-1111-1111-1111-111111111111",
|
|
943
|
+
type: "raw", title: "Short Prefix Guard", body: "x", category: "test",
|
|
944
|
+
provenance: { agent: "tester" },
|
|
945
|
+
});
|
|
946
|
+
assert.equal(await store.get("dead"), null, "a <8-char token is never a prefix match");
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test("exact full-id get is unchanged (R4)", async () => {
|
|
950
|
+
const id = await store.create({
|
|
951
|
+
type: "raw", title: "Exact Id", body: "e", category: "test",
|
|
952
|
+
provenance: { agent: "tester" },
|
|
953
|
+
});
|
|
954
|
+
const rec = await store.get(id);
|
|
955
|
+
assert.equal(rec.id, id, "exact-id get still returns the record");
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// -----------------------------------------------------------------------
|
|
960
|
+
// §16 identity resolution — slug aliases (AC2, R2)
|
|
961
|
+
//
|
|
962
|
+
// Records carry human-readable, category-scoped slug aliases; `get`/`getLinks`
|
|
963
|
+
// resolve them through the same surface, whether set at create or via update.
|
|
964
|
+
// -----------------------------------------------------------------------
|
|
965
|
+
describe("identity: slug alias resolution (AC2)", () => {
|
|
966
|
+
let dir, store;
|
|
967
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
968
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
969
|
+
|
|
970
|
+
test("a record created with a slug alias resolves via get(slug) and getLinks(slug)", async () => {
|
|
971
|
+
const slug = "decision.strategy/2026-07-03-gtm-direction";
|
|
972
|
+
const targetId = await store.create({
|
|
973
|
+
type: "concept", title: "GTM Target", body: "t",
|
|
974
|
+
category: "decision.strategy", provenance: { agent: "tester" },
|
|
975
|
+
});
|
|
976
|
+
const id = await store.create({
|
|
977
|
+
type: "compiled", title: "GTM Direction", body: "the decision",
|
|
978
|
+
category: "decision.strategy", aliases: [slug],
|
|
979
|
+
links: [{ target_id: targetId, kind: "related" }],
|
|
980
|
+
provenance: { agent: "tester" },
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const rec = await store.get(slug);
|
|
984
|
+
assert.ok(rec, "slug resolves to a record");
|
|
985
|
+
assert.equal(rec.id, id, "get(slug) returns the aliased record");
|
|
986
|
+
|
|
987
|
+
const { forward } = await store.getLinks(slug);
|
|
988
|
+
assert.ok(forward.some((l) => l.target_id === targetId),
|
|
989
|
+
"getLinks(slug) returns the aliased record's links");
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test("a slug alias added via update resolves through the same surface", async () => {
|
|
993
|
+
const id = await store.create({
|
|
994
|
+
type: "concept", title: "Later Slug", body: "b",
|
|
995
|
+
category: "decision.strategy", provenance: { agent: "tester" },
|
|
996
|
+
});
|
|
997
|
+
await store.update(id, { aliases: ["decision.strategy/added-later"] }, { agent: "tester" });
|
|
998
|
+
const rec = await store.get("decision.strategy/added-later");
|
|
999
|
+
assert.ok(rec, "update-added slug resolves");
|
|
1000
|
+
assert.equal(rec.id, id, "get(added slug) returns the record");
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("a malformed slug alias is rejected at create (MISSING_EVIDENCE)", () =>
|
|
1004
|
+
assertMissingEvidence(
|
|
1005
|
+
() => store.create({
|
|
1006
|
+
type: "raw", title: "Bad Slug", body: "b", category: "test",
|
|
1007
|
+
aliases: ["Not A Valid Slug!"], provenance: { agent: "tester" },
|
|
1008
|
+
}),
|
|
1009
|
+
"malformed slug alias"
|
|
1010
|
+
));
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// -----------------------------------------------------------------------
|
|
1014
|
+
// §17 identity resolution — survives restructure (AC3, R3)
|
|
1015
|
+
//
|
|
1016
|
+
// After `update` recategorizes a record (obsidian: the file relocates folders),
|
|
1017
|
+
// the previously issued slug AND short-id prefix still resolve to the same
|
|
1018
|
+
// record — the alias map is keyed by full id, never by category or path.
|
|
1019
|
+
// -----------------------------------------------------------------------
|
|
1020
|
+
describe("identity: alias resolution survives restructure (AC3)", () => {
|
|
1021
|
+
let dir, store;
|
|
1022
|
+
before(() => { dir = makeTempDir(); store = makeStore(dir); });
|
|
1023
|
+
after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
1024
|
+
|
|
1025
|
+
test("old slug and short-id prefix still resolve after a recategorize", async () => {
|
|
1026
|
+
const slug = "decision.strategy/2026-06-18-market-fork";
|
|
1027
|
+
const id = await store.create({
|
|
1028
|
+
type: "concept", title: "Market Fork Decision", body: "original",
|
|
1029
|
+
category: "decision.strategy", aliases: [slug],
|
|
1030
|
+
provenance: { agent: "tester" },
|
|
1031
|
+
});
|
|
1032
|
+
const prefix = id.slice(0, 8);
|
|
1033
|
+
|
|
1034
|
+
// Pre-move sanity: both handles resolve.
|
|
1035
|
+
assert.equal((await store.get(slug)).id, id, "slug resolves before move");
|
|
1036
|
+
assert.equal((await store.get(prefix)).id, id, "prefix resolves before move");
|
|
1037
|
+
|
|
1038
|
+
// Restructure: recategorize. In the obsidian adapter this relocates the
|
|
1039
|
+
// note from decision/strategy/ to decision/gtm/.
|
|
1040
|
+
await store.update(id, { category: "decision.gtm" }, { agent: "tester" });
|
|
1041
|
+
const moved = await store.get(id);
|
|
1042
|
+
assert.equal(moved.category, "decision.gtm", "category changed on disk");
|
|
1043
|
+
|
|
1044
|
+
// The whole point of the alias map: old handles survive the restructure.
|
|
1045
|
+
assert.equal((await store.get(slug)).id, id, "old slug survives the restructure");
|
|
1046
|
+
assert.equal((await store.get(prefix)).id, id, "short-id prefix survives the restructure");
|
|
1047
|
+
|
|
1048
|
+
const { reverse } = await store.getLinks(id);
|
|
1049
|
+
assert.ok(Array.isArray(reverse), "getLinks still answers for the moved record");
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
876
1052
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "knowledge.promote",
|
|
3
|
+
"version": "1.0",
|
|
4
|
+
"description": "Knowledge Kit promote sub-flow (the codebase-facing 'flow within a flow'): ingest a delivered session's artifacts -> distill durable draft deltas (decision topics per the registry contract, CONTEXT.md vocabulary, learnings) -> link provenance (PR, merge SHA, session archive, touched topics) -> health-check the registry for contradictions (overlapping subject nouns with divergent status/content) and propose merge-repair. Invokable standalone AND composable from the Builder promote step via uses_flow. PROPOSALS-ONLY: every output lands as a draft under <session>/proposals/ for the promote step's human/agent to apply; the sub-flow never writes docs directly.",
|
|
5
|
+
"steps": [
|
|
6
|
+
{ "id": "ingest", "next": "distill" },
|
|
7
|
+
{ "id": "distill", "next": "link" },
|
|
8
|
+
{ "id": "link", "next": "health" },
|
|
9
|
+
{ "id": "health", "next": "done" },
|
|
10
|
+
{ "id": "done", "next": null }
|
|
11
|
+
],
|
|
12
|
+
"exports": [
|
|
13
|
+
"knowledge.promote.ingest",
|
|
14
|
+
"knowledge.promote.distill",
|
|
15
|
+
"knowledge.promote.link",
|
|
16
|
+
"knowledge.promote.health"
|
|
17
|
+
],
|
|
18
|
+
"gates": {
|
|
19
|
+
"ingest-gate": {
|
|
20
|
+
"step": "ingest",
|
|
21
|
+
"expects": [
|
|
22
|
+
{
|
|
23
|
+
"id": "session-residue-ingested",
|
|
24
|
+
"kind": "trust.bundle",
|
|
25
|
+
"required": true,
|
|
26
|
+
"description": "A completed session directory has been read (read-only) and its durable residue collected: the plan / Definition Of Done, verification evidence, review critique, learning records, and delegate transcripts. Ingest never mutates the session and never reads outside the given session directory. The residue is the raw material the remaining steps distill — nothing is proposed yet.",
|
|
27
|
+
"bundle_claim": {
|
|
28
|
+
"claimType": "knowledge.promote.ingest",
|
|
29
|
+
"subjectType": "artifact",
|
|
30
|
+
"accepted_statuses": ["trusted", "accepted"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"distill-gate": {
|
|
36
|
+
"step": "distill",
|
|
37
|
+
"expects": [
|
|
38
|
+
{
|
|
39
|
+
"id": "draft-deltas-schema-valid",
|
|
40
|
+
"kind": "trust.bundle",
|
|
41
|
+
"required": true,
|
|
42
|
+
"description": "The distilled residue is rendered as DRAFT deltas that validate BEFORE they are proposed: decision-registry topic files whose frontmatter satisfies schemas/decision-record.schema.json (one file per decision SUBJECT, the subject a CONTEXT.md vocabulary noun), CONTEXT.md vocabulary additions for any new subject noun, and docs/learnings entries. At least one schema-valid draft decision delta is produced for a session that ratified a durable decision. Every draft is written ONLY under <session>/proposals/ — the sub-flow proposes, it never writes into docs/.",
|
|
43
|
+
"bundle_claim": {
|
|
44
|
+
"claimType": "knowledge.promote.distill",
|
|
45
|
+
"subjectType": "artifact",
|
|
46
|
+
"accepted_statuses": ["trusted", "accepted"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"link-gate": {
|
|
52
|
+
"step": "link",
|
|
53
|
+
"expects": [
|
|
54
|
+
{
|
|
55
|
+
"id": "provenance-links-attached",
|
|
56
|
+
"kind": "trust.bundle",
|
|
57
|
+
"required": true,
|
|
58
|
+
"description": "Every draft delta carries its provenance: the pull request, the merge commit SHA, the archived session artifact path, and the touched decision topics. Links become evidence[] refs on the draft decision topics (kinds pr / commit / session-archive / doc) so a reader can trace each proposed delta back to the delivery that produced it. Secret-shaped refs are refused.",
|
|
59
|
+
"bundle_claim": {
|
|
60
|
+
"claimType": "knowledge.promote.link",
|
|
61
|
+
"subjectType": "artifact",
|
|
62
|
+
"accepted_statuses": ["trusted", "accepted"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"health-gate": {
|
|
68
|
+
"step": "health",
|
|
69
|
+
"expects": [
|
|
70
|
+
{
|
|
71
|
+
"id": "contradiction-report-and-merge-proposal",
|
|
72
|
+
"kind": "trust.bundle",
|
|
73
|
+
"required": true,
|
|
74
|
+
"description": "The registry is health-checked over the #317 git-repo provider interface (the SAME reader every provider shares — no forked reader): the proposed draft decisions are folded into the existing registry graph and the duplicate-detection health verb flags topics with overlapping subject nouns. Where two CURRENT decisions collide over one subject with divergent content, the step emits a merge-repair PROPOSAL naming BOTH topics and a merge target. Merge-repair is a proposal, never an auto-edit; it lands under <session>/proposals/health/ for the operator to apply through the registry's supersede/merge edit.",
|
|
75
|
+
"bundle_claim": {
|
|
76
|
+
"claimType": "knowledge.promote.health",
|
|
77
|
+
"subjectType": "artifact",
|
|
78
|
+
"accepted_statuses": ["trusted", "accepted"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/kits/knowledge/kit.json
CHANGED
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
"id": "knowledge.hygiene-review",
|
|
60
60
|
"path": "flows/hygiene-review.flow.json",
|
|
61
61
|
"description": "Hygiene orchestrator (#106 #5, closes the issue): a THIN, opt-in orchestrator over the four hygiene flows (audit-freshness, detect-contradictions, glossary-sync, canonicalize-category). orchestrate → review-gate. Runs each opted-in audit via its EXISTING flow-runner method + gates (reimplements no detection), folds their findings into one operator-facing review of proposed actions normalized to adopt / retire / merge, each citing its sourceFlow + the evidence its origin flow's gate already vouched. Read-only by default; forks NO propose→approve gate — the operator enacts each decision through the source flow's existing gated op (retire via knowledge.retire; adopt via capture/compile or glossary-sync apply; merge via update/recategorize). The only mutation it can trigger is glossary apply, delegated verbatim to knowledge.glossary-sync's propose→apply lineage (consume-never-fork)."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "knowledge.promote",
|
|
65
|
+
"path": "flows/promote.flow.json",
|
|
66
|
+
"description": "Knowledge promote sub-flow (issue #313, the codebase-facing 'flow within a flow'): ingest a delivered session's artifacts -> distill schema-valid DRAFT decision/vocabulary/learning deltas per the decision-registry contract -> link provenance (PR, merge SHA, session archive, touched topics) -> health-check the registry for contradictions (overlapping subject nouns + divergent content) and propose merge-repair. Invokable standalone AND composable from the Builder promote step via uses_flow. PROPOSALS-ONLY: outputs land under <session>/proposals/ for the promote step to apply; the sub-flow never writes docs directly. Executable logic: promote/index.js."
|
|
62
67
|
}
|
|
63
68
|
],
|
|
64
69
|
"docs": [
|
|
@@ -91,6 +96,36 @@
|
|
|
91
96
|
"id": "knowledge.obsidian-store",
|
|
92
97
|
"path": "adapters/obsidian-store/index.js",
|
|
93
98
|
"description": "Obsidian store adapter: each record is one human-canonical Obsidian markdown note. Frontmatter carries all contract fields; body rendered with callouts (raw), readable prose + Sources/Related wikilink sections (compiled/concept/snapshot). Category dots map to folder hierarchy; title-slugified filenames with collision suffix; superseded records move to archive/ (supersede-not-delete invariant). Backed by shared codec with default-store."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "knowledge.markdown-vault-provider",
|
|
102
|
+
"path": "providers/markdown-vault/index.js",
|
|
103
|
+
"description": "Knowledge store PROVIDER (issue #317): adapts the markdown-vault store behind the typed graph model (nodes/edges/provenance). Thin read + proposals-only wrapper over an existing store adapter; existing skills unaffected."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "knowledge.git-repo-provider",
|
|
107
|
+
"path": "providers/git-repo/index.js",
|
|
108
|
+
"description": "Knowledge store PROVIDER (issue #317): reads git-tracked docs (docs/decisions/* tombstones+evidence, CONTEXT.md glossary, docs/learnings/*) as decision/note nodes with supersedes/merged-into/evidence-of edges. Write side = decision-topic proposals shaped for the promote sub-flow."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "knowledge.work-item-provider",
|
|
112
|
+
"path": "providers/work-item/index.js",
|
|
113
|
+
"description": "Knowledge store PROVIDER/source-sink adapter (issue #317): maps GitHub issues to issue nodes and flow-agents:work-item-metadata blockers + prose refs to blocks/relates edges via an injectable gh runner. Write side = draft comment/label proposals. Enables backlog hygiene as a knowledge-health pass."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "knowledge.health-verbs",
|
|
117
|
+
"path": "providers/health/index.js",
|
|
118
|
+
"description": "Provider-agnostic knowledge-health verbs (issue #317): detectDuplicates + checkDependencyLinkIntegrity over any provider's graph, returning schema-valid health reports (schemas/knowledge/health-report.schema.json)."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "knowledge.store-providers",
|
|
122
|
+
"path": "providers/index.js",
|
|
123
|
+
"description": "Public entry point for the knowledge store providers, health verbs, graph model helpers, and schema validator. See context/contracts/knowledge-store-contract.md."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "knowledge.promote-runner",
|
|
127
|
+
"path": "promote/index.js",
|
|
128
|
+
"description": "Executable logic for the knowledge.promote sub-flow (issue #313): runPromote({sessionDir, repoRoot, provenance}) runs ingest -> distill -> link -> health and writes DRAFT proposals ONLY under <session>/proposals/. Builds on the #317 knowledge-store provider interface (git-repo provider as the shared registry reader; provider-agnostic duplicate-detection health verb) and validates every draft decision delta against schemas/decision-record.schema.json before proposing. Proposals-only by construction; never mutates docs/, CONTEXT.md, or any path outside the session dir."
|
|
94
129
|
}
|
|
95
130
|
],
|
|
96
131
|
"evals": [
|
|
@@ -153,6 +188,21 @@
|
|
|
153
188
|
"id": "knowledge.hygiene-review-suite",
|
|
154
189
|
"path": "evals/hygiene-review/suite.test.js",
|
|
155
190
|
"description": "Eval cases for hygiene-review (#106 hygiene #5, closes the issue): opt-in orchestration (omitted audit block is skipped; empty review does nothing); each opted-in audit runs via its existing flow-runner method and its findings are collected verbatim (no detection reimplemented); proposals are normalized to adopt/retire/merge and each cites its sourceFlow + evidence; thin-orchestrator/consume-never-fork invariants — read-only by default mutates no record, no new gate is forked, the only mutation (glossary apply:true) is delegated to glossarySync's own gated propose→apply (verified via proposes-link + mutation-log on the resulting concept); sub-flow telemetry is folded in plus hygiene-review's own orchestrate-gate + review-gate; module-level export delegates to the runner."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "knowledge.provider-conformance-suite",
|
|
194
|
+
"path": "providers/conformance/suite.test.js",
|
|
195
|
+
"description": "Conformance suite (issue #317, AC3): parameterized suite every knowledge store provider (markdown-vault, git-repo, work-item) must pass — read interface returns schema-valid nodes/edges with provenance, closed edge vocabulary, queryByType subset consistency, and proposeWrite returns a schema-valid 'proposed' proposal that mutates nothing (proposals-only)."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": "knowledge.provider-health-pass-suite",
|
|
199
|
+
"path": "providers/health/health-pass.test.js",
|
|
200
|
+
"description": "Health-pass suite (issue #317, AC1+AC2): the same health command over vault + git-repo providers yields schema-valid reports (AC1); a backlog pass over the work-item adapter driven by a seeded fixture flags a duplicate and a broken blocker link (AC2) — never the real board."
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"id": "knowledge.promote-suite",
|
|
204
|
+
"path": "promote/promote.test.js",
|
|
205
|
+
"description": "Eval cases for the knowledge.promote sub-flow (issue #313): AC1 (a real-modeled completed session dir yields >=1 schema-valid draft decision delta with PR/merge-SHA/session-archive provenance, validated against the real #310 registry validator); AC2 (a seeded-contradiction registry fixture yields a duplicate-detection report naming BOTH topics + a schema-valid merge-repair proposal naming a merge target; a resolved supersede/merge pair is not re-flagged); AC3 (filesystem diff proves zero writes outside the session/proposal dir); R1 (the FlowDefinition exports every gate claim so a parent uses_flow step resolves it)."
|
|
156
206
|
}
|
|
157
207
|
],
|
|
158
208
|
"skills": [
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 2 — DISTILL.
|
|
3
|
+
*
|
|
4
|
+
* Turn the ingested residue into DRAFT deltas that validate BEFORE they are
|
|
5
|
+
* proposed (R2): decision-registry topic files (one per decision SUBJECT, the
|
|
6
|
+
* subject a CONTEXT.md vocabulary noun), CONTEXT.md vocabulary additions for any
|
|
7
|
+
* new subject noun, and docs/learnings entries. Nothing is written to disk here
|
|
8
|
+
* — distill returns draft objects; writing (only under <session>/proposals/)
|
|
9
|
+
* happens in the orchestrator.
|
|
10
|
+
*
|
|
11
|
+
* @module promote/distill
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { slugify, readGlossaryTerms, validateDecisionDelta } from "./lib.js";
|
|
15
|
+
|
|
16
|
+
function deriveSubject(decision) {
|
|
17
|
+
if (decision.subject) return decision.subject;
|
|
18
|
+
// Derive a rough noun-phrase subject from the outcome body (draft — the
|
|
19
|
+
// operator refines it at apply time). Take the leading clause, title-case.
|
|
20
|
+
const clause = String(decision.body || "").split(/[.;:]/)[0].trim().split(/\s+/).slice(0, 6).join(" ");
|
|
21
|
+
if (!clause) return "Delivered decision";
|
|
22
|
+
return clause.charAt(0).toUpperCase() + clause.slice(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} residue from ingestSession.
|
|
27
|
+
* @param {object} [options]
|
|
28
|
+
* @param {string} [options.repoRoot] repo whose CONTEXT.md is checked for vocabulary gaps.
|
|
29
|
+
* @param {string} [options.decided] ISO date to stamp on drafts (default: today).
|
|
30
|
+
* @returns {{ decisions: object[], vocabulary: object[], learnings: object[], warnings: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
export function distill(residue, options = {}) {
|
|
33
|
+
const decided = options.decided || new Date().toISOString().slice(0, 10);
|
|
34
|
+
const glossary = options.repoRoot ? readGlossaryTerms(options.repoRoot) : new Set();
|
|
35
|
+
const warnings = [];
|
|
36
|
+
|
|
37
|
+
const decisions = [];
|
|
38
|
+
const vocabulary = [];
|
|
39
|
+
const seenSlugs = new Set();
|
|
40
|
+
const seenVocab = new Set();
|
|
41
|
+
|
|
42
|
+
for (const raw of residue.decisions || []) {
|
|
43
|
+
const subject = deriveSubject(raw);
|
|
44
|
+
const slug = slugify(subject);
|
|
45
|
+
if (!slug || seenSlugs.has(slug)) continue;
|
|
46
|
+
seenSlugs.add(slug);
|
|
47
|
+
|
|
48
|
+
// Base provenance so the draft is already schema-valid at distill time
|
|
49
|
+
// (R2: validate BEFORE proposing). link() enriches with PR + merge SHA.
|
|
50
|
+
const evidence = [
|
|
51
|
+
{ kind: "session-archive", ref: `${residue.sessionDir}/${residue.sessionMarkdown || "state.json"}` },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const newVocabulary = !glossary.has(subject.toLowerCase());
|
|
55
|
+
const delta = {
|
|
56
|
+
subject,
|
|
57
|
+
slug,
|
|
58
|
+
status: "current",
|
|
59
|
+
decided,
|
|
60
|
+
body: raw.body || subject,
|
|
61
|
+
evidence,
|
|
62
|
+
derived: !!raw.derived,
|
|
63
|
+
newVocabulary,
|
|
64
|
+
};
|
|
65
|
+
const errors = validateDecisionDelta(delta);
|
|
66
|
+
if (errors.length) {
|
|
67
|
+
warnings.push(`decision draft '${slug}' failed pre-proposal validation: ${errors.join("; ")}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
decisions.push(delta);
|
|
71
|
+
|
|
72
|
+
if (newVocabulary && !seenVocab.has(slug)) {
|
|
73
|
+
seenVocab.add(slug);
|
|
74
|
+
vocabulary.push({
|
|
75
|
+
term: subject,
|
|
76
|
+
slug,
|
|
77
|
+
definition: (raw.body || subject).split(/[.;]/)[0].trim(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const learnings = (residue.learnings || []).map((rec, i) => ({
|
|
83
|
+
id: rec.id || `learn-${residue.slug}-${i + 1}`,
|
|
84
|
+
title: rec.summary ? rec.summary.split(/[.;]/)[0].trim() : `Learning ${i + 1}`,
|
|
85
|
+
body: rec.summary || "",
|
|
86
|
+
outcome: rec.outcome || null,
|
|
87
|
+
source_refs: Array.isArray(rec.source_refs) ? rec.source_refs : [],
|
|
88
|
+
correction: rec.correction && rec.correction.needed ? {
|
|
89
|
+
type: rec.correction.type || null,
|
|
90
|
+
recurrence_key: rec.correction.recurrence_key || null,
|
|
91
|
+
prevention: rec.correction.prevention || null,
|
|
92
|
+
} : null,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
return { decisions, vocabulary, learnings, warnings };
|
|
96
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: current
|
|
3
|
+
subject: Cache eviction policy
|
|
4
|
+
decided: 2026-06-10
|
|
5
|
+
evidence:
|
|
6
|
+
- kind: issue
|
|
7
|
+
ref: https://github.com/kontourai/flow-agents/issues/401
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Cache eviction policy
|
|
11
|
+
|
|
12
|
+
Entries are evicted using a least-recently-used (LRU) policy.
|
package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: current
|
|
3
|
+
subject: Cache eviction policy
|
|
4
|
+
decided: 2026-06-25
|
|
5
|
+
evidence:
|
|
6
|
+
- kind: issue
|
|
7
|
+
ref: https://github.com/kontourai/flow-agents/issues/415
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Cache eviction policy
|
|
11
|
+
|
|
12
|
+
Entries are evicted using a least-frequently-used (LFU) policy.
|