@nomos-arc/arc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.nomos-config.json +5 -0
  3. package/CLAUDE.md +108 -0
  4. package/LICENSE +190 -0
  5. package/README.md +569 -0
  6. package/dist/cli.js +21120 -0
  7. package/docs/auth/googel_plan.yaml +1093 -0
  8. package/docs/auth/google_task.md +235 -0
  9. package/docs/auth/hardened_blueprint.yaml +1658 -0
  10. package/docs/auth/red_team_report.yaml +336 -0
  11. package/docs/auth/session_state.yaml +162 -0
  12. package/docs/certificate/cer_enhance_plan.md +605 -0
  13. package/docs/certificate/certificate_report.md +338 -0
  14. package/docs/dev_overview.md +419 -0
  15. package/docs/feature_assessment.md +156 -0
  16. package/docs/how_it_works.md +78 -0
  17. package/docs/infrastructure/map.md +867 -0
  18. package/docs/init/master_plan.md +3581 -0
  19. package/docs/init/red_team_report.md +215 -0
  20. package/docs/init/report_phase_1a.md +304 -0
  21. package/docs/integrity-gate/enhance_drift.md +703 -0
  22. package/docs/integrity-gate/overview.md +108 -0
  23. package/docs/management/manger-task.md +99 -0
  24. package/docs/management/scafffold.md +76 -0
  25. package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
  26. package/docs/map/RED_TEAM_REPORT.md +159 -0
  27. package/docs/map/map_task.md +147 -0
  28. package/docs/map/semantic_graph_task.md +792 -0
  29. package/docs/map/semantic_master_plan.md +705 -0
  30. package/docs/phase7/TEAM_RED.md +249 -0
  31. package/docs/phase7/plan.md +1682 -0
  32. package/docs/phase7/task.md +275 -0
  33. package/docs/prompts/USAGE.md +312 -0
  34. package/docs/prompts/architect.md +165 -0
  35. package/docs/prompts/executer.md +190 -0
  36. package/docs/prompts/hardener.md +190 -0
  37. package/docs/prompts/red_team.md +146 -0
  38. package/docs/verification/goveranance-overview.md +396 -0
  39. package/docs/verification/governance-overview.md +245 -0
  40. package/docs/verification/verification-arc-ar.md +560 -0
  41. package/docs/verification/verification-architecture.md +560 -0
  42. package/docs/very_next.md +52 -0
  43. package/docs/whitepaper.md +89 -0
  44. package/overview.md +1469 -0
  45. package/package.json +63 -0
  46. package/src/adapters/__tests__/git.test.ts +296 -0
  47. package/src/adapters/__tests__/stdio.test.ts +70 -0
  48. package/src/adapters/git.ts +226 -0
  49. package/src/adapters/pty.ts +159 -0
  50. package/src/adapters/stdio.ts +113 -0
  51. package/src/cli.ts +83 -0
  52. package/src/commands/apply.ts +47 -0
  53. package/src/commands/auth.ts +301 -0
  54. package/src/commands/certificate.ts +89 -0
  55. package/src/commands/discard.ts +24 -0
  56. package/src/commands/drift.ts +116 -0
  57. package/src/commands/index.ts +78 -0
  58. package/src/commands/init.ts +121 -0
  59. package/src/commands/list.ts +75 -0
  60. package/src/commands/map.ts +55 -0
  61. package/src/commands/plan.ts +30 -0
  62. package/src/commands/review.ts +58 -0
  63. package/src/commands/run.ts +63 -0
  64. package/src/commands/search.ts +147 -0
  65. package/src/commands/show.ts +63 -0
  66. package/src/commands/status.ts +59 -0
  67. package/src/core/__tests__/budget.test.ts +213 -0
  68. package/src/core/__tests__/certificate.test.ts +385 -0
  69. package/src/core/__tests__/config.test.ts +191 -0
  70. package/src/core/__tests__/preflight.test.ts +24 -0
  71. package/src/core/__tests__/prompt.test.ts +358 -0
  72. package/src/core/__tests__/review.test.ts +161 -0
  73. package/src/core/__tests__/state.test.ts +362 -0
  74. package/src/core/auth/__tests__/manager.test.ts +166 -0
  75. package/src/core/auth/__tests__/server.test.ts +220 -0
  76. package/src/core/auth/gcp-projects.ts +160 -0
  77. package/src/core/auth/manager.ts +114 -0
  78. package/src/core/auth/server.ts +141 -0
  79. package/src/core/budget.ts +119 -0
  80. package/src/core/certificate.ts +502 -0
  81. package/src/core/config.ts +212 -0
  82. package/src/core/errors.ts +54 -0
  83. package/src/core/factory.ts +49 -0
  84. package/src/core/graph/__tests__/builder.test.ts +272 -0
  85. package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
  86. package/src/core/graph/__tests__/enricher.test.ts +299 -0
  87. package/src/core/graph/__tests__/parser.test.ts +200 -0
  88. package/src/core/graph/__tests__/pipeline.test.ts +202 -0
  89. package/src/core/graph/__tests__/renderer.test.ts +128 -0
  90. package/src/core/graph/__tests__/resolver.test.ts +185 -0
  91. package/src/core/graph/__tests__/scanner.test.ts +231 -0
  92. package/src/core/graph/__tests__/show.test.ts +134 -0
  93. package/src/core/graph/builder.ts +303 -0
  94. package/src/core/graph/constraints.ts +94 -0
  95. package/src/core/graph/contract-writer.ts +93 -0
  96. package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
  97. package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
  98. package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
  99. package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
  100. package/src/core/graph/drift/classifier.ts +165 -0
  101. package/src/core/graph/drift/comparator.ts +205 -0
  102. package/src/core/graph/drift/reporter.ts +77 -0
  103. package/src/core/graph/enricher.ts +251 -0
  104. package/src/core/graph/grammar-paths.ts +30 -0
  105. package/src/core/graph/html-template.ts +493 -0
  106. package/src/core/graph/map-schema.ts +137 -0
  107. package/src/core/graph/parser.ts +336 -0
  108. package/src/core/graph/pipeline.ts +209 -0
  109. package/src/core/graph/renderer.ts +92 -0
  110. package/src/core/graph/resolver.ts +195 -0
  111. package/src/core/graph/scanner.ts +145 -0
  112. package/src/core/logger.ts +46 -0
  113. package/src/core/orchestrator.ts +792 -0
  114. package/src/core/plan-file-manager.ts +66 -0
  115. package/src/core/preflight.ts +64 -0
  116. package/src/core/prompt.ts +173 -0
  117. package/src/core/review.ts +95 -0
  118. package/src/core/state.ts +294 -0
  119. package/src/core/worktree-coordinator.ts +77 -0
  120. package/src/search/__tests__/chunk-extractor.test.ts +339 -0
  121. package/src/search/__tests__/embedder-auth.test.ts +124 -0
  122. package/src/search/__tests__/embedder.test.ts +267 -0
  123. package/src/search/__tests__/graph-enricher.test.ts +178 -0
  124. package/src/search/__tests__/indexer.test.ts +518 -0
  125. package/src/search/__tests__/integration.test.ts +649 -0
  126. package/src/search/__tests__/query-engine.test.ts +334 -0
  127. package/src/search/__tests__/similarity.test.ts +78 -0
  128. package/src/search/__tests__/vector-store.test.ts +281 -0
  129. package/src/search/chunk-extractor.ts +167 -0
  130. package/src/search/embedder.ts +209 -0
  131. package/src/search/graph-enricher.ts +95 -0
  132. package/src/search/indexer.ts +483 -0
  133. package/src/search/lexical-searcher.ts +190 -0
  134. package/src/search/query-engine.ts +225 -0
  135. package/src/search/vector-store.ts +311 -0
  136. package/src/types/index.ts +572 -0
  137. package/src/utils/__tests__/ansi.test.ts +54 -0
  138. package/src/utils/__tests__/frontmatter.test.ts +79 -0
  139. package/src/utils/__tests__/sanitize.test.ts +229 -0
  140. package/src/utils/ansi.ts +19 -0
  141. package/src/utils/context.ts +44 -0
  142. package/src/utils/frontmatter.ts +27 -0
  143. package/src/utils/sanitize.ts +78 -0
  144. package/test/e2e/lifecycle.test.ts +330 -0
  145. package/test/fixtures/mock-planner-hang.ts +5 -0
  146. package/test/fixtures/mock-planner.ts +26 -0
  147. package/test/fixtures/mock-reviewer-bad.ts +8 -0
  148. package/test/fixtures/mock-reviewer-retry.ts +34 -0
  149. package/test/fixtures/mock-reviewer.ts +18 -0
  150. package/test/fixtures/sample-project/src/circular-a.ts +6 -0
  151. package/test/fixtures/sample-project/src/circular-b.ts +6 -0
  152. package/test/fixtures/sample-project/src/config.ts +15 -0
  153. package/test/fixtures/sample-project/src/main.ts +19 -0
  154. package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
  155. package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
  156. package/test/fixtures/sample-project/src/types.ts +14 -0
  157. package/test/fixtures/sample-project/src/utils/index.ts +14 -0
  158. package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
  159. package/tsconfig.json +20 -0
  160. package/vitest.config.ts +12 -0
@@ -0,0 +1,335 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { compare } from '../comparator.js';
3
+ import type { FileNode, ProjectMap, SymbolEntry, ImportEntry, SemanticInfo } from '../../../../types/index.js';
4
+
5
+ // ─── Factory helpers ──────────────────────────────────────────────────────────
6
+
7
+ function makeSymbol(
8
+ overrides: Pick<SymbolEntry, 'name'> & Partial<SymbolEntry>,
9
+ ): SymbolEntry {
10
+ return {
11
+ kind: 'function',
12
+ line: 1,
13
+ end_line: null,
14
+ signature: null,
15
+ exported: false,
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ function makeImport(
21
+ overrides: Pick<ImportEntry, 'source'> & Partial<ImportEntry>,
22
+ ): ImportEntry {
23
+ return {
24
+ resolved: null,
25
+ symbols: [],
26
+ is_external: false,
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ function makeSemanticInfo(sourceHash: string): SemanticInfo {
32
+ return {
33
+ overview: 'test overview',
34
+ purpose: 'test purpose',
35
+ key_logic: [],
36
+ usage_context: [],
37
+ source_hash: sourceHash,
38
+ enriched_at: '2026-01-01T00:00:00.000Z',
39
+ model: 'gemini-1.5-pro',
40
+ };
41
+ }
42
+
43
+ function makeFileNode(
44
+ file: string,
45
+ hash: string,
46
+ overrides: Partial<Omit<FileNode, 'file' | 'hash'>> = {},
47
+ ): FileNode {
48
+ return {
49
+ file,
50
+ hash,
51
+ language: 'typescript',
52
+ symbols: [],
53
+ imports: [],
54
+ dependents: [],
55
+ dependencies: [],
56
+ depth: 0,
57
+ last_parsed_at: null,
58
+ semantic: null,
59
+ enrichment_status: 'structural',
60
+ ...overrides,
61
+ };
62
+ }
63
+
64
+ function makeMap(
65
+ files: Record<string, FileNode>,
66
+ statsOverrides: Partial<ProjectMap['stats']> = {},
67
+ ): ProjectMap {
68
+ return {
69
+ schema_version: 1,
70
+ generated_at: '2026-01-01T00:00:00.000Z',
71
+ root: '/project',
72
+ files,
73
+ stats: {
74
+ total_files: Object.keys(files).length,
75
+ total_symbols: 0,
76
+ total_edges: 0,
77
+ core_modules: [],
78
+ structural_only: Object.keys(files).length,
79
+ semantically_enriched: 0,
80
+ indexed: 0,
81
+ ...statsOverrides,
82
+ },
83
+ };
84
+ }
85
+
86
+ // ─── Tests ────────────────────────────────────────────────────────────────────
87
+
88
+ describe('compare()', () => {
89
+ it('1. empty maps → empty DriftReport with all counts 0', () => {
90
+ const result = compare(makeMap({}), makeMap({}));
91
+ expect(result.files).toHaveLength(0);
92
+ expect(result.symbols).toHaveLength(0);
93
+ expect(result.imports).toHaveLength(0);
94
+ expect(result.graph.depth_changes).toHaveLength(0);
95
+ expect(result.graph.core_modules_added).toHaveLength(0);
96
+ expect(result.graph.core_modules_removed).toHaveLength(0);
97
+ expect(result.stale_enrichments).toHaveLength(0);
98
+ expect(result.summary.files_added).toBe(0);
99
+ expect(result.summary.files_removed).toBe(0);
100
+ expect(result.summary.files_modified).toBe(0);
101
+ expect(result.summary.symbols_added).toBe(0);
102
+ expect(result.summary.symbols_removed).toBe(0);
103
+ expect(result.summary.symbols_changed).toBe(0);
104
+ expect(result.summary.imports_added).toBe(0);
105
+ expect(result.summary.imports_removed).toBe(0);
106
+ expect(result.summary.depth_changes).toBe(0);
107
+ expect(result.summary.stale_enrichments).toBe(0);
108
+ });
109
+
110
+ it('2. file added → status "added", hash_before null', () => {
111
+ const baseline = makeMap({});
112
+ const current = makeMap({ 'src/new.ts': makeFileNode('src/new.ts', 'hash-new') });
113
+ const result = compare(baseline, current);
114
+ expect(result.files).toHaveLength(1);
115
+ expect(result.files[0].status).toBe('added');
116
+ expect(result.files[0].file).toBe('src/new.ts');
117
+ expect(result.files[0].hash_before).toBeNull();
118
+ expect(result.files[0].hash_after).toBe('hash-new');
119
+ expect(result.summary.files_added).toBe(1);
120
+ });
121
+
122
+ it('3. file removed → status "removed", hash_after null', () => {
123
+ const baseline = makeMap({ 'src/old.ts': makeFileNode('src/old.ts', 'hash-old') });
124
+ const current = makeMap({});
125
+ const result = compare(baseline, current);
126
+ expect(result.files).toHaveLength(1);
127
+ expect(result.files[0].status).toBe('removed');
128
+ expect(result.files[0].file).toBe('src/old.ts');
129
+ expect(result.files[0].hash_before).toBe('hash-old');
130
+ expect(result.files[0].hash_after).toBeNull();
131
+ expect(result.summary.files_removed).toBe(1);
132
+ });
133
+
134
+ it('4. file modified (hash change) → status "modified"', () => {
135
+ const baseline = makeMap({ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1') });
136
+ const current = makeMap({ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2') });
137
+ const result = compare(baseline, current);
138
+ expect(result.files).toHaveLength(1);
139
+ expect(result.files[0].status).toBe('modified');
140
+ expect(result.files[0].hash_before).toBe('hash-v1');
141
+ expect(result.files[0].hash_after).toBe('hash-v2');
142
+ expect(result.summary.files_modified).toBe(1);
143
+ });
144
+
145
+ it('5. file unchanged (same hash) → not in files[]', () => {
146
+ const node = makeFileNode('src/same.ts', 'hash-stable');
147
+ const result = compare(
148
+ makeMap({ 'src/same.ts': node }),
149
+ makeMap({ 'src/same.ts': { ...node } }),
150
+ );
151
+ expect(result.files).toHaveLength(0);
152
+ });
153
+
154
+ it('6. symbol added → status "added"', () => {
155
+ const baseline = makeMap({ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1') });
156
+ const current = makeMap({
157
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2', {
158
+ symbols: [makeSymbol({ name: 'doSomething', kind: 'function', exported: true })],
159
+ }),
160
+ });
161
+ const result = compare(baseline, current);
162
+ expect(result.symbols).toHaveLength(1);
163
+ expect(result.symbols[0].status).toBe('added');
164
+ expect(result.symbols[0].name).toBe('doSomething');
165
+ expect(result.summary.symbols_added).toBe(1);
166
+ });
167
+
168
+ it('7. symbol removed (exported, has dependents) → status "removed", dependents_affected populated', () => {
169
+ const baseline = makeMap({
170
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1', {
171
+ symbols: [makeSymbol({ name: 'doSomething', kind: 'function', exported: true })],
172
+ dependents: ['src/consumer.ts', 'src/other.ts'],
173
+ }),
174
+ });
175
+ const current = makeMap({ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2') });
176
+ const result = compare(baseline, current);
177
+ expect(result.symbols).toHaveLength(1);
178
+ expect(result.symbols[0].status).toBe('removed');
179
+ expect(result.symbols[0].exported).toBe(true);
180
+ expect(result.symbols[0].dependents_affected).toEqual(['src/consumer.ts', 'src/other.ts']);
181
+ expect(result.summary.symbols_removed).toBe(1);
182
+ });
183
+
184
+ it('8. symbol removed (not exported) → status "removed", exported false', () => {
185
+ const baseline = makeMap({
186
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1', {
187
+ symbols: [makeSymbol({ name: 'internalHelper', kind: 'function', exported: false })],
188
+ }),
189
+ });
190
+ const current = makeMap({ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2') });
191
+ const result = compare(baseline, current);
192
+ expect(result.symbols).toHaveLength(1);
193
+ expect(result.symbols[0].status).toBe('removed');
194
+ expect(result.symbols[0].exported).toBe(false);
195
+ });
196
+
197
+ it('9. signature changed → status "signature_changed", signature_before/after populated', () => {
198
+ const baseline = makeMap({
199
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1', {
200
+ symbols: [
201
+ makeSymbol({ name: 'doSomething', kind: 'function', signature: '(a: string): void' }),
202
+ ],
203
+ }),
204
+ });
205
+ const current = makeMap({
206
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2', {
207
+ symbols: [
208
+ makeSymbol({
209
+ name: 'doSomething',
210
+ kind: 'function',
211
+ signature: '(a: string, b: number): void',
212
+ }),
213
+ ],
214
+ }),
215
+ });
216
+ const result = compare(baseline, current);
217
+ expect(result.symbols).toHaveLength(1);
218
+ expect(result.symbols[0].status).toBe('signature_changed');
219
+ expect(result.symbols[0].signature_before).toBe('(a: string): void');
220
+ expect(result.symbols[0].signature_after).toBe('(a: string, b: number): void');
221
+ expect(result.summary.symbols_changed).toBe(1);
222
+ });
223
+
224
+ it('10. import added → status "added" in imports[]', () => {
225
+ const baseline = makeMap({ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1') });
226
+ const current = makeMap({
227
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2', {
228
+ imports: [
229
+ makeImport({ source: 'src/bar.ts', resolved: 'src/bar.ts', is_external: false }),
230
+ ],
231
+ }),
232
+ });
233
+ const result = compare(baseline, current);
234
+ expect(result.imports).toHaveLength(1);
235
+ expect(result.imports[0].status).toBe('added');
236
+ expect(result.imports[0].source).toBe('src/bar.ts');
237
+ expect(result.summary.imports_added).toBe(1);
238
+ });
239
+
240
+ it('11. import removed → status "removed" in imports[]', () => {
241
+ const baseline = makeMap({
242
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1', {
243
+ imports: [
244
+ makeImport({ source: 'src/bar.ts', resolved: 'src/bar.ts', is_external: false }),
245
+ ],
246
+ }),
247
+ });
248
+ const current = makeMap({ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2') });
249
+ const result = compare(baseline, current);
250
+ expect(result.imports).toHaveLength(1);
251
+ expect(result.imports[0].status).toBe('removed');
252
+ expect(result.summary.imports_removed).toBe(1);
253
+ });
254
+
255
+ it('12. depth change → graph.depth_changes[] populated', () => {
256
+ const baseline = makeMap({
257
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-same', { depth: 1 }),
258
+ });
259
+ const current = makeMap({
260
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-same', { depth: 4 }),
261
+ });
262
+ const result = compare(baseline, current);
263
+ expect(result.graph.depth_changes).toHaveLength(1);
264
+ expect(result.graph.depth_changes[0]).toEqual({ file: 'src/foo.ts', before: 1, after: 4 });
265
+ expect(result.summary.depth_changes).toBe(1);
266
+ });
267
+
268
+ it('13. core module promoted → graph.core_modules_added[] populated, removed empty', () => {
269
+ const baseline = makeMap({}, { core_modules: [] });
270
+ const current = makeMap({}, { core_modules: ['src/core.ts'] });
271
+ const result = compare(baseline, current);
272
+ expect(result.graph.core_modules_added).toContain('src/core.ts');
273
+ expect(result.graph.core_modules_removed).toHaveLength(0);
274
+ });
275
+
276
+ it('14. stale enrichment (source_hash !== hash) → stale_enrichments[] populated', () => {
277
+ const current = makeMap({
278
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-new', {
279
+ enrichment_status: 'semantic',
280
+ semantic: makeSemanticInfo('hash-old'), // source_hash differs from file hash 'hash-new'
281
+ }),
282
+ });
283
+ const result = compare(makeMap({}), current);
284
+ expect(result.stale_enrichments).toHaveLength(1);
285
+ expect(result.stale_enrichments[0].file).toBe('src/foo.ts');
286
+ expect(result.stale_enrichments[0].hash_changed).toBe(true);
287
+ expect(result.summary.stale_enrichments).toBe(1);
288
+ });
289
+
290
+ it('15. summary counts are accurate for mixed changes', () => {
291
+ const baseline = makeMap({
292
+ 'src/removed.ts': makeFileNode('src/removed.ts', 'hash-rm'),
293
+ 'src/modified.ts': makeFileNode('src/modified.ts', 'hash-v1', {
294
+ symbols: [makeSymbol({ name: 'fn', kind: 'function' })],
295
+ }),
296
+ });
297
+ const current = makeMap({
298
+ 'src/added.ts': makeFileNode('src/added.ts', 'hash-new'),
299
+ 'src/modified.ts': makeFileNode('src/modified.ts', 'hash-v2', {
300
+ symbols: [
301
+ makeSymbol({ name: 'fn', kind: 'function', signature: '(x: number): void' }),
302
+ makeSymbol({ name: 'newFn', kind: 'function' }),
303
+ ],
304
+ }),
305
+ });
306
+ const result = compare(baseline, current);
307
+ expect(result.summary.files_added).toBe(1);
308
+ expect(result.summary.files_removed).toBe(1);
309
+ expect(result.summary.files_modified).toBe(1);
310
+ expect(result.summary.symbols_changed).toBe(1);
311
+ expect(result.summary.symbols_added).toBe(1);
312
+ expect(result.summary.symbols_removed).toBe(0);
313
+ });
314
+
315
+ it('16. same-name symbols with different kinds tracked separately via name:kind composite key', () => {
316
+ const typeSym = makeSymbol({ name: 'Config', kind: 'type', exported: true });
317
+ const fnSym = makeSymbol({ name: 'Config', kind: 'function', exported: true });
318
+ const baseline = makeMap({
319
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v1', {
320
+ symbols: [typeSym, fnSym],
321
+ }),
322
+ });
323
+ const current = makeMap({
324
+ 'src/foo.ts': makeFileNode('src/foo.ts', 'hash-v2', {
325
+ symbols: [], // both removed
326
+ }),
327
+ });
328
+ const result = compare(baseline, current);
329
+ expect(result.symbols).toHaveLength(2);
330
+ const compositeKeys = result.symbols.map((s) => `${s.name}:${s.kind}`);
331
+ expect(compositeKeys).toContain('Config:type');
332
+ expect(compositeKeys).toContain('Config:function');
333
+ expect(result.summary.symbols_removed).toBe(2);
334
+ });
335
+ });