@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.
- package/.claude/settings.local.json +10 -0
- package/.nomos-config.json +5 -0
- package/CLAUDE.md +108 -0
- package/LICENSE +190 -0
- package/README.md +569 -0
- package/dist/cli.js +21120 -0
- package/docs/auth/googel_plan.yaml +1093 -0
- package/docs/auth/google_task.md +235 -0
- package/docs/auth/hardened_blueprint.yaml +1658 -0
- package/docs/auth/red_team_report.yaml +336 -0
- package/docs/auth/session_state.yaml +162 -0
- package/docs/certificate/cer_enhance_plan.md +605 -0
- package/docs/certificate/certificate_report.md +338 -0
- package/docs/dev_overview.md +419 -0
- package/docs/feature_assessment.md +156 -0
- package/docs/how_it_works.md +78 -0
- package/docs/infrastructure/map.md +867 -0
- package/docs/init/master_plan.md +3581 -0
- package/docs/init/red_team_report.md +215 -0
- package/docs/init/report_phase_1a.md +304 -0
- package/docs/integrity-gate/enhance_drift.md +703 -0
- package/docs/integrity-gate/overview.md +108 -0
- package/docs/management/manger-task.md +99 -0
- package/docs/management/scafffold.md +76 -0
- package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
- package/docs/map/RED_TEAM_REPORT.md +159 -0
- package/docs/map/map_task.md +147 -0
- package/docs/map/semantic_graph_task.md +792 -0
- package/docs/map/semantic_master_plan.md +705 -0
- package/docs/phase7/TEAM_RED.md +249 -0
- package/docs/phase7/plan.md +1682 -0
- package/docs/phase7/task.md +275 -0
- package/docs/prompts/USAGE.md +312 -0
- package/docs/prompts/architect.md +165 -0
- package/docs/prompts/executer.md +190 -0
- package/docs/prompts/hardener.md +190 -0
- package/docs/prompts/red_team.md +146 -0
- package/docs/verification/goveranance-overview.md +396 -0
- package/docs/verification/governance-overview.md +245 -0
- package/docs/verification/verification-arc-ar.md +560 -0
- package/docs/verification/verification-architecture.md +560 -0
- package/docs/very_next.md +52 -0
- package/docs/whitepaper.md +89 -0
- package/overview.md +1469 -0
- package/package.json +63 -0
- package/src/adapters/__tests__/git.test.ts +296 -0
- package/src/adapters/__tests__/stdio.test.ts +70 -0
- package/src/adapters/git.ts +226 -0
- package/src/adapters/pty.ts +159 -0
- package/src/adapters/stdio.ts +113 -0
- package/src/cli.ts +83 -0
- package/src/commands/apply.ts +47 -0
- package/src/commands/auth.ts +301 -0
- package/src/commands/certificate.ts +89 -0
- package/src/commands/discard.ts +24 -0
- package/src/commands/drift.ts +116 -0
- package/src/commands/index.ts +78 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/list.ts +75 -0
- package/src/commands/map.ts +55 -0
- package/src/commands/plan.ts +30 -0
- package/src/commands/review.ts +58 -0
- package/src/commands/run.ts +63 -0
- package/src/commands/search.ts +147 -0
- package/src/commands/show.ts +63 -0
- package/src/commands/status.ts +59 -0
- package/src/core/__tests__/budget.test.ts +213 -0
- package/src/core/__tests__/certificate.test.ts +385 -0
- package/src/core/__tests__/config.test.ts +191 -0
- package/src/core/__tests__/preflight.test.ts +24 -0
- package/src/core/__tests__/prompt.test.ts +358 -0
- package/src/core/__tests__/review.test.ts +161 -0
- package/src/core/__tests__/state.test.ts +362 -0
- package/src/core/auth/__tests__/manager.test.ts +166 -0
- package/src/core/auth/__tests__/server.test.ts +220 -0
- package/src/core/auth/gcp-projects.ts +160 -0
- package/src/core/auth/manager.ts +114 -0
- package/src/core/auth/server.ts +141 -0
- package/src/core/budget.ts +119 -0
- package/src/core/certificate.ts +502 -0
- package/src/core/config.ts +212 -0
- package/src/core/errors.ts +54 -0
- package/src/core/factory.ts +49 -0
- package/src/core/graph/__tests__/builder.test.ts +272 -0
- package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
- package/src/core/graph/__tests__/enricher.test.ts +299 -0
- package/src/core/graph/__tests__/parser.test.ts +200 -0
- package/src/core/graph/__tests__/pipeline.test.ts +202 -0
- package/src/core/graph/__tests__/renderer.test.ts +128 -0
- package/src/core/graph/__tests__/resolver.test.ts +185 -0
- package/src/core/graph/__tests__/scanner.test.ts +231 -0
- package/src/core/graph/__tests__/show.test.ts +134 -0
- package/src/core/graph/builder.ts +303 -0
- package/src/core/graph/constraints.ts +94 -0
- package/src/core/graph/contract-writer.ts +93 -0
- package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
- package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
- package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
- package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
- package/src/core/graph/drift/classifier.ts +165 -0
- package/src/core/graph/drift/comparator.ts +205 -0
- package/src/core/graph/drift/reporter.ts +77 -0
- package/src/core/graph/enricher.ts +251 -0
- package/src/core/graph/grammar-paths.ts +30 -0
- package/src/core/graph/html-template.ts +493 -0
- package/src/core/graph/map-schema.ts +137 -0
- package/src/core/graph/parser.ts +336 -0
- package/src/core/graph/pipeline.ts +209 -0
- package/src/core/graph/renderer.ts +92 -0
- package/src/core/graph/resolver.ts +195 -0
- package/src/core/graph/scanner.ts +145 -0
- package/src/core/logger.ts +46 -0
- package/src/core/orchestrator.ts +792 -0
- package/src/core/plan-file-manager.ts +66 -0
- package/src/core/preflight.ts +64 -0
- package/src/core/prompt.ts +173 -0
- package/src/core/review.ts +95 -0
- package/src/core/state.ts +294 -0
- package/src/core/worktree-coordinator.ts +77 -0
- package/src/search/__tests__/chunk-extractor.test.ts +339 -0
- package/src/search/__tests__/embedder-auth.test.ts +124 -0
- package/src/search/__tests__/embedder.test.ts +267 -0
- package/src/search/__tests__/graph-enricher.test.ts +178 -0
- package/src/search/__tests__/indexer.test.ts +518 -0
- package/src/search/__tests__/integration.test.ts +649 -0
- package/src/search/__tests__/query-engine.test.ts +334 -0
- package/src/search/__tests__/similarity.test.ts +78 -0
- package/src/search/__tests__/vector-store.test.ts +281 -0
- package/src/search/chunk-extractor.ts +167 -0
- package/src/search/embedder.ts +209 -0
- package/src/search/graph-enricher.ts +95 -0
- package/src/search/indexer.ts +483 -0
- package/src/search/lexical-searcher.ts +190 -0
- package/src/search/query-engine.ts +225 -0
- package/src/search/vector-store.ts +311 -0
- package/src/types/index.ts +572 -0
- package/src/utils/__tests__/ansi.test.ts +54 -0
- package/src/utils/__tests__/frontmatter.test.ts +79 -0
- package/src/utils/__tests__/sanitize.test.ts +229 -0
- package/src/utils/ansi.ts +19 -0
- package/src/utils/context.ts +44 -0
- package/src/utils/frontmatter.ts +27 -0
- package/src/utils/sanitize.ts +78 -0
- package/test/e2e/lifecycle.test.ts +330 -0
- package/test/fixtures/mock-planner-hang.ts +5 -0
- package/test/fixtures/mock-planner.ts +26 -0
- package/test/fixtures/mock-reviewer-bad.ts +8 -0
- package/test/fixtures/mock-reviewer-retry.ts +34 -0
- package/test/fixtures/mock-reviewer.ts +18 -0
- package/test/fixtures/sample-project/src/circular-a.ts +6 -0
- package/test/fixtures/sample-project/src/circular-b.ts +6 -0
- package/test/fixtures/sample-project/src/config.ts +15 -0
- package/test/fixtures/sample-project/src/main.ts +19 -0
- package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
- package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
- package/test/fixtures/sample-project/src/types.ts +14 -0
- package/test/fixtures/sample-project/src/utils/index.ts +14 -0
- package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
- package/tsconfig.json +20 -0
- 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
|
+
});
|