@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,94 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { readProjectMap, migrateProjectMap } from './map-schema.js';
4
+ import { NomosError } from '../errors.js';
5
+
6
+ // ─── readArchitecturalConstraints ─────────────────────────────────────────────
7
+
8
+ /**
9
+ * Reads `project_map.json` and builds an architectural constraints string
10
+ * describing what symbols from `contextFiles` are consumed by their dependents.
11
+ *
12
+ * Returns `null` if the map doesn't exist or no constraints are found.
13
+ *
14
+ * [AMB-7 FIX] Context file paths are normalized to project-root-relative
15
+ * forward-slash paths before map lookup — handles relative paths like
16
+ * `../src/core/state.ts` correctly.
17
+ */
18
+ export async function readArchitecturalConstraints(
19
+ projectRoot: string,
20
+ outputDir: string,
21
+ contextFiles: string[],
22
+ ): Promise<string | null> {
23
+ // ── 1. Read project_map.json ───────────────────────────────────────────────
24
+ const mapPath = path.join(outputDir, 'project_map.json');
25
+ let raw: string;
26
+ try {
27
+ raw = await fs.readFile(mapPath, 'utf-8');
28
+ } catch (err: unknown) {
29
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
30
+ throw new NomosError(
31
+ 'graph_parse_error',
32
+ `Failed to read project map at ${mapPath}: ${String(err)}`,
33
+ );
34
+ }
35
+
36
+ // ── 2. Parse with migrateProjectMap ───────────────────────────────────────
37
+ let parsed: unknown;
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ } catch {
41
+ return null;
42
+ }
43
+
44
+ let map;
45
+ try {
46
+ map = migrateProjectMap(parsed);
47
+ } catch {
48
+ return null;
49
+ }
50
+
51
+ // ── 3. Collect constraints for each context file ──────────────────────────
52
+ const lines: string[] = [];
53
+
54
+ for (const contextFile of contextFiles) {
55
+ // [AMB-7 FIX] Normalize to project-root-relative forward-slash path
56
+ const normalizedPath = path.relative(projectRoot, path.resolve(projectRoot, contextFile))
57
+ .split(path.sep).join('/');
58
+
59
+ const fileNode = map.files[normalizedPath];
60
+ if (!fileNode) continue;
61
+
62
+ // ── 4. Collect dependents with non-null semantic ───────────────────────
63
+ const enrichedDependents = fileNode.dependents
64
+ .map(dep => map.files[dep])
65
+ .filter((dep): dep is NonNullable<typeof dep> => dep != null && dep.semantic !== null);
66
+
67
+ if (enrichedDependents.length === 0) continue;
68
+
69
+ // For each exported symbol in the context file, find which dependents consume it
70
+ for (const symbol of fileNode.symbols) {
71
+ if (!symbol.exported) continue;
72
+
73
+ const consumers = enrichedDependents.filter(dep =>
74
+ dep.imports.some(imp =>
75
+ imp.resolved === normalizedPath && imp.symbols.includes(symbol.name),
76
+ ),
77
+ );
78
+
79
+ if (consumers.length === 0) continue;
80
+
81
+ const consumerNames = consumers.map(c => c.file).join(', ');
82
+ const contract = consumers[0]?.semantic?.purpose ?? consumers[0]?.semantic?.overview ?? '';
83
+
84
+ lines.push(
85
+ `- ${normalizedPath} → symbol: ${symbol.signature ?? symbol.name}`,
86
+ ` Consumed by: ${consumerNames}`,
87
+ ` Contract: "${contract}"`,
88
+ );
89
+ }
90
+ }
91
+
92
+ if (lines.length === 0) return null;
93
+ return lines.join('\n');
94
+ }
@@ -0,0 +1,93 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import type { FileNode } from '../../types/index.js';
4
+
5
+ // ─── ContractWriter ───────────────────────────────────────────────────────────
6
+
7
+ export class ContractWriter {
8
+ constructor(
9
+ private readonly projectRoot: string,
10
+ private readonly logger: {
11
+ info(msg: string): void;
12
+ debug?(msg: string): void;
13
+ },
14
+ ) {}
15
+
16
+ /**
17
+ * Writes `.semantic.md` files next to each source file that has been enriched.
18
+ * Skips files where `semantic === null`.
19
+ * Skips files where the existing `.semantic.md` already contains the current `source_hash`.
20
+ *
21
+ * [AMB-5 FIX] Creates parent directory before writing, even if it doesn't exist.
22
+ */
23
+ async writeContracts(fileNodes: Map<string, FileNode>): Promise<void> {
24
+ for (const fileNode of fileNodes.values()) {
25
+ if (fileNode.semantic === null) continue;
26
+
27
+ const sourcePath = path.join(this.projectRoot, fileNode.file);
28
+ const ext = path.extname(sourcePath);
29
+ const outputPath = ext
30
+ ? sourcePath.slice(0, -ext.length) + '.semantic.md'
31
+ : sourcePath + '.semantic.md';
32
+
33
+ // Overwrite skip: if existing file already contains current source_hash
34
+ try {
35
+ const existing = await fs.readFile(outputPath, 'utf-8');
36
+ if (existing.includes(fileNode.semantic.source_hash)) {
37
+ continue;
38
+ }
39
+ } catch (err: unknown) {
40
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
41
+ // File doesn't exist — proceed to write
42
+ }
43
+
44
+ // [AMB-5 FIX] Ensure parent directory exists before writing
45
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
46
+
47
+ const markdown = renderContract(fileNode);
48
+ await fs.writeFile(outputPath, markdown, 'utf-8');
49
+
50
+ this.logger.debug?.(
51
+ `[nomos:graph:debug] Wrote semantic contract: ${path.relative(this.projectRoot, outputPath)}`,
52
+ );
53
+ }
54
+ }
55
+ }
56
+
57
+ // ─── Template renderer ────────────────────────────────────────────────────────
58
+
59
+ function renderContract(fileNode: FileNode): string {
60
+ const semantic = fileNode.semantic!;
61
+ const filename = path.basename(fileNode.file);
62
+
63
+ const keyLogicLines = semantic.key_logic
64
+ .map((item, i) => `${i + 1}. ${item}`)
65
+ .join('\n');
66
+
67
+ const usageContextLines = semantic.usage_context.map((item) => `- ${item}`).join('\n');
68
+
69
+ const dependentsList =
70
+ fileNode.dependents.length > 0 ? fileNode.dependents.join(', ') : '(none)';
71
+
72
+ return [
73
+ `# ${filename} — Semantic Contract`,
74
+ `> Auto-generated by \`arc map\` — do not edit manually.`,
75
+ '',
76
+ `## Overview`,
77
+ semantic.overview,
78
+ '',
79
+ `## Purpose`,
80
+ semantic.purpose,
81
+ '',
82
+ `## Key Logic`,
83
+ keyLogicLines,
84
+ '',
85
+ `## Usage Context`,
86
+ `Used by: ${dependentsList}`,
87
+ usageContextLines,
88
+ '',
89
+ '---',
90
+ `*Enriched at: ${semantic.enriched_at} | Model: ${semantic.model} | Hash: ${semantic.source_hash}*`,
91
+ '',
92
+ ].join('\n');
93
+ }
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { classify } from '../classifier.js';
3
+ import type { DriftReport } from '../../../../types/index.js';
4
+
5
+ // ─── Fixture helpers ──────────────────────────────────────────────────────────
6
+
7
+ function emptySummary(): DriftReport['summary'] {
8
+ return {
9
+ files_added: 0,
10
+ files_removed: 0,
11
+ files_modified: 0,
12
+ symbols_added: 0,
13
+ symbols_removed: 0,
14
+ symbols_changed: 0,
15
+ imports_added: 0,
16
+ imports_removed: 0,
17
+ depth_changes: 0,
18
+ stale_enrichments: 0,
19
+ };
20
+ }
21
+
22
+ function emptyReport(): DriftReport {
23
+ return {
24
+ files: [],
25
+ symbols: [],
26
+ imports: [],
27
+ graph: { depth_changes: [], core_modules_added: [], core_modules_removed: [] },
28
+ stale_enrichments: [],
29
+ summary: emptySummary(),
30
+ };
31
+ }
32
+
33
+ // ─── Tests ────────────────────────────────────────────────────────────────────
34
+
35
+ describe('classify()', () => {
36
+ it('1. exported symbol removed, 3 dependents → severity "breaking"', () => {
37
+ const report = emptyReport();
38
+ report.symbols.push({
39
+ file: 'src/foo.ts',
40
+ name: 'doSomething',
41
+ kind: 'function',
42
+ status: 'removed',
43
+ exported: true,
44
+ signature_before: null,
45
+ signature_after: null,
46
+ dependents_affected: ['a.ts', 'b.ts', 'c.ts'],
47
+ });
48
+ const result = classify(report);
49
+ expect(result.changes).toHaveLength(1);
50
+ expect(result.changes[0].severity).toBe('breaking');
51
+ expect(result.has_breaking).toBe(true);
52
+ });
53
+
54
+ it('2. exported symbol removed, 0 dependents → severity "warning"', () => {
55
+ const report = emptyReport();
56
+ report.symbols.push({
57
+ file: 'src/foo.ts',
58
+ name: 'doSomething',
59
+ kind: 'function',
60
+ status: 'removed',
61
+ exported: true,
62
+ signature_before: null,
63
+ signature_after: null,
64
+ dependents_affected: [],
65
+ });
66
+ const result = classify(report);
67
+ expect(result.changes).toHaveLength(1);
68
+ expect(result.changes[0].severity).toBe('warning');
69
+ expect(result.has_breaking).toBe(false);
70
+ });
71
+
72
+ it('3. non-exported symbol removed → severity "info"', () => {
73
+ const report = emptyReport();
74
+ report.symbols.push({
75
+ file: 'src/foo.ts',
76
+ name: 'internalHelper',
77
+ kind: 'function',
78
+ status: 'removed',
79
+ exported: false,
80
+ signature_before: null,
81
+ signature_after: null,
82
+ dependents_affected: [],
83
+ });
84
+ const result = classify(report);
85
+ expect(result.changes).toHaveLength(1);
86
+ expect(result.changes[0].severity).toBe('info');
87
+ });
88
+
89
+ it('4. file removed → severity "warning"', () => {
90
+ const report = emptyReport();
91
+ report.files.push({
92
+ file: 'src/utils.ts',
93
+ status: 'removed',
94
+ hash_before: 'abc123',
95
+ hash_after: null,
96
+ });
97
+ const result = classify(report);
98
+ const fileChange = result.changes.find((c) => c.category === 'file');
99
+ expect(fileChange).toBeDefined();
100
+ expect(fileChange!.severity).toBe('warning');
101
+ expect(fileChange!.message).toContain('src/utils.ts');
102
+ });
103
+
104
+ it('5. file added → severity "info"', () => {
105
+ const report = emptyReport();
106
+ report.files.push({
107
+ file: 'src/new.ts',
108
+ status: 'added',
109
+ hash_before: null,
110
+ hash_after: 'def456',
111
+ });
112
+ const result = classify(report);
113
+ const fileChange = result.changes.find((c) => c.category === 'file');
114
+ expect(fileChange).toBeDefined();
115
+ expect(fileChange!.severity).toBe('info');
116
+ });
117
+
118
+ it('6. stale enrichment (hash_changed true) → severity "stale"', () => {
119
+ const report = emptyReport();
120
+ report.stale_enrichments.push({
121
+ file: 'src/foo.ts',
122
+ hash_changed: true,
123
+ was_semantic_now_structural: false,
124
+ });
125
+ const result = classify(report);
126
+ const staleChange = result.changes.find((c) => c.category === 'enrichment');
127
+ expect(staleChange).toBeDefined();
128
+ expect(staleChange!.severity).toBe('stale');
129
+ expect(staleChange!.suggestion).toContain('arc map');
130
+ });
131
+
132
+ it('7. has_breaking is true when breaking changes exist', () => {
133
+ const report = emptyReport();
134
+ report.symbols.push({
135
+ file: 'src/foo.ts',
136
+ name: 'criticalFn',
137
+ kind: 'function',
138
+ status: 'removed',
139
+ exported: true,
140
+ signature_before: null,
141
+ signature_after: null,
142
+ dependents_affected: ['consumer.ts'],
143
+ });
144
+ const result = classify(report);
145
+ expect(result.has_breaking).toBe(true);
146
+ });
147
+
148
+ it('8. has_breaking is false when no breaking changes exist', () => {
149
+ const report = emptyReport();
150
+ report.files.push({
151
+ file: 'src/new.ts',
152
+ status: 'added',
153
+ hash_before: null,
154
+ hash_after: 'abc',
155
+ });
156
+ const result = classify(report);
157
+ expect(result.has_breaking).toBe(false);
158
+ });
159
+
160
+ it('9. core module demoted → severity "warning"', () => {
161
+ const report = emptyReport();
162
+ report.graph.core_modules_removed.push('src/core/critical.ts');
163
+ const result = classify(report);
164
+ const graphChange = result.changes.find(
165
+ (c) => c.category === 'graph' && c.file === 'src/core/critical.ts',
166
+ );
167
+ expect(graphChange).toBeDefined();
168
+ expect(graphChange!.severity).toBe('warning');
169
+ expect(graphChange!.message).toContain('demoted');
170
+ });
171
+
172
+ it('10. depth increase >= 2 → severity "warning"', () => {
173
+ const report = emptyReport();
174
+ report.graph.depth_changes.push({ file: 'src/foo.ts', before: 1, after: 4 });
175
+ const result = classify(report);
176
+ const depthChange = result.changes.find((c) => c.category === 'graph');
177
+ expect(depthChange).toBeDefined();
178
+ expect(depthChange!.severity).toBe('warning');
179
+ expect(depthChange!.detail).toContain('+3');
180
+ });
181
+
182
+ it('depth change with abs(delta) < 2 → no graph change classified', () => {
183
+ const report = emptyReport();
184
+ report.graph.depth_changes.push({ file: 'src/foo.ts', before: 1, after: 2 }); // delta = 1
185
+ const result = classify(report);
186
+ expect(result.changes.filter((c) => c.category === 'graph')).toHaveLength(0);
187
+ });
188
+
189
+ it('exported symbol signature_changed with dependents → severity "breaking"', () => {
190
+ const report = emptyReport();
191
+ report.symbols.push({
192
+ file: 'src/foo.ts',
193
+ name: 'computeHash',
194
+ kind: 'function',
195
+ status: 'signature_changed',
196
+ exported: true,
197
+ signature_before: '(input: string): string',
198
+ signature_after: '(input: string, salt: string): string',
199
+ dependents_affected: ['src/consumer.ts'],
200
+ });
201
+ const result = classify(report);
202
+ expect(result.changes[0].severity).toBe('breaking');
203
+ expect(result.changes[0].detail).toContain('Before:');
204
+ expect(result.changes[0].detail).toContain('After:');
205
+ });
206
+
207
+ it('summary is copied from DriftReport', () => {
208
+ const report = emptyReport();
209
+ report.summary.files_added = 3;
210
+ report.summary.symbols_removed = 2;
211
+ const result = classify(report);
212
+ expect(result.summary.files_added).toBe(3);
213
+ expect(result.summary.symbols_removed).toBe(2);
214
+ });
215
+ });