@opensip-cli/yagni 0.1.11 → 0.1.13

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 (118) hide show
  1. package/README.md +3 -3
  2. package/dist/__tests__/apply-advisory-exit.test.js +2 -1
  3. package/dist/__tests__/apply-advisory-exit.test.js.map +1 -1
  4. package/dist/__tests__/architecture-invariants.test.d.ts +6 -0
  5. package/dist/__tests__/architecture-invariants.test.d.ts.map +1 -0
  6. package/dist/__tests__/architecture-invariants.test.js +37 -0
  7. package/dist/__tests__/architecture-invariants.test.js.map +1 -0
  8. package/dist/__tests__/detector-progress.test.d.ts +2 -0
  9. package/dist/__tests__/detector-progress.test.d.ts.map +1 -0
  10. package/dist/__tests__/detector-progress.test.js +77 -0
  11. package/dist/__tests__/detector-progress.test.js.map +1 -0
  12. package/dist/__tests__/duplicate-parity.test.d.ts +15 -0
  13. package/dist/__tests__/duplicate-parity.test.d.ts.map +1 -0
  14. package/dist/__tests__/duplicate-parity.test.js +304 -0
  15. package/dist/__tests__/duplicate-parity.test.js.map +1 -0
  16. package/dist/__tests__/hardening.test.d.ts +2 -0
  17. package/dist/__tests__/hardening.test.d.ts.map +1 -0
  18. package/dist/__tests__/hardening.test.js +102 -0
  19. package/dist/__tests__/hardening.test.js.map +1 -0
  20. package/dist/__tests__/session-payload.test.d.ts +2 -0
  21. package/dist/__tests__/session-payload.test.d.ts.map +1 -0
  22. package/dist/__tests__/session-payload.test.js +61 -0
  23. package/dist/__tests__/session-payload.test.js.map +1 -0
  24. package/dist/__tests__/walk-typescript-files.test.js +15 -1
  25. package/dist/__tests__/walk-typescript-files.test.js.map +1 -1
  26. package/dist/__tests__/yagni-coverage.test.js +49 -202
  27. package/dist/__tests__/yagni-coverage.test.js.map +1 -1
  28. package/dist/__tests__/yagni-golden.test.js +8 -24
  29. package/dist/__tests__/yagni-golden.test.js.map +1 -1
  30. package/dist/__tests__/yagni-presentation.test.js +5 -5
  31. package/dist/__tests__/yagni-presentation.test.js.map +1 -1
  32. package/dist/baseline-strategy.d.ts +1 -2
  33. package/dist/baseline-strategy.d.ts.map +1 -1
  34. package/dist/baseline-strategy.js +21 -16
  35. package/dist/baseline-strategy.js.map +1 -1
  36. package/dist/cli/__tests__/yagni-runner.test.js +10 -7
  37. package/dist/cli/__tests__/yagni-runner.test.js.map +1 -1
  38. package/dist/cli/execute-yagni.d.ts +15 -4
  39. package/dist/cli/execute-yagni.d.ts.map +1 -1
  40. package/dist/cli/execute-yagni.js +68 -30
  41. package/dist/cli/execute-yagni.js.map +1 -1
  42. package/dist/cli/report-data.d.ts +0 -2
  43. package/dist/cli/report-data.d.ts.map +1 -1
  44. package/dist/cli/report-data.js +0 -2
  45. package/dist/cli/report-data.js.map +1 -1
  46. package/dist/cli/yagni-command-spec.d.ts.map +1 -1
  47. package/dist/cli/yagni-command-spec.js +4 -19
  48. package/dist/cli/yagni-command-spec.js.map +1 -1
  49. package/dist/cli/yagni-config-schema.d.ts +1 -7
  50. package/dist/cli/yagni-config-schema.d.ts.map +1 -1
  51. package/dist/cli/yagni-config-schema.js +4 -6
  52. package/dist/cli/yagni-config-schema.js.map +1 -1
  53. package/dist/cli/yagni-config.d.ts.map +1 -1
  54. package/dist/cli/yagni-config.js +3 -7
  55. package/dist/cli/yagni-config.js.map +1 -1
  56. package/dist/cli/yagni-presentation.d.ts +1 -2
  57. package/dist/cli/yagni-presentation.d.ts.map +1 -1
  58. package/dist/cli/yagni-presentation.js +4 -4
  59. package/dist/cli/yagni-presentation.js.map +1 -1
  60. package/dist/cli/yagni-runner.d.ts +0 -2
  61. package/dist/cli/yagni-runner.d.ts.map +1 -1
  62. package/dist/cli/yagni-runner.js +40 -8
  63. package/dist/cli/yagni-runner.js.map +1 -1
  64. package/dist/detectors/duplicate-body-candidate.d.ts +9 -2
  65. package/dist/detectors/duplicate-body-candidate.d.ts.map +1 -1
  66. package/dist/detectors/duplicate-body-candidate.js +123 -120
  67. package/dist/detectors/duplicate-body-candidate.js.map +1 -1
  68. package/dist/detectors/registry.d.ts +9 -1
  69. package/dist/detectors/registry.d.ts.map +1 -1
  70. package/dist/detectors/registry.js +9 -1
  71. package/dist/detectors/registry.js.map +1 -1
  72. package/dist/detectors/types.d.ts +1 -2
  73. package/dist/detectors/types.d.ts.map +1 -1
  74. package/dist/detectors/unused-config-surface.d.ts.map +1 -1
  75. package/dist/detectors/unused-config-surface.js +0 -2
  76. package/dist/detectors/unused-config-surface.js.map +1 -1
  77. package/dist/index.d.ts +1 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/lib/build-ts-inventory.d.ts +33 -0
  80. package/dist/lib/build-ts-inventory.d.ts.map +1 -0
  81. package/dist/lib/build-ts-inventory.js +252 -0
  82. package/dist/lib/build-ts-inventory.js.map +1 -0
  83. package/dist/persistence/session-payload.d.ts +7 -10
  84. package/dist/persistence/session-payload.d.ts.map +1 -1
  85. package/dist/persistence/session-payload.js +64 -6
  86. package/dist/persistence/session-payload.js.map +1 -1
  87. package/dist/scoring/__tests__/confidence.test.js +3 -4
  88. package/dist/scoring/__tests__/confidence.test.js.map +1 -1
  89. package/dist/scoring/confidence.d.ts +1 -1
  90. package/dist/scoring/confidence.d.ts.map +1 -1
  91. package/dist/scoring/confidence.js +1 -2
  92. package/dist/scoring/confidence.js.map +1 -1
  93. package/dist/tool.d.ts.map +1 -1
  94. package/dist/tool.js +3 -1
  95. package/dist/tool.js.map +1 -1
  96. package/dist/types/yagni-config.d.ts +1 -4
  97. package/dist/types/yagni-config.d.ts.map +1 -1
  98. package/dist/types/yagni-config.js +0 -1
  99. package/dist/types/yagni-config.js.map +1 -1
  100. package/dist/types/yagni-metadata.d.ts +0 -1
  101. package/dist/types/yagni-metadata.d.ts.map +1 -1
  102. package/package.json +10 -10
  103. package/dist/__tests__/graph-evidence.test.d.ts +0 -2
  104. package/dist/__tests__/graph-evidence.test.d.ts.map +0 -1
  105. package/dist/__tests__/graph-evidence.test.js +0 -159
  106. package/dist/__tests__/graph-evidence.test.js.map +0 -1
  107. package/dist/evidence/graph-evidence.d.ts +0 -15
  108. package/dist/evidence/graph-evidence.d.ts.map +0 -1
  109. package/dist/evidence/graph-evidence.js +0 -89
  110. package/dist/evidence/graph-evidence.js.map +0 -1
  111. package/dist/evidence/load-graph-adapters.d.ts +0 -13
  112. package/dist/evidence/load-graph-adapters.d.ts.map +0 -1
  113. package/dist/evidence/load-graph-adapters.js +0 -67
  114. package/dist/evidence/load-graph-adapters.js.map +0 -1
  115. package/dist/lib/isolate-exit-code.d.ts +0 -8
  116. package/dist/lib/isolate-exit-code.d.ts.map +0 -1
  117. package/dist/lib/isolate-exit-code.js +0 -27
  118. package/dist/lib/isolate-exit-code.js.map +0 -1
@@ -0,0 +1,102 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { RunScope, createSignal, runWithScope, runWithScopeSync } from '@opensip-cli/core';
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import { executeYagni } from '../cli/execute-yagni.js';
7
+ import { YagniConfigSchema } from '../cli/yagni-config-schema.js';
8
+ import { duplicateBodyCandidateDetector } from '../detectors/duplicate-body-candidate.js';
9
+ import { buildTsInventory } from '../lib/build-ts-inventory.js';
10
+ function stubCli() {
11
+ return {
12
+ scope: { datastore: () => undefined },
13
+ deliverSignals: vi.fn(() => Promise.resolve({ delivered: false })),
14
+ reportFailure: vi.fn(() => Promise.resolve()),
15
+ };
16
+ }
17
+ describe('yagni hardening (H1–H4)', () => {
18
+ it('rejects unknown keys in the strict yagni config block (H1)', () => {
19
+ expect(YagniConfigSchema.safeParse({ graphMode: 'build' }).success).toBe(false);
20
+ });
21
+ it('emits project-relative paths under the project root (H2)', () => {
22
+ const dir = mkdtempSync(join(tmpdir(), 'yagni-hardening-'));
23
+ try {
24
+ mkdirSync(join(dir, 'src'));
25
+ writeFileSync(join(dir, 'src', 'sample.ts'), `export function secretFn() { return 'SECRET_TOKEN_abc123'; }\n`);
26
+ const scope = new RunScope();
27
+ const candidates = runWithScopeSync(scope, () => buildTsInventory(dir));
28
+ for (const c of candidates) {
29
+ expect(c.filePath.startsWith('../')).toBe(false);
30
+ expect(c.filePath.includes('..')).toBe(false);
31
+ }
32
+ }
33
+ finally {
34
+ rmSync(dir, { recursive: true, force: true });
35
+ }
36
+ });
37
+ it('never surfaces raw body source in logger events or signals (H4)', async () => {
38
+ const dir = mkdtempSync(join(tmpdir(), 'yagni-secret-'));
39
+ const secret = 'SECRET_TOKEN_abc123';
40
+ const info = vi.fn();
41
+ try {
42
+ mkdirSync(join(dir, 'src'));
43
+ writeFileSync(join(dir, 'src', 'secret.ts'), `export function leak() { return '${secret}'; }\n`);
44
+ const scope = new RunScope({
45
+ logger: { debug: vi.fn(), info, warn: vi.fn(), error: vi.fn() },
46
+ });
47
+ const result = await runWithScope(scope, () => duplicateBodyCandidateDetector.run({
48
+ cwd: dir,
49
+ config: {},
50
+ graphCatalog: null,
51
+ includeTests: true,
52
+ }));
53
+ const logText = JSON.stringify(info.mock.calls);
54
+ const signalText = JSON.stringify(result.signals);
55
+ expect(logText).not.toContain(secret);
56
+ expect(signalText).not.toContain(secret);
57
+ }
58
+ finally {
59
+ rmSync(dir, { recursive: true, force: true });
60
+ }
61
+ });
62
+ it('honors documented @yagni-ignore-next-line directives', async () => {
63
+ const dir = mkdtempSync(join(tmpdir(), 'yagni-ignore-'));
64
+ try {
65
+ mkdirSync(join(dir, 'src'));
66
+ writeFileSync(join(dir, 'src', 'sample.ts'), [
67
+ '// @yagni-ignore-next-line duplicate-body-candidate -- fixture documents an intentional duplicate shape',
68
+ 'export function mirrored(): number { return 1; }',
69
+ ].join('\n'));
70
+ const detector = {
71
+ id: 'duplicate-body-candidate',
72
+ slug: 'yagni:duplicate-body-candidate',
73
+ description: 'test detector',
74
+ run: () => Promise.resolve({
75
+ durationMs: 0,
76
+ signals: [
77
+ createSignal({
78
+ source: 'yagni:duplicate-body-candidate',
79
+ provider: 'yagni',
80
+ ruleId: 'yagni:duplicate-body-candidate',
81
+ severity: 'medium',
82
+ category: 'quality',
83
+ message: 'duplicate',
84
+ code: { file: 'src/sample.ts', line: 2, column: 0 },
85
+ }),
86
+ ],
87
+ }),
88
+ };
89
+ const outcome = await executeYagni({ cwd: dir, config: { defaultMinConfidence: 'low' } }, stubCli(), [detector]);
90
+ expect(outcome.envelope.signals).toHaveLength(0);
91
+ expect(outcome.envelope.units[0]).toMatchObject({
92
+ slug: 'yagni:duplicate-body-candidate',
93
+ violationCount: 0,
94
+ ignoredCount: 1,
95
+ });
96
+ }
97
+ finally {
98
+ rmSync(dir, { recursive: true, force: true });
99
+ }
100
+ });
101
+ });
102
+ //# sourceMappingURL=hardening.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardening.test.js","sourceRoot":"","sources":["../../src/__tests__/hardening.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-payload.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-payload.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/session-payload.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildYagniSessionPayload, readYagniSessionPayload, } from '../persistence/session-payload.js';
3
+ import { buildYagniRunSummary } from '../scoring/confidence.js';
4
+ function minimalEnvelope() {
5
+ return {
6
+ schemaVersion: 2,
7
+ tool: 'yagni',
8
+ runId: 'run-1',
9
+ createdAt: '2026-06-25T00:00:00.000Z',
10
+ verdict: {
11
+ score: 100,
12
+ passed: true,
13
+ summary: { total: 0, passed: 0, failed: 0, errors: 0, warnings: 0 },
14
+ },
15
+ units: [],
16
+ signals: [],
17
+ baselineIdentity: {
18
+ fingerprintStrategyId: 'yagni.sha256-detector-locations',
19
+ fingerprintStrategyVersion: 1,
20
+ },
21
+ };
22
+ }
23
+ describe('YagniSessionPayload', () => {
24
+ it('round-trips a fresh payload without graph fields', () => {
25
+ const summary = buildYagniRunSummary([], []);
26
+ const payload = buildYagniSessionPayload(minimalEnvelope(), [], summary);
27
+ expect(payload.summary).not.toHaveProperty('graphMode');
28
+ expect(payload.summary).not.toHaveProperty('graphBuilt');
29
+ expect(payload.summary).not.toHaveProperty('graphDetail');
30
+ expect(readYagniSessionPayload(payload)).toEqual(payload);
31
+ });
32
+ it('forward-compat loads pre-feature rows carrying removed graph fields', () => {
33
+ const legacy = {
34
+ __version: 1,
35
+ summary: {
36
+ total: 1,
37
+ passed: 1,
38
+ failed: 0,
39
+ errors: 0,
40
+ warnings: 0,
41
+ skippedDetectors: [],
42
+ graphMode: 'off',
43
+ graphBuilt: false,
44
+ graphDetail: 'legacy detail',
45
+ yagni: {
46
+ totalCandidates: 0,
47
+ byConfidence: { high: 0, medium: 0, low: 0 },
48
+ estimatedTotalLocReduction: 0,
49
+ graphMode: 'off',
50
+ skippedDetectors: [],
51
+ },
52
+ },
53
+ checks: [],
54
+ };
55
+ const loaded = readYagniSessionPayload(legacy);
56
+ expect(loaded).toBeDefined();
57
+ expect(loaded?.summary).not.toHaveProperty('graphMode');
58
+ expect(loaded?.summary.yagni).not.toHaveProperty('graphMode');
59
+ });
60
+ });
61
+ //# sourceMappingURL=session-payload.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-payload.test.js","sourceRoot":"","sources":["../../src/__tests__/session-payload.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EACL,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIhE,SAAS,eAAe;IACtB,OAAO;QACL,aAAa,EAAE,CAAC;QAChB,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,OAAO;QACd,SAAS,EAAE,0BAA0B;QACrC,OAAO,EAAE;YACP,KAAK,EAAE,GAAG;YACV,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;SACpE;QACD,KAAK,EAAE,EAAE;QACT,OAAO,EAAE,EAAE;QACX,gBAAgB,EAAE;YAChB,qBAAqB,EAAE,iCAAiC;YACxD,0BAA0B,EAAE,CAAC;SAC9B;KACF,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,OAAO,GAAG,oBAAoB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,wBAAwB,CAAC,eAAe,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;QACzE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QACxD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;QACzD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QAC1D,MAAM,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,MAAM,GAAG;YACb,SAAS,EAAE,CAAC;YACZ,OAAO,EAAE;gBACP,KAAK,EAAE,CAAC;gBACR,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,CAAC;gBACT,MAAM,EAAE,CAAC;gBACT,QAAQ,EAAE,CAAC;gBACX,gBAAgB,EAAE,EAAE;gBACpB,SAAS,EAAE,KAAK;gBAChB,UAAU,EAAE,KAAK;gBACjB,WAAW,EAAE,eAAe;gBAC5B,KAAK,EAAE;oBACL,eAAe,EAAE,CAAC;oBAClB,YAAY,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE;oBAC5C,0BAA0B,EAAE,CAAC;oBAC7B,SAAS,EAAE,KAAK;oBAChB,gBAAgB,EAAE,EAAE;iBACrB;aACF;YACD,MAAM,EAAE,EAAE;SACX,CAAC;QACF,MAAM,MAAM,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QACxD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -17,6 +17,10 @@ function makeFixtureTree() {
17
17
  }
18
18
  for (const file of [
19
19
  'src/index.ts',
20
+ 'src/view.tsx',
21
+ 'src/types.d.ts',
22
+ 'src/module.mts',
23
+ 'src/common.cts',
20
24
  'src/__tests__/unit.test.ts',
21
25
  'src/__tests__/fixtures/sample.ts',
22
26
  'src/__tests__/__fixtures__/golden.ts',
@@ -35,12 +39,22 @@ describe('walkTypeScriptFiles', () => {
35
39
  });
36
40
  it('excludes tests and fixtures unless includeTests is enabled', () => {
37
41
  const root = makeFixtureTree();
38
- expect(rel(root, walkTypeScriptFiles(root, false))).toEqual(['src/index.ts']);
42
+ expect(rel(root, walkTypeScriptFiles(root, false))).toEqual([
43
+ 'src/common.cts',
44
+ 'src/index.ts',
45
+ 'src/module.mts',
46
+ 'src/types.d.ts',
47
+ 'src/view.tsx',
48
+ ]);
39
49
  expect(rel(root, walkTypeScriptFiles(root, true))).toEqual([
40
50
  'src/__tests__/__fixtures__/golden.ts',
41
51
  'src/__tests__/fixtures/sample.ts',
42
52
  'src/__tests__/unit.test.ts',
53
+ 'src/common.cts',
43
54
  'src/index.ts',
55
+ 'src/module.mts',
56
+ 'src/types.d.ts',
57
+ 'src/view.tsx',
44
58
  ]);
45
59
  });
46
60
  });
@@ -1 +1 @@
1
- {"version":3,"file":"walk-typescript-files.test.js","sourceRoot":"","sources":["../../src/__tests__/walk-typescript-files.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAEtE,MAAM,KAAK,GAAa,EAAE,CAAC;AAE3B,SAAS,eAAe;IACtB,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,KAAK,MAAM,GAAG,IAAI;QAChB,KAAK;QACL,eAAe;QACf,wBAAwB;QACxB,4BAA4B;KAC7B,EAAE,CAAC;QACF,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,KAAK,MAAM,IAAI,IAAI;QACjB,cAAc;QACd,4BAA4B;QAC5B,kCAAkC;QAClC,sCAAsC;KACvC,EAAE,CAAC;QACF,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,2BAA2B,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,GAAG,CAAC,IAAY,EAAE,KAAwB;IACjD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAChF,CAAC;AAED,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,IAAI,GAAG,eAAe,EAAE,CAAC;QAE/B,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC;QAC9E,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YACzD,sCAAsC;YACtC,kCAAkC;YAClC,4BAA4B;YAC5B,cAAc;SACf,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"walk-typescript-files.test.js","sourceRoot":"","sources":["../../src/__tests__/walk-typescript-files.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAEtE,MAAM,KAAK,GAAa,EAAE,CAAC;AAE3B,SAAS,eAAe;IACtB,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,KAAK,MAAM,GAAG,IAAI;QAChB,KAAK;QACL,eAAe;QACf,wBAAwB;QACxB,4BAA4B;KAC7B,EAAE,CAAC;QACF,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,KAAK,MAAM,IAAI,IAAI;QACjB,cAAc;QACd,cAAc;QACd,gBAAgB;QAChB,gBAAgB;QAChB,gBAAgB;QAChB,4BAA4B;QAC5B,kCAAkC;QAClC,sCAAsC;KACvC,EAAE,CAAC;QACF,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,2BAA2B,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,GAAG,CAAC,IAAY,EAAE,KAAwB;IACjD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAChF,CAAC;AAED,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,IAAI,GAAG,eAAe,EAAE,CAAC;QAE/B,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YAC1D,gBAAgB;YAChB,cAAc;YACd,gBAAgB;YAChB,gBAAgB;YAChB,cAAc;SACf,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YACzD,sCAAsC;YACtC,kCAAkC;YAClC,4BAA4B;YAC5B,gBAAgB;YAChB,cAAc;YACd,gBAAgB;YAChB,gBAAgB;YAChB,cAAc;SACf,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -11,7 +11,6 @@ import { YagniConfigSchema, yagniConfigDeclaration } from '../cli/yagni-config-s
11
11
  import { loadYagniConfig } from '../cli/yagni-config.js';
12
12
  import { buildYagniRunPresentation, buildYagniPresentationLines, } from '../cli/yagni-presentation.js';
13
13
  import { createYagniSignal } from '../detectors/create-yagni-signal.js';
14
- import { duplicateBodyCandidateDetector } from '../detectors/duplicate-body-candidate.js';
15
14
  import { unusedConfigSurfaceDetector } from '../detectors/unused-config-surface.js';
16
15
  import { resolveYagniPositionalPaths } from '../lib/resolve-positional-paths.js';
17
16
  import { buildYagniSessionPayload } from '../persistence/session-payload.js';
@@ -43,6 +42,7 @@ function makeCli() {
43
42
  writeSarif: vi.fn(() => Promise.resolve()),
44
43
  maybeOpenReport: vi.fn(() => Promise.resolve()),
45
44
  _state: state,
45
+ reportFailure: vi.fn(() => Promise.resolve()),
46
46
  };
47
47
  }
48
48
  function signal(id, confidence, netEstimate, estimateKind, category = 'config') {
@@ -72,20 +72,6 @@ function signal(id, confidence, netEstimate, estimateKind, category = 'config')
72
72
  },
73
73
  });
74
74
  }
75
- function graphOccurrence(overrides) {
76
- return {
77
- kind: 'function-declaration',
78
- params: [],
79
- returnType: null,
80
- enclosingClass: null,
81
- decorators: [],
82
- visibility: 'exported',
83
- inTestFile: false,
84
- definedInGenerated: false,
85
- calls: [],
86
- ...overrides,
87
- };
88
- }
89
75
  function envelope(input) {
90
76
  const summary = input.summary ?? {
91
77
  total: input.units?.length ?? 0,
@@ -102,6 +88,10 @@ function envelope(input) {
102
88
  verdict: { score: 1, passed: summary.errors === 0, summary },
103
89
  units: input.units ?? [],
104
90
  signals: input.signals,
91
+ baselineIdentity: {
92
+ fingerprintStrategyId: 'yagni.sha256-detector-locations',
93
+ fingerprintStrategyVersion: 1,
94
+ },
105
95
  };
106
96
  }
107
97
  async function runCommandInScope(scope, rawOpts, cli) {
@@ -115,7 +105,6 @@ describe('yagni config, tool metadata, and command handler', () => {
115
105
  Object.assign(scoped, {
116
106
  toolConfig: {
117
107
  yagni: {
118
- graphMode: 'off',
119
108
  defaultMinConfidence: 'high',
120
109
  includeTests: true,
121
110
  disabledDetectors: ['x'],
@@ -124,14 +113,12 @@ describe('yagni config, tool metadata, and command handler', () => {
124
113
  });
125
114
  const scopedConfig = runWithScopeSync(scoped, () => loadYagniConfig('/unused'));
126
115
  expect(scopedConfig).toMatchObject({
127
- graphMode: 'off',
128
116
  defaultMinConfidence: 'high',
129
117
  includeTests: true,
130
118
  disabledDetectors: ['x'],
131
119
  });
132
120
  const emptyScope = new RunScope();
133
121
  expect(runWithScopeSync(emptyScope, () => loadYagniConfig('/unused'))).toMatchObject({
134
- graphMode: 'auto',
135
122
  defaultMinConfidence: 'medium',
136
123
  includeTests: false,
137
124
  });
@@ -139,42 +126,43 @@ describe('yagni config, tool metadata, and command handler', () => {
139
126
  writeFileSync(join(dir, 'opensip-cli.config.yml'), [
140
127
  'schemaVersion: 1',
141
128
  'yagni:',
142
- ' graphMode: reuse',
143
129
  ' defaultMinConfidence: low',
144
130
  ' failOnWarnings: 2',
145
131
  ' detectorSettings:',
146
- ' duplicate-body-candidate:',
147
- ' minOccurrences: 3',
132
+ ' unused-config-surface:',
133
+ ' someKnob: 3',
148
134
  ].join('\n'));
149
135
  expect(loadYagniConfig(dir)).toMatchObject({
150
- graphMode: 'reuse',
151
136
  defaultMinConfidence: 'low',
152
137
  failOnWarnings: 2,
153
- detectorSettings: { 'duplicate-body-candidate': { minOccurrences: 3 } },
138
+ detectorSettings: { 'unused-config-surface': { someKnob: 3 } },
154
139
  });
155
140
  const invalidDir = tempDir();
156
141
  writeFileSync(join(invalidDir, 'opensip-cli.config.yml'), 'schemaVersion: 1\nyagni: nope\n');
157
- expect(loadYagniConfig(invalidDir)).toMatchObject({ graphMode: 'auto' });
142
+ expect(loadYagniConfig(invalidDir)).toMatchObject({
143
+ defaultMinConfidence: 'medium',
144
+ });
158
145
  });
159
146
  it('exports config schema, report data, and tool metadata', () => {
160
147
  expect(YagniConfigSchema.parse({
161
148
  failOnErrors: 1,
162
149
  failOnWarnings: 0,
163
150
  defaultMinConfidence: 'medium',
164
- graphMode: 'build',
165
151
  includeTests: false,
166
- disabledDetectors: ['duplicate-body-candidate'],
167
- detectorSettings: { 'duplicate-body-candidate': { minBodyLines: 8 } },
168
- })).toMatchObject({ graphMode: 'build' });
169
- expect(YagniConfigSchema.safeParse({ graphMode: 'sometimes' }).success).toBe(false);
170
- expect(yagniConfigDeclaration.env?.map((entry) => entry.envVar)).toContain('OPENSIP_YAGNI_GRAPH_MODE');
152
+ disabledDetectors: ['unused-config-surface'],
153
+ detectorSettings: { 'unused-config-surface': { someKnob: 8 } },
154
+ })).toMatchObject({ defaultMinConfidence: 'medium' });
155
+ expect(YagniConfigSchema.safeParse({ graphMode: 'build' }).success).toBe(false);
156
+ expect(yagniConfigDeclaration.env?.map((entry) => entry.envVar)).toContain('OPENSIP_YAGNI_MIN_CONFIDENCE');
171
157
  const reportData = collectYagniReportData({});
172
158
  expect(reportData.yagniSummary).toMatchObject({
173
159
  detectorCount: 2,
174
- graphBackedCount: 1,
175
160
  contractVersion: YAGNI_CONTRACT_VERSION,
176
161
  });
177
- expect(reportData.yagniCatalog).toEqual(expect.arrayContaining([expect.objectContaining({ slug: 'yagni:unused-config-surface' })]));
162
+ expect(reportData.yagniCatalog).toEqual(expect.arrayContaining([
163
+ expect.objectContaining({ slug: 'yagni:unused-config-surface' }),
164
+ expect.objectContaining({ slug: 'yagni:duplicate-body-candidate' }),
165
+ ]));
178
166
  expect(yagniTool.metadata).toMatchObject({
179
167
  id: YAGNI_STABLE_ID,
180
168
  name: 'yagni',
@@ -187,12 +175,11 @@ describe('yagni config, tool metadata, and command handler', () => {
187
175
  const jsonCli = makeCli();
188
176
  const scope = new RunScope();
189
177
  Object.assign(scope, {
190
- toolConfig: { yagni: { graphMode: 'off', defaultMinConfidence: 'low' } },
178
+ toolConfig: { yagni: { defaultMinConfidence: 'low' } },
191
179
  });
192
180
  await runCommandInScope(scope, {
193
181
  cwd: FIXTURE_ROOT,
194
182
  json: true,
195
- graph: 'off',
196
183
  minConfidence: 'low',
197
184
  includeTests: true,
198
185
  detector: ['unused-config-surface'],
@@ -210,7 +197,6 @@ describe('yagni config, tool metadata, and command handler', () => {
210
197
  await runCommandInScope(scope, {
211
198
  cwd: FIXTURE_ROOT,
212
199
  verbose: true,
213
- graph: 'invalid',
214
200
  minConfidence: 'invalid',
215
201
  category: 'config',
216
202
  includeTests: true,
@@ -224,145 +210,6 @@ describe('yagni config, tool metadata, and command handler', () => {
224
210
  });
225
211
  });
226
212
  describe('yagni detectors and scoring helpers', () => {
227
- it('emits duplicate-body candidates from graph body hashes', async () => {
228
- const catalog = {
229
- version: '1',
230
- tool: 'graph',
231
- language: 'typescript',
232
- builtAt: '2026-06-22T00:00:00.000Z',
233
- functions: {
234
- a: [
235
- graphOccurrence({
236
- qualifiedName: 'pkgA.alpha',
237
- simpleName: 'alpha',
238
- filePath: join(FIXTURE_ROOT, 'src', 'a.ts'),
239
- line: 10,
240
- column: 2,
241
- endLine: 18,
242
- bodyHash: 'same-body-hash',
243
- package: '@opensip-cli/a',
244
- }),
245
- ],
246
- b: [
247
- graphOccurrence({
248
- qualifiedName: 'pkgB.strip.test.<arrow:packages/languages/lang-python/src/__tests__/strip.test.ts:69:29>',
249
- simpleName: '<arrow:packages/languages/lang-python/src/__tests__/strip.test.ts:69:29>',
250
- filePath: join(FIXTURE_ROOT, 'src', 'b.ts'),
251
- line: 20,
252
- column: 4,
253
- endLine: 28,
254
- bodyHash: 'same-body-hash',
255
- package: '@opensip-cli/b',
256
- }),
257
- graphOccurrence({
258
- qualifiedName: 'pkgB.short',
259
- simpleName: 'short',
260
- filePath: join(FIXTURE_ROOT, 'src', 'short.ts'),
261
- line: 1,
262
- column: 1,
263
- endLine: 2,
264
- bodyHash: 'short-body-hash',
265
- }),
266
- ],
267
- },
268
- };
269
- const result = await duplicateBodyCandidateDetector.run({
270
- cwd: FIXTURE_ROOT,
271
- config: { detectorSettings: { 'duplicate-body-candidate': { minBodyLines: 3 } } },
272
- graphCatalog: catalog,
273
- includeTests: false,
274
- });
275
- expect(result.signals).toHaveLength(1);
276
- const metadata = result.signals[0] === undefined ? undefined : readYagniMetadata(result.signals[0]);
277
- const suggestedAction = metadata?.suggestedAction ?? '';
278
- expect(suggestedAction).toBe('Consolidate with src/b.ts:20 (arrow function).');
279
- expect(suggestedAction).not.toContain('<arrow:');
280
- expect(metadata).toMatchObject({
281
- detector: 'duplicate-body-candidate',
282
- reductionCategory: 'dedupe',
283
- confidence: 'medium',
284
- evidence: [
285
- expect.objectContaining({
286
- data: expect.objectContaining({
287
- occurrenceCount: 2,
288
- packages: ['@opensip-cli/a', '@opensip-cli/b'],
289
- peer: expect.objectContaining({
290
- qualifiedName: 'pkgB.strip.test.<arrow:packages/languages/lang-python/src/__tests__/strip.test.ts:69:29>',
291
- }),
292
- }),
293
- }),
294
- ],
295
- });
296
- const noGraph = await duplicateBodyCandidateDetector.run({
297
- cwd: FIXTURE_ROOT,
298
- config: {},
299
- graphCatalog: null,
300
- includeTests: false,
301
- });
302
- expect(noGraph.signals).toEqual([]);
303
- });
304
- it('formats duplicate-body peer names for human CLI output', async () => {
305
- function pair(bodyHash, peer) {
306
- return [
307
- graphOccurrence({
308
- qualifiedName: `${bodyHash}.anchor`,
309
- simpleName: 'anchor',
310
- filePath: join(FIXTURE_ROOT, 'src', `${bodyHash}-anchor.ts`),
311
- line: 10,
312
- column: 1,
313
- endLine: 16,
314
- bodyHash,
315
- }),
316
- graphOccurrence({
317
- qualifiedName: `zz.${bodyHash}`,
318
- simpleName: 'peer',
319
- filePath: join(FIXTURE_ROOT, 'src', `${bodyHash}-peer.ts`),
320
- line: 30,
321
- column: 1,
322
- endLine: 36,
323
- bodyHash,
324
- ...peer,
325
- }),
326
- ];
327
- }
328
- const catalog = {
329
- version: '1',
330
- tool: 'graph',
331
- language: 'typescript',
332
- builtAt: '2026-06-22T00:00:00.000Z',
333
- functions: {
334
- normal: pair('normal', { simpleName: 'namedHelper' }),
335
- qualifiedArrow: pair('qualified-arrow', {
336
- simpleName: 'packages/example/src/file.ts',
337
- qualifiedName: 'zz.qualified.<arrow:packages/example/src/file.ts:5:9>',
338
- }),
339
- qualifiedTail: pair('qualified-tail', {
340
- simpleName: 'packages/example/src/file.ts',
341
- qualifiedName: 'zz.qualified.namedTail',
342
- }),
343
- fallback: pair('fallback', {
344
- simpleName: 'packages/example/src/file.ts',
345
- qualifiedName: 'zz.qualified.packages/example/src/file.ts',
346
- }),
347
- },
348
- };
349
- const result = await duplicateBodyCandidateDetector.run({
350
- cwd: FIXTURE_ROOT,
351
- config: { detectorSettings: { 'duplicate-body-candidate': { minBodyLines: 3 } } },
352
- graphCatalog: catalog,
353
- includeTests: false,
354
- });
355
- const actions = result.signals
356
- .map((signal) => readYagniMetadata(signal)?.suggestedAction)
357
- .filter((action) => action !== undefined);
358
- expect(actions).toEqual(expect.arrayContaining([
359
- 'Consolidate with src/normal-peer.ts:30 (namedHelper).',
360
- 'Consolidate with src/qualified-arrow-peer.ts:30 (arrow function).',
361
- 'Consolidate with src/qualified-tail-peer.ts:30 (namedTail).',
362
- 'Consolidate with src/fallback-peer.ts:30 (function).',
363
- ]));
364
- expect(actions.join('\n')).not.toContain('<arrow:');
365
- });
366
213
  it('sorts, filters, summarizes, and persists YAGNI signal metadata', () => {
367
214
  const high = signal('high', 'high', 5, 'exact', 'config');
368
215
  const medium = signal('medium', 'medium', 8, 'heuristic', 'dedupe');
@@ -402,35 +249,38 @@ describe('yagni detectors and scoring helpers', () => {
402
249
  'yagni:alpha',
403
250
  'yagni:beta',
404
251
  ]);
405
- const summary = buildYagniRunSummary([low, medium, high, plain], 'reuse', [
406
- { id: 'skipped', slug: 'yagni:skipped', reason: 'disabled' },
407
- { id: 'graph', slug: 'yagni:graph', reason: 'graph-required', detail: 'missing' },
408
- ]);
252
+ const summary = buildYagniRunSummary([low, medium, high, plain], [{ id: 'skipped', slug: 'yagni:skipped', reason: 'disabled' }]);
409
253
  expect(summary).toMatchObject({
410
254
  totalCandidates: 4,
411
255
  byConfidence: { high: 1, medium: 1, low: 1 },
412
256
  estimatedTotalLocReduction: 33,
413
- graphMode: 'reuse',
414
257
  });
415
- expect(summary.skippedDetectors).toEqual([
416
- { slug: 'yagni:skipped', reason: 'disabled' },
417
- { slug: 'yagni:graph', reason: 'graph-required', detail: 'missing' },
418
- ]);
258
+ expect(summary.skippedDetectors).toEqual([{ slug: 'yagni:skipped', reason: 'disabled' }]);
419
259
  const payload = buildYagniSessionPayload(envelope({
420
260
  signals: [high],
421
- units: [{ slug: high.source, passed: false, violationCount: 1, durationMs: 7 }],
261
+ units: [
262
+ {
263
+ slug: high.source,
264
+ passed: false,
265
+ violationCount: 1,
266
+ durationMs: 7,
267
+ },
268
+ ],
422
269
  summary: { total: 1, passed: 0, failed: 1, errors: 0, warnings: 1 },
423
- }), [], {
424
- graphMode: 'reuse',
425
- graphBuilt: false,
426
- yagniSummary: summary,
427
- });
270
+ }), [], summary);
428
271
  expect(payload.checks[0]).toMatchObject({
429
272
  checkSlug: high.source,
430
273
  violationCount: 1,
431
- findings: [expect.objectContaining({ severity: 'warning', metadata: high.metadata })],
274
+ findings: [
275
+ expect.objectContaining({
276
+ severity: 'warning',
277
+ metadata: high.metadata,
278
+ }),
279
+ ],
432
280
  });
433
- expect(payload.summary.graphDetail).toBeUndefined();
281
+ expect(payload.summary).not.toHaveProperty('graphMode');
282
+ expect(payload.summary).not.toHaveProperty('graphBuilt');
283
+ expect(payload.summary).not.toHaveProperty('graphDetail');
434
284
  const errorFinding = {
435
285
  ...high,
436
286
  severity: 'high',
@@ -443,12 +293,7 @@ describe('yagni detectors and scoring helpers', () => {
443
293
  signals: [errorFinding, duplicateSource],
444
294
  units: [{ slug: high.source, passed: false, durationMs: 9 }],
445
295
  summary: { total: 1, passed: 0, failed: 1, errors: 1, warnings: 1 },
446
- }), [], {
447
- graphMode: 'build',
448
- graphBuilt: true,
449
- graphDetail: 'built fresh catalog',
450
- yagniSummary: summary,
451
- });
296
+ }), [], summary);
452
297
  expect(detailedPayload.checks[0]).toMatchObject({
453
298
  violationCount: 2,
454
299
  findings: [
@@ -458,7 +303,7 @@ describe('yagni detectors and scoring helpers', () => {
458
303
  });
459
304
  expect(detailedPayload.checks[0]?.findings[0]).not.toHaveProperty('line');
460
305
  expect(detailedPayload.checks[0]?.findings[0]).not.toHaveProperty('suggestion');
461
- expect(detailedPayload.summary.graphDetail).toBe('built fresh catalog');
306
+ expect(detailedPayload.summary).not.toHaveProperty('graphDetail');
462
307
  });
463
308
  it('covers presentation variants and unreadable/oversized source skips', async () => {
464
309
  const outcome = await unusedConfigSurfaceDetector.run({
@@ -484,7 +329,7 @@ describe('yagni detectors and scoring helpers', () => {
484
329
  units: [{ slug: 'yagni:unused-config-surface', passed: true, durationMs: 1 }],
485
330
  signals: outcome.signals,
486
331
  summary: { total: 1, passed: 0, failed: 1, errors: 0, warnings: 1 },
487
- }), FIXTURE_ROOT, 'off', [{ id: 'x', slug: 'yagni:x', reason: 'disabled', detail: 'configured' }], true);
332
+ }), FIXTURE_ROOT, [{ id: 'x', slug: 'yagni:x', reason: 'disabled', detail: 'configured' }], true);
488
333
  expect(verboseLines.join('\n')).toContain('Skipped detectors');
489
334
  const presentation = buildYagniRunPresentation({
490
335
  envelope: envelope({
@@ -492,12 +337,14 @@ describe('yagni detectors and scoring helpers', () => {
492
337
  summary: { total: 0, passed: 1, failed: 0, errors: 0, warnings: 0 },
493
338
  }),
494
339
  cwd: FIXTURE_ROOT,
495
- graphMode: 'off',
496
340
  skippedDetectors: [],
497
341
  verbose: false,
498
342
  durationMs: 12,
499
343
  });
500
- expect(presentation.envelope.verdict.summary).toMatchObject({ total: 0, warnings: 0 });
344
+ expect(presentation.envelope.verdict.summary).toMatchObject({
345
+ total: 0,
346
+ warnings: 0,
347
+ });
501
348
  const highNoDetails = signal('no-details', 'high', 0, 'exact');
502
349
  const metadata = readYagniMetadata(highNoDetails);
503
350
  const sparse = {
@@ -520,7 +367,7 @@ describe('yagni detectors and scoring helpers', () => {
520
367
  const sparseLines = buildYagniPresentationLines(envelope({
521
368
  signals: [noMetadata, sparse],
522
369
  units: [{ slug: 'yagni:sparse', passed: false, durationMs: 1 }],
523
- }), FIXTURE_ROOT, 'reuse', [{ id: 'plain', slug: 'yagni:plain', reason: 'disabled' }], true);
370
+ }), FIXTURE_ROOT, [{ id: 'plain', slug: 'yagni:plain', reason: 'disabled' }], true);
524
371
  const sparseText = sparseLines.join('\n');
525
372
  expect(sparseText).toContain('<unknown>');
526
373
  expect(sparseText).toContain('yagni:plain: disabled');