@boyingliu01/xp-gate 0.31.1 → 0.31.3
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/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +37 -1
- package/adapters/python.sh +18 -4
- package/gate-3.sh +254 -10
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +254 -10
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +403 -165
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
package/lib/arch.js
CHANGED
|
@@ -21,9 +21,51 @@ const BASELINE_FILE = '.architecture-baseline.json';
|
|
|
21
21
|
// (`archlint diff .architecture-baseline.json --fail-on high`).
|
|
22
22
|
const FAIL_ON = 'high';
|
|
23
23
|
const BLOCKING_SEVERITIES = new Set(['high', 'critical']);
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Cap on captured archlint output.
|
|
26
|
+
*
|
|
27
|
+
* Written as a megabyte multiplier so the unit is in the name AND the literal is a
|
|
28
|
+
* named constant — a bare 64 next to 1024 reads as an arbitrary product.
|
|
29
|
+
*/
|
|
30
|
+
const MEBIBYTE = 1024 * 1024;
|
|
31
|
+
const MAX_OUTPUT_MIB = 64;
|
|
32
|
+
const MAX_OUTPUT_BYTES = MAX_OUTPUT_MIB * MEBIBYTE;
|
|
25
33
|
const BLOCKING_PREVIEW_LIMIT = 10;
|
|
26
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Source extensions counted when measuring how much of a project a scan covered.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately the same extension set archlint's detectors understand, plus the
|
|
39
|
+
* mainstream statically-typed ones it does not — the point of the census is to notice
|
|
40
|
+
* a project whose bulk is in a language no detector covers (#590's Java case), and
|
|
41
|
+
* that only works if those files are counted.
|
|
42
|
+
*/
|
|
43
|
+
const LANGUAGE_BY_EXTENSION = new Map([
|
|
44
|
+
['.ts', 'typescript'],
|
|
45
|
+
['.tsx', 'typescript'],
|
|
46
|
+
['.js', 'javascript'],
|
|
47
|
+
['.jsx', 'javascript'],
|
|
48
|
+
['.mjs', 'javascript'],
|
|
49
|
+
['.cjs', 'javascript'],
|
|
50
|
+
['.java', 'java'],
|
|
51
|
+
['.kt', 'kotlin'],
|
|
52
|
+
['.kts', 'kotlin'],
|
|
53
|
+
['.py', 'python'],
|
|
54
|
+
['.go', 'go'],
|
|
55
|
+
['.rs', 'rust'],
|
|
56
|
+
['.cs', 'csharp'],
|
|
57
|
+
['.rb', 'ruby'],
|
|
58
|
+
['.php', 'php'],
|
|
59
|
+
['.swift', 'swift'],
|
|
60
|
+
['.dart', 'dart'],
|
|
61
|
+
['.scala', 'scala'],
|
|
62
|
+
['.c', 'c'],
|
|
63
|
+
['.cc', 'cpp'],
|
|
64
|
+
['.cpp', 'cpp'],
|
|
65
|
+
['.h', 'c'],
|
|
66
|
+
['.hpp', 'cpp'],
|
|
67
|
+
]);
|
|
68
|
+
|
|
27
69
|
/**
|
|
28
70
|
* Windows extended-length prefixes (`\\?\C:\...`, `\\?\UNC\server\share\...`) —
|
|
29
71
|
* strip for readable output. UNC first: its prefix also starts with `\\?\`.
|
|
@@ -84,10 +126,18 @@ function ownDependencyEntry() {
|
|
|
84
126
|
|
|
85
127
|
/** First match of `name` on PATH, or null. */
|
|
86
128
|
function findOnPath(name) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
let lookup;
|
|
130
|
+
try {
|
|
131
|
+
lookup = spawnSync(process.platform === 'win32' ? 'where' : 'which', [name], {
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// A throw here means the lookup binary itself is unavailable. That is "not found on
|
|
136
|
+
// PATH", which is exactly what this function reports, not a reason to crash.
|
|
137
|
+
console.error(`[xp-gate arch] PATH lookup for ${name} could not run: ${err.message}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (lookup.error || lookup.status !== 0 || !lookup.stdout) return null;
|
|
91
141
|
const first = lookup.stdout.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
92
142
|
return first && fs.existsSync(first) ? first : null;
|
|
93
143
|
}
|
|
@@ -132,7 +182,18 @@ function quoteForShell(arg) {
|
|
|
132
182
|
* otherwise output is inherited so the user sees archlint's own report.
|
|
133
183
|
* Returns { ok, status, stdout, stderr }; ok:false means the tool never ran.
|
|
134
184
|
*/
|
|
135
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Work out the executable + argument list for a resolved archlint.
|
|
187
|
+
*
|
|
188
|
+
* Split from `runArchlint` to keep that function inside the long-function limit, and
|
|
189
|
+
* because the tier→invocation mapping is the part worth reading on its own: only the
|
|
190
|
+
* `node-script` tier avoids a shell, which is why it is the pinned default.
|
|
191
|
+
*
|
|
192
|
+
* @param {{kind: string, script?: string, bin?: string}} resolved
|
|
193
|
+
* @param {string[]} args arguments to the archlint CLI
|
|
194
|
+
* @returns {{command: string, commandArgs: string[], useShell: boolean}}
|
|
195
|
+
*/
|
|
196
|
+
function archlintInvocation(resolved, args) {
|
|
136
197
|
let command;
|
|
137
198
|
let commandArgs;
|
|
138
199
|
let useShell = false;
|
|
@@ -153,21 +214,45 @@ function runArchlint(resolved, args, { cwd, capture = false } = {}) {
|
|
|
153
214
|
command = quoteForShell(command);
|
|
154
215
|
commandArgs = commandArgs.map(quoteForShell);
|
|
155
216
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
217
|
+
return { command, commandArgs, useShell };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Report why archlint could not be launched, in terms the reader can act on.
|
|
222
|
+
*
|
|
223
|
+
* @param {{source: string}} resolved
|
|
224
|
+
* @param {Error} error spawnSync's error, or a caught throw
|
|
225
|
+
*/
|
|
226
|
+
function reportLaunchFailure(resolved, error) {
|
|
227
|
+
if (error.code === 'ENOENT') {
|
|
228
|
+
console.error(`[xp-gate arch] ERROR: archlint not found (${resolved.source}).`);
|
|
229
|
+
console.error(` Fix: npm install --save-dev ${ARCHLINT_PACKAGE} in your project,`);
|
|
230
|
+
console.error(' or reinstall xp-gate when its own pinned copy is the missing one.');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
console.error(`[xp-gate arch] ERROR: failed to run archlint (${resolved.source}): ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function runArchlint(resolved, args, { cwd, capture = false } = {}) {
|
|
237
|
+
const { command, commandArgs, useShell } = archlintInvocation(resolved, args);
|
|
238
|
+
let result;
|
|
239
|
+
try {
|
|
240
|
+
result = spawnSync(command, commandArgs, {
|
|
241
|
+
cwd,
|
|
242
|
+
encoding: 'utf8',
|
|
243
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
244
|
+
shell: useShell,
|
|
245
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
246
|
+
});
|
|
247
|
+
} catch (err) {
|
|
248
|
+
// spawnSync normally reports launch failures via `result.error`; a throw means the
|
|
249
|
+
// call itself was malformed. Both are "the tool never ran", which must never read as
|
|
250
|
+
// a clean scan.
|
|
251
|
+
console.error(`[xp-gate arch] ERROR: could not invoke archlint (${resolved.source}): ${err.message}`);
|
|
252
|
+
return { ok: false, status: 1, stdout: '', stderr: '' };
|
|
253
|
+
}
|
|
163
254
|
if (result.error) {
|
|
164
|
-
|
|
165
|
-
console.error(`[xp-gate arch] ERROR: archlint not found (${resolved.source}).`);
|
|
166
|
-
console.error(` Fix: npm install --save-dev ${ARCHLINT_PACKAGE} in your project,`);
|
|
167
|
-
console.error(' or reinstall xp-gate when its own pinned copy is the missing one.');
|
|
168
|
-
} else {
|
|
169
|
-
console.error(`[xp-gate arch] ERROR: failed to run archlint (${resolved.source}): ${result.error.message}`);
|
|
170
|
-
}
|
|
255
|
+
reportLaunchFailure(resolved, result.error);
|
|
171
256
|
return { ok: false, status: 1, stdout: '', stderr: '' };
|
|
172
257
|
}
|
|
173
258
|
return {
|
|
@@ -182,7 +267,106 @@ function runArchlint(resolved, args, { cwd, capture = false } = {}) {
|
|
|
182
267
|
* Parse `archlint scan --format json` output into a verdict.
|
|
183
268
|
* Fail-closed: unparseable output is reported as an error, never as "clean".
|
|
184
269
|
*/
|
|
185
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Below this fraction of the project's source files, a scan is treated as not having
|
|
272
|
+
* covered the project.
|
|
273
|
+
*
|
|
274
|
+
* Not zero, because archlint legitimately analyses only the files its detectors
|
|
275
|
+
* understand, and a polyglot repo will always have some files outside its extensions.
|
|
276
|
+
* A quarter is deliberately generous: the case this exists for is 1-of-140, where any
|
|
277
|
+
* threshold catches it, and a real JS/TS project sits far above 0.25.
|
|
278
|
+
*/
|
|
279
|
+
const MIN_ANALYSIS_COVERAGE = 0.25;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Source-file census for the project, used to judge whether a scan covered it.
|
|
283
|
+
*
|
|
284
|
+
* Walks the tree once, skipping the usual vendored directories. Cheap enough to run
|
|
285
|
+
* before a scan that takes seconds, and it is the ONLY way to know that
|
|
286
|
+
* `files_analyzed: 1` is a fifth of a percent rather than "this project is tiny".
|
|
287
|
+
*
|
|
288
|
+
* A failure to count returns `{}` rather than a guess: an unknown project size must
|
|
289
|
+
* leave the grade alone (see the `no project size known` case in the tests), never
|
|
290
|
+
* produce a spurious "not covered" verdict.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} projectRoot repository root
|
|
293
|
+
* @returns {{projectFileCount?: number, dominantLanguage?: string}}
|
|
294
|
+
*/
|
|
295
|
+
function projectScanContext(projectRoot) {
|
|
296
|
+
const counts = new Map();
|
|
297
|
+
let total = 0;
|
|
298
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'target', 'coverage', 'vendor']);
|
|
299
|
+
const walk = (dir, depth) => {
|
|
300
|
+
// Depth bound: a symlink cycle would otherwise walk forever.
|
|
301
|
+
if (depth > 24) return;
|
|
302
|
+
let entries;
|
|
303
|
+
try {
|
|
304
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
305
|
+
} catch {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (entry.isDirectory()) {
|
|
310
|
+
if (SKIP.has(entry.name)) continue;
|
|
311
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!entry.isFile()) continue;
|
|
315
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
316
|
+
if (!LANGUAGE_BY_EXTENSION.has(ext)) continue;
|
|
317
|
+
total += 1;
|
|
318
|
+
const lang = LANGUAGE_BY_EXTENSION.get(ext);
|
|
319
|
+
counts.set(lang, (counts.get(lang) || 0) + 1);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
walk(projectRoot, 0);
|
|
323
|
+
if (total === 0) return {};
|
|
324
|
+
let dominantLanguage;
|
|
325
|
+
let best = -1;
|
|
326
|
+
// Deterministic tie-break: candidates in sorted order, strict `>` keeps the first.
|
|
327
|
+
for (const [lang, n] of [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
328
|
+
if (n > best) {
|
|
329
|
+
best = n;
|
|
330
|
+
dominantLanguage = lang;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { projectFileCount: total, dominantLanguage };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Decide whether a scan covered enough of the project for its grade to mean anything.
|
|
338
|
+
*
|
|
339
|
+
* #590: a Java project with 140 `.java` files and no JS/TS got
|
|
340
|
+
* `files_analyzed: 1`, `total_smells: 0`, and therefore `10.0 / Excellent`. archlint's
|
|
341
|
+
* detectors are import-graph based over JS/TS extensions, so on that project every
|
|
342
|
+
* architecture check Gate 6 exists for — cycles, god modules, dead code, shotgun
|
|
343
|
+
* surgery — was silently skipped, while the report said the architecture was excellent.
|
|
344
|
+
*
|
|
345
|
+
* "Did not look" and "looked and found nothing" must be distinguishable. This is the
|
|
346
|
+
* same shape as #589 (Gate 3 permanently SKIP) and #594 (NaN coverage as PASS): the
|
|
347
|
+
* third instance today, which is why the check lives here rather than in the printer.
|
|
348
|
+
*
|
|
349
|
+
* @param {object|null} summary archlint's summary object
|
|
350
|
+
* @param {{projectFileCount?: number, dominantLanguage?: string}} [context]
|
|
351
|
+
* @returns {string|null} why the grade is untrustworthy, or null when it is fine
|
|
352
|
+
*/
|
|
353
|
+
function analysisCoverageGap(summary, context) {
|
|
354
|
+
if (!summary) return null; // no grade at all: nothing to withhold
|
|
355
|
+
const analyzed = Number(summary.files_analyzed);
|
|
356
|
+
const total = Number(context && context.projectFileCount);
|
|
357
|
+
// Without both numbers there is nothing to compare. Reporting a gap on a guess would
|
|
358
|
+
// withhold grades from every caller that has not supplied a count — a worse outcome
|
|
359
|
+
// than the defect, and the reason the existing arch tests keep working unchanged.
|
|
360
|
+
if (!Number.isFinite(analyzed) || !Number.isFinite(total) || total <= 0) return null;
|
|
361
|
+
if (analyzed >= total * MIN_ANALYSIS_COVERAGE) return null;
|
|
362
|
+
|
|
363
|
+
const lang = (context && context.dominantLanguage) || 'the project';
|
|
364
|
+
// The numbers are in the message on purpose: an operator who sees "1 of 140" believes
|
|
365
|
+
// it, where "low coverage" reads as a threshold opinion.
|
|
366
|
+
return `archlint analysed ${analyzed} of ${total} source file(s), so it did not cover ${lang}. Its grade is withheld.`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function verdictFromScanJson(jsonText, context) {
|
|
186
370
|
let parsed;
|
|
187
371
|
try {
|
|
188
372
|
parsed = JSON.parse(jsonText);
|
|
@@ -207,16 +391,29 @@ function verdictFromScanJson(jsonText) {
|
|
|
207
391
|
if (blockingDetails.length < BLOCKING_PREVIEW_LIMIT) blockingDetails.push(smell);
|
|
208
392
|
}
|
|
209
393
|
}
|
|
394
|
+
const summary = (parsed && parsed.summary) || null;
|
|
395
|
+
// #590: withhold an untrustworthy grade. The findings themselves are still reported —
|
|
396
|
+
// they are real regardless of how much was covered — but a 10.0 is a claim about a
|
|
397
|
+
// project, and this is the check that stops the claim being made about a project that
|
|
398
|
+
// was not read.
|
|
399
|
+
const coverageGap = analysisCoverageGap(summary, context);
|
|
210
400
|
return {
|
|
211
401
|
parseError: null,
|
|
212
402
|
total: smells.length,
|
|
213
403
|
blocking,
|
|
214
404
|
blockingDetails,
|
|
215
|
-
summary:
|
|
405
|
+
summary: coverageGap ? null : summary,
|
|
406
|
+
coverageGap,
|
|
216
407
|
};
|
|
217
408
|
}
|
|
218
409
|
|
|
219
410
|
function printScanSummary(verdict) {
|
|
411
|
+
if (verdict.coverageGap) {
|
|
412
|
+
// Say it before the findings line: an operator reading top-down must know the grade
|
|
413
|
+
// is missing and why, or the absence looks like a formatting quirk.
|
|
414
|
+
console.log(`[xp-gate arch] Grade withheld: ${verdict.coverageGap}`);
|
|
415
|
+
console.log(' Architecture was NOT measured for the languages archlint has no detector for.');
|
|
416
|
+
}
|
|
220
417
|
const grade = verdict.summary && verdict.summary.architecture_grade;
|
|
221
418
|
if (grade && grade.score !== undefined) {
|
|
222
419
|
console.log(`[xp-gate arch] Architecture quality: ${grade.score}/10 (${grade.level})`);
|
|
@@ -237,30 +434,44 @@ function printScanSummary(verdict) {
|
|
|
237
434
|
* Tool failures (spawn error, non-zero scan exit, unparseable JSON) always
|
|
238
435
|
* exit non-zero — a broken scan must never read as a clean one.
|
|
239
436
|
*/
|
|
437
|
+
/**
|
|
438
|
+
* Baseline-ratchet mode: only NEW high/critical findings block.
|
|
439
|
+
*
|
|
440
|
+
* Split out of `runBuiltinScan` to keep that function inside the long-function limit
|
|
441
|
+
* (the #590 census pushed it over), and because this mode's contract — the exit code
|
|
442
|
+
* itself IS the verdict — deserves to be readable in one place.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} projectRoot repository root
|
|
445
|
+
* @param {{kind: string}} resolved resolved archlint
|
|
446
|
+
* @param {string} baselinePath path to the baseline file
|
|
447
|
+
* @returns {number} exit code
|
|
448
|
+
*/
|
|
449
|
+
function runBaselineRatchet(projectRoot, resolved, baselinePath) {
|
|
450
|
+
console.log(`[xp-gate arch] Baseline ratchet mode — ${BASELINE_FILE} present; blocking new ${FAIL_ON}+ findings only`);
|
|
451
|
+
const diff = runArchlint(resolved, ['diff', baselinePath, '--fail-on', FAIL_ON], { cwd: projectRoot });
|
|
452
|
+
if (!diff.ok) return 1;
|
|
453
|
+
// `--fail-on high` makes the exit code itself the verdict contract: 0 = no new high+
|
|
454
|
+
// findings, 1 = at least one. This is the exact mechanism Gate 6's pre-commit has used
|
|
455
|
+
// since before #447, so parity here is the point, not a shortcut. Anything other than
|
|
456
|
+
// 0/1 is a tool failure and blocks.
|
|
457
|
+
if (diff.status === 0) {
|
|
458
|
+
console.log(`✅ arch: no new ${FAIL_ON}+ architecture findings (baseline ratchet)`);
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
if (diff.status === 1) {
|
|
462
|
+
console.error(`❌ BLOCKED - new ${FAIL_ON}+ architecture findings (baseline ratchet mode)`);
|
|
463
|
+
console.error(' Fix the findings above, or refresh the baseline with:');
|
|
464
|
+
console.error(` npx ${ARCHLINT_PACKAGE} snapshot -o ${BASELINE_FILE}`);
|
|
465
|
+
return 1;
|
|
466
|
+
}
|
|
467
|
+
console.error(`❌ ERROR - archlint diff exited ${diff.status}; tool failure is treated as blocking (never a silent pass)`);
|
|
468
|
+
return 1;
|
|
469
|
+
}
|
|
470
|
+
|
|
240
471
|
function runBuiltinScan(projectRoot, resolved) {
|
|
241
472
|
const baselinePath = path.join(projectRoot, BASELINE_FILE);
|
|
242
473
|
if (fs.existsSync(baselinePath)) {
|
|
243
|
-
|
|
244
|
-
const diff = runArchlint(resolved, ['diff', baselinePath, '--fail-on', FAIL_ON], { cwd: projectRoot });
|
|
245
|
-
if (!diff.ok) return 1;
|
|
246
|
-
// `--fail-on high` makes the exit code itself the verdict contract: 0 = no
|
|
247
|
-
// new high+ findings, 1 = at least one. This is the exact mechanism Gate
|
|
248
|
-
// 6's pre-commit has used since before #447 (issue evidence: "Gate 6
|
|
249
|
-
// actually uses: archlint diff .architecture-baseline.json --fail-on high
|
|
250
|
-
// ✅ normal"), so parity here is the point, not a shortcut. Anything other
|
|
251
|
-
// than 0/1 is a tool failure and blocks.
|
|
252
|
-
if (diff.status === 0) {
|
|
253
|
-
console.log(`✅ arch: no new ${FAIL_ON}+ architecture findings (baseline ratchet)`);
|
|
254
|
-
return 0;
|
|
255
|
-
}
|
|
256
|
-
if (diff.status === 1) {
|
|
257
|
-
console.error(`❌ BLOCKED - new ${FAIL_ON}+ architecture findings (baseline ratchet mode)`);
|
|
258
|
-
console.error(' Fix the findings above, or refresh the baseline with:');
|
|
259
|
-
console.error(` npx ${ARCHLINT_PACKAGE} snapshot -o ${BASELINE_FILE}`);
|
|
260
|
-
return 1;
|
|
261
|
-
}
|
|
262
|
-
console.error(`❌ ERROR - archlint diff exited ${diff.status}; tool failure is treated as blocking (never a silent pass)`);
|
|
263
|
-
return 1;
|
|
474
|
+
return runBaselineRatchet(projectRoot, resolved, baselinePath);
|
|
264
475
|
}
|
|
265
476
|
|
|
266
477
|
console.log('[xp-gate arch] Zero-tolerance mode — no .architecture-baseline.json; blocking any high/critical finding');
|
|
@@ -281,7 +492,11 @@ function runBuiltinScan(projectRoot, resolved) {
|
|
|
281
492
|
if (scan.stderr) process.stderr.write(scan.stderr);
|
|
282
493
|
return 1;
|
|
283
494
|
}
|
|
284
|
-
|
|
495
|
+
// #590: give the verdict the two facts it needs to judge its own grade — how many
|
|
496
|
+
// source files the project has, and which language dominates it. Without them a scan
|
|
497
|
+
// of 1-of-140 files still reports 10.0/Excellent, which is the vacuous pass this
|
|
498
|
+
// issue is about.
|
|
499
|
+
const verdict = verdictFromScanJson(scan.stdout, projectScanContext(projectRoot));
|
|
285
500
|
if (verdict.parseError) {
|
|
286
501
|
console.error(`❌ ERROR - ${verdict.parseError}; tool failure is treated as blocking (never a silent pass)`);
|
|
287
502
|
return 1;
|
|
@@ -360,6 +575,8 @@ module.exports = {
|
|
|
360
575
|
binEntryFromPackageDir,
|
|
361
576
|
resolveArchlint,
|
|
362
577
|
verdictFromScanJson,
|
|
578
|
+
analysisCoverageGap,
|
|
579
|
+
projectScanContext,
|
|
363
580
|
quoteForShell,
|
|
364
581
|
stripExtendedPrefix,
|
|
365
582
|
BLOCKING_SEVERITIES,
|
|
@@ -68,6 +68,29 @@ if (jsonMode) {
|
|
|
68
68
|
console.log('Status:', report.alignment_status);
|
|
69
69
|
console.log('Score:', report.score);
|
|
70
70
|
console.log('Misaligned:', report.misaligned_tests.length);
|
|
71
|
+
|
|
72
|
+
// #573 (recurrence of #57): a non-PASS verdict used to print only the status
|
|
73
|
+
// and counters. On a real run that read:
|
|
74
|
+
// Status: BLOCKED
|
|
75
|
+
// Score: 0
|
|
76
|
+
// Misaligned: 0
|
|
77
|
+
// which contradicts itself — zero misaligned tests, yet blocked — while the
|
|
78
|
+
// actual reason sat in report.errors that nothing ever printed. A gate that
|
|
79
|
+
// blocks without saying why is not actionable, and the usual response to an
|
|
80
|
+
// unactionable red is to bypass it, which is how dead gates are made.
|
|
81
|
+
if (report.alignment_status !== 'PASS') {
|
|
82
|
+
const reasons = Array.isArray(report.errors) ? report.errors : [];
|
|
83
|
+
if (reasons.length > 0) {
|
|
84
|
+
console.error('Reasons:');
|
|
85
|
+
for (const reason of reasons) console.error(' -', reason);
|
|
86
|
+
} else {
|
|
87
|
+
// Saying so is the point: a blocked verdict with no recorded reason is a
|
|
88
|
+
// defect in whoever produced the report, not something the reader can fix
|
|
89
|
+
// by editing their own files.
|
|
90
|
+
console.error('Reasons: (none recorded) — this is a bug in the report producer:');
|
|
91
|
+
console.error(' a non-PASS alignment verdict must carry at least one entry in report.errors.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
71
94
|
}
|
|
72
95
|
// Set exitCode instead of process.exit() so buffered stdout (notably the
|
|
73
96
|
// --json document) flushes fully before the process terminates (#392 review).
|
package/lib/check.js
CHANGED
|
@@ -147,7 +147,10 @@ async function checkAllGates(target) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
for (const gate of invokableGates) {
|
|
150
|
-
|
|
150
|
+
// #589: `--all` means the whole tree. Passing the scope keeps the gates that
|
|
151
|
+
// read CHANGED_FILES from scanning an empty diff and reporting SKIP — which
|
|
152
|
+
// read like a pass while checking LESS than an ordinary commit.
|
|
153
|
+
const code = await runGate(gate.id, target, { scope: 'all' });
|
|
151
154
|
if (code === 0) tally.passed += 1;
|
|
152
155
|
else tally.failed += 1;
|
|
153
156
|
console.log('');
|