@kontourai/flow-agents 1.4.0 → 2.0.1
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 +29 -0
- package/.github/actions/trust-verify/action.yml +145 -0
- package/.github/workflows/ci.yml +11 -4
- package/.github/workflows/kit-gates-demo.yml +2 -2
- package/.github/workflows/publish-npm.yml +10 -2
- package/.github/workflows/release-please.yml +1 -1
- package/.github/workflows/runtime-compat.yml +1 -1
- package/.github/workflows/trust-reconcile.yml +113 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +103 -0
- package/CONTRIBUTING.md +4 -4
- package/README.md +1 -0
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/init.js +242 -20
- package/build/src/cli/validate-workflow-artifacts.js +19 -2
- package/build/src/cli/verify.d.ts +1 -0
- package/build/src/cli/verify.js +90 -0
- package/build/src/cli/workflow-sidecar.d.ts +316 -8
- package/build/src/cli/workflow-sidecar.js +1996 -91
- package/build/src/cli.js +2 -3
- package/build/src/lib/flow-resolver.d.ts +111 -0
- package/build/src/lib/flow-resolver.js +308 -0
- package/build/src/tools/build-universal-bundles.js +34 -22
- package/build/src/tools/generate-context-map.js +3 -16
- package/build/src/tools/validate-source-tree.d.ts +1 -1
- package/build/src/tools/validate-source-tree.js +42 -162
- package/context/contracts/artifact-contract.md +10 -0
- package/context/contracts/delivery-contract.md +1 -0
- package/context/contracts/review-contract.md +1 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/gate-awareness.md +39 -0
- package/context/scripts/hooks/stop-goal-fit.js +632 -70
- package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
- package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
- 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/0007-skill-audit.md +1 -1
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
- package/docs/adr/0011-mcp-posture.md +100 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
- package/docs/adr/0013-context-lifecycle.md +151 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
- package/docs/adr/0016-three-hard-boundary-model.md +71 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
- package/docs/agent-system-guidebook.md +5 -12
- package/docs/context-map.md +4 -10
- package/docs/index.md +3 -2
- package/docs/integrations/framework-adapter.md +19 -6
- package/docs/integrations/index.md +2 -2
- package/docs/north-star.md +4 -4
- package/docs/operating-layers.md +3 -3
- package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
- package/docs/repository-structure.md +2 -2
- package/docs/skills-map.md +1 -0
- package/docs/spec/runtime-hook-surface.md +62 -9
- package/docs/standards-register.md +3 -3
- package/docs/survey-utterance-check.md +1 -1
- package/docs/trust-anchor-adoption.md +197 -0
- package/docs/verifiable-trust.md +95 -0
- package/docs/veritas-integration.md +2 -2
- package/docs/workflow-usage-guide.md +69 -0
- package/evals/acceptance/DEMO-false-completion.md +144 -0
- package/evals/acceptance/demo-cast.sh +92 -0
- package/evals/acceptance/demo-false-completion.sh +72 -0
- package/evals/acceptance/demo-real-evidence.sh +104 -0
- package/evals/acceptance/demo.tape +29 -0
- package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
- package/evals/acceptance/prove-capture-teeth.sh +114 -0
- package/evals/acceptance/prove-teeth.sh +105 -0
- package/evals/ci/antigaming-suite.sh +55 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
- package/evals/integration/test_builder_step_producers.sh +379 -0
- package/evals/integration/test_bundle_install.sh +35 -71
- package/evals/integration/test_bundle_lifecycle.sh +39 -2
- package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
- package/evals/integration/test_checkpoint_signing.sh +489 -0
- package/evals/integration/test_claim_lookup.sh +352 -0
- package/evals/integration/test_command_log_fork_classification.sh +134 -0
- package/evals/integration/test_command_log_integrity.sh +275 -0
- package/evals/integration/test_context_map.sh +0 -2
- package/evals/integration/test_dual_emit_flow_step.sh +278 -0
- package/evals/integration/test_enforcer_expects_driven.sh +281 -0
- package/evals/integration/test_evidence_capture_hook.sh +185 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +273 -0
- package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
- package/evals/integration/test_gate_bypass_chain.sh +448 -0
- package/evals/integration/test_gate_lockdown.sh +1137 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
- package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
- package/evals/integration/test_goal_fit_hook.sh +69 -4
- package/evals/integration/test_goal_fit_rederive.sh +263 -0
- package/evals/integration/test_install_merge.sh +1176 -0
- package/evals/integration/test_kit_identity_trust.sh +393 -0
- package/evals/integration/test_mint_attestation.sh +373 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
- package/evals/integration/test_publish_delivery.sh +269 -0
- package/evals/integration/test_reconcile_soundness.sh +528 -0
- package/evals/integration/test_resolvefirststep_security.sh +208 -0
- package/evals/integration/test_session_resume_roundtrip.sh +286 -0
- package/evals/integration/test_trust_checkpoint.sh +325 -0
- package/evals/integration/test_trust_reconcile.sh +293 -0
- package/evals/integration/test_verify_cli.sh +208 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
- package/evals/lib/node.sh +0 -6
- package/evals/run.sh +47 -0
- package/evals/static/test_workflow_skills.sh +6 -13
- package/install.sh +0 -7
- package/integrations/strands-ts/README.md +25 -15
- package/integrations/veritas/flow-agents.adapter.json +1 -2
- package/kits/builder/flows/build.flow.json +59 -12
- package/kits/builder/kit.json +85 -15
- package/kits/builder/skills/continue-work/SKILL.md +116 -0
- package/kits/builder/skills/deliver/SKILL.md +36 -6
- package/kits/builder/skills/design-probe/SKILL.md +28 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -1
- package/kits/builder/skills/gate-review/SKILL.md +234 -0
- package/kits/builder/skills/learning-review/SKILL.md +30 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
- package/kits/builder/skills/plan-work/SKILL.md +13 -1
- package/kits/builder/skills/pull-work/SKILL.md +19 -0
- package/kits/knowledge/adapters/default-store/index.js +38 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
- package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
- package/kits/knowledge/docs/store-contract.md +314 -0
- package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
- package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
- package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
- package/kits/knowledge/evals/entities/suite.test.js +40 -0
- package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
- package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
- package/kits/knowledge/evals/retirement/suite.test.js +145 -0
- package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
- package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
- package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
- package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
- package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
- package/kits/knowledge/kit.json +51 -1
- package/package.json +6 -6
- package/packaging/conformance/README.md +10 -2
- package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
- package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
- package/packaging/conformance/run-conformance.js +1 -1
- package/scripts/README.md +2 -1
- package/scripts/build-universal-bundles.js +0 -1
- package/scripts/ci/mint-attestation.js +221 -0
- package/scripts/ci/trust-reconcile.js +545 -0
- package/scripts/hooks/config-protection.js +423 -1
- package/scripts/hooks/evidence-capture.js +348 -0
- package/scripts/hooks/lib/liveness-read.js +113 -0
- package/scripts/hooks/run-hook.js +6 -1
- package/scripts/hooks/stop-goal-fit.js +1524 -79
- package/scripts/hooks/workflow-steering.js +135 -5
- package/scripts/install-codex-home.sh +39 -0
- package/scripts/install-merge.js +330 -0
- package/scripts/repair-command-log.js +115 -0
- package/src/cli/init.ts +218 -20
- package/src/cli/validate-workflow-artifacts.ts +18 -2
- package/src/cli/verify.ts +100 -0
- package/src/cli/workflow-sidecar.ts +2127 -84
- package/src/cli.ts +2 -3
- package/src/lib/flow-resolver.ts +369 -0
- package/src/tools/build-universal-bundles.ts +34 -21
- package/src/tools/generate-context-map.ts +3 -17
- package/src/tools/validate-source-tree.ts +44 -104
- package/build/src/tools/filter-installed-packs.d.ts +0 -2
- package/build/src/tools/filter-installed-packs.js +0 -135
- package/packaging/packs.json +0 -49
- package/scripts/filter-installed-packs.js +0 -2
- package/src/tools/filter-installed-packs.ts +0 -132
|
@@ -142,6 +142,32 @@ export class ObsidianKnowledgeStore {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a store-relative path from the persisted path index.
|
|
147
|
+
*
|
|
148
|
+
* The path index is local metadata, but it may be tampered with. Never feed
|
|
149
|
+
* an indexed path directly to fs/path helpers without this containment check.
|
|
150
|
+
*/
|
|
151
|
+
_resolveStorePath(relPath) {
|
|
152
|
+
if (typeof relPath !== "string" || !relPath) {
|
|
153
|
+
throw new Error("Invalid store path in path index");
|
|
154
|
+
}
|
|
155
|
+
if (path.isAbsolute(relPath)) {
|
|
156
|
+
throw new Error(`Path index entry escapes store root: ${relPath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const absPath = path.resolve(this._root, relPath);
|
|
160
|
+
const relativeToRoot = path.relative(this._root, absPath);
|
|
161
|
+
if (
|
|
162
|
+
relativeToRoot === ".."
|
|
163
|
+
|| relativeToRoot.startsWith(`..${path.sep}`)
|
|
164
|
+
|| path.isAbsolute(relativeToRoot)
|
|
165
|
+
) {
|
|
166
|
+
throw new Error(`Path index entry escapes store root: ${relPath}`);
|
|
167
|
+
}
|
|
168
|
+
return absPath;
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
// -------------------------------------------------------------------------
|
|
146
172
|
// Path index I/O
|
|
147
173
|
// -------------------------------------------------------------------------
|
|
@@ -230,7 +256,7 @@ export class ObsidianKnowledgeStore {
|
|
|
230
256
|
_getAbsPath(id, pathIndex) {
|
|
231
257
|
const entry = (pathIndex || this._loadPathIndex()).by_id[id];
|
|
232
258
|
if (!entry) return null;
|
|
233
|
-
return
|
|
259
|
+
return this._resolveStorePath(entry.path);
|
|
234
260
|
}
|
|
235
261
|
|
|
236
262
|
/**
|
|
@@ -266,24 +292,28 @@ export class ObsidianKnowledgeStore {
|
|
|
266
292
|
|
|
267
293
|
if (existingEntry && existingEntry.archived) {
|
|
268
294
|
// Archived record — keep in archive path (supersede-not-delete writes back there)
|
|
295
|
+
this._resolveStorePath(existingEntry.path);
|
|
269
296
|
targetRelPath = existingEntry.path;
|
|
270
297
|
} else if (existingEntry) {
|
|
271
298
|
// Existing active record — check if path needs to change (title changed)
|
|
272
299
|
const newRelPath = this._computeRelPath(record.category, record.title, record.id, pathIndex, record.type);
|
|
300
|
+
this._resolveStorePath(newRelPath);
|
|
273
301
|
if (newRelPath !== existingEntry.path) {
|
|
274
302
|
// Move: delete old file, register new path
|
|
275
|
-
const oldAbs =
|
|
303
|
+
const oldAbs = this._resolveStorePath(existingEntry.path);
|
|
276
304
|
if (fs.existsSync(oldAbs)) fs.unlinkSync(oldAbs);
|
|
277
305
|
delete pathIndex.by_path[existingEntry.path];
|
|
278
306
|
pathIndex.by_id[record.id] = { path: newRelPath, archived: false };
|
|
279
307
|
pathIndex.by_path[newRelPath] = record.id;
|
|
280
308
|
targetRelPath = newRelPath;
|
|
281
309
|
} else {
|
|
310
|
+
this._resolveStorePath(existingEntry.path);
|
|
282
311
|
targetRelPath = existingEntry.path;
|
|
283
312
|
}
|
|
284
313
|
} else {
|
|
285
314
|
// New record
|
|
286
315
|
const newRelPath = this._computeRelPath(record.category, record.title, record.id, pathIndex, record.type);
|
|
316
|
+
this._resolveStorePath(newRelPath);
|
|
287
317
|
pathIndex.by_id[record.id] = { path: newRelPath, archived: false };
|
|
288
318
|
pathIndex.by_path[newRelPath] = record.id;
|
|
289
319
|
targetRelPath = newRelPath;
|
|
@@ -303,7 +333,7 @@ export class ObsidianKnowledgeStore {
|
|
|
303
333
|
const obsidianBody = this._renderObsidianBody(record, pathIndex);
|
|
304
334
|
const text = `---\n${serializeYaml(frontmatter)}\n---\n\n${obsidianBody}`;
|
|
305
335
|
|
|
306
|
-
const absPath =
|
|
336
|
+
const absPath = this._resolveStorePath(targetRelPath);
|
|
307
337
|
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
308
338
|
fs.writeFileSync(absPath, text, "utf8");
|
|
309
339
|
|
|
@@ -326,10 +356,10 @@ export class ObsidianKnowledgeStore {
|
|
|
326
356
|
}
|
|
327
357
|
|
|
328
358
|
const archiveRelPath = `archive/${entry.path}`;
|
|
329
|
-
const archiveAbs =
|
|
359
|
+
const archiveAbs = this._resolveStorePath(archiveRelPath);
|
|
330
360
|
fs.mkdirSync(path.dirname(archiveAbs), { recursive: true });
|
|
331
361
|
|
|
332
|
-
const currentAbs =
|
|
362
|
+
const currentAbs = this._resolveStorePath(entry.path);
|
|
333
363
|
if (fs.existsSync(currentAbs)) fs.renameSync(currentAbs, archiveAbs);
|
|
334
364
|
|
|
335
365
|
delete pathIndex.by_path[entry.path];
|
|
@@ -350,7 +380,7 @@ export class ObsidianKnowledgeStore {
|
|
|
350
380
|
_idToFilename(id, pathIndex) {
|
|
351
381
|
const entry = pathIndex.by_id[id];
|
|
352
382
|
if (!entry) return id;
|
|
353
|
-
return path.basename(entry.path, ".md");
|
|
383
|
+
return path.basename(this._resolveStorePath(entry.path), ".md");
|
|
354
384
|
}
|
|
355
385
|
|
|
356
386
|
/**
|
|
@@ -167,6 +167,20 @@ The default adapter stores the index as `graph-index.json` at the store root.
|
|
|
167
167
|
}
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
### 5.2 Reindex (recovery)
|
|
171
|
+
|
|
172
|
+
The graph index is a **derived cache**: each record's own `links` array is the source of truth.
|
|
173
|
+
An adapter MAY expose a `reindex()` recovery operation that rebuilds the index from scratch by
|
|
174
|
+
scanning every record's `links` — the supported way to recover from a lost, hand-edited, or
|
|
175
|
+
drifted `graph-index.json`.
|
|
176
|
+
|
|
177
|
+
| Operation | Signature | Behavior |
|
|
178
|
+
| --- | --- | --- |
|
|
179
|
+
| `reindex` | `() => { records, links, forwardSources, reverseTargets, changed }` | Rebuild the index authoritatively from records' `links`. Deterministic (records processed in id order). `changed` is true when the rebuilt index differs from the one on disk (compared order-independently), so callers can detect drift. Does **not** mutate records or append to the mutation log — it only repairs the derived cache. |
|
|
180
|
+
|
|
181
|
+
`reindex()` is optional in the contract (not every adapter maintains a separate index), but any
|
|
182
|
+
adapter that persists a derived index SHOULD provide it as the recovery path.
|
|
183
|
+
|
|
170
184
|
---
|
|
171
185
|
|
|
172
186
|
## 6. Mutation Operations
|
|
@@ -649,6 +663,37 @@ Retired records MUST remain reachable from:
|
|
|
649
663
|
There is no deletion of records. Physical purge (if ever needed) is a separate, future policy
|
|
650
664
|
hook not defined in this version.
|
|
651
665
|
|
|
666
|
+
### B.7 Proposal-Artifact Lifecycle (close-on-apply)
|
|
667
|
+
|
|
668
|
+
A flow that gates a change through `propose` → `apply` mints a transient **proposal artifact**:
|
|
669
|
+
a `raw` record (e.g. the `knowledge.retire` flow's `"Retirement proposal: <title>"` record) whose
|
|
670
|
+
sole purpose is to carry the `"proposes"` link and the proposal text to the target.
|
|
671
|
+
|
|
672
|
+
Once the proposal has been **applied**, that artifact is *spent*. It MUST NOT be left as an
|
|
673
|
+
`active` record:
|
|
674
|
+
|
|
675
|
+
- A lingering active artifact pollutes the default working set (`listByType` / `listByCategory`)
|
|
676
|
+
and is re-surfaced by hygiene sweeps.
|
|
677
|
+
- Hand-retiring it spawns a double-prefixed twin
|
|
678
|
+
(`"Retirement proposal: Retirement proposal: …"`), because the retire flow mints a *new*
|
|
679
|
+
proposal artifact for the artifact itself (#106).
|
|
680
|
+
|
|
681
|
+
**Rule.** On `apply` (and only on `apply`), the flow auto-closes the spent proposal artifact by
|
|
682
|
+
**retiring it via the existing `retire` op** (§B.4) — `active → retired`. There is no separate
|
|
683
|
+
"close" mutation; the close reuses `retire`. This close is:
|
|
684
|
+
|
|
685
|
+
- **safe** — it touches only the named artifact, never the apply target;
|
|
686
|
+
- **idempotent** — a no-op if the artifact is already retired/implemented (the §B.2 transition
|
|
687
|
+
table makes `retired` terminal, so a re-close is rejected and treated as a no-op);
|
|
688
|
+
- **non-fatal** — the proposal has already been applied; a failure to close the artifact does not
|
|
689
|
+
fail the flow (it is surfaced on the result instead).
|
|
690
|
+
|
|
691
|
+
**`reject` is unchanged.** A rejected proposal is *not* spent — the artifact remains `active` so the
|
|
692
|
+
proposal can be revisited. Closing happens only on the apply path.
|
|
693
|
+
|
|
694
|
+
Do not hand-retire proposal artifacts: applying the proposal closes them. The retire lifecycle
|
|
695
|
+
expects the artifact to exist transiently and to be auto-closed on apply.
|
|
696
|
+
|
|
652
697
|
---
|
|
653
698
|
|
|
654
699
|
## Addendum C — Person Record Type (Entity Cards)
|
|
@@ -720,3 +765,272 @@ Card merge uses the existing `propose → apply/reject` gate:
|
|
|
720
765
|
- **apply**: unions body, adds `alias:<duplicate title>` tag to primary, unions `appears-in` links, calls
|
|
721
766
|
`store.supersede(primaryId, [duplicateId])` to archive the duplicate (supersede-not-delete invariant).
|
|
722
767
|
- **reject**: both cards remain byte-identical.
|
|
768
|
+
|
|
769
|
+
## Addendum D — Freshness Audit (Hygiene #1, #106)
|
|
770
|
+
|
|
771
|
+
### D.1 Read-Only Maintenance Layer
|
|
772
|
+
|
|
773
|
+
`knowledge.audit-freshness` is the first of the Knowledge Kit's *maintenance* flows. The Kit is
|
|
774
|
+
strong at filing (ingest → compile → synthesize → consolidate → retire) but, until this slice, had
|
|
775
|
+
no way to surface records that have gone stale. The audit is a **read-only** survey: it NEVER
|
|
776
|
+
mutates a record. It returns *flags* proposing an action; the operator routes each flag through an
|
|
777
|
+
existing gated flow — `knowledge.retire` to archive, or a fresh `capture`/`compile` to refresh. The
|
|
778
|
+
audit forks no new mutation path.
|
|
779
|
+
|
|
780
|
+
Staleness is domain-sensitive (a `radar` signal goes stale in days; a `decisions` record may stay
|
|
781
|
+
canonical for a year), so the audit is **optional and configurable** — thresholds are supplied per
|
|
782
|
+
call. A category with no configured threshold (and no default) is simply skipped: auditing is
|
|
783
|
+
**opt-in**.
|
|
784
|
+
|
|
785
|
+
### D.2 `auditFreshness` Flow-Runner Operation
|
|
786
|
+
|
|
787
|
+
`KnowledgeFlowRunner.auditFreshness(options)` (also the module-level `auditFreshness({ store, ... })`):
|
|
788
|
+
|
|
789
|
+
**Options:**
|
|
790
|
+
|
|
791
|
+
| Option | Type | Default | Description |
|
|
792
|
+
|---|---|---|---|
|
|
793
|
+
| `thresholds` | `{ [category]: number }` | `{}` | Per-category staleness threshold in **days**. |
|
|
794
|
+
| `defaultThresholdDays` | `number` | none | Fallback for categories not matched by `thresholds`. When absent, unmatched categories are skipped. |
|
|
795
|
+
| `actions` | `{ [category]: "archive"\|"refresh" }` | `{}` | Per-category override of the proposed action. |
|
|
796
|
+
| `defaultAction` | `"archive"\|"refresh"` | `"refresh"` | Proposed action when no per-category action matches. |
|
|
797
|
+
| `types` | `string[]` | `["raw","compiled","concept","snapshot"]` | Record types to audit. |
|
|
798
|
+
| `now` | `string\|number\|Date` | current time | Reference "now" for the age computation (injectable for tests). |
|
|
799
|
+
| `agent` | `string` | runner agent | Agent recorded on the audit telemetry. |
|
|
800
|
+
|
|
801
|
+
**Threshold / action resolution** is dot-hierarchy **longest-prefix**: a record in
|
|
802
|
+
`radar.signals.weak` prefers a `radar.signals` entry over a `radar` one, then falls back to the
|
|
803
|
+
default. The matched key is surfaced on each flag as `matchedThresholdKey` (`"*"` denotes the
|
|
804
|
+
default).
|
|
805
|
+
|
|
806
|
+
**Last mutation** is the most recent of the record's `updated_at` and its latest `mutation_log`
|
|
807
|
+
entry `at` — both are refreshed by every mutating op (§1.1 / §4.2), so the later of the two is
|
|
808
|
+
authoritative even if an adapter lags one. It falls back to `created_at` when neither is present.
|
|
809
|
+
|
|
810
|
+
**Flagging:** a record is flagged only when its age in **whole days** *strictly exceeds* its
|
|
811
|
+
resolved threshold (`ageDays > thresholdDays`). Retired records are never flagged — the default
|
|
812
|
+
`listByType` query excludes them, and `retired` is terminal.
|
|
813
|
+
|
|
814
|
+
### D.3 Flag Evidence Guarantee
|
|
815
|
+
|
|
816
|
+
Every flag carries the evidence that produced it — a flag can never be emitted without citing both
|
|
817
|
+
the last-mutation instant and the threshold that fired:
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
interface FreshnessFlag {
|
|
821
|
+
recordId: string;
|
|
822
|
+
title: string;
|
|
823
|
+
type: string;
|
|
824
|
+
category: string;
|
|
825
|
+
status: string;
|
|
826
|
+
lastMutationAt: string; // ISO-8601 — the cited last mutation
|
|
827
|
+
ageDays: number; // whole days since lastMutationAt
|
|
828
|
+
thresholdDays: number; // the threshold that fired
|
|
829
|
+
matchedThresholdKey: string; // category key the threshold matched ("*" = default)
|
|
830
|
+
proposedAction: "archive" | "refresh";
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
`auditFreshness` returns `{ audited, skipped, flags, telemetryEvents }` where `audited` counts
|
|
835
|
+
records that had a resolvable threshold, `skipped` counts opt-out categories, and `flags` lists the
|
|
836
|
+
stale records. Gate telemetry is emitted at `collect-gate` and `flag-gate`
|
|
837
|
+
(`knowledge.audit-freshness`).
|
|
838
|
+
|
|
839
|
+
## Addendum E — Category Canonicalization (Hygiene #4, #106)
|
|
840
|
+
|
|
841
|
+
### E.1 Read-Only Category-Sprawl Audit
|
|
842
|
+
|
|
843
|
+
`knowledge.canonicalize-category` is a *maintenance* flow, like the freshness audit (Addendum D): a
|
|
844
|
+
**read-only** survey that NEVER mutates a record. Where freshness measures records against *time*,
|
|
845
|
+
this audit measures the *shape of the category hierarchy*. As a Knowledge base grows by filing, its
|
|
846
|
+
category tree degrades in concrete ways the dogfooding surfaced (#106) — orphan prefixes, parents
|
|
847
|
+
that fan out into too many leaves, and records that were implemented but never retired. The audit
|
|
848
|
+
returns *findings* proposing a fix; the operator routes each through an existing gated flow —
|
|
849
|
+
`knowledge.retire` to retire, or an `update` (recategorize) to flatten/regroup. The audit forks no
|
|
850
|
+
new mutation path.
|
|
851
|
+
|
|
852
|
+
Sprawl is domain-sensitive (a flat radar feed differs from a deep decisions taxonomy), so each
|
|
853
|
+
check is **optional and configurable**: a disabled check — or an empty implemented-marker list —
|
|
854
|
+
contributes no findings. The audit is **opt-in** per check.
|
|
855
|
+
|
|
856
|
+
### E.2 `canonicalizeCategory` Flow-Runner Operation
|
|
857
|
+
|
|
858
|
+
`KnowledgeFlowRunner.canonicalizeCategory(options)` (also the module-level
|
|
859
|
+
`canonicalizeCategory({ store, ... })`):
|
|
860
|
+
|
|
861
|
+
**Options:**
|
|
862
|
+
|
|
863
|
+
| Option | Type | Default | Description |
|
|
864
|
+
|---|---|---|---|
|
|
865
|
+
| `checkOrphanPrefixes` | `boolean` | `true` | Enable the orphan-prefix check. |
|
|
866
|
+
| `maxLeavesPerParent` | `number` | none | Leaf fan-out budget; a parent with strictly more direct child leaf categories is flagged. Omit to disable the check. |
|
|
867
|
+
| `implementedMarkers` | `string[]` | `[]` | Tag markers meaning "implemented" (case-insensitive). Empty → the implemented-active check is disabled. |
|
|
868
|
+
| `types` | `string[]` | `["raw","compiled","concept","snapshot"]` | Record types to survey. |
|
|
869
|
+
| `agent` | `string` | runner agent | Agent recorded on the audit telemetry. |
|
|
870
|
+
|
|
871
|
+
**Finding kinds** (each independently toggleable):
|
|
872
|
+
|
|
873
|
+
- **`orphan-prefix`** (`proposedAction: "flatten"`) — an intermediate prefix node that holds **no
|
|
874
|
+
record directly** while it has descendants (`metric: "empty-intermediate-node"`), OR a
|
|
875
|
+
multi-segment prefix whose **entire subtree is a single record** carried directly there
|
|
876
|
+
(`metric: "single-record-deep-path"`) — depth without branching value.
|
|
877
|
+
- **`too-many-leaves`** (`proposedAction: "regroup"`) — a parent prefix whose count of direct child
|
|
878
|
+
**leaf** categories *strictly exceeds* `maxLeavesPerParent` (`metric: "leaf-fan-out"`). A leaf is
|
|
879
|
+
a record-bearing category with no record-bearing descendant. The finding lists the offending
|
|
880
|
+
leaves.
|
|
881
|
+
- **`implemented-active`** (`proposedAction: "retire"`) — a record still `status:"active"` that
|
|
882
|
+
carries an `implementedMarkers` tag (`metric: "implemented-marker-on-active"`); it should have
|
|
883
|
+
transitioned via `retire` (§B.4) but lingers in the working set.
|
|
884
|
+
|
|
885
|
+
Retired records are never flagged — the default `listByType` query excludes them, and `retired` is
|
|
886
|
+
terminal (so it is not sprawl to flatten).
|
|
887
|
+
|
|
888
|
+
### E.3 Finding Evidence Guarantee
|
|
889
|
+
|
|
890
|
+
Every finding carries the evidence that produced it — a finding can never be emitted without citing
|
|
891
|
+
the metric that fired and the offending category / record ids:
|
|
892
|
+
|
|
893
|
+
```ts
|
|
894
|
+
interface CategoryFinding {
|
|
895
|
+
kind: "orphan-prefix" | "too-many-leaves" | "implemented-active";
|
|
896
|
+
category: string; // the offending category / parent prefix
|
|
897
|
+
recordIds: string[]; // the affected record ids
|
|
898
|
+
metric: string; // the rule that fired
|
|
899
|
+
evidence: object; // rule-specific (counts, leaf list, matched markers, reason)
|
|
900
|
+
proposedAction: "flatten" | "regroup" | "retire";
|
|
901
|
+
}
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
`canonicalizeCategory` returns `{ surveyed, categories, findings, telemetryEvents }` where
|
|
905
|
+
`surveyed` counts the records examined, `categories` counts the distinct category prefixes in the
|
|
906
|
+
tree, and `findings` lists the sprawl. Gate telemetry is emitted at `survey-gate` and `propose-gate`
|
|
907
|
+
(`knowledge.canonicalize-category`).
|
|
908
|
+
|
|
909
|
+
## Addendum F — Glossary Sync (Hygiene #3, #106)
|
|
910
|
+
|
|
911
|
+
### F.1 Keeping the Glossary in Sync with Canonical Docs
|
|
912
|
+
|
|
913
|
+
`knowledge.glossary-sync` is a *maintenance* flow that keeps the **glossary** — the working set of
|
|
914
|
+
`concept` records — in sync with the **canonical docs** that define those terms. The Kit can file
|
|
915
|
+
concepts but, until this slice, had no way to (a) promote a term that a canonical doc defines but no
|
|
916
|
+
concept yet captures (a **gap**), or (b) notice when a concept's definition has **drifted** from its
|
|
917
|
+
canonical source (**out-of-date**).
|
|
918
|
+
|
|
919
|
+
The flow surveys a **configurable** glossary source list (the issue's "glossary source list") and is
|
|
920
|
+
**opt-in**: an empty/absent source list does nothing. It is **read-only by default** — it returns a
|
|
921
|
+
classification plan and mutates nothing; `apply: true` enacts the plan through the **existing**
|
|
922
|
+
concept-record ops (no forked mutation path).
|
|
923
|
+
|
|
924
|
+
### F.2 `glossarySync` Flow-Runner Operation
|
|
925
|
+
|
|
926
|
+
`KnowledgeFlowRunner.glossarySync(options)` (also module-level `glossarySync({ store, ... })`):
|
|
927
|
+
|
|
928
|
+
| Option | Type | Default | Description |
|
|
929
|
+
|---|---|---|---|
|
|
930
|
+
| `sources` | `Array<string \| { category, prefix? }>` | `[]` | The configurable glossary source list: each entry is a canonical-doc record id, or a category selector. An unknown id is rejected (the list is evidence). Empty → opt-in no-op. |
|
|
931
|
+
| `termExtractor` | `(doc) => Array<{ term, definition }>` | `defaultTermExtractor` | Pluggable extractor; the default parses glossary-style lines (`**Term** — def`, `Term: def`, list items). |
|
|
932
|
+
| `conceptCategory` | `string` | source doc's category | Category for matched/proposed concepts. |
|
|
933
|
+
| `apply` | `boolean` | `false` | `false` → read-only plan. `true` → enact via store ops. |
|
|
934
|
+
|
|
935
|
+
**Classification.** Each extracted term is matched against existing `concept` records by **normalized
|
|
936
|
+
term** (case/space-insensitive title) within the resolved category:
|
|
937
|
+
|
|
938
|
+
- **gap** — no concept captures the term.
|
|
939
|
+
- **outdated** — a concept exists but its body has **drifted** from the canonical definition.
|
|
940
|
+
Drift is **whitespace-insensitive** (cosmetic reflow is not drift; substantive change is).
|
|
941
|
+
- **current** — the concept matches the canonical definition.
|
|
942
|
+
|
|
943
|
+
### F.3 Consume-Never-Fork Enactment
|
|
944
|
+
|
|
945
|
+
In `apply` mode the plan is enacted through the existing gated ops, with the **canonical doc as the
|
|
946
|
+
proposer** (it is the evidence for the definition) — no new mutation path is forked:
|
|
947
|
+
|
|
948
|
+
- **gap** → `store.create(concept)` then `store.propose` + `store.apply` (doc → concept).
|
|
949
|
+
- **outdated** → `store.propose` + `store.apply` on the existing concept (`new_body` = canonical).
|
|
950
|
+
|
|
951
|
+
`glossarySync` returns `{ sourcesAudited, entries, gaps, outdated, current, applied, telemetryEvents }`.
|
|
952
|
+
Every classified entry cites its evidence — the source doc id + title, the term, and the canonical
|
|
953
|
+
definition (outdated entries also cite the drifted `currentBody`). Gate telemetry is emitted at
|
|
954
|
+
`collect-gate`, `diff-gate`, and `propose-gate` (`knowledge.glossary-sync`).
|
|
955
|
+
|
|
956
|
+
## Addendum G — Contradiction Detection (Hygiene #2, #106)
|
|
957
|
+
|
|
958
|
+
### G.1 Read-Only Maintenance Layer
|
|
959
|
+
|
|
960
|
+
`knowledge.detect-contradictions` is the second of the Knowledge Kit's *maintenance* flows. Like
|
|
961
|
+
`audit-freshness` (Addendum D) it is a **read-only** survey: it NEVER mutates a record. It returns
|
|
962
|
+
*flags* identifying a conflicting pair; the operator routes each through an existing gated flow —
|
|
963
|
+
`knowledge.retire` to drop the stale assertion, or a fresh `capture`/`compile`/`consolidate` to
|
|
964
|
+
reconcile. The audit forks no new mutation path.
|
|
965
|
+
|
|
966
|
+
Contradiction is domain-sensitive (what counts as a conflict in `radar` differs from `decisions`),
|
|
967
|
+
so the audit is **optional and configurable**: it audits only the categories supplied in
|
|
968
|
+
`categories` (or, when omitted, every category present in the compiled set), and it judges conflicts
|
|
969
|
+
with a **pluggable contradiction fn**.
|
|
970
|
+
|
|
971
|
+
### G.2 `detectContradictions` Flow-Runner Operation
|
|
972
|
+
|
|
973
|
+
`KnowledgeFlowRunner.detectContradictions(options)` (also the module-level
|
|
974
|
+
`detectContradictions({ store, ... })`) compares **compiled** records **within a category** and
|
|
975
|
+
flags conflicting assertions. Comparison is two-staged and reuses the Kit's existing pluggable
|
|
976
|
+
adapters (consume-never-fork):
|
|
977
|
+
|
|
978
|
+
1. **Scope** with the `SimilarityDetector` (the same interface `synthesize` uses) — only records the
|
|
979
|
+
detector deems similar (about the same thing) are candidates. The vector similarity adapter drops
|
|
980
|
+
straight in.
|
|
981
|
+
2. **Judge** with the `ContradictionDetector` — for each similar pair, decide whether the assertions
|
|
982
|
+
conflict.
|
|
983
|
+
|
|
984
|
+
**Options:**
|
|
985
|
+
|
|
986
|
+
| Option | Type | Default | Description |
|
|
987
|
+
|---|---|---|---|
|
|
988
|
+
| `categories` | `string[]` | all present | Categories to audit. Opt-in scoping; omit to audit every category in the compiled set. |
|
|
989
|
+
| `similarityDetector` | `fn` | `defaultSimilarityDetector` | Pluggable `(record, candidates, store) => string[]` — scopes which pairs are compared. |
|
|
990
|
+
| `contradictionDetector` | `fn` | `defaultContradictionDetector` | Pluggable `(recordA, recordB, store?) => null \| { reason }` — judges whether a pair conflicts. May be async. |
|
|
991
|
+
| `agent` | `string` | runner agent | Agent recorded on the audit telemetry. |
|
|
992
|
+
|
|
993
|
+
**Comparison scope:** retired records are excluded (the default `listByType` query drops them, and
|
|
994
|
+
`retired` is terminal); records are grouped by exact category, so cross-category pairs are never
|
|
995
|
+
formed; each unordered pair is compared at most once.
|
|
996
|
+
|
|
997
|
+
### G.3 `ContradictionDetector` Interface and the Default
|
|
998
|
+
|
|
999
|
+
```ts
|
|
1000
|
+
type ContradictionDetector = (
|
|
1001
|
+
recordA: Record,
|
|
1002
|
+
recordB: Record,
|
|
1003
|
+
store?: KnowledgeStoreAdapter
|
|
1004
|
+
) => (null | { reason: string }) | Promise<null | { reason: string }>;
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
The two records passed in are already known to be *about the same thing* (the caller scopes by
|
|
1008
|
+
category + similarity). The detector's only job is to decide whether their assertions conflict.
|
|
1009
|
+
Return `null` for no conflict, or `{ reason }` carrying a human-readable explanation.
|
|
1010
|
+
|
|
1011
|
+
The default (`defaultContradictionDetector`) is a deliberately conservative **opposing-polarity
|
|
1012
|
+
heuristic**: it splits each body into clauses, tags each affirmative or negative by the presence of
|
|
1013
|
+
a negation, and reports a conflict when one record AFFIRMS a clause whose salient content tokens
|
|
1014
|
+
contain those of a clause the other NEGATES (token-containment, not exact equality, so trailing
|
|
1015
|
+
detail on one side does not hide the conflict). It is intentionally replaceable — an embedding/NLI
|
|
1016
|
+
model is the obvious upgrade, injected the same way the vector similarity adapter is.
|
|
1017
|
+
|
|
1018
|
+
### G.4 Flag Evidence Guarantee
|
|
1019
|
+
|
|
1020
|
+
Every flag cites **both** conflicting record ids — a flag can never be emitted without them:
|
|
1021
|
+
|
|
1022
|
+
```ts
|
|
1023
|
+
interface ContradictionFlag {
|
|
1024
|
+
recordIdA: string; // canonically ordered: recordIdA < recordIdB
|
|
1025
|
+
recordIdB: string;
|
|
1026
|
+
titleA: string;
|
|
1027
|
+
titleB: string;
|
|
1028
|
+
category: string; // the shared category
|
|
1029
|
+
reason: string; // why the contradiction fn fired
|
|
1030
|
+
}
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
`detectContradictions` returns `{ audited, compared, flags, telemetryEvents }` where `audited`
|
|
1034
|
+
counts the in-scope compiled records, `compared` counts the similar pairs the contradiction fn
|
|
1035
|
+
judged, and `flags` lists the conflicting pairs. Gate telemetry is emitted at `collect-gate` and
|
|
1036
|
+
`flag-gate` (`knowledge.detect-contradictions`).
|