@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.
Files changed (218) hide show
  1. package/dist/access-cli.js +16 -16
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +9 -9
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +8 -8
  6. package/dist/{access-service-BCuaiNHa.d.ts → access-service-S9oGKPZc.d.ts} +2 -2
  7. package/dist/access-service.d.ts +4 -4
  8. package/dist/access-service.js +7 -7
  9. package/dist/action-confidence.d.ts +1 -1
  10. package/dist/active-memory-bridge.d.ts +1 -1
  11. package/dist/active-recall.d.ts +1 -1
  12. package/dist/active-recall.js +1 -1
  13. package/dist/behavior-learner.d.ts +1 -1
  14. package/dist/behavior-signals.d.ts +1 -1
  15. package/dist/bootstrap.d.ts +3 -3
  16. package/dist/briefing.d.ts +1 -1
  17. package/dist/briefing.js +3 -3
  18. package/dist/buffer-surprise-report.d.ts +1 -1
  19. package/dist/buffer.d.ts +1 -1
  20. package/dist/calibration.d.ts +1 -1
  21. package/dist/capabilities.d.ts +1 -1
  22. package/dist/causal-behavior.d.ts +1 -1
  23. package/dist/causal-consolidation.d.ts +1 -1
  24. package/dist/causal-consolidation.js +4 -4
  25. package/dist/{chunk-MPXYHC35.js → chunk-32RD3GIW.js} +17 -17
  26. package/dist/{chunk-BQJUPECT.js → chunk-3OKWZT7F.js} +2 -2
  27. package/dist/{chunk-2MXEVL75.js → chunk-6VP3YUCS.js} +2 -2
  28. package/dist/{chunk-WXXLSZHA.js → chunk-ATRB6Q25.js} +2 -2
  29. package/dist/{chunk-KOXGLQS7.js → chunk-B6IUW76R.js} +2 -2
  30. package/dist/{chunk-QHWJG5C5.js → chunk-CPVV2UEL.js} +5 -5
  31. package/dist/{chunk-AARDBQTA.js → chunk-CTCPB57O.js} +2 -2
  32. package/dist/{chunk-WKMCC4NQ.js → chunk-EUM7CZFM.js} +2 -2
  33. package/dist/{chunk-46WUVFOD.js → chunk-F7OWUP3G.js} +4 -4
  34. package/dist/{chunk-AZBV4RRY.js → chunk-FMSDA2D3.js} +1 -1
  35. package/dist/chunk-FMSDA2D3.js.map +1 -0
  36. package/dist/{chunk-SXYCVRLK.js → chunk-GSTYVG5L.js} +3 -3
  37. package/dist/{chunk-XMWF6AU3.js → chunk-LVTTO3VC.js} +2 -2
  38. package/dist/{chunk-4T7P2HLJ.js → chunk-LXVOZ2O6.js} +2 -2
  39. package/dist/{chunk-TJ7HH5LB.js → chunk-LZSMQHXC.js} +2 -2
  40. package/dist/{chunk-CTAV55JM.js → chunk-MLVMBV2C.js} +75 -1
  41. package/dist/chunk-MLVMBV2C.js.map +1 -0
  42. package/dist/{chunk-QZ7ODIVL.js → chunk-MNUPGYIV.js} +2 -2
  43. package/dist/{chunk-MR4PJ277.js → chunk-MTJ2LFAJ.js} +2 -2
  44. package/dist/{chunk-2TCHDANJ.js → chunk-NXBXM7Q6.js} +2 -2
  45. package/dist/{chunk-TIJYQXDI.js → chunk-OHX52AOS.js} +2 -2
  46. package/dist/{chunk-PEPHBH2W.js → chunk-PYTATYUV.js} +2 -2
  47. package/dist/{chunk-UVUTV7CM.js → chunk-TGN4M5MB.js} +7 -7
  48. package/dist/{chunk-JTPXSXHC.js → chunk-V4ZHKCGA.js} +2 -2
  49. package/dist/{chunk-OI4BXFSB.js → chunk-VL5JJOOY.js} +2 -2
  50. package/dist/{chunk-Q2LQZYQ7.js → chunk-Z4GALEO3.js} +3 -3
  51. package/dist/{chunk-A5TEHAR4.js → chunk-ZCVPFDHB.js} +3 -3
  52. package/dist/{chunk-4FJKKC2N.js → chunk-ZI6A7X4V.js} +12 -12
  53. package/dist/chunk-ZI6A7X4V.js.map +1 -0
  54. package/dist/{cli-C98xlwYA.d.ts → cli-B2Ve7R22.d.ts} +3 -3
  55. package/dist/cli.d.ts +5 -5
  56. package/dist/cli.js +21 -21
  57. package/dist/compounding/engine.d.ts +1 -1
  58. package/dist/compounding/engine.js +3 -3
  59. package/dist/compounding/preference-consolidator.d.ts +1 -1
  60. package/dist/compression-optimizer.d.ts +1 -1
  61. package/dist/config.d.ts +1 -1
  62. package/dist/config.js +1 -1
  63. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  64. package/dist/connectors/codex-materialize-runner.js +3 -3
  65. package/dist/connectors/codex-materialize.d.ts +1 -1
  66. package/dist/connectors/index.d.ts +1 -1
  67. package/dist/connectors/index.js +3 -3
  68. package/dist/consolidation-provenance-check.d.ts +1 -1
  69. package/dist/consolidation-undo.d.ts +1 -1
  70. package/dist/contradiction/index.d.ts +1 -1
  71. package/dist/conversation-index/backend.d.ts +1 -1
  72. package/dist/conversation-index/chunker.d.ts +1 -1
  73. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  74. package/dist/conversation-index/indexer.d.ts +1 -1
  75. package/dist/conversation-index/search.d.ts +1 -1
  76. package/dist/day-summary.d.ts +1 -1
  77. package/dist/delinearize.d.ts +1 -1
  78. package/dist/direct-answer-wiring.d.ts +1 -1
  79. package/dist/direct-answer.d.ts +1 -1
  80. package/dist/embedding-fallback.d.ts +1 -1
  81. package/dist/enrichment/index.d.ts +1 -1
  82. package/dist/entity-retrieval.d.ts +1 -1
  83. package/dist/entity-retrieval.js +3 -3
  84. package/dist/entity-schema.d.ts +1 -1
  85. package/dist/explicit-capture.d.ts +3 -3
  86. package/dist/extraction-judge-telemetry.d.ts +1 -1
  87. package/dist/extraction-judge-training.d.ts +1 -1
  88. package/dist/extraction-judge.d.ts +1 -1
  89. package/dist/extraction.d.ts +1 -1
  90. package/dist/fallback-llm.d.ts +1 -1
  91. package/dist/identity-continuity.d.ts +1 -1
  92. package/dist/importance.d.ts +1 -1
  93. package/dist/index.d.ts +8 -8
  94. package/dist/index.js +26 -26
  95. package/dist/intent.d.ts +1 -1
  96. package/dist/lcm/engine.d.ts +1 -1
  97. package/dist/lcm/index.d.ts +1 -1
  98. package/dist/lcm/tools.d.ts +1 -1
  99. package/dist/lifecycle.d.ts +1 -1
  100. package/dist/live-connectors-runner.d.ts +1 -1
  101. package/dist/local-llm.d.ts +1 -1
  102. package/dist/maintenance/memory-governance.d.ts +1 -1
  103. package/dist/maintenance/memory-governance.js +3 -3
  104. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  105. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  106. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  107. package/dist/memory-action-policy.d.ts +1 -1
  108. package/dist/memory-cache.d.ts +1 -1
  109. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  110. package/dist/memory-projection-store.d.ts +1 -1
  111. package/dist/memory-provenance.d.ts +1 -1
  112. package/dist/memory-worth-outcomes.d.ts +1 -1
  113. package/dist/models-json.d.ts +1 -1
  114. package/dist/namespaces/migrate.d.ts +1 -1
  115. package/dist/namespaces/migrate.js +4 -4
  116. package/dist/namespaces/principal.d.ts +1 -1
  117. package/dist/namespaces/search.d.ts +1 -1
  118. package/dist/namespaces/storage.d.ts +1 -1
  119. package/dist/namespaces/storage.js +3 -3
  120. package/dist/native-knowledge.d.ts +1 -1
  121. package/dist/operator-toolkit.d.ts +1 -1
  122. package/dist/operator-toolkit.js +7 -7
  123. package/dist/{orchestrator-DyP9QYsh.d.ts → orchestrator-7zPqGupX.d.ts} +2 -2
  124. package/dist/orchestrator.d.ts +3 -3
  125. package/dist/orchestrator.js +12 -12
  126. package/dist/patterns-cli.d.ts +1 -1
  127. package/dist/policy-runtime.d.ts +1 -1
  128. package/dist/qmd-recall-cache.d.ts +1 -1
  129. package/dist/qmd.d.ts +1 -1
  130. package/dist/recall-disclosure-escalation.d.ts +1 -1
  131. package/dist/recall-explain-renderer.d.ts +1 -1
  132. package/dist/recall-explain-renderer.js +3 -3
  133. package/dist/recall-planner-llm.d.ts +1 -1
  134. package/dist/recall-state.d.ts +1 -1
  135. package/dist/recall-tag-filter.d.ts +1 -1
  136. package/dist/recall-xray-cli.d.ts +1 -1
  137. package/dist/recall-xray-cli.js +4 -4
  138. package/dist/recall-xray-renderer.d.ts +1 -1
  139. package/dist/recall-xray-renderer.js +3 -3
  140. package/dist/recall-xray.d.ts +1 -1
  141. package/dist/recall-xray.js +2 -2
  142. package/dist/resolve-auth-token.d.ts +1 -1
  143. package/dist/resume-bundles.js +2 -2
  144. package/dist/retrieval-agents.d.ts +1 -1
  145. package/dist/retrieval-tiers.d.ts +1 -1
  146. package/dist/routing/engine.d.ts +1 -1
  147. package/dist/routing/store.d.ts +1 -1
  148. package/dist/schemas.d.ts +22 -22
  149. package/dist/search/embed-helper.d.ts +1 -1
  150. package/dist/search/factory.d.ts +1 -1
  151. package/dist/search/index.d.ts +1 -1
  152. package/dist/search/lancedb-backend.d.ts +1 -1
  153. package/dist/search/meilisearch-backend.d.ts +1 -1
  154. package/dist/search/noop-backend.d.ts +1 -1
  155. package/dist/search/orama-backend.d.ts +1 -1
  156. package/dist/search/port.d.ts +1 -1
  157. package/dist/search/remote-backend.d.ts +1 -1
  158. package/dist/{semantic-consolidation-BICZvQ3C.d.ts → semantic-consolidation-BX9Z9_aK.d.ts} +1 -1
  159. package/dist/semantic-consolidation.d.ts +2 -2
  160. package/dist/semantic-consolidation.js +4 -4
  161. package/dist/semantic-rule-promotion.js +3 -3
  162. package/dist/semantic-rule-verifier.d.ts +1 -1
  163. package/dist/semantic-rule-verifier.js +3 -3
  164. package/dist/session-observer-bands.d.ts +1 -1
  165. package/dist/session-observer-state.d.ts +1 -1
  166. package/dist/shared-context/manager.d.ts +1 -1
  167. package/dist/signal.d.ts +1 -1
  168. package/dist/storage.d.ts +1 -1
  169. package/dist/storage.js +2 -2
  170. package/dist/summarizer.d.ts +1 -1
  171. package/dist/summary-snapshot.d.ts +1 -1
  172. package/dist/temporal-supersession.d.ts +1 -1
  173. package/dist/temporal-validity.d.ts +1 -1
  174. package/dist/threading.d.ts +1 -1
  175. package/dist/tier-migration.d.ts +1 -1
  176. package/dist/tier-routing.d.ts +1 -1
  177. package/dist/topics.d.ts +1 -1
  178. package/dist/transcript.d.ts +1 -1
  179. package/dist/transfer/types.d.ts +12 -12
  180. package/dist/{types-D96bCB3C.d.ts → types-D3pm4NhH.d.ts} +30 -1
  181. package/dist/types.d.ts +1 -1
  182. package/dist/types.js +1 -1
  183. package/dist/utility-runtime.d.ts +1 -1
  184. package/dist/verified-recall.js +3 -3
  185. package/package.json +1 -1
  186. package/src/coding/coding-knowledge-config.ts +111 -0
  187. package/src/coding/decision-records.test.ts +434 -0
  188. package/src/coding/decision-records.ts +507 -0
  189. package/src/config.test.ts +118 -0
  190. package/src/config.ts +3 -2
  191. package/src/orchestrator.ts +2 -2
  192. package/src/types.ts +35 -0
  193. package/dist/chunk-4FJKKC2N.js.map +0 -1
  194. package/dist/chunk-AZBV4RRY.js.map +0 -1
  195. package/dist/chunk-CTAV55JM.js.map +0 -1
  196. /package/dist/{chunk-MPXYHC35.js.map → chunk-32RD3GIW.js.map} +0 -0
  197. /package/dist/{chunk-BQJUPECT.js.map → chunk-3OKWZT7F.js.map} +0 -0
  198. /package/dist/{chunk-2MXEVL75.js.map → chunk-6VP3YUCS.js.map} +0 -0
  199. /package/dist/{chunk-WXXLSZHA.js.map → chunk-ATRB6Q25.js.map} +0 -0
  200. /package/dist/{chunk-KOXGLQS7.js.map → chunk-B6IUW76R.js.map} +0 -0
  201. /package/dist/{chunk-QHWJG5C5.js.map → chunk-CPVV2UEL.js.map} +0 -0
  202. /package/dist/{chunk-AARDBQTA.js.map → chunk-CTCPB57O.js.map} +0 -0
  203. /package/dist/{chunk-WKMCC4NQ.js.map → chunk-EUM7CZFM.js.map} +0 -0
  204. /package/dist/{chunk-46WUVFOD.js.map → chunk-F7OWUP3G.js.map} +0 -0
  205. /package/dist/{chunk-SXYCVRLK.js.map → chunk-GSTYVG5L.js.map} +0 -0
  206. /package/dist/{chunk-XMWF6AU3.js.map → chunk-LVTTO3VC.js.map} +0 -0
  207. /package/dist/{chunk-4T7P2HLJ.js.map → chunk-LXVOZ2O6.js.map} +0 -0
  208. /package/dist/{chunk-TJ7HH5LB.js.map → chunk-LZSMQHXC.js.map} +0 -0
  209. /package/dist/{chunk-QZ7ODIVL.js.map → chunk-MNUPGYIV.js.map} +0 -0
  210. /package/dist/{chunk-MR4PJ277.js.map → chunk-MTJ2LFAJ.js.map} +0 -0
  211. /package/dist/{chunk-2TCHDANJ.js.map → chunk-NXBXM7Q6.js.map} +0 -0
  212. /package/dist/{chunk-TIJYQXDI.js.map → chunk-OHX52AOS.js.map} +0 -0
  213. /package/dist/{chunk-PEPHBH2W.js.map → chunk-PYTATYUV.js.map} +0 -0
  214. /package/dist/{chunk-UVUTV7CM.js.map → chunk-TGN4M5MB.js.map} +0 -0
  215. /package/dist/{chunk-JTPXSXHC.js.map → chunk-V4ZHKCGA.js.map} +0 -0
  216. /package/dist/{chunk-OI4BXFSB.js.map → chunk-VL5JJOOY.js.map} +0 -0
  217. /package/dist/{chunk-Q2LQZYQ7.js.map → chunk-Z4GALEO3.js.map} +0 -0
  218. /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
+ });