@remnic/core 9.3.669 → 9.3.671
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/dist/access-cli.js +16 -16
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +8 -8
- package/dist/{access-service-BCuaiNHa.d.ts → access-service-S9oGKPZc.d.ts} +2 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +7 -7
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-MPXYHC35.js → chunk-32RD3GIW.js} +17 -17
- package/dist/{chunk-BQJUPECT.js → chunk-3OKWZT7F.js} +2 -2
- package/dist/{chunk-2MXEVL75.js → chunk-6VP3YUCS.js} +2 -2
- package/dist/{chunk-WXXLSZHA.js → chunk-ATRB6Q25.js} +2 -2
- package/dist/{chunk-KOXGLQS7.js → chunk-B6IUW76R.js} +2 -2
- package/dist/{chunk-QHWJG5C5.js → chunk-CPVV2UEL.js} +5 -5
- package/dist/{chunk-AARDBQTA.js → chunk-CTCPB57O.js} +2 -2
- package/dist/{chunk-WKMCC4NQ.js → chunk-EUM7CZFM.js} +2 -2
- package/dist/{chunk-46WUVFOD.js → chunk-F7OWUP3G.js} +4 -4
- package/dist/{chunk-AZBV4RRY.js → chunk-FMSDA2D3.js} +1 -1
- package/dist/chunk-FMSDA2D3.js.map +1 -0
- package/dist/{chunk-SXYCVRLK.js → chunk-GSTYVG5L.js} +3 -3
- package/dist/{chunk-XMWF6AU3.js → chunk-LVTTO3VC.js} +2 -2
- package/dist/{chunk-4T7P2HLJ.js → chunk-LXVOZ2O6.js} +2 -2
- package/dist/{chunk-TJ7HH5LB.js → chunk-LZSMQHXC.js} +2 -2
- package/dist/{chunk-CTAV55JM.js → chunk-MLVMBV2C.js} +75 -1
- package/dist/chunk-MLVMBV2C.js.map +1 -0
- package/dist/{chunk-QZ7ODIVL.js → chunk-MNUPGYIV.js} +2 -2
- package/dist/{chunk-MR4PJ277.js → chunk-MTJ2LFAJ.js} +2 -2
- package/dist/{chunk-2TCHDANJ.js → chunk-NXBXM7Q6.js} +2 -2
- package/dist/{chunk-TIJYQXDI.js → chunk-OHX52AOS.js} +2 -2
- package/dist/{chunk-PEPHBH2W.js → chunk-PYTATYUV.js} +2 -2
- package/dist/{chunk-UVUTV7CM.js → chunk-TGN4M5MB.js} +7 -7
- package/dist/{chunk-JTPXSXHC.js → chunk-V4ZHKCGA.js} +2 -2
- package/dist/{chunk-OI4BXFSB.js → chunk-VL5JJOOY.js} +2 -2
- package/dist/{chunk-Q2LQZYQ7.js → chunk-Z4GALEO3.js} +3 -3
- package/dist/{chunk-A5TEHAR4.js → chunk-ZCVPFDHB.js} +3 -3
- package/dist/{chunk-4FJKKC2N.js → chunk-ZI6A7X4V.js} +12 -12
- package/dist/chunk-ZI6A7X4V.js.map +1 -0
- package/dist/{cli-C98xlwYA.d.ts → cli-B2Ve7R22.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +21 -21
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +26 -26
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +7 -7
- package/dist/{orchestrator-DyP9QYsh.d.ts → orchestrator-7zPqGupX.d.ts} +2 -2
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +12 -12
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BICZvQ3C.d.ts → semantic-consolidation-BX9Z9_aK.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-D96bCB3C.d.ts → types-D3pm4NhH.d.ts} +30 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/coding/coding-knowledge-config.ts +111 -0
- package/src/coding/decision-records.test.ts +434 -0
- package/src/coding/decision-records.ts +507 -0
- package/src/config.test.ts +118 -0
- package/src/config.ts +3 -2
- package/src/orchestrator.ts +2 -2
- package/src/types.ts +35 -0
- package/dist/chunk-4FJKKC2N.js.map +0 -1
- package/dist/chunk-AZBV4RRY.js.map +0 -1
- package/dist/chunk-CTAV55JM.js.map +0 -1
- /package/dist/{chunk-MPXYHC35.js.map → chunk-32RD3GIW.js.map} +0 -0
- /package/dist/{chunk-BQJUPECT.js.map → chunk-3OKWZT7F.js.map} +0 -0
- /package/dist/{chunk-2MXEVL75.js.map → chunk-6VP3YUCS.js.map} +0 -0
- /package/dist/{chunk-WXXLSZHA.js.map → chunk-ATRB6Q25.js.map} +0 -0
- /package/dist/{chunk-KOXGLQS7.js.map → chunk-B6IUW76R.js.map} +0 -0
- /package/dist/{chunk-QHWJG5C5.js.map → chunk-CPVV2UEL.js.map} +0 -0
- /package/dist/{chunk-AARDBQTA.js.map → chunk-CTCPB57O.js.map} +0 -0
- /package/dist/{chunk-WKMCC4NQ.js.map → chunk-EUM7CZFM.js.map} +0 -0
- /package/dist/{chunk-46WUVFOD.js.map → chunk-F7OWUP3G.js.map} +0 -0
- /package/dist/{chunk-SXYCVRLK.js.map → chunk-GSTYVG5L.js.map} +0 -0
- /package/dist/{chunk-XMWF6AU3.js.map → chunk-LVTTO3VC.js.map} +0 -0
- /package/dist/{chunk-4T7P2HLJ.js.map → chunk-LXVOZ2O6.js.map} +0 -0
- /package/dist/{chunk-TJ7HH5LB.js.map → chunk-LZSMQHXC.js.map} +0 -0
- /package/dist/{chunk-QZ7ODIVL.js.map → chunk-MNUPGYIV.js.map} +0 -0
- /package/dist/{chunk-MR4PJ277.js.map → chunk-MTJ2LFAJ.js.map} +0 -0
- /package/dist/{chunk-2TCHDANJ.js.map → chunk-NXBXM7Q6.js.map} +0 -0
- /package/dist/{chunk-TIJYQXDI.js.map → chunk-OHX52AOS.js.map} +0 -0
- /package/dist/{chunk-PEPHBH2W.js.map → chunk-PYTATYUV.js.map} +0 -0
- /package/dist/{chunk-UVUTV7CM.js.map → chunk-TGN4M5MB.js.map} +0 -0
- /package/dist/{chunk-JTPXSXHC.js.map → chunk-V4ZHKCGA.js.map} +0 -0
- /package/dist/{chunk-OI4BXFSB.js.map → chunk-VL5JJOOY.js.map} +0 -0
- /package/dist/{chunk-Q2LQZYQ7.js.map → chunk-Z4GALEO3.js.map} +0 -0
- /package/dist/{chunk-A5TEHAR4.js.map → chunk-ZCVPFDHB.js.map} +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `decision-records` — the pure track-A storage contract.
|
|
3
|
+
*
|
|
4
|
+
* Issue #1548 Track A PR 1: decision-record data shape, parse/serialize,
|
|
5
|
+
* validation, and the supersede mutation. Storage format is markdown +
|
|
6
|
+
* YAML frontmatter under the coding namespace; this file exercises the
|
|
7
|
+
* pure module so wiring into orchestrator persist (PR 2) is a thin
|
|
8
|
+
* registration, not a behaviour change.
|
|
9
|
+
*
|
|
10
|
+
* Pure module under test — no filesystem, no orchestrator. Tests assert
|
|
11
|
+
* the contract, not the current implementation detail.
|
|
12
|
+
*/
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ACTIVE_DECISION_STATUSES,
|
|
18
|
+
DEFAULT_DECISION_STATUS,
|
|
19
|
+
DECISION_STATUSES,
|
|
20
|
+
type DecisionRecord,
|
|
21
|
+
type DecisionRecordInput,
|
|
22
|
+
applySupersede,
|
|
23
|
+
isDecisionStatus,
|
|
24
|
+
listActive,
|
|
25
|
+
parseDecisionRecord,
|
|
26
|
+
serializeDecisionRecord,
|
|
27
|
+
} from "./decision-records.js";
|
|
28
|
+
|
|
29
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Fixture factories
|
|
31
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function makeInput(overrides: Partial<DecisionRecordInput> = {}): DecisionRecordInput {
|
|
34
|
+
return {
|
|
35
|
+
id: "ADR-0001",
|
|
36
|
+
title: "Use markdown+frontmatter for decision storage",
|
|
37
|
+
status: "accepted",
|
|
38
|
+
context: "Decision records need to be queryable through QMD and human-readable.",
|
|
39
|
+
decision:
|
|
40
|
+
"Store decision records as markdown files with YAML frontmatter under the coding namespace.",
|
|
41
|
+
consequences:
|
|
42
|
+
"QMD search and human review work without a separate indexer; round-trip is the contract.",
|
|
43
|
+
entityRefs: ["docs/architecture", "code:coding/decision-records.ts"],
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mkRec(id: string, status: DecisionRecord["status"]): DecisionRecord {
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
title: `${id} title`,
|
|
52
|
+
status,
|
|
53
|
+
context: "",
|
|
54
|
+
decision: "",
|
|
55
|
+
consequences: undefined,
|
|
56
|
+
entityRefs: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// Round-trip parse/serialize
|
|
62
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
test("serializeDecisionRecord: produces byte-identical output for the same record", () => {
|
|
65
|
+
const input = makeInput();
|
|
66
|
+
const a = serializeDecisionRecord(input);
|
|
67
|
+
const b = serializeDecisionRecord(input);
|
|
68
|
+
assert.equal(a, b, "serialize must be deterministic byte-for-byte");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("parseDecisionRecord: round-trips a serialised record verbatim", () => {
|
|
72
|
+
const input = makeInput();
|
|
73
|
+
const serialized = serializeDecisionRecord(input);
|
|
74
|
+
const parsed = parseDecisionRecord(serialized);
|
|
75
|
+
assert.deepEqual(parsed, input, "round-trip must preserve the record");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("serializeDecisionRecord: emits frontmatter keys in a fixed declaration order", () => {
|
|
79
|
+
const out = serializeDecisionRecord(makeInput());
|
|
80
|
+
const closeAt = out.indexOf("\n---", 4);
|
|
81
|
+
const fm = out.slice("---\n".length, closeAt);
|
|
82
|
+
const keysInOrder = fm
|
|
83
|
+
.split("\n")
|
|
84
|
+
.filter((line) => /^[a-zA-Z]+:/.test(line))
|
|
85
|
+
.map((line) => line.split(":")[0]!);
|
|
86
|
+
assert.deepEqual(keysInOrder, [
|
|
87
|
+
"id",
|
|
88
|
+
"title",
|
|
89
|
+
"status",
|
|
90
|
+
"context",
|
|
91
|
+
"decision",
|
|
92
|
+
"consequences",
|
|
93
|
+
"entityRefs",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("serializeDecisionRecord: frontmatter is delimited by --- lines", () => {
|
|
98
|
+
const out = serializeDecisionRecord(makeInput());
|
|
99
|
+
assert.ok(out.startsWith("---\n"), "must open with frontmatter fence");
|
|
100
|
+
assert.ok(out.includes("\n---\n\n"), "must close with a blank-line-terminated fence");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("serializeDecisionRecord: scalar strings preserve reserved chars via quoting", () => {
|
|
104
|
+
const out = serializeDecisionRecord(
|
|
105
|
+
makeInput({
|
|
106
|
+
title: "Use # for cross-references, include: colons",
|
|
107
|
+
decision: "Multi-line\ndecision body with\nthree\nparagraphs.",
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
const parsed = parseDecisionRecord(out);
|
|
111
|
+
assert.equal(parsed.title, "Use # for cross-references, include: colons");
|
|
112
|
+
assert.equal(parsed.decision, "Multi-line\ndecision body with\nthree\nparagraphs.");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("serializeDecisionRecord: optional supersedes is rendered when present", () => {
|
|
116
|
+
const input = makeInput({ supersedes: "ADR-0000" });
|
|
117
|
+
const out = serializeDecisionRecord(input);
|
|
118
|
+
assert.match(out, /^supersedes: "ADR-0000"$/m);
|
|
119
|
+
const parsed = parseDecisionRecord(out);
|
|
120
|
+
assert.equal(parsed.supersedes, "ADR-0000");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("serializeDecisionRecord: empty entityRefs list is preserved", () => {
|
|
124
|
+
const out = serializeDecisionRecord(makeInput({ entityRefs: [] }));
|
|
125
|
+
assert.match(out, /^entityRefs: \[\]$/m);
|
|
126
|
+
const parsed = parseDecisionRecord(out);
|
|
127
|
+
assert.deepEqual(parsed.entityRefs, []);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// Validation
|
|
132
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
test("parseDecisionRecord: invalid status rejects with the valid set in the error", () => {
|
|
135
|
+
const out = serializeDecisionRecord(
|
|
136
|
+
makeInput({ status: "wrong" as DecisionRecord["status"] }),
|
|
137
|
+
);
|
|
138
|
+
assert.throws(
|
|
139
|
+
() => parseDecisionRecord(out),
|
|
140
|
+
(err: unknown) => {
|
|
141
|
+
assert.ok(err instanceof Error, "rejection must be an Error");
|
|
142
|
+
for (const s of DECISION_STATUSES) {
|
|
143
|
+
assert.ok(
|
|
144
|
+
err.message.includes(s),
|
|
145
|
+
`error message must mention '${s}' as a valid status; got: ${err.message}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("parseDecisionRecord: missing/undefined status defaults to 'proposed' (least-privileged)", () => {
|
|
154
|
+
// Status omitted; every other required field supplied so the parser's
|
|
155
|
+
// contract test for the status default is isolated.
|
|
156
|
+
const fm = [
|
|
157
|
+
'id: "ADR-0007"',
|
|
158
|
+
'title: "Draft with no declared status yet"',
|
|
159
|
+
'context: "Pending review."',
|
|
160
|
+
'decision: "Defer until next design review."',
|
|
161
|
+
].join("\n");
|
|
162
|
+
const doc = `---\n${fm}\n---\n\nPending review.\n`;
|
|
163
|
+
const parsed = parseDecisionRecord(doc);
|
|
164
|
+
assert.equal(parsed.status, DEFAULT_DECISION_STATUS);
|
|
165
|
+
assert.notEqual(parsed.status, "accepted", "default must never be 'accepted'");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("parseDecisionRecord: rejects unknown frontmatter keys (no silent drift)", () => {
|
|
169
|
+
const fm = [
|
|
170
|
+
'id: "ADR-0008"',
|
|
171
|
+
'title: "Padding the contract surfaces an unknown field"',
|
|
172
|
+
'status: "proposed"',
|
|
173
|
+
'secretKey: "oh no"',
|
|
174
|
+
].join("\n");
|
|
175
|
+
const doc = `---\n${fm}\n---\n\nBody.\n`;
|
|
176
|
+
assert.throws(() => parseDecisionRecord(doc), /secretKey/);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("isDecisionStatus: narrows raw strings to DecisionStatus; rejects booleans/numbers", () => {
|
|
180
|
+
assert.ok(isDecisionStatus("accepted"));
|
|
181
|
+
assert.ok(isDecisionStatus("rejected"));
|
|
182
|
+
assert.ok(isDecisionStatus("superseded"));
|
|
183
|
+
assert.ok(isDecisionStatus("proposed"));
|
|
184
|
+
assert.ok(!isDecisionStatus("ACCEPTED"), "case-sensitive");
|
|
185
|
+
assert.ok(!isDecisionStatus(""), "empty rejected");
|
|
186
|
+
assert.ok(!isDecisionStatus(1), "numbers rejected");
|
|
187
|
+
assert.ok(!isDecisionStatus(true), "booleans rejected");
|
|
188
|
+
assert.ok(!isDecisionStatus(null), "null rejected");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
192
|
+
// Supersede
|
|
193
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
test("applySupersede: writes the new record BEFORE mutating the old one", () => {
|
|
196
|
+
const events: string[] = [];
|
|
197
|
+
const initial: DecisionRecord[] = [
|
|
198
|
+
{
|
|
199
|
+
id: "ADR-0001",
|
|
200
|
+
title: "Use markdown+frontmatter",
|
|
201
|
+
status: "accepted",
|
|
202
|
+
context: "C",
|
|
203
|
+
decision: "D",
|
|
204
|
+
consequences: undefined,
|
|
205
|
+
entityRefs: [],
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
const replacement: DecisionRecord = {
|
|
209
|
+
id: "ADR-0002",
|
|
210
|
+
title: "Switch to TOML frontmatter",
|
|
211
|
+
status: "accepted",
|
|
212
|
+
context: "C2",
|
|
213
|
+
decision: "D2",
|
|
214
|
+
consequences: undefined,
|
|
215
|
+
entityRefs: [],
|
|
216
|
+
};
|
|
217
|
+
const next = applySupersede(initial, "ADR-0001", replacement, (event) => events.push(event));
|
|
218
|
+
assert.deepEqual(events, ["write:ADR-0002", "mutate:ADR-0001:superseded"]);
|
|
219
|
+
const a = next.find((r) => r.id === "ADR-0001");
|
|
220
|
+
const b = next.find((r) => r.id === "ADR-0002");
|
|
221
|
+
assert.ok(a && b, "both records must be present");
|
|
222
|
+
assert.equal(a.status, "superseded");
|
|
223
|
+
assert.equal(b.supersedes, "ADR-0001");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("applySupersede: throws when the replaced record is missing", () => {
|
|
227
|
+
const replacement: DecisionRecord = {
|
|
228
|
+
id: "ADR-0099",
|
|
229
|
+
title: "Orphan",
|
|
230
|
+
status: "accepted",
|
|
231
|
+
context: "",
|
|
232
|
+
decision: "",
|
|
233
|
+
consequences: undefined,
|
|
234
|
+
entityRefs: [],
|
|
235
|
+
};
|
|
236
|
+
assert.throws(() => applySupersede([], "ADR-0001", replacement), /ADR-0001/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
test("applySupersede: throws when the replacement id collides with an unrelated existing record (chatgpt-codex review)", () => {
|
|
241
|
+
// chatgpt-codex-connector review (PR #1590): an operator typo or MCP
|
|
242
|
+
// rename that supplies a replacement.id already present in the record
|
|
243
|
+
// set must not silently overwrite that unrelated decision. The collision
|
|
244
|
+
// path MUST throw — the caller picks a fresh id and retries.
|
|
245
|
+
const records: DecisionRecord[] = [
|
|
246
|
+
{
|
|
247
|
+
id: "ADR-0001",
|
|
248
|
+
title: "Old",
|
|
249
|
+
status: "accepted",
|
|
250
|
+
context: "",
|
|
251
|
+
decision: "",
|
|
252
|
+
consequences: undefined,
|
|
253
|
+
entityRefs: [],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "ADR-0002",
|
|
257
|
+
title: "Unrelated",
|
|
258
|
+
status: "accepted",
|
|
259
|
+
context: "",
|
|
260
|
+
decision: "",
|
|
261
|
+
consequences: undefined,
|
|
262
|
+
entityRefs: [],
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
const replacement: DecisionRecord = {
|
|
266
|
+
id: "ADR-0002", // collision with the unrelated record
|
|
267
|
+
title: "New",
|
|
268
|
+
status: "accepted",
|
|
269
|
+
context: "",
|
|
270
|
+
decision: "",
|
|
271
|
+
consequences: undefined,
|
|
272
|
+
entityRefs: [],
|
|
273
|
+
};
|
|
274
|
+
assert.throws(
|
|
275
|
+
() => applySupersede(records, "ADR-0001", replacement),
|
|
276
|
+
/replacement id 'ADR-0002' already exists/,
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("applySupersede: allows same-id replacement (in-place body swap)", () => {
|
|
281
|
+
// The one collision we intentionally allow: replacement.id === targetId.
|
|
282
|
+
// This is the in-place body-swap path — the entry stays in its position
|
|
283
|
+
// in the listing and the new record carries the supersede edge back to
|
|
284
|
+
// the OLD body that was replaced.
|
|
285
|
+
const records: DecisionRecord[] = [
|
|
286
|
+
{
|
|
287
|
+
id: "ADR-0001",
|
|
288
|
+
title: "Original",
|
|
289
|
+
status: "accepted",
|
|
290
|
+
context: "C",
|
|
291
|
+
decision: "D",
|
|
292
|
+
consequences: undefined,
|
|
293
|
+
entityRefs: [],
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
const replacement: DecisionRecord = {
|
|
297
|
+
id: "ADR-0001",
|
|
298
|
+
title: "Revised",
|
|
299
|
+
status: "accepted",
|
|
300
|
+
context: "C2",
|
|
301
|
+
decision: "D2",
|
|
302
|
+
consequences: undefined,
|
|
303
|
+
entityRefs: [],
|
|
304
|
+
};
|
|
305
|
+
const next = applySupersede(records, "ADR-0001", replacement);
|
|
306
|
+
assert.equal(next.length, 1, "in-place swap must not duplicate the entry");
|
|
307
|
+
assert.equal(next[0]!.title, "Revised");
|
|
308
|
+
assert.equal(next[0]!.supersedes, "ADR-0001");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("applySupersede result round-trips through serialize/parse (issue #1548 review)", () => {
|
|
312
|
+
// The classic on-disk shape after supersede: the old record carries
|
|
313
|
+
// `status: "superseded"` and no `supersedes` field (the edge lives on
|
|
314
|
+
// the replacement). The parser MUST accept both shapes — only the
|
|
315
|
+
// listing filter excludes superseded records, not the parser.
|
|
316
|
+
const initial: DecisionRecord[] = [
|
|
317
|
+
{
|
|
318
|
+
id: "ADR-0001",
|
|
319
|
+
title: "Use markdown+frontmatter",
|
|
320
|
+
status: "accepted",
|
|
321
|
+
context: "C",
|
|
322
|
+
decision: "D",
|
|
323
|
+
consequences: undefined,
|
|
324
|
+
entityRefs: [],
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
const replacement: DecisionRecord = {
|
|
328
|
+
id: "ADR-0002",
|
|
329
|
+
title: "Switch to TOML",
|
|
330
|
+
status: "accepted",
|
|
331
|
+
context: "C2",
|
|
332
|
+
decision: "D2",
|
|
333
|
+
consequences: undefined,
|
|
334
|
+
entityRefs: [],
|
|
335
|
+
};
|
|
336
|
+
const next = applySupersede(initial, "ADR-0001", replacement);
|
|
337
|
+
// Round-trip the whole set through serialize + parse. Comparing the
|
|
338
|
+
// status-presence and id/path fields rather than `deepEqual` because
|
|
339
|
+
// `supersedes: undefined` differs from "key absent" under strict
|
|
340
|
+
// equality (deepEqual) even though the data is the same — the surface
|
|
341
|
+
// contract is "each record must parse without throwing", not byte
|
|
342
|
+
// identity.
|
|
343
|
+
for (const record of next) {
|
|
344
|
+
const serialized = serializeDecisionRecord(record);
|
|
345
|
+
const reparsed = parseDecisionRecord(serialized);
|
|
346
|
+
assert.equal(reparsed.id, record.id);
|
|
347
|
+
assert.equal(reparsed.status, record.status);
|
|
348
|
+
assert.equal(reparsed.title, record.title);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Spot-check the canonical shape invariants on the superseded record.
|
|
352
|
+
const a = next.find((r) => r.id === "ADR-0001");
|
|
353
|
+
if (!a) throw new Error("ADR-0001 missing after applySupersede (test invariant violated)");
|
|
354
|
+
assert.equal(a.status, "superseded");
|
|
355
|
+
assert.equal(a.supersedes, undefined, "superseded record must NOT carry its own edge");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("parseDecisionRecord: accepts status 'superseded' without a supersedes field", () => {
|
|
359
|
+
// Round-trip a record produced by applySupersede — the on-disk shape
|
|
360
|
+
// for a superseded record has no supersedes field (the edge is on the
|
|
361
|
+
// *replacement*). The parser is permissive (rule 51: only the four
|
|
362
|
+
// declared enum values are valid; field presence is the listing filter's
|
|
363
|
+
// concern).
|
|
364
|
+
const serialized = serializeDecisionRecord({
|
|
365
|
+
id: "ADR-0001",
|
|
366
|
+
title: "Old guidance",
|
|
367
|
+
status: "superseded",
|
|
368
|
+
context: "",
|
|
369
|
+
decision: "",
|
|
370
|
+
consequences: undefined,
|
|
371
|
+
entityRefs: [],
|
|
372
|
+
});
|
|
373
|
+
const reparsed = parseDecisionRecord(serialized);
|
|
374
|
+
assert.equal(reparsed.status, "superseded");
|
|
375
|
+
assert.equal(reparsed.supersedes, undefined);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("serializeDecisionRecord: round-trips entityRefs containing backslashes and quotes", () => {
|
|
379
|
+
// Cursor review-round bug d16b2a18: `parseFlowList` used to not honour
|
|
380
|
+
// backslash escapes inside quoted elements, so any ref containing a `"`
|
|
381
|
+
// would close its own element early and merge with the next ref. The
|
|
382
|
+
// serializer must escape such refs and the parser must decode them.
|
|
383
|
+
for (const refs of [
|
|
384
|
+
["plain"],
|
|
385
|
+
["with\"quote"],
|
|
386
|
+
["with\\backslash"],
|
|
387
|
+
["mix\"ed\\both", "second"],
|
|
388
|
+
["a", "b\"c", "d\\\\e", "f"],
|
|
389
|
+
]) {
|
|
390
|
+
const input = makeInput({ entityRefs: refs });
|
|
391
|
+
const out = serializeDecisionRecord(input);
|
|
392
|
+
const parsed = parseDecisionRecord(out);
|
|
393
|
+
assert.deepEqual(
|
|
394
|
+
parsed.entityRefs,
|
|
395
|
+
refs,
|
|
396
|
+
`entityRefs ${JSON.stringify(refs)} must round-trip verbatim`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
402
|
+
// Listing
|
|
403
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
test("ACTIVE_DECISION_STATUSES: explicit set covers proposed + accepted only", () => {
|
|
406
|
+
assert.deepEqual(new Set(ACTIVE_DECISION_STATUSES), new Set(["proposed", "accepted"]));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("listActive: filters superseded + rejected out; keeps proposed + accepted", () => {
|
|
410
|
+
const records: DecisionRecord[] = [
|
|
411
|
+
mkRec("ADR-0010", "accepted"),
|
|
412
|
+
mkRec("ADR-0011", "proposed"),
|
|
413
|
+
mkRec("ADR-0012", "superseded"),
|
|
414
|
+
mkRec("ADR-0013", "rejected"),
|
|
415
|
+
];
|
|
416
|
+
const active = listActive(records).map((r) => r.id);
|
|
417
|
+
assert.deepEqual(active, ["ADR-0010", "ADR-0011"]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("listActive: empty input returns empty array (no null/undefined leaking)", () => {
|
|
421
|
+
assert.deepEqual(listActive([]), []);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
425
|
+
// Determinism contract (rule 23 — hash rawContent, not timestamped rendering)
|
|
426
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
test("serializeDecisionRecord: body changes change the byte output (so dedup hashes can detect them)", () => {
|
|
429
|
+
const input = makeInput();
|
|
430
|
+
const a = serializeDecisionRecord(input);
|
|
431
|
+
const input2: DecisionRecord = { ...input, decision: "Different decision body." };
|
|
432
|
+
const c = serializeDecisionRecord(input2);
|
|
433
|
+
assert.notEqual(a, c, "body changes must change the serialised bytes");
|
|
434
|
+
});
|