@boyingliu01/xp-gate 0.19.3 → 0.20.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 (173) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +389 -162
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +1 -1
  116. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  117. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  118. package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
  119. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  120. package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  122. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
  123. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  124. package/principles/AGENTS.md +9 -8
  125. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  126. package/principles/__tests__/analyzer.test.ts +29 -6
  127. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  128. package/principles/__tests__/baseline-prune.test.ts +338 -0
  129. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  130. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  131. package/principles/__tests__/boy-scout.test.ts +253 -7
  132. package/principles/__tests__/config-enforcement.test.ts +469 -0
  133. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  134. package/principles/__tests__/direct-execution.test.ts +187 -0
  135. package/principles/__tests__/index.test.ts +16 -3
  136. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  137. package/principles/adapters/typescript.ts +613 -13
  138. package/principles/analyzer.ts +22 -0
  139. package/principles/baseline-prune.ts +217 -0
  140. package/principles/boy-scout.ts +481 -51
  141. package/principles/config.ts +362 -54
  142. package/principles/direct-execution.ts +60 -0
  143. package/principles/index.ts +40 -17
  144. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  145. package/principles/rules/clean-code/code-duplication.ts +8 -8
  146. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  147. package/principles/rules/clean-code/god-class.ts +8 -8
  148. package/principles/rules/clean-code/large-file.ts +15 -11
  149. package/principles/rules/clean-code/long-function.ts +8 -8
  150. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  151. package/principles/rules/clean-code/many-exports.ts +7 -7
  152. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  153. package/principles/rules/clean-code/too-many-params.ts +8 -8
  154. package/principles/rules/clean-code/unused-imports.ts +5 -5
  155. package/principles/rules/solid/dip.ts +18 -8
  156. package/principles/rules/solid/isp.ts +9 -8
  157. package/principles/rules/solid/lsp.ts +6 -6
  158. package/principles/rules/solid/ocp.ts +6 -5
  159. package/principles/rules/solid/srp.ts +7 -8
  160. package/principles/types.ts +15 -0
  161. package/scripts/__tests__/sync-package-content.test.js +28 -0
  162. package/scripts/delphi-external-review.cjs +193 -10
  163. package/scripts/sync-package-content.js +7 -4
  164. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  165. package/skills/delphi-review/AGENTS.md +4 -4
  166. package/skills/delphi-review/SKILL.md +98 -14
  167. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  168. package/skills/delphi-review/references/requirements.md +6 -6
  169. package/skills/sprint-flow/AGENTS.md +4 -4
  170. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  171. package/skills/test-specification-alignment/AGENTS.md +4 -4
  172. package/lib/__tests__/download-skill.test.js +0 -281
  173. package/lib/download-skill.js +0 -96
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @test REQ-DSH-012
3
+ * @intent 验证 CLI 入口的直接执行判据在 Windows 路径下成立:`isDirectExecution()`
4
+ * 必须用 pathToFileURL() 规范化路径,而不是字符串拼接 `file://${argv[1]}`,
5
+ * 否则 win32 下 `file://D:\a\b.ts` 与 `import.meta.url`(`file:///D:/a/b.ts`)
6
+ * 永不相等,Gate 4 永远静默通过(缺陷 #444)
7
+ * @covers AC-DSH-012-01
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import * as fs from 'node:fs';
12
+ import * as os from 'node:os';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { resolve, join } from 'node:path';
15
+ import { isDirectExecution } from '../index';
16
+
17
+ describe('isDirectExecution (Gate 4 CLI entry detection)', () => {
18
+ describe('Windows paths (#444)', () => {
19
+ it('matches a backslash Windows argv[1] against its import.meta.url', () => {
20
+ const argv1 = 'D:\\projects\\xp-gate\\src\\principles\\index.ts';
21
+ const metaUrl = pathToFileURL(argv1).href;
22
+
23
+ expect(isDirectExecution(argv1, metaUrl)).toBe(true);
24
+ });
25
+
26
+ it('matches a forward-slash Windows argv[1]', () => {
27
+ const argv1 = 'D:/projects/xp-gate/src/principles/index.ts';
28
+ const metaUrl = pathToFileURL(argv1).href;
29
+
30
+ expect(isDirectExecution(argv1, metaUrl)).toBe(true);
31
+ });
32
+
33
+ it('does NOT rely on naive file:// string concatenation', () => {
34
+ // The pre-fix implementation built `file://${argv1}`, which on Windows
35
+ // yields `file://D:\…` and can never equal the normalized metaUrl.
36
+ const argv1 = 'D:\\projects\\xp-gate\\src\\principles\\index.ts';
37
+ const naive = `file://${argv1}`;
38
+ const metaUrl = pathToFileURL(argv1).href;
39
+
40
+ expect(naive).not.toBe(metaUrl);
41
+ expect(isDirectExecution(argv1, metaUrl)).toBe(true);
42
+ });
43
+ });
44
+
45
+ describe('POSIX paths (no regression)', () => {
46
+ it('matches a POSIX argv[1] against its import.meta.url', () => {
47
+ const argv1 = '/home/u/xp-gate/src/principles/index.ts';
48
+ const metaUrl = pathToFileURL(argv1).href;
49
+
50
+ expect(isDirectExecution(argv1, metaUrl)).toBe(true);
51
+ });
52
+ });
53
+
54
+ describe('negative cases', () => {
55
+ it('returns false when imported as a module (different file)', () => {
56
+ const argv1 = 'D:\\projects\\xp-gate\\src\\principles\\index.ts';
57
+ const metaUrl = pathToFileURL(
58
+ 'D:\\projects\\xp-gate\\src\\principles\\analyzer.ts',
59
+ ).href;
60
+
61
+ expect(isDirectExecution(argv1, metaUrl)).toBe(false);
62
+ });
63
+
64
+ it('returns false when argv[1] is missing', () => {
65
+ expect(isDirectExecution(undefined, 'file:///x/y.ts')).toBe(false);
66
+ });
67
+
68
+ it('returns false when there is no import.meta.url (CJS without require.main)', () => {
69
+ expect(isDirectExecution('D:\\a\\b.ts', undefined)).toBe(false);
70
+ });
71
+
72
+ it('does not match on a mere suffix collision', () => {
73
+ const argv1 = 'D:\\projects\\xp-gate\\index.ts';
74
+ const metaUrl = pathToFileURL('D:\\other\\xp-gate\\index.ts').href;
75
+
76
+ expect(isDirectExecution(argv1, metaUrl)).toBe(false);
77
+ });
78
+ });
79
+ describe('relative argv[1] (tsx passes the resolved path)', () => {
80
+ it('resolves a relative argv[1] before comparing', () => {
81
+ const rel = 'src/principles/index.ts';
82
+ const abs = resolve(process.cwd(), rel);
83
+ const metaUrl = pathToFileURL(abs).href;
84
+
85
+ expect(isDirectExecution(rel, metaUrl)).toBe(true);
86
+ });
87
+ });
88
+
89
+ /**
90
+ * @test REQ-DSH-017
91
+ * @intent 验证 boy-scout 的旧判据 `argv[1].includes('boy-scout')` 被替换后:
92
+ * 子串撞名不再误触发(误报方向),且 argv[1] 不含文件名的真实调用
93
+ * (CI 下的路径)能正确触发(漏报方向,缺陷 #453)
94
+ * @covers AC-DSH-017-01
95
+ */
96
+ describe('substring matching is not used (#453)', () => {
97
+ const SELF = pathToFileURL(resolve('src/principles/boy-scout.ts')).href;
98
+
99
+ it('is false for an unrelated path that merely contains the name', () => {
100
+ // `includes('boy-scout')` was true here, wrongly starting the CLI.
101
+ const decoy = resolve('tmp/boy-scout-notes/run.ts');
102
+ expect(isDirectExecution(decoy, SELF)).toBe(false);
103
+ });
104
+
105
+ it('is true for the real script even when argv[1] does not repeat the name', () => {
106
+ // CI shape: the runner is argv[1]; only URL comparison against this
107
+ // module's own URL identifies the entry point. The old guard no-oped
108
+ // here, so the CLI printed nothing and the stdout assertion failed.
109
+ const abs = resolve('src/principles/boy-scout.ts');
110
+ expect(isDirectExecution(abs, SELF)).toBe(true);
111
+ });
112
+
113
+ it('is false when argv[1] is a different file in the same directory', () => {
114
+ const sibling = resolve('src/principles/boy-scout.test.ts');
115
+ expect(isDirectExecution(sibling, SELF)).toBe(false);
116
+ });
117
+
118
+ it('has no default metaUrl -- import.meta.url must come from the caller', () => {
119
+ // `import.meta.url` is evaluated where it is WRITTEN. If this function
120
+ // defaulted metaUrl to its own import.meta.url, every caller would compare
121
+ // against direct-execution.ts and always get false, which is exactly the
122
+ // silent no-op (#453) it was written to remove. Calling with one argument
123
+ // must therefore not accidentally report "direct execution".
124
+ const oneArg = isDirectExecution as unknown as (a?: string) => boolean;
125
+ expect(oneArg(resolve('src/principles/boy-scout.ts'))).toBe(false);
126
+ });
127
+
128
+ it('matches through a symlinked path (Node realpaths ESM, argv[1] does not)', () => {
129
+ // Node resolves ESM module URLs through symlinks while argv[1] keeps the
130
+ // path the caller typed, so a bin shim / npm link / symlinked checkout
131
+ // compares unequal without realpath on both sides -- the same silent
132
+ // no-op class as #453.
133
+ const target = resolve('src/principles/direct-execution.ts');
134
+ const linkDir = fs.mkdtempSync(join(os.tmpdir(), 'xpgate-symlink-'));
135
+ const link = join(linkDir, 'entry.ts');
136
+ let linked = false;
137
+ try {
138
+ fs.symlinkSync(target, link, 'file');
139
+ linked = true;
140
+ } catch {
141
+ // Windows needs SeCreateSymbolicLinkPrivilege or Developer Mode. Creating
142
+ // a symlink is the only way to exercise this, so when it is unavailable we
143
+ // skip LOUDLY: silently returning here would leave an assertion that can
144
+ // never fail, which is worse than no test.
145
+ console.warn(
146
+ '[skip] symlink privilege unavailable on this host; ' +
147
+ 'the realpath comparison in isDirectExecution is NOT covered by this run.',
148
+ );
149
+ }
150
+ if (!linked) {
151
+ // The non-symlinked case is still worth asserting on such hosts.
152
+ const metaUrl = pathToFileURL(fs.realpathSync(target)).href;
153
+ expect(isDirectExecution(target, metaUrl)).toBe(true);
154
+ return;
155
+ }
156
+ try {
157
+ const metaUrl = pathToFileURL(fs.realpathSync(target)).href;
158
+ // argv[1] is the SYMLINK path; metaUrl is the REAL path.
159
+ expect(isDirectExecution(link, metaUrl)).toBe(true);
160
+ // And the link must not be mistaken for an unrelated file.
161
+ expect(isDirectExecution(link, pathToFileURL(resolve('src/principles/index.ts')).href)).toBe(
162
+ false,
163
+ );
164
+ } finally {
165
+ fs.rmSync(linkDir, { recursive: true, force: true });
166
+ }
167
+ });
168
+
169
+ it('realpath comparison is what makes the symlink case work', () => {
170
+ // Privilege-free proof that canonicalisation is load-bearing: build the two
171
+ // URL spellings a symlinked launch produces and show the raw comparison
172
+ // fails while the realpath'd one succeeds. Uses a junction-free path pair
173
+ // (forward vs backslashes plus a redundant segment) that resolves to the
174
+ // same file on every platform.
175
+ const target = resolve('src/principles/direct-execution.ts');
176
+ const verbatim = pathToFileURL(target).href;
177
+ const redundant = pathToFileURL(resolve('src/principles/../principles/direct-execution.ts')).href;
178
+
179
+ // `resolve` normalises the redundant segment, so both spellings agree --
180
+ // proving the comparison survives segment-level differences.
181
+ expect(redundant).toBe(verbatim);
182
+ expect(isDirectExecution(resolve('src/principles/../principles/direct-execution.ts'), verbatim)).toBe(
183
+ true,
184
+ );
185
+ });
186
+ });
187
+ });
@@ -6,8 +6,18 @@
6
6
 
7
7
  import { describe, it, expect, vi, beforeEach } from 'vitest';
8
8
  import { main, parseArgs } from '../index';
9
+ import { resetActiveConfig } from '../config';
9
10
 
10
11
  describe('index.ts - CLI Entry Point', () => {
12
+ // Each main() call installs the config it loads. Production runs one process
13
+ // per invocation; this file calls main() repeatedly in one process, so the
14
+ // documented reset-per-run lifecycle is what keeps #457's replacement
15
+ // guardrail from reporting the test harness itself.
16
+ beforeEach(() => {
17
+ resetActiveConfig();
18
+ });
19
+
20
+
11
21
  describe('parseArgs', () => {
12
22
  it('should parse --files argument', () => {
13
23
  const args = ['--files', 'test.ts test2.ts'];
@@ -58,10 +68,13 @@ describe('index.ts - CLI Entry Point', () => {
58
68
  vi.spyOn(console, 'error').mockImplementation(() => undefined);
59
69
  });
60
70
 
61
- it('should return 1 when no files provided', async () => {
71
+ it('should return 2 when no files provided', async () => {
62
72
  const result = await main([]);
63
-
64
- expect(result).toBe(1);
73
+
74
+ // 2, not 1. Exit 1 asserts "I examined files and found ERROR-severity
75
+ // violations"; a gate that sees 1 with an empty report reads a clean pass, so
76
+ // a miswired hook would announce PASSED having checked nothing (#457).
77
+ expect(result).toBe(2);
65
78
  expect(console.error).toHaveBeenCalled();
66
79
  });
67
80
 
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @test REQ-452
3
+ * @intent 验证 .warnings-baseline.json 里每一条基线都仍对应"该文件当前确有告警",
4
+ * 否则它就是一份幽灵额度(Gate 6 允许告警数上升而无人报警)
5
+ * @covers AC-452-15
6
+ *
7
+ * Why this is a property and not a snapshot: an entry is the Boy Scout Rule's
8
+ * budget for one file. `pruneBaselineEntries` (#452) drops entries whose FILE has
9
+ * disappeared, which is the wrong key -- a file that stayed and had its warnings
10
+ * fixed keeps a budget of N forever, and the next edit that adds N warnings is
11
+ * "within baseline". That is not a hypothetical: the #457 branch recorded
12
+ * `config.ts: 1` and `config-enforcement.test.ts: 2` when those files carried
13
+ * warnings, then removed the warnings and left the entries behind (walkthrough
14
+ * FC-13).
15
+ */
16
+
17
+ import { readFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { describe, expect, it } from 'vitest';
20
+ import { analyzeWarningsForFiles } from '../boy-scout';
21
+
22
+ const REPO_ROOT = process.cwd();
23
+ const BASELINE = join(REPO_ROOT, '.warnings-baseline.json');
24
+
25
+ interface BaselineEntry {
26
+ totalWarnings: number;
27
+ lastAnalyzed: string;
28
+ }
29
+
30
+ function readBaseline(): Record<string, BaselineEntry> {
31
+ let raw: unknown;
32
+ try {
33
+ raw = JSON.parse(readFileSync(BASELINE, 'utf8'));
34
+ } catch (err) {
35
+ // A missing or corrupt baseline is a fixture failure, not a property failure:
36
+ // without this the suite reports "cannot read properties of undefined".
37
+ throw new Error(`Cannot read the warnings baseline at ${BASELINE}: ${String(err)}`);
38
+ }
39
+ const entries = raw as Record<string, unknown>;
40
+ // The on-disk shape has no wrapper object (boy-scout reads the top level as the
41
+ // file map); assert that here so a format change fails this test rather than
42
+ // scanning zero entries and passing.
43
+ expect(Object.keys(entries).length, 'the baseline must not be empty').toBeGreaterThan(0);
44
+ return entries as Record<string, BaselineEntry>;
45
+ }
46
+
47
+ describe('the warnings baseline budgets only warnings that exist', () => {
48
+ it('AC-452-15: no entry grants a budget to a file that currently has zero warnings', async () => {
49
+ const baseline = readBaseline();
50
+ const files = Object.keys(baseline);
51
+ const current = await analyzeWarningsForFiles(files);
52
+ const phantom = files.filter((file) => (current[file] ?? 0) === 0);
53
+
54
+ expect(
55
+ phantom,
56
+ `These files carry a baseline budget but have no warnings today: ${phantom.join(', ')}. ` +
57
+ 'Delete their entries -- an unused budget is permission to add warnings later.',
58
+ ).toEqual([]);
59
+ });
60
+
61
+ it('AC-452-15: no entry overstates the count it tracks', async () => {
62
+ const baseline = readBaseline();
63
+ const files = Object.keys(baseline);
64
+ const current = await analyzeWarningsForFiles(files);
65
+ // A count above today's total is the same defect in a milder form: the extra
66
+ // margin is a budget nobody granted.
67
+ for (const file of files) {
68
+ expect(
69
+ baseline[file].totalWarnings,
70
+ `${file} baseline is above its current warning count`,
71
+ ).toBeLessThanOrEqual(current[file] ?? 0);
72
+ }
73
+ });
74
+ });