@remnic/core 9.3.688 → 9.3.690

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 (163) hide show
  1. package/dist/access-boundary.d.ts +5 -4
  2. package/dist/access-boundary.js +12 -11
  3. package/dist/access-cli.js +90 -26
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +4 -3
  6. package/dist/access-http.js +15 -14
  7. package/dist/access-mcp.d.ts +11 -3
  8. package/dist/access-mcp.js +14 -13
  9. package/dist/access-operations.d.ts +11 -5
  10. package/dist/access-operations.js +15 -12
  11. package/dist/{access-service-DmCHJ4cH.d.ts → access-service-Dujr3MTm.d.ts} +62 -2
  12. package/dist/access-service.d.ts +4 -3
  13. package/dist/access-service.js +11 -10
  14. package/dist/access-surface-catalog.d.ts +4 -3
  15. package/dist/access-surface-catalog.js +2 -0
  16. package/dist/access-surface-catalog.js.map +1 -1
  17. package/dist/bootstrap.d.ts +3 -2
  18. package/dist/briefing.d.ts +1 -0
  19. package/dist/briefing.js +3 -2
  20. package/dist/buffer.d.ts +1 -0
  21. package/dist/{catalog-COqWZlZ6.d.ts → catalog-CKPtJ114.d.ts} +1 -1
  22. package/dist/causal-consolidation.js +4 -3
  23. package/dist/causal-consolidation.js.map +1 -1
  24. package/dist/{chunk-B4XVLHJA.js → chunk-2N6UNJSX.js} +2 -2
  25. package/dist/{chunk-NN7QYW5W.js → chunk-46URPRE6.js} +2 -2
  26. package/dist/chunk-46URPRE6.js.map +1 -0
  27. package/dist/{chunk-4N3TFFPH.js → chunk-54PVJDO5.js} +2 -2
  28. package/dist/{chunk-PH3HOKYW.js → chunk-67MMWC74.js} +2 -2
  29. package/dist/{chunk-STOEE37X.js → chunk-BKAMHZYR.js} +2 -2
  30. package/dist/{chunk-B43NZNMG.js → chunk-CE6CBRCV.js} +432 -2240
  31. package/dist/chunk-CE6CBRCV.js.map +1 -0
  32. package/dist/{chunk-RVYD6LR3.js → chunk-CP2NZQLT.js} +15 -4
  33. package/dist/chunk-CP2NZQLT.js.map +1 -0
  34. package/dist/{chunk-BLIWOONZ.js → chunk-CUNTLEJP.js} +4 -12
  35. package/dist/chunk-CUNTLEJP.js.map +1 -0
  36. package/dist/{chunk-2SJCWLQD.js → chunk-CWE74HRG.js} +3 -3
  37. package/dist/{chunk-VX6OBUDW.js → chunk-GR77Z2BM.js} +2 -2
  38. package/dist/{chunk-CTOQEZSN.js → chunk-LXIEXSHU.js} +2 -2
  39. package/dist/{chunk-KFBOZYME.js → chunk-NSVXK7M5.js} +33 -4
  40. package/dist/chunk-NSVXK7M5.js.map +1 -0
  41. package/dist/{chunk-473JIN2U.js → chunk-OMKUJTVJ.js} +37 -5
  42. package/dist/chunk-OMKUJTVJ.js.map +1 -0
  43. package/dist/{chunk-GS55WYRL.js → chunk-PHZKALOE.js} +2 -2
  44. package/dist/{chunk-6O6A6YUO.js → chunk-RGNEARXW.js} +3 -3
  45. package/dist/{chunk-4FE2K57M.js → chunk-RTFAZOIR.js} +2 -2
  46. package/dist/{chunk-UTYBJR7M.js → chunk-SB6CQTKP.js} +2 -2
  47. package/dist/chunk-SVOZFLIQ.js +16 -0
  48. package/dist/chunk-SVOZFLIQ.js.map +1 -0
  49. package/dist/{chunk-2KAYTPPT.js → chunk-TYF3D4MS.js} +5 -5
  50. package/dist/{chunk-WIHPNY65.js → chunk-UD5OKH4J.js} +2 -2
  51. package/dist/{chunk-QANVLERJ.js → chunk-UPIBE2DK.js} +2 -2
  52. package/dist/{chunk-CCWHPGT4.js → chunk-WROKD3XC.js} +45 -30
  53. package/dist/chunk-WROKD3XC.js.map +1 -0
  54. package/dist/{chunk-ZPTISBQU.js → chunk-X5J3JZR3.js} +7 -7
  55. package/dist/{chunk-OV4D5T7V.js → chunk-X7RLU5CR.js} +2 -2
  56. package/dist/{chunk-S2OU5DZY.js → chunk-XKUKJIOY.js} +30 -7
  57. package/dist/chunk-XKUKJIOY.js.map +1 -0
  58. package/dist/{chunk-FUCUR2OZ.js → chunk-Z7KILAOU.js} +603 -9
  59. package/dist/chunk-Z7KILAOU.js.map +1 -0
  60. package/dist/chunk-ZU7N3S6V.js +2190 -0
  61. package/dist/chunk-ZU7N3S6V.js.map +1 -0
  62. package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
  63. package/dist/{cli-D8nZ2MPH.d.ts → cli-BkDp6WNi.d.ts} +2 -2
  64. package/dist/cli.d.ts +5 -4
  65. package/dist/cli.js +32 -30
  66. package/dist/compounding/engine.d.ts +1 -0
  67. package/dist/compounding/engine.js +3 -2
  68. package/dist/connectors/codex-materialize-runner.js +3 -2
  69. package/dist/connectors/index.js +3 -2
  70. package/dist/consolidation-provenance-check.d.ts +1 -0
  71. package/dist/consolidation-undo.d.ts +1 -0
  72. package/dist/contradiction/index.d.ts +1 -0
  73. package/dist/conversation-index/backend.js +2 -2
  74. package/dist/entity-retrieval.d.ts +1 -0
  75. package/dist/entity-retrieval.js +3 -2
  76. package/dist/explicit-capture.d.ts +3 -2
  77. package/dist/index.d.ts +6 -5
  78. package/dist/index.js +47 -44
  79. package/dist/index.js.map +1 -1
  80. package/dist/lcm/engine.js +3 -3
  81. package/dist/lcm/index.js +5 -5
  82. package/dist/maintenance/memory-governance.js +3 -2
  83. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -2
  84. package/dist/maintenance/rebuild-memory-projection.js +4 -3
  85. package/dist/mcp-memory-inspector-app.d.ts +4 -3
  86. package/dist/memory-worth-outcomes.d.ts +1 -0
  87. package/dist/namespaces/migrate.d.ts +2 -1
  88. package/dist/namespaces/migrate.js +10 -9
  89. package/dist/namespaces/search.js +6 -6
  90. package/dist/namespaces/storage.d.ts +2 -1
  91. package/dist/namespaces/storage.js +3 -2
  92. package/dist/operator-toolkit.d.ts +1 -0
  93. package/dist/operator-toolkit.js +13 -11
  94. package/dist/{orchestrator-CA6ouzBn.d.ts → orchestrator-B7ixmUkP.d.ts} +145 -1
  95. package/dist/orchestrator.d.ts +3 -2
  96. package/dist/orchestrator.js +22 -20
  97. package/dist/recall-pipeline-stages.d.ts +167 -0
  98. package/dist/recall-pipeline-stages.js +61 -0
  99. package/dist/recall-pipeline-stages.js.map +1 -0
  100. package/dist/schemas.d.ts +10 -10
  101. package/dist/search/factory.js +5 -5
  102. package/dist/search/index.js +9 -9
  103. package/dist/semantic-consolidation.js +4 -3
  104. package/dist/semantic-rule-promotion.js +3 -2
  105. package/dist/semantic-rule-verifier.js +3 -2
  106. package/dist/storage.d.ts +3 -27
  107. package/dist/storage.js +5 -3
  108. package/dist/structured-attributes.d.ts +29 -0
  109. package/dist/structured-attributes.js +8 -0
  110. package/dist/structured-attributes.js.map +1 -0
  111. package/dist/temporal-supersession.d.ts +1 -0
  112. package/dist/tier-migration.d.ts +1 -0
  113. package/dist/verified-recall.js +3 -2
  114. package/package.json +2 -2
  115. package/src/access-boundary.ts +2 -1
  116. package/src/access-cli.test.ts +40 -0
  117. package/src/access-cli.ts +90 -2
  118. package/src/access-http.ts +35 -6
  119. package/src/access-mcp.ts +34 -0
  120. package/src/access-operations.ts +45 -0
  121. package/src/access-service.ts +60 -0
  122. package/src/access-surface-catalog.test.ts +1 -1
  123. package/src/access-surface-catalog.ts +2 -0
  124. package/src/cli.ts +18 -0
  125. package/src/coding/architecture-card.test.ts +544 -0
  126. package/src/coding/architecture-card.ts +687 -0
  127. package/src/coding/architecture-surfaces.test.ts +579 -0
  128. package/src/coding/architecture-surfaces.ts +457 -0
  129. package/src/maintenance/namespace-maintenance-fanout.test.ts +595 -0
  130. package/src/maintenance/namespace-maintenance-fanout.ts +318 -0
  131. package/src/maintenance/namespace-planner.ts +74 -16
  132. package/src/operator-toolkit.ts +25 -0
  133. package/src/orchestrator.ts +144 -0
  134. package/src/recall-pipeline-stages.test.ts +269 -0
  135. package/src/recall-pipeline-stages.ts +294 -0
  136. package/src/storage.ts +6 -20
  137. package/src/structured-attributes.ts +39 -0
  138. package/dist/chunk-473JIN2U.js.map +0 -1
  139. package/dist/chunk-B43NZNMG.js.map +0 -1
  140. package/dist/chunk-BLIWOONZ.js.map +0 -1
  141. package/dist/chunk-CCWHPGT4.js.map +0 -1
  142. package/dist/chunk-FUCUR2OZ.js.map +0 -1
  143. package/dist/chunk-KFBOZYME.js.map +0 -1
  144. package/dist/chunk-NN7QYW5W.js.map +0 -1
  145. package/dist/chunk-RVYD6LR3.js.map +0 -1
  146. package/dist/chunk-S2OU5DZY.js.map +0 -1
  147. /package/dist/{chunk-B4XVLHJA.js.map → chunk-2N6UNJSX.js.map} +0 -0
  148. /package/dist/{chunk-4N3TFFPH.js.map → chunk-54PVJDO5.js.map} +0 -0
  149. /package/dist/{chunk-PH3HOKYW.js.map → chunk-67MMWC74.js.map} +0 -0
  150. /package/dist/{chunk-STOEE37X.js.map → chunk-BKAMHZYR.js.map} +0 -0
  151. /package/dist/{chunk-2SJCWLQD.js.map → chunk-CWE74HRG.js.map} +0 -0
  152. /package/dist/{chunk-VX6OBUDW.js.map → chunk-GR77Z2BM.js.map} +0 -0
  153. /package/dist/{chunk-CTOQEZSN.js.map → chunk-LXIEXSHU.js.map} +0 -0
  154. /package/dist/{chunk-GS55WYRL.js.map → chunk-PHZKALOE.js.map} +0 -0
  155. /package/dist/{chunk-6O6A6YUO.js.map → chunk-RGNEARXW.js.map} +0 -0
  156. /package/dist/{chunk-4FE2K57M.js.map → chunk-RTFAZOIR.js.map} +0 -0
  157. /package/dist/{chunk-UTYBJR7M.js.map → chunk-SB6CQTKP.js.map} +0 -0
  158. /package/dist/{chunk-2KAYTPPT.js.map → chunk-TYF3D4MS.js.map} +0 -0
  159. /package/dist/{chunk-WIHPNY65.js.map → chunk-UD5OKH4J.js.map} +0 -0
  160. /package/dist/{chunk-QANVLERJ.js.map → chunk-UPIBE2DK.js.map} +0 -0
  161. /package/dist/{chunk-ZPTISBQU.js.map → chunk-X5J3JZR3.js.map} +0 -0
  162. /package/dist/{chunk-OV4D5T7V.js.map → chunk-X7RLU5CR.js.map} +0 -0
  163. /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
@@ -0,0 +1,544 @@
1
+ /**
2
+ * Tests for the architecture-card pure builder (issue #1548 Track A PR 3).
3
+ *
4
+ * Contract under test:
5
+ * - Deterministic: two runs over the same fixture produce byte-identical
6
+ * output (rule 38 — everything sorted before serialising).
7
+ * - Capped: card truncated with a visible marker when it exceeds the
8
+ * byte limit (rule 34 — never silently incomplete).
9
+ * - Invalid input rejected with a tagged failure (rule 51).
10
+ * - LLM summary: opt-in, deterministic card ships unchanged on failure
11
+ * (rules 13, 48).
12
+ *
13
+ * All fixtures are synthetic temp dirs — no real repos (public-repo policy).
14
+ */
15
+ import assert from "node:assert/strict";
16
+ import { mkdtemp, mkdir, writeFile, rm, chmod } from "node:fs/promises";
17
+ import { tmpdir } from "node:os";
18
+ import path from "node:path";
19
+ import test from "node:test";
20
+
21
+ import {
22
+ buildArchitectureCard,
23
+ createArchitectureCardSummariser,
24
+ type ArchitectureCardLlmClient,
25
+ ARCHITECTURE_CARD_MAX_BYTES,
26
+ } from "./architecture-card.js";
27
+
28
+ // ──────────────────────────────────────────────────────────────────────────
29
+ // Fixture builder — synthetic repo tree in a temp dir
30
+ // ──────────────────────────────────────────────────────────────────────────
31
+
32
+ async function makeFixtureRepo(overrides: {
33
+ files?: Array<{ path: string; content?: string }>;
34
+ dirs?: string[];
35
+ } = {}): Promise<string> {
36
+ const dir = await mkdtemp(path.join(tmpdir(), "archcard-"));
37
+ for (const d of overrides.dirs ?? []) {
38
+ await mkdir(path.join(dir, d), { recursive: true });
39
+ }
40
+ for (const f of overrides.files ?? []) {
41
+ const fullPath = path.join(dir, f.path);
42
+ await mkdir(path.dirname(fullPath), { recursive: true });
43
+ await writeFile(fullPath, f.content ?? "", "utf-8");
44
+ }
45
+ return dir;
46
+ }
47
+
48
+ const NOW = new Date("2026-07-05T12:00:00.000Z");
49
+
50
+ // ──────────────────────────────────────────────────────────────────────────
51
+ // Determinism — byte-stable across two runs (rule 38)
52
+ // ──────────────────────────────────────────────────────────────────────────
53
+
54
+ test("deterministic: two runs over the same fixture produce byte-identical output", async () => {
55
+ const repo = await makeFixtureRepo({
56
+ dirs: ["src", "tests", "docs", "node_modules", ".git"],
57
+ files: [
58
+ { path: "package.json", content: JSON.stringify({ name: "test-repo", main: "src/index.ts", scripts: { start: "node ." } }) },
59
+ { path: "src/index.ts", content: "export const x = 1;" },
60
+ { path: "src/utils.ts", content: "export const y = 2;" },
61
+ { path: "src/helper.py", content: "x = 1" },
62
+ { path: "tests/a.test.ts", content: "test;" },
63
+ { path: "README.md", content: "# Test" },
64
+ ],
65
+ });
66
+ try {
67
+ const result1 = await buildArchitectureCard(repo, { now: NOW });
68
+ const result2 = await buildArchitectureCard(repo, { now: NOW });
69
+ assert.equal(result1.ok, true);
70
+ assert.equal(result2.ok, true);
71
+ if (!result1.ok || !result2.ok) return;
72
+ assert.equal(result1.card.content, result2.card.content, "byte-identical content");
73
+ assert.equal(result1.card.generatedAt, NOW.toISOString());
74
+ assert.equal(result1.card.byteSize, result2.card.byteSize);
75
+ assert.equal(result1.card.truncated, false);
76
+ } finally {
77
+ await rm(repo, { recursive: true, force: true });
78
+ }
79
+ });
80
+
81
+ // ──────────────────────────────────────────────────────────────────────────
82
+ // Sorted output — directories, manifests, histogram all sorted (rule 38)
83
+ // ──────────────────────────────────────────────────────────────────────────
84
+
85
+ test("sorted: top-level directories appear in lexicographic order", async () => {
86
+ const repo = await makeFixtureRepo({
87
+ dirs: ["zebra", "alpha", "monkey", "node_modules"],
88
+ files: [{ path: "package.json", content: "{}" }],
89
+ });
90
+ try {
91
+ const result = await buildArchitectureCard(repo, { now: NOW });
92
+ assert.equal(result.ok, true);
93
+ if (!result.ok) return;
94
+ assert.ok(result.card.content.includes("alpha"), "alpha present");
95
+ const alphaIdx = result.card.content.indexOf("alpha");
96
+ const monkeyIdx = result.card.content.indexOf("monkey");
97
+ const zebraIdx = result.card.content.indexOf("zebra");
98
+ assert.ok(alphaIdx < monkeyIdx, "alpha before monkey");
99
+ assert.ok(monkeyIdx < zebraIdx, "monkey before zebra");
100
+ assert.equal(result.card.content.includes("node_modules"), false, "node_modules skipped");
101
+ } finally {
102
+ await rm(repo, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test("sorted: language histogram by count desc, then ext alpha", async () => {
107
+ const repo = await makeFixtureRepo({
108
+ dirs: ["src"],
109
+ files: [
110
+ { path: "src/a.ts" }, { path: "src/b.ts" }, { path: "src/c.ts" },
111
+ { path: "src/a.py" }, { path: "src/b.py" },
112
+ { path: "src/a.js" },
113
+ ],
114
+ });
115
+ try {
116
+ const result = await buildArchitectureCard(repo, { now: NOW });
117
+ assert.equal(result.ok, true);
118
+ if (!result.ok) return;
119
+ // .ts (3) should appear before .py (2) before .js (1)
120
+ const tsIdx = result.card.content.indexOf(".ts:");
121
+ const pyIdx = result.card.content.indexOf(".py:");
122
+ const jsIdx = result.card.content.indexOf(".js:");
123
+ assert.ok(tsIdx > -1 && pyIdx > -1 && jsIdx > -1, "all exts present");
124
+ assert.ok(tsIdx < pyIdx, ".ts before .py");
125
+ assert.ok(pyIdx < jsIdx, ".py before .js");
126
+ } finally {
127
+ await rm(repo, { recursive: true, force: true });
128
+ }
129
+ });
130
+
131
+ // ──────────────────────────────────────────────────────────────────────────
132
+ // Byte cap — truncation marker visible (rule 34)
133
+ // ──────────────────────────────────────────────────────────────────────────
134
+
135
+ test("capped: card truncated with visible marker when exceeding maxBytes", async () => {
136
+ const dirs = Array.from({ length: 200 }, (_, i) => `dir${String(i).padStart(3, "0")}`);
137
+ const files = dirs.map((d) => ({ path: `${d}/file.txt`, content: "x".repeat(50) }));
138
+ const repo = await makeFixtureRepo({ dirs, files });
139
+ try {
140
+ const result = await buildArchitectureCard(repo, { now: NOW, maxBytes: 500 });
141
+ assert.equal(result.ok, true);
142
+ if (!result.ok) return;
143
+ assert.equal(result.card.truncated, true);
144
+ assert.ok(result.card.content.includes("truncated"), "truncation marker visible");
145
+ assert.ok(result.card.byteSize <= 500, "byte size within cap");
146
+ } finally {
147
+ await rm(repo, { recursive: true, force: true });
148
+ }
149
+ });
150
+
151
+ test("uncapped: normal fixture fits without truncation", async () => {
152
+ const repo = await makeFixtureRepo({
153
+ files: [{ path: "package.json", content: JSON.stringify({ name: "small" }) }],
154
+ });
155
+ try {
156
+ const result = await buildArchitectureCard(repo, { now: NOW });
157
+ assert.equal(result.ok, true);
158
+ if (!result.ok) return;
159
+ assert.equal(result.card.truncated, false);
160
+ assert.ok(result.card.byteSize <= ARCHITECTURE_CARD_MAX_BYTES);
161
+ } finally {
162
+ await rm(repo, { recursive: true, force: true });
163
+ }
164
+ });
165
+
166
+ // ──────────────────────────────────────────────────────────────────────────
167
+ // Invalid input — tagged failures (rule 51)
168
+ // ──────────────────────────────────────────────────────────────────────────
169
+
170
+ test("invalid: empty repoRoot rejected with invalid_root", async () => {
171
+ const result = await buildArchitectureCard("");
172
+ assert.equal(result.ok, false);
173
+ if (result.ok) return;
174
+ assert.equal(result.code, "invalid_root");
175
+ });
176
+
177
+ test("invalid: non-existent path rejected with invalid_root", async () => {
178
+ const result = await buildArchitectureCard("/definitely/does/not/exist/repo-xyz");
179
+ assert.equal(result.ok, false);
180
+ if (result.ok) return;
181
+ assert.equal(result.code, "invalid_root");
182
+ });
183
+
184
+ test("invalid: path that is a file, not a directory, rejected", async () => {
185
+ const dir = await mkdtemp(path.join(tmpdir(), "archcard-file-"));
186
+ const filePath = path.join(dir, "not-a-dir.txt");
187
+ await writeFile(filePath, "hello", "utf-8");
188
+ try {
189
+ const result = await buildArchitectureCard(filePath);
190
+ assert.equal(result.ok, false);
191
+ if (result.ok) return;
192
+ assert.equal(result.code, "invalid_root");
193
+ } finally {
194
+ await rm(dir, { recursive: true, force: true });
195
+ }
196
+ });
197
+
198
+ test("invalid: unreadable root surfaces scan_failed, not a silent empty card (codex review)", async () => {
199
+ // Skip under root — root bypasses permission bits so the readdir would
200
+ // succeed and the assertion would be meaningless.
201
+ if (process.getuid?.() === 0) return;
202
+ const repo = await makeFixtureRepo({
203
+ files: [{ path: "package.json", content: JSON.stringify({ name: "guarded" }) }],
204
+ });
205
+ try {
206
+ await chmod(repo, 0o000);
207
+ const result = await buildArchitectureCard(repo, { now: NOW });
208
+ assert.equal(result.ok, false);
209
+ if (result.ok) return;
210
+ assert.equal(result.code, "scan_failed", "unreadable root is scan_failed, not a sparse success");
211
+ } finally {
212
+ // Restore perms so rm can clean up.
213
+ await chmod(repo, 0o755).catch(() => {});
214
+ await rm(repo, { recursive: true, force: true });
215
+ }
216
+ });
217
+
218
+ // ──────────────────────────────────────────────────────────────────────────
219
+ // Manifest parse — project name + entry points surface in the card
220
+ // ──────────────────────────────────────────────────────────────────────────
221
+
222
+ test("manifest: package.json name and entry points surface in card", async () => {
223
+ const repo = await makeFixtureRepo({
224
+ files: [{
225
+ path: "package.json",
226
+ content: JSON.stringify({
227
+ name: "my-pkg",
228
+ main: "src/index.ts",
229
+ bin: { "my-cli": "src/cli.ts" },
230
+ scripts: { start: "node .", dev: "tsx watch" },
231
+ }),
232
+ }],
233
+ });
234
+ try {
235
+ const result = await buildArchitectureCard(repo, { now: NOW });
236
+ assert.equal(result.ok, true);
237
+ if (!result.ok) return;
238
+ assert.ok(result.card.content.includes("my-pkg"), "project name surfaced");
239
+ assert.ok(result.card.content.includes("src/index.ts"), "main entry surfaced");
240
+ assert.ok(result.card.content.includes("src/cli.ts"), "bin target path surfaced");
241
+ assert.ok(result.card.content.includes("scripts.start"), "start script surfaced");
242
+ } finally {
243
+ await rm(repo, { recursive: true, force: true });
244
+ }
245
+ });
246
+
247
+ test("manifest: multiple manifests all listed sorted", async () => {
248
+ const repo = await makeFixtureRepo({
249
+ files: [
250
+ { path: "package.json", content: JSON.stringify({ name: "ts-side" }) },
251
+ { path: "go.mod", content: "module github.com/example/go-side\n\ngo 1.21\n" },
252
+ ],
253
+ });
254
+ try {
255
+ const result = await buildArchitectureCard(repo, { now: NOW });
256
+ assert.equal(result.ok, true);
257
+ if (!result.ok) return;
258
+ const pkgIdx = result.card.content.indexOf("package.json");
259
+ const goIdx = result.card.content.indexOf("go.mod");
260
+ assert.ok(pkgIdx > -1 && goIdx > -1, "both manifests present");
261
+ // Alphabetical order: go.mod (g) before package.json (p)
262
+ assert.ok(goIdx < pkgIdx, "go.mod before package.json (sorted)");
263
+ } finally {
264
+ await rm(repo, { recursive: true, force: true });
265
+ }
266
+ });
267
+
268
+ // ──────────────────────────────────────────────────────────────────────────
269
+ // LLM summary — opt-in, deterministic card on failure (rules 13, 48)
270
+ // ──────────────────────────────────────────────────────────────────────────
271
+
272
+ test("llm summary: off by default — no summariser call", async () => {
273
+ const repo = await makeFixtureRepo({
274
+ files: [{ path: "package.json", content: "{}" }],
275
+ });
276
+ let called = false;
277
+ try {
278
+ const result = await buildArchitectureCard(repo, {
279
+ now: NOW,
280
+ summariser: async () => { called = true; return "SUMMARY"; },
281
+ });
282
+ assert.equal(result.ok, true);
283
+ assert.equal(called, false, "summariser not called when llmSummary is off/default");
284
+ } finally {
285
+ await rm(repo, { recursive: true, force: true });
286
+ }
287
+ });
288
+
289
+ test("llm summary: on — summary prepended before deterministic card", async () => {
290
+ const repo = await makeFixtureRepo({
291
+ files: [{ path: "package.json", content: JSON.stringify({ name: "test" }) }],
292
+ });
293
+ try {
294
+ const result = await buildArchitectureCard(repo, {
295
+ now: NOW,
296
+ llmSummary: true,
297
+ summariser: async () => "## Overview\n\nThis is a test repo.",
298
+ });
299
+ assert.equal(result.ok, true);
300
+ if (!result.ok) return;
301
+ assert.ok(result.card.content.startsWith("## Overview"), "summary prepended");
302
+ assert.ok(result.card.content.includes("---"), "separator between summary and deterministic card");
303
+ assert.ok(result.card.content.includes("Architecture Card"), "deterministic card still present");
304
+ } finally {
305
+ await rm(repo, { recursive: true, force: true });
306
+ }
307
+ });
308
+
309
+ test("llm summary: summariser failure → deterministic card unchanged (rule 13)", async () => {
310
+ const repo = await makeFixtureRepo({
311
+ files: [{ path: "package.json", content: JSON.stringify({ name: "test" }) }],
312
+ });
313
+ try {
314
+ const deterministic = await buildArchitectureCard(repo, { now: NOW });
315
+ assert.equal(deterministic.ok, true);
316
+
317
+ const withFailingLlm = await buildArchitectureCard(repo, {
318
+ now: NOW,
319
+ llmSummary: true,
320
+ summariser: async () => { throw new Error("LLM down"); },
321
+ });
322
+ assert.equal(withFailingLlm.ok, true);
323
+ if (!deterministic.ok || !withFailingLlm.ok) return;
324
+ assert.equal(withFailingLlm.card.content, deterministic.card.content, "deterministic card unchanged on LLM failure");
325
+ } finally {
326
+ await rm(repo, { recursive: true, force: true });
327
+ }
328
+ });
329
+
330
+ test("llm summary: null return → deterministic card unchanged", async () => {
331
+ const repo = await makeFixtureRepo({
332
+ files: [{ path: "package.json", content: JSON.stringify({ name: "test" }) }],
333
+ });
334
+ try {
335
+ const deterministic = await buildArchitectureCard(repo, { now: NOW });
336
+ const withNullLlm = await buildArchitectureCard(repo, {
337
+ now: NOW,
338
+ llmSummary: true,
339
+ summariser: async () => null,
340
+ });
341
+ assert.equal(deterministic.ok, true);
342
+ assert.equal(withNullLlm.ok, true);
343
+ if (!deterministic.ok || !withNullLlm.ok) return;
344
+ assert.equal(withNullLlm.card.content, deterministic.card.content);
345
+ } finally {
346
+ await rm(repo, { recursive: true, force: true });
347
+ }
348
+ });
349
+
350
+ test("llm summary: over-long summary clamped so deterministic card survives (codex review)", async () => {
351
+ const repo = await makeFixtureRepo({
352
+ files: [{ path: "package.json", content: JSON.stringify({ name: "clamped" }) }],
353
+ });
354
+ try {
355
+ const result = await buildArchitectureCard(repo, {
356
+ now: NOW,
357
+ llmSummary: true,
358
+ summariser: async () => "X".repeat(8192),
359
+ });
360
+ assert.equal(result.ok, true);
361
+ if (!result.ok) return;
362
+ // The deterministic card must FULLY survive — not merely that its leading
363
+ // project name is present (a truncated tail would still keep the prefix).
364
+ // Assert the card is not truncated at all and its trailing deterministic
365
+ // sections are intact (kilo review: prove survival, not just prefix).
366
+ assert.equal(result.card.truncated, false, "deterministic card not truncated by the summary");
367
+ assert.ok(result.card.content.includes("clamped"), "deterministic project name survived summary clamp");
368
+ assert.ok(result.card.content.trimEnd().endsWith("clamped") || /##\s/.test(result.card.content), "deterministic section structure intact");
369
+ assert.ok(result.card.byteSize <= ARCHITECTURE_CARD_MAX_BYTES, "card within cap");
370
+ } finally {
371
+ await rm(repo, { recursive: true, force: true });
372
+ }
373
+ });
374
+
375
+ // ──────────────────────────────────────────────────────────────────────────
376
+ // createArchitectureCardSummariser — factory wiring (rule 48)
377
+ // ──────────────────────────────────────────────────────────────────────────
378
+
379
+ /** Structural stub satisfying ArchitectureCardLlmClient — no cast needed. */
380
+ function makeStubLlmClient(content: string | null): ArchitectureCardLlmClient {
381
+ return {
382
+ async chatCompletion() {
383
+ return content === null ? null : { content };
384
+ },
385
+ };
386
+ }
387
+
388
+ test("summariser factory: null client → undefined (builder LLM branch stays inert)", () => {
389
+ assert.equal(createArchitectureCardSummariser(null), undefined);
390
+ });
391
+
392
+ test("summariser factory: client → summariser prepends overview end-to-end", async () => {
393
+ const repo = await makeFixtureRepo({
394
+ files: [{ path: "package.json", content: JSON.stringify({ name: "test" }) }],
395
+ });
396
+ try {
397
+ const summariser = createArchitectureCardSummariser(makeStubLlmClient("## Overview\n\nA test repo."));
398
+ assert.ok(summariser, "summariser defined for a real client");
399
+ const result = await buildArchitectureCard(repo, { now: NOW, llmSummary: true, summariser });
400
+ assert.equal(result.ok, true);
401
+ if (!result.ok) return;
402
+ assert.ok(result.card.content.startsWith("## Overview"), "factory-built summary prepended");
403
+ assert.ok(result.card.content.includes("---"), "separator present");
404
+ assert.ok(result.card.content.includes("Architecture Card"), "deterministic card still present");
405
+ } finally {
406
+ await rm(repo, { recursive: true, force: true });
407
+ }
408
+ });
409
+
410
+ test("summariser factory: chatCompletion returning null → deterministic card unchanged (rule 13)", async () => {
411
+ const repo = await makeFixtureRepo({
412
+ files: [{ path: "package.json", content: JSON.stringify({ name: "test" }) }],
413
+ });
414
+ try {
415
+ const deterministic = await buildArchitectureCard(repo, { now: NOW });
416
+ const summariser = createArchitectureCardSummariser(makeStubLlmClient(null));
417
+ assert.ok(summariser);
418
+ const withNullClient = await buildArchitectureCard(repo, { now: NOW, llmSummary: true, summariser });
419
+ assert.equal(deterministic.ok, true);
420
+ assert.equal(withNullClient.ok, true);
421
+ if (!deterministic.ok || !withNullClient.ok) return;
422
+ assert.equal(withNullClient.card.content, deterministic.card.content);
423
+ } finally {
424
+ await rm(repo, { recursive: true, force: true });
425
+ }
426
+ });
427
+
428
+ test("summariser factory: records the LLM operation tag (background priority)", async () => {
429
+ let captured: { operation?: string; priority?: string } | undefined;
430
+ const client: ArchitectureCardLlmClient = {
431
+ async chatCompletion(_messages, options) {
432
+ captured = options;
433
+ return { content: "summary" };
434
+ },
435
+ };
436
+ const summariser = createArchitectureCardSummariser(client);
437
+ assert.ok(summariser);
438
+ await summariser("card", "/repo");
439
+ assert.equal(captured?.operation, "architecture-card-summary");
440
+ assert.equal(captured?.priority, "background");
441
+ });
442
+
443
+ // ──────────────────────────────────────────────────────────────────────────
444
+ // Privacy — file contents never read except manifests
445
+ // ──────────────────────────────────────────────────────────────────────────
446
+
447
+ test("privacy: non-manifest file contents are not read (only extensions counted)", async () => {
448
+ const repo = await makeFixtureRepo({
449
+ files: [
450
+ { path: "src/secret.ts", content: "SECRET_VALUE_SHOULD_NOT_APPEAR" },
451
+ { path: "package.json", content: JSON.stringify({ name: "test" }) },
452
+ ],
453
+ });
454
+ try {
455
+ const result = await buildArchitectureCard(repo, { now: NOW });
456
+ assert.equal(result.ok, true);
457
+ if (!result.ok) return;
458
+ assert.equal(result.card.content.includes("SECRET_VALUE"), false, "non-manifest file content never surfaced");
459
+ } finally {
460
+ await rm(repo, { recursive: true, force: true });
461
+ }
462
+ });
463
+
464
+ // ──────────────────────────────────────────────────────────────────────────
465
+ // Empty repo — graceful, not a crash
466
+ // ──────────────────────────────────────────────────────────────────────────
467
+
468
+ test("empty repo: produces a valid (if sparse) card without crashing", async () => {
469
+ const repo = await makeFixtureRepo();
470
+ try {
471
+ const result = await buildArchitectureCard(repo, { now: NOW });
472
+ assert.equal(result.ok, true);
473
+ if (!result.ok) return;
474
+ assert.ok(result.card.content.includes("Architecture Card"), "title always present");
475
+ assert.equal(result.card.truncated, false);
476
+ } finally {
477
+ await rm(repo, { recursive: true, force: true });
478
+ }
479
+ });
480
+
481
+ // ──────────────────────────────────────────────────────────────────────────
482
+ // pyproject.toml [project.scripts] — EOF handling (codex P2 review)
483
+ //
484
+ // The scripts-table regex once used `\Z`, which JavaScript treats as a
485
+ // literal `Z`. When `[project.scripts]` is the final section (the common
486
+ // layout, with or without a trailing newline) the regex never matched and
487
+ // every console-script entry point was dropped from the card.
488
+ // ──────────────────────────────────────────────────────────────────────────
489
+
490
+ test("pyproject: scripts at EOF with no trailing newline surface as entry points", async () => {
491
+ // [project.scripts] is the LAST section and the file has NO trailing newline.
492
+ const pyproject =
493
+ '[project]\n' +
494
+ 'name = "demo-py"\n' +
495
+ 'version = "1.0.0"\n' +
496
+ '[project.scripts]\n' +
497
+ 'mycli = "demo_py.cli:main"\n' +
498
+ 'worker = "demo_py.worker:run"'; // no trailing newline, no next section
499
+ const repo = await makeFixtureRepo({
500
+ dirs: ["src"],
501
+ files: [
502
+ { path: "pyproject.toml", content: pyproject },
503
+ { path: "src/demo_py/__init__.py", content: "" },
504
+ ],
505
+ });
506
+ try {
507
+ const result = await buildArchitectureCard(repo, { now: NOW });
508
+ assert.equal(result.ok, true);
509
+ if (!result.ok) return;
510
+ assert.ok(result.card.content.includes("- scripts.mycli"), "mycli entry point surfaced");
511
+ assert.ok(result.card.content.includes("- scripts.worker"), "worker entry point surfaced");
512
+ } finally {
513
+ await rm(repo, { recursive: true, force: true });
514
+ }
515
+ });
516
+
517
+ test("pyproject: scripts followed by another section still parse correctly", async () => {
518
+ // Guards the non-EOF path: the next `[section]` header must still terminate
519
+ // the capture, so entries after a following section are NOT mis-attributed.
520
+ const pyproject =
521
+ '[project]\n' +
522
+ 'name = "demo-py"\n' +
523
+ '[project.scripts]\n' +
524
+ 'mycli = "demo_py.cli:main"\n' +
525
+ '\n' +
526
+ '[tool.poetry]\n' +
527
+ 'not_a_script = "should-not-appear"\n';
528
+ const repo = await makeFixtureRepo({
529
+ dirs: ["src"],
530
+ files: [
531
+ { path: "pyproject.toml", content: pyproject },
532
+ { path: "src/demo_py/__init__.py", content: "" },
533
+ ],
534
+ });
535
+ try {
536
+ const result = await buildArchitectureCard(repo, { now: NOW });
537
+ assert.equal(result.ok, true);
538
+ if (!result.ok) return;
539
+ assert.ok(result.card.content.includes("- scripts.mycli"), "mycli entry point surfaced");
540
+ assert.equal(result.card.content.includes("should-not-appear"), false, "poetry-section line not mis-attributed as a script");
541
+ } finally {
542
+ await rm(repo, { recursive: true, force: true });
543
+ }
544
+ });