@kontourai/flow-agents 1.3.0 → 2.0.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 +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/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/console-learning-projection.d.ts +1 -0
- package/build/src/cli/effective-backlog-settings.d.ts +1 -0
- package/build/src/cli/fixture-retirement-audit.d.ts +2 -0
- package/build/src/cli/init.d.ts +17 -0
- package/build/src/cli/init.js +242 -20
- package/build/src/cli/kit.d.ts +1 -0
- package/build/src/cli/promote-workflow-artifact.d.ts +1 -0
- package/build/src/cli/publish-change-helper.d.ts +1 -0
- package/build/src/cli/pull-work-provider.d.ts +1 -0
- package/build/src/cli/runtime-adapter.d.ts +1 -0
- package/build/src/cli/telemetry-doctor.d.ts +1 -0
- package/build/src/cli/usage-feedback.d.ts +1 -0
- package/build/src/cli/utterance-check.d.ts +1 -0
- package/build/src/cli/validate-hook-influence.d.ts +1 -0
- package/build/src/cli/validate-source-tree.d.ts +1 -0
- package/build/src/cli/validate-workflow-artifacts.d.ts +2 -0
- 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/veritas-governance.d.ts +1 -0
- package/build/src/cli/workflow-artifact-cleanup-audit.d.ts +1 -0
- package/build/src/cli/workflow-sidecar.d.ts +324 -0
- package/build/src/cli/workflow-sidecar.js +1973 -90
- package/build/src/cli.d.ts +2 -0
- package/build/src/cli.js +2 -3
- package/build/src/flow-kit/validate.d.ts +81 -0
- package/build/src/index.d.ts +5 -0
- package/build/src/index.js +36 -0
- package/build/src/lib/args.d.ts +8 -0
- package/build/src/lib/flow-resolver.d.ts +82 -0
- package/build/src/lib/flow-resolver.js +237 -0
- package/build/src/lib/fs.d.ts +7 -0
- package/build/src/lib/workflow-learning-projection.d.ts +132 -0
- package/build/src/runtime-adapters.d.ts +18 -0
- package/build/src/tools/build-universal-bundles.d.ts +2 -0
- package/build/src/tools/build-universal-bundles.js +34 -22
- package/build/src/tools/common.d.ts +9 -0
- package/build/src/tools/generate-context-map.d.ts +2 -0
- package/build/src/tools/generate-context-map.js +3 -16
- package/build/src/tools/validate-package.d.ts +2 -0
- package/build/src/tools/validate-source-tree.d.ts +2 -0
- 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/developer-architecture.md +14 -0
- 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 +78 -10
- 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 +54 -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_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_hook_category_behaviors.sh +14 -0
- package/evals/integration/test_install_merge.sh +1176 -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_library_exports.sh +85 -0
- package/evals/static/test_universal_bundles.sh +15 -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 +13 -4
- 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 +1471 -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/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 +2093 -84
- package/src/cli.ts +2 -3
- package/src/index.ts +53 -0
- package/src/lib/flow-resolver.ts +284 -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/tsconfig.json +1 -0
- 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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Kit — Detect-Contradictions Eval Suite (#106 hygiene #2)
|
|
3
|
+
*
|
|
4
|
+
* knowledge.detect-contradictions compares compiled records within a category,
|
|
5
|
+
* reusing the similarity adapter to scope comparisons to records about the same
|
|
6
|
+
* thing, then flags conflicting assertions — each flag citing BOTH record ids.
|
|
7
|
+
* The audit is READ-ONLY, OPTIONAL (opt-in per category), and CONFIGURABLE
|
|
8
|
+
* (pluggable similarity detector + pluggable contradiction fn).
|
|
9
|
+
*
|
|
10
|
+
* Covers:
|
|
11
|
+
* - default opposing-polarity heuristic: affirmative vs negation of the same
|
|
12
|
+
* stem within a category is flagged; agreeing records are not.
|
|
13
|
+
* - every flag cites BOTH ids (recordIdA + recordIdB) + category + reason.
|
|
14
|
+
* - similarity scoping: only records the similarity adapter deems similar are
|
|
15
|
+
* compared (an injected detector that returns [] yields zero flags).
|
|
16
|
+
* - pluggable contradiction fn: an injected fn overrides the default verdict.
|
|
17
|
+
* - opt-in scoping: only categories in `categories` are audited; others skipped.
|
|
18
|
+
* - each unordered pair is compared at most once (no duplicate flags).
|
|
19
|
+
* - cross-category records are never paired (different subjects).
|
|
20
|
+
* - retired compiled records are excluded from the comparison set.
|
|
21
|
+
* - read-only invariant: no record is mutated by the audit.
|
|
22
|
+
* - gate telemetry (collect-gate + flag-gate) is emitted.
|
|
23
|
+
* - module-level detectContradictions export delegates to the runner.
|
|
24
|
+
*
|
|
25
|
+
* Run:
|
|
26
|
+
* node --test kits/knowledge/evals/detect-contradictions/suite.test.js
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { test, describe, before, after } from "node:test";
|
|
30
|
+
import assert from "node:assert/strict";
|
|
31
|
+
import * as fs from "node:fs";
|
|
32
|
+
import * as path from "node:path";
|
|
33
|
+
import * as os from "node:os";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
|
|
36
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const KIT_ROOT = path.resolve(__dirname, "../..");
|
|
38
|
+
|
|
39
|
+
const adapterPath = path.join(KIT_ROOT, "adapters/default-store/index.js");
|
|
40
|
+
const runnerPath = path.join(KIT_ROOT, "adapters/flow-runner/index.js");
|
|
41
|
+
|
|
42
|
+
const { DefaultKnowledgeStore } = await import(adapterPath);
|
|
43
|
+
const { KnowledgeFlowRunner, detectContradictions, defaultContradictionDetector } =
|
|
44
|
+
await import(runnerPath);
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function makeTempDir() {
|
|
51
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "knowledge-detect-contradictions-"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeStore(dir) {
|
|
55
|
+
return new DefaultKnowledgeStore({ storeRoot: dir });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeRunner(store, dir) {
|
|
59
|
+
return new KnowledgeFlowRunner({
|
|
60
|
+
store,
|
|
61
|
+
workspace: dir,
|
|
62
|
+
agent: "detect-contradictions-test-runner",
|
|
63
|
+
sessionId: "detect-contradictions-session-001",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readTelemetryEvents(dir) {
|
|
68
|
+
const sinkPath = path.join(dir, ".telemetry", "full.jsonl");
|
|
69
|
+
if (!fs.existsSync(sinkPath)) return [];
|
|
70
|
+
return fs
|
|
71
|
+
.readFileSync(sinkPath, "utf8")
|
|
72
|
+
.trim()
|
|
73
|
+
.split("\n")
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((line) => JSON.parse(line));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeCompiled(store, { id, title, category, body }) {
|
|
79
|
+
return store.create({
|
|
80
|
+
id,
|
|
81
|
+
type: "compiled",
|
|
82
|
+
title,
|
|
83
|
+
body,
|
|
84
|
+
category,
|
|
85
|
+
provenance: { agent: "fixture" },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Suite
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
describe("Knowledge Kit Detect-Contradictions Suite (#106)", () => {
|
|
94
|
+
let dir;
|
|
95
|
+
let store;
|
|
96
|
+
let runner;
|
|
97
|
+
|
|
98
|
+
before(async () => {
|
|
99
|
+
dir = makeTempDir();
|
|
100
|
+
store = makeStore(dir);
|
|
101
|
+
runner = makeRunner(store, dir);
|
|
102
|
+
|
|
103
|
+
// ops.decisions: a clear contradiction — one says use REST, one says do not.
|
|
104
|
+
await makeCompiled(store, {
|
|
105
|
+
id: "rest-yes",
|
|
106
|
+
title: "Decision: use REST",
|
|
107
|
+
category: "ops.decisions",
|
|
108
|
+
body: "We should use REST for the public API.",
|
|
109
|
+
});
|
|
110
|
+
await makeCompiled(store, {
|
|
111
|
+
id: "rest-no",
|
|
112
|
+
title: "Decision: drop REST",
|
|
113
|
+
category: "ops.decisions",
|
|
114
|
+
body: "We should not use REST; gRPC is the new direction.",
|
|
115
|
+
});
|
|
116
|
+
// ops.decisions: an agreeing record — no contradiction with rest-yes.
|
|
117
|
+
await makeCompiled(store, {
|
|
118
|
+
id: "rest-agree",
|
|
119
|
+
title: "Note: REST confirmed",
|
|
120
|
+
category: "ops.decisions",
|
|
121
|
+
body: "We should use REST for all external endpoints.",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// radar.signals: a separate category with its own (non-conflicting) record —
|
|
125
|
+
// it must never be paired with the ops.decisions records.
|
|
126
|
+
await makeCompiled(store, {
|
|
127
|
+
id: "radar-note",
|
|
128
|
+
title: "Radar: gRPC interest",
|
|
129
|
+
category: "radar.signals",
|
|
130
|
+
body: "We should use gRPC internally.",
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
after(() => {
|
|
135
|
+
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("default heuristic flags opposing assertions within a category", async () => {
|
|
139
|
+
const result = await runner.detectContradictions();
|
|
140
|
+
const pair = result.flags.find(
|
|
141
|
+
(f) =>
|
|
142
|
+
(f.recordIdA === "rest-no" && f.recordIdB === "rest-yes") ||
|
|
143
|
+
(f.recordIdA === "rest-yes" && f.recordIdB === "rest-no")
|
|
144
|
+
);
|
|
145
|
+
assert.ok(pair, "the use-REST vs do-not-use-REST pair is flagged");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("agreeing records in the same category are NOT flagged", async () => {
|
|
149
|
+
const result = await runner.detectContradictions();
|
|
150
|
+
const agreeFlag = result.flags.find(
|
|
151
|
+
(f) =>
|
|
152
|
+
(f.recordIdA === "rest-yes" && f.recordIdB === "rest-agree") ||
|
|
153
|
+
(f.recordIdA === "rest-agree" && f.recordIdB === "rest-yes")
|
|
154
|
+
);
|
|
155
|
+
assert.ok(!agreeFlag, "two records that both affirm REST are not a contradiction");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("every flag cites BOTH record ids + category + reason (evidence guarantee)", async () => {
|
|
159
|
+
const result = await runner.detectContradictions();
|
|
160
|
+
assert.ok(result.flags.length > 0, "at least one flag to inspect");
|
|
161
|
+
for (const flag of result.flags) {
|
|
162
|
+
assert.ok(flag.recordIdA, "flag cites recordIdA");
|
|
163
|
+
assert.ok(flag.recordIdB, "flag cites recordIdB");
|
|
164
|
+
assert.notEqual(flag.recordIdA, flag.recordIdB, "the two ids are distinct");
|
|
165
|
+
assert.ok(flag.category, "flag cites the shared category");
|
|
166
|
+
assert.ok(flag.reason && flag.reason.trim(), "flag cites a non-empty reason");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("flag ids are canonically ordered (recordIdA < recordIdB)", async () => {
|
|
171
|
+
const result = await runner.detectContradictions();
|
|
172
|
+
for (const flag of result.flags) {
|
|
173
|
+
assert.ok(flag.recordIdA < flag.recordIdB, "ids are stably ordered for a stable pair key");
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("cross-category records are never paired", async () => {
|
|
178
|
+
const result = await runner.detectContradictions();
|
|
179
|
+
const crossed = result.flags.find(
|
|
180
|
+
(f) => f.recordIdA === "radar-note" || f.recordIdB === "radar-note"
|
|
181
|
+
);
|
|
182
|
+
assert.ok(!crossed, "radar.signals record is never paired with ops.decisions records");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("similarity scoping: a detector that finds nothing yields zero flags", async () => {
|
|
186
|
+
const result = await runner.detectContradictions({
|
|
187
|
+
similarityDetector: async () => [],
|
|
188
|
+
});
|
|
189
|
+
assert.equal(result.flags.length, 0, "no similar pairs → nothing to compare → no flags");
|
|
190
|
+
assert.equal(result.compared, 0, "no pairs are compared when nothing is similar");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("pluggable contradiction fn overrides the default verdict", async () => {
|
|
194
|
+
// A fn that flags EVERY similar pair, regardless of content.
|
|
195
|
+
const everythingConflicts = () => ({ reason: "forced conflict" });
|
|
196
|
+
const result = await runner.detectContradictions({
|
|
197
|
+
categories: ["ops.decisions"],
|
|
198
|
+
contradictionDetector: everythingConflicts,
|
|
199
|
+
});
|
|
200
|
+
// 3 ops.decisions records → C(3,2) = 3 unordered pairs, all flagged.
|
|
201
|
+
assert.equal(result.flags.length, 3, "every similar pair is flagged by the injected fn");
|
|
202
|
+
assert.ok(
|
|
203
|
+
result.flags.every((f) => f.reason === "forced conflict"),
|
|
204
|
+
"the injected fn's reason is carried on every flag"
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("opt-in scoping: only categories in `categories` are audited", async () => {
|
|
209
|
+
const result = await runner.detectContradictions({ categories: ["radar.signals"] });
|
|
210
|
+
assert.equal(result.flags.length, 0, "radar.signals has no conflicting pair");
|
|
211
|
+
assert.ok(
|
|
212
|
+
!result.flags.some((f) => f.category === "ops.decisions"),
|
|
213
|
+
"ops.decisions is not audited when out of scope"
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("each unordered pair is compared at most once", async () => {
|
|
218
|
+
const result = await runner.detectContradictions({
|
|
219
|
+
categories: ["ops.decisions"],
|
|
220
|
+
contradictionDetector: () => ({ reason: "x" }),
|
|
221
|
+
});
|
|
222
|
+
const keys = result.flags.map((f) => `${f.recordIdA}|${f.recordIdB}`);
|
|
223
|
+
assert.equal(new Set(keys).size, keys.length, "no duplicate pair flags");
|
|
224
|
+
assert.equal(keys.length, 3, "exactly C(3,2) pairs for 3 records");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("retired compiled records are excluded from the comparison set", async () => {
|
|
228
|
+
const rdir = makeTempDir();
|
|
229
|
+
const rstore = makeStore(rdir);
|
|
230
|
+
const rrunner = makeRunner(rstore, rdir);
|
|
231
|
+
try {
|
|
232
|
+
await makeCompiled(rstore, {
|
|
233
|
+
id: "live-yes",
|
|
234
|
+
title: "Use REST",
|
|
235
|
+
category: "ops.decisions",
|
|
236
|
+
body: "We should use REST.",
|
|
237
|
+
});
|
|
238
|
+
await makeCompiled(rstore, {
|
|
239
|
+
id: "retired-no",
|
|
240
|
+
title: "Drop REST (retired)",
|
|
241
|
+
category: "ops.decisions",
|
|
242
|
+
body: "We should not use REST.",
|
|
243
|
+
});
|
|
244
|
+
await rstore.retire("retired-no", "retired", {
|
|
245
|
+
agent: "fixture",
|
|
246
|
+
rationale: "Superseded.",
|
|
247
|
+
});
|
|
248
|
+
const result = await rrunner.detectContradictions();
|
|
249
|
+
assert.equal(
|
|
250
|
+
result.flags.length,
|
|
251
|
+
0,
|
|
252
|
+
"a retired record is never compared — no contradiction surfaces"
|
|
253
|
+
);
|
|
254
|
+
} finally {
|
|
255
|
+
fs.rmSync(rdir, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("read-only invariant: the audit mutates no record", async () => {
|
|
260
|
+
const before = {};
|
|
261
|
+
for (const id of ["rest-yes", "rest-no", "rest-agree", "radar-note"]) {
|
|
262
|
+
before[id] = fs.readFileSync(path.join(dir, "records", `${id}.md`), "utf8");
|
|
263
|
+
}
|
|
264
|
+
await runner.detectContradictions();
|
|
265
|
+
for (const id of Object.keys(before)) {
|
|
266
|
+
const after = fs.readFileSync(path.join(dir, "records", `${id}.md`), "utf8");
|
|
267
|
+
assert.equal(after, before[id], `record ${id} is byte-identical after the audit`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("gate telemetry: collect-gate and flag-gate events are emitted", async () => {
|
|
272
|
+
const tdir = makeTempDir();
|
|
273
|
+
const tstore = makeStore(tdir);
|
|
274
|
+
const trunner = makeRunner(tstore, tdir);
|
|
275
|
+
try {
|
|
276
|
+
await makeCompiled(tstore, {
|
|
277
|
+
id: "t-yes",
|
|
278
|
+
title: "Use REST",
|
|
279
|
+
category: "ops.decisions",
|
|
280
|
+
body: "We should use REST.",
|
|
281
|
+
});
|
|
282
|
+
await makeCompiled(tstore, {
|
|
283
|
+
id: "t-no",
|
|
284
|
+
title: "Drop REST",
|
|
285
|
+
category: "ops.decisions",
|
|
286
|
+
body: "We should not use REST.",
|
|
287
|
+
});
|
|
288
|
+
const result = await trunner.detectContradictions();
|
|
289
|
+
assert.ok(
|
|
290
|
+
result.telemetryEvents.length >= 4,
|
|
291
|
+
"collect-gate + flag-gate produce at least 4 in/out events"
|
|
292
|
+
);
|
|
293
|
+
const persisted = readTelemetryEvents(tdir);
|
|
294
|
+
const events = persisted.filter((e) =>
|
|
295
|
+
JSON.stringify(e).includes("knowledge.detect-contradictions")
|
|
296
|
+
);
|
|
297
|
+
assert.ok(events.length > 0, "detect-contradictions flow telemetry is persisted to the sink");
|
|
298
|
+
} finally {
|
|
299
|
+
fs.rmSync(tdir, { recursive: true, force: true });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("module-level detectContradictions export delegates to the runner", async () => {
|
|
304
|
+
const result = await detectContradictions({
|
|
305
|
+
store,
|
|
306
|
+
workspace: dir,
|
|
307
|
+
agent: "detect-contradictions-test-runner",
|
|
308
|
+
});
|
|
309
|
+
const pair = result.flags.find(
|
|
310
|
+
(f) =>
|
|
311
|
+
(f.recordIdA === "rest-no" && f.recordIdB === "rest-yes") ||
|
|
312
|
+
(f.recordIdA === "rest-yes" && f.recordIdB === "rest-no")
|
|
313
|
+
);
|
|
314
|
+
assert.ok(pair, "module-level export surfaces the same contradiction as the runner");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("defaultContradictionDetector unit: opposing polarity over a shared stem", () => {
|
|
318
|
+
const a = { id: "a", body: "We should use REST." };
|
|
319
|
+
const b = { id: "b", body: "We should not use REST." };
|
|
320
|
+
const c = { id: "c", body: "We should use REST everywhere." };
|
|
321
|
+
assert.ok(defaultContradictionDetector(a, b), "affirm vs negate of the same stem conflicts");
|
|
322
|
+
assert.equal(defaultContradictionDetector(a, c), null, "two affirmations do not conflict");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -655,6 +655,46 @@ describe("AC4: Obsidian adapter — people/ folder and wikilink rendering", () =
|
|
|
655
655
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
656
656
|
}
|
|
657
657
|
});
|
|
658
|
+
|
|
659
|
+
test("AC4: Obsidian adapter rejects tampered path index escapes", async () => {
|
|
660
|
+
const parentDir = makeTempDir();
|
|
661
|
+
const dir = path.join(parentDir, "nested", "store");
|
|
662
|
+
const sentinelPath = path.join(parentDir, "escape.md");
|
|
663
|
+
const sentinelContent = "outside sentinel";
|
|
664
|
+
try {
|
|
665
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
666
|
+
fs.writeFileSync(sentinelPath, sentinelContent, "utf8");
|
|
667
|
+
|
|
668
|
+
const store = new ObsidianStore({ storeRoot: dir });
|
|
669
|
+
const id = await store.create({
|
|
670
|
+
type: "person",
|
|
671
|
+
title: "Tampered Person",
|
|
672
|
+
body: "Test body",
|
|
673
|
+
category: "test",
|
|
674
|
+
provenance: { agent: "tester" },
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const pathIndexPath = path.join(dir, ".graph-index.json");
|
|
678
|
+
fs.writeFileSync(pathIndexPath, JSON.stringify({
|
|
679
|
+
by_id: {
|
|
680
|
+
[id]: { path: "../../escape.md", archived: false },
|
|
681
|
+
},
|
|
682
|
+
by_path: {
|
|
683
|
+
"../../escape.md": id,
|
|
684
|
+
},
|
|
685
|
+
}, null, 2), "utf8");
|
|
686
|
+
|
|
687
|
+
await assert.rejects(
|
|
688
|
+
() => store.update(id, { title: "Should Not Write" }, { agent: "tester" }),
|
|
689
|
+
/escapes store root/
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
assert.equal(fs.readFileSync(sentinelPath, "utf8"), sentinelContent);
|
|
693
|
+
assert.equal(fs.existsSync(path.join(parentDir, "should-not-write.md")), false);
|
|
694
|
+
} finally {
|
|
695
|
+
fs.rmSync(parentDir, { recursive: true, force: true });
|
|
696
|
+
}
|
|
697
|
+
});
|
|
658
698
|
}
|
|
659
699
|
});
|
|
660
700
|
|