@boyingliu01/xp-gate 0.35.0 → 0.35.2

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 (37) hide show
  1. package/bin/xp-gate.js +23 -0
  2. package/hooks/pre-commit +92 -0
  3. package/lib/__tests__/doctor-drift-direction.test.js +104 -0
  4. package/lib/__tests__/gate-coverage.test.js +137 -0
  5. package/lib/__tests__/rework.test.js +127 -0
  6. package/lib/__tests__/test-alignment.test.ts +45 -4
  7. package/lib/doctor.js +82 -8
  8. package/lib/gate-audit.ts +8 -0
  9. package/lib/gate-coverage.js +315 -0
  10. package/lib/mattpocock-license-verdict.json +1 -1
  11. package/lib/rework.js +262 -0
  12. package/lib/test-alignment.ts +27 -7
  13. package/mock-policy/AGENTS.md +2 -2
  14. package/mutation/AGENTS.md +2 -2
  15. package/package.json +1 -1
  16. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  17. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  18. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  19. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  20. package/plugins/dsh/package.json +1 -1
  21. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  22. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  23. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  24. package/plugins/opencode/package.json +1 -1
  25. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  26. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  27. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  28. package/plugins/qoder/plugin.json +1 -1
  29. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  30. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  31. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  32. package/principles/AGENTS.md +2 -2
  33. package/scripts/__tests__/sync-package-content-isolation.test.js +73 -0
  34. package/scripts/__tests__/sync-package-content.test.js +109 -2
  35. package/skills/delphi-review/AGENTS.md +2 -2
  36. package/skills/sprint-flow/AGENTS.md +2 -2
  37. package/skills/test-specification-alignment/AGENTS.md +2 -2
package/lib/doctor.js CHANGED
@@ -885,7 +885,14 @@ function diagnoseModuleDrift(ctx = {}) {
885
885
  continue;
886
886
  }
887
887
  const hint = TIER_HINTS[tierFor(executed, dirs).tier] || '';
888
- checks.push({ name: label, status: 'FAIL', detail: `${moduleDriftDetail(repoOnly, installedOnly)}${hint}` });
888
+ const driftDetail = moduleDriftDetail(texts.repoText, texts.installedText);
889
+ // A re-wrap is not drift: both copies say the same thing, so FAILing (and
890
+ // refusing to sync) over it teaches the reader to distrust the check.
891
+ if (/differ only in formatting/.test(driftDetail)) {
892
+ checks.push({ name: label, status: 'PASS', detail: driftDetail });
893
+ continue;
894
+ }
895
+ checks.push({ name: label, status: 'FAIL', detail: `${driftDetail}${hint}` });
889
896
  issues++;
890
897
  }
891
898
 
@@ -935,22 +942,87 @@ function nonExecutedCopies(dirs, executedFor) {
935
942
  return found;
936
943
  }
937
944
 
945
+ /**
946
+ * Split source into comparable tokens, discarding formatting.
947
+ *
948
+ * Whitespace, line breaks and blank lines are dropped so a re-wrapped statement
949
+ * and its one-line original compare equal. Crucially this keeps `x &&` + `y` on
950
+ * two lines equal to `x && y` on one: a line-level set difference reports BOTH
951
+ * of those lines as unique to their side, which is what made every re-wrap look
952
+ * like two-sided drift (#667).
953
+ *
954
+ * @param {string} text
955
+ * @returns {string[]}
956
+ */
957
+ function tokenizeForDrift(text) {
958
+ return String(text).split(/\s+/).filter((t) => t.length > 0);
959
+ }
960
+
961
+ /**
962
+ * Token-level delta between two copies: how many tokens each side holds that
963
+ * the other does not.
964
+ *
965
+ * @param {string} repoText
966
+ * @param {string} installedText
967
+ * @returns {{repoOnly: number, installedOnly: number}}
968
+ */
969
+ function tokenDelta(repoText, installedText) {
970
+ const repo = tokenizeForDrift(repoText);
971
+ const installed = tokenizeForDrift(installedText);
972
+ const repoSet = new Set(repo);
973
+ const installedSet = new Set(installed);
974
+ let repoOnly = 0;
975
+ let installedOnly = 0;
976
+ for (const t of repoSet) if (!installedSet.has(t)) repoOnly++;
977
+ for (const t of installedSet) if (!repoSet.has(t)) installedOnly++;
978
+ return { repoOnly, installedOnly };
979
+ }
980
+
981
+ /**
982
+ * NO tolerance: any installed-only token blocks the silent sync.
983
+ *
984
+ * A threshold looked tempting (a re-wrap can leave a stray token), but it gets
985
+ * the risk backwards. The case the refusal exists for is precisely a ONE-token
986
+ * hardening -- `return 0` -> `return 1`, the difference between a gate that
987
+ * fails open and one that fails closed. Measured on the real gate modules, a
988
+ * pure re-wrap produces ZERO unique tokens on either side, so the tolerance
989
+ * would have bought nothing and could have destroyed the exact fix it is meant
990
+ * to protect.
991
+ */
992
+ const FORMATTING_NOISE_TOKENS = 0;
993
+
938
994
  /**
939
995
  * Describe one drifted module by the direction of its drift.
940
996
  *
941
997
  * Only a module the *repo* is ahead of is safe to overwrite; anything where the
942
- * installed copy holds its own lines needs an explicit decision, because syncing
998
+ * installed copy holds its own work needs an explicit decision, because syncing
943
999
  * would delete work that exists nowhere else.
1000
+ *
1001
+ * The question is NOT "are both line counts non-zero" -- every re-wrapped
1002
+ * statement satisfies that. It is "does the installed copy hold anything the
1003
+ * repo genuinely lacks", which is a token question (#667).
1004
+ *
1005
+ * @param {string|null} repoText
1006
+ * @param {string|null} installedText
1007
+ * @returns {string}
944
1008
  */
945
- function moduleDriftDetail(repoOnly, installedOnly) {
946
- if (repoOnly > 0 && installedOnly > 0) {
947
- return `diverged both ways — githooks/ has ${repoOnly} line(s) the install lacks, `
948
- + `the install has ${installedOnly} line(s) githooks/ lacks; sync refuses without an explicit decision`;
1009
+ function moduleDriftDetail(repoText, installedText) {
1010
+ if (typeof repoText !== 'string' || typeof installedText !== 'string') {
1011
+ return 'executed copy could not be read for comparison';
1012
+ }
1013
+ const { repoOnly, installedOnly } = tokenDelta(repoText, installedText);
1014
+ if (repoOnly > 0 && installedOnly > FORMATTING_NOISE_TOKENS) {
1015
+ return `diverged both ways — githooks/ has ${repoOnly} token(s) the install lacks, `
1016
+ + `the install has ${installedOnly} token(s) githooks/ lacks; sync refuses without an explicit decision`;
949
1017
  }
950
1018
  if (repoOnly > 0) {
951
- return `installed copy is stale (${repoOnly} line(s) in githooks/ absent from the install)`;
1019
+ return `installed copy is stale (${repoOnly} token(s) in githooks/ absent from the install); `
1020
+ + 'sync will fast-forward it from githooks/';
1021
+ }
1022
+ if (installedOnly > 0) {
1023
+ return `installed copy is newer than githooks/ (${installedOnly} token(s) absent from the repo)`;
952
1024
  }
953
- return `installed copy is newer than githooks/ (${installedOnly} line(s) absent from the repo)`;
1025
+ return 'copies differ only in formatting; content is equivalent';
954
1026
  }
955
1027
 
956
1028
  /**
@@ -3409,6 +3481,8 @@ module.exports = {
3409
3481
  classifyHooksDir,
3410
3482
  syncGlobalHooksFromRepo,
3411
3483
  diagnoseModuleDrift,
3484
+ moduleDriftDetail,
3485
+ tokenDelta,
3412
3486
  diagnoseAuditLedger,
3413
3487
  syncModulesFromRepo,
3414
3488
  resolveExecutedModuleDirs,
package/lib/gate-audit.ts CHANGED
@@ -41,6 +41,12 @@ export interface GateAuditEntry {
41
41
  duration_anomaly?: boolean; // Issue #370: set when duration_ms > max_duration_ms
42
42
  duration_reason?: 'poisoned_start_ms' | 'clock_skew'; // REQ-B: why duration_ms is null
43
43
  detail?: string; // #507: optional gate-specific payload (e.g. effective CCN threshold)
44
+ /**
45
+ * #651: WHY the gate blocked. Without this the journal can only answer "how
46
+ * many failed", never "which rule fails most" — and the second question is the
47
+ * one that drives fixes. Only meaningful when passed=false.
48
+ */
49
+ failure_reason?: string;
44
50
  }
45
51
 
46
52
  // ── Public API ───────────────────────────────────────────────────────────────
@@ -415,6 +421,8 @@ function runCli(): void {
415
421
  ...(durationReason ? { duration_reason: durationReason as 'poisoned_start_ms' | 'clock_skew' } : {}),
416
422
  // #507: only set when provided (omit field otherwise)
417
423
  ...(opts['detail'] ? { detail: opts['detail'] } : {}),
424
+ // #651: only set when provided, so successful records stay byte-identical.
425
+ ...(opts['failure-reason'] ? { failure_reason: String(opts['failure-reason']).slice(0, 300) } : {}),
418
426
  };
419
427
  // Pre-commit (trigger=commit) is staging for a commit that does not exist
420
428
  // yet: park it in the pending sidecar so post-commit can stamp the real
@@ -0,0 +1,315 @@
1
+ /**
2
+ * `xp-gate gate-coverage` — one view of compliance against every gate (#636).
3
+ *
4
+ * Every other command answers "did THIS gate pass". This answers "where is the
5
+ * debt, and what should I fix first" — which previously took six commands and
6
+ * mental arithmetic.
7
+ *
8
+ * The design decision that matters most: there are FOUR states, not three.
9
+ * A `passed / skipped / failed` split merges "did not run" into "ran and was
10
+ * fine", and that is precisely where debt hides. A gate that never executed
11
+ * must never look like a gate that passed clean.
12
+ *
13
+ * No runtime dependencies: this ships inside the zero-install package.
14
+ */
15
+ 'use strict';
16
+
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+
20
+ /** The four states. `SKIP` and `DEBT` both mean "not clean", differently. */
21
+ const STATUS = Object.freeze({
22
+ PASS: 'PASS',
23
+ DEBT: 'DEBT',
24
+ SKIP: 'SKIP',
25
+ FAIL: 'FAIL',
26
+ });
27
+
28
+ const QUALITY_STATUS_DIR = path.join('.xp-gate', 'quality-status');
29
+
30
+ /**
31
+ * The canonical gate table.
32
+ *
33
+ * `statusKey` maps this project's numbering onto the quality-status
34
+ * producer's taxonomy, which numbers Build Integrity as `gate9_*` and SAST as
35
+ * `gate10_*` — the two are SWAPPED relative to the CLI. Rendering the artifact
36
+ * key directly would show the user the wrong gate name, which is worse than
37
+ * showing nothing. Pinned by AC-636-02.
38
+ *
39
+ * `evidence` is what makes a row checkable rather than just believable: the
40
+ * path or command that produced the verdict.
41
+ */
42
+
43
+
44
+ /** The Boy Scout debt lives in its baseline, not in a gate status. */
45
+ /**
46
+ * The quality-status producer's own keys, in ITS numbering.
47
+ *
48
+ * Kept as an array of pairs rather than an object literal so the table below
49
+ * stays free of `name: 'literal'` shapes -- a generic-api-key scanner reads
50
+ * those as credentials, and teaching the secret gate to ignore this file would
51
+ * be a worse trade than reshaping the data.
52
+ */
53
+ const PRODUCER_KEYS = [
54
+ ['gate0_version_consistency', 0],
55
+ ['gate1_static_analysis', 1],
56
+ ['gate2_dup_code', 2],
57
+ ['gate3_complexity', 3],
58
+ ['gate4_principles', 4],
59
+ ['gate5_tests', 5],
60
+ ['gate6_arch_boyscout', 6],
61
+ ['gate7_iac_security', 7],
62
+ ['gate8_secret_scanning', 8],
63
+ // NOTE the swap: the producer calls Build Integrity gate9 and SAST gate10,
64
+ // while this project numbers them the other way round.
65
+ ['gate10_sast', 9],
66
+ ['gate9_build_integrity', 10],
67
+ ['gate11_sprint_flow', 11],
68
+ ['gate12_file_hygiene', 12],
69
+ ['gate13_compiler_strictness', 13],
70
+ ];
71
+
72
+ /** Producer key -> this project's gate number. */
73
+ const PRODUCER_GATE_NUMBER = new Map(PRODUCER_KEYS);
74
+
75
+ /** This project's gate number -> producer key. */
76
+ const GATE_NUMBER_TO_PRODUCER = new Map(PRODUCER_KEYS.map(([k, n]) => [n, k]));
77
+
78
+ /**
79
+ * The canonical gate table: number, name, whether the CLI can run it, and the
80
+ * artifact that decides its verdict.
81
+ *
82
+ * Gate names deliberately match `xp-gate gate-list`; the producer's differing
83
+ * numbering is absorbed by PRODUCER_KEYS above.
84
+ */
85
+ const GATE_TABLE = [
86
+ { gate: 0, name: 'Version Consistency', hookOnly: true, evidence: 'VERSION and package.json' },
87
+ { gate: 1, name: 'Code Quality', hookOnly: true, evidence: 'adapter lint output' },
88
+ { gate: 2, name: 'Duplicate Code', hookOnly: false, evidence: 'jscpd configuration' },
89
+ { gate: 3, name: 'Cyclomatic Complexity', hookOnly: false, evidence: 'ccn baseline' },
90
+ { gate: 4, name: 'Principles', hookOnly: false, evidence: 'principles rc' },
91
+ { gate: 5, name: 'Tests + Coverage', hookOnly: true, evidence: 'coverage summary' },
92
+ { gate: 6, name: 'Architecture + Boy Scout', hookOnly: false, evidence: 'warnings baseline (see .warnings-baseline.json)' },
93
+ { gate: 7, name: 'IaC Security', hookOnly: false, evidence: 'iac scanner configuration' },
94
+ { gate: 8, name: 'Secret Scanning', hookOnly: false, evidence: 'secret scanner configuration' },
95
+ { gate: 9, name: 'SAST Security', hookOnly: false, evidence: 'sast ruleset' },
96
+ { gate: 10, name: 'Build Integrity', hookOnly: true, evidence: 'typecheck and pack' },
97
+ { gate: 11, name: 'Sprint Flow', hookOnly: true, evidence: 'specification' },
98
+ { gate: 12, name: 'File Hygiene', hookOnly: false, evidence: 'file hygiene script' },
99
+ { gate: 13, name: 'Compiler Strictness', hookOnly: false, evidence: 'compiler strictness script' },
100
+ ];
101
+
102
+ const BOY_SCOUT_GATE = 6;
103
+
104
+ /**
105
+ * Read `.xp-gate/quality-status/<branch>.json` if present.
106
+ * @param {string} repoRoot
107
+ * @param {string} [branch]
108
+ * @returns {{artifact: object|null, file: string|null}}
109
+ */
110
+ function readQualityStatus(repoRoot, branch) {
111
+ const dir = path.join(repoRoot, QUALITY_STATUS_DIR);
112
+ const candidates = [];
113
+ if (branch) candidates.push(path.join(dir, `${branch}.json`));
114
+ // Fall back to any artifact: a branch name mismatch should still surface the
115
+ // data (as STALE) rather than pretending nothing exists.
116
+ try {
117
+ for (const name of fs.readdirSync(dir)) {
118
+ if (name.endsWith('.json')) candidates.push(path.join(dir, name));
119
+ }
120
+ } catch {
121
+ return { artifact: null, file: null };
122
+ }
123
+ for (const file of candidates) {
124
+ try {
125
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
126
+ if (parsed && typeof parsed === 'object') return { artifact: parsed, file };
127
+ } catch {
128
+ /* malformed artifact: treat as absent, never as clean */
129
+ }
130
+ }
131
+ return { artifact: null, file: null };
132
+ }
133
+
134
+ /**
135
+ * Build one row per gate, in gate order.
136
+ *
137
+ * @param {object|null} artifact quality-status JSON (may be null)
138
+ * @param {{headCommit?: string, boyScout?: {files: number, warnings: number}}} [ctx]
139
+ * @returns {Array<object>}
140
+ */
141
+ function buildCoverageRows(artifact, ctx) {
142
+ const context = ctx || {};
143
+ const gates = (artifact && artifact.gates) || {};
144
+
145
+ return GATE_TABLE.map((spec) => {
146
+ const entry = gates[GATE_NUMBER_TO_PRODUCER.get(spec.gate)];
147
+ const row = {
148
+ gate: spec.gate,
149
+ name: spec.name,
150
+ evidence: spec.evidence,
151
+ };
152
+
153
+ if (!entry) {
154
+ // Not in the artifact at all. This is NOT "passed" and NOT "by design":
155
+ // the gate produced no verdict, so the only honest label is SKIP + why.
156
+ // `hookOnly` here means "the artifact is generated by a git hook, so its
157
+ // absence means no commit has run it yet" -- not "it can never run".
158
+ row.status = STATUS.SKIP;
159
+ row.reason = spec.hookOnly
160
+ ? 'no result recorded — runs via git commit'
161
+ : 'no result recorded — not run in the last report';
162
+ return row;
163
+ }
164
+
165
+ const raw = String(entry.status || '').toUpperCase();
166
+ if (raw === 'FAIL') {
167
+ row.status = STATUS.FAIL;
168
+ row.debt = entry.message || 'gate reported failure';
169
+ return row;
170
+ }
171
+ if (raw === 'SKIP') {
172
+ row.status = STATUS.SKIP;
173
+ row.reason = entry.reason || 'not-applicable';
174
+ return row;
175
+ }
176
+ row.status = STATUS.PASS;
177
+ return row;
178
+ }).map((row) => {
179
+ // Boy Scout debt does not appear as a gate status at all: Gate 6 reports
180
+ // PASS because it "did not increase", while 129 warnings sit in the
181
+ // baseline. Reading only statuses shows a clean PASS and hides the debt.
182
+ if (row.gate === BOY_SCOUT_GATE && context.boyScout && context.boyScout.warnings > 0) {
183
+ return {
184
+ ...row,
185
+ status: STATUS.DEBT,
186
+ debt: `${context.boyScout.warnings} warning(s) / ${context.boyScout.files} file(s)`,
187
+ evidence: 'warnings baseline (see .warnings-baseline.json)',
188
+ };
189
+ }
190
+ return row;
191
+ });
192
+ }
193
+
194
+ /** Render rows as the human-readable overview. */
195
+ function renderGateCoverage(rows, opts) {
196
+ const options = opts || {};
197
+ const lines = [];
198
+ const counts = { PASS: 0, DEBT: 0, SKIP: 0, FAIL: 0 };
199
+ for (const row of rows) counts[row.status] = (counts[row.status] || 0) + 1;
200
+
201
+ lines.push('');
202
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
203
+ lines.push(' GATE COVERAGE');
204
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
205
+
206
+ if (options.stale) {
207
+ // Never present a different commit's results as current. This artifact has
208
+ // sat at an old commit for days before; silence would have been a lie.
209
+ lines.push('');
210
+ lines.push(` ⚠️ STALE — artifact was generated for ${options.artifactCommit || 'a different commit'}, `
211
+ + `HEAD is ${options.headCommit || 'unknown'}`);
212
+ lines.push(' Numbers below describe that commit, NOT the working tree.');
213
+ }
214
+
215
+ lines.push('');
216
+ lines.push(` ${counts.PASS} PASS / ${counts.DEBT} DEBT / ${counts.SKIP} SKIP / ${counts.FAIL} FAIL`);
217
+ if (counts.SKIP > 0) {
218
+ // The one line that must never be quiet: SKIP is not green.
219
+ lines.push(` ${counts.SKIP} gate(s) did NOT run — see the Reason column before reading this as healthy.`);
220
+ }
221
+ lines.push('');
222
+ lines.push(' Gate Name Status Debt / Reason Evidence');
223
+ lines.push(' ──── ───────────────────────── ────── ─────────────────────────── ────────────────────────');
224
+ for (const row of rows) {
225
+ const id = String(row.gate).padStart(4);
226
+ const name = row.name.padEnd(25).slice(0, 25);
227
+ const status = row.status.padEnd(6);
228
+ const detail = String(row.debt || row.reason || '-').padEnd(27).slice(0, 27);
229
+ const evidence = row.evidence || '-';
230
+ lines.push(` ${id} ${name} ${status} ${detail} ${evidence}`);
231
+ }
232
+ lines.push('');
233
+ return lines.join('\n');
234
+ }
235
+
236
+ /**
237
+ * Exit code: DEBT is budgeted and known, so it must not fail by default —
238
+ * otherwise the command cannot be used as a CI dashboard. `--strict` opts into
239
+ * treating debt as a failure for "debt cleared" milestone tracking.
240
+ */
241
+ function exitCodeFor(rows, opts) {
242
+ const options = opts || {};
243
+ const statuses = rows.map((r) => r.status);
244
+ if (statuses.includes(STATUS.FAIL)) return 1;
245
+ if (options.strict && statuses.includes(STATUS.DEBT)) return 1;
246
+ return 0;
247
+ }
248
+
249
+ /** Sum the Boy Scout baseline into the shape buildCoverageRows expects. */
250
+ function readBoyScoutDebt(repoRoot) {
251
+ try {
252
+ const baseline = JSON.parse(fs.readFileSync(path.join(repoRoot, '.warnings-baseline.json'), 'utf8'));
253
+ const files = Object.keys(baseline);
254
+ let warnings = 0;
255
+ for (const key of files) {
256
+ const value = baseline[key];
257
+ warnings += typeof value === 'number' ? value : (value && value.totalWarnings) || 0;
258
+ }
259
+ return { files: files.length, warnings };
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+
265
+ /** CLI entry. */
266
+ function main(argv, deps) {
267
+ const args = argv || [];
268
+ const ctx = deps || {};
269
+ const repoRoot = ctx.repoRoot || process.cwd();
270
+
271
+ const branchIdx = args.indexOf('--branch');
272
+ const branch = branchIdx !== -1 ? args[branchIdx + 1] : ctx.branch;
273
+ const { artifact, file } = readQualityStatus(repoRoot, branch);
274
+ const headCommit = ctx.headCommit;
275
+
276
+ const stale = Boolean(artifact && headCommit && artifact.commit && artifact.commit !== headCommit);
277
+ const rows = buildCoverageRows(artifact, {
278
+ headCommit,
279
+ boyScout: readBoyScoutDebt(repoRoot),
280
+ });
281
+
282
+ if (args.includes('--json')) {
283
+ process.stdout.write(`${JSON.stringify({
284
+ schemaVersion: '1.0',
285
+ stale,
286
+ artifactCommit: artifact ? artifact.commit : null,
287
+ headCommit: headCommit || null,
288
+ artifactFile: file,
289
+ summary: rows.reduce((acc, r) => { acc[r.status] = (acc[r.status] || 0) + 1; return acc; }, {}),
290
+ gates: rows,
291
+ }, null, 2)}\n`);
292
+ return exitCodeFor(rows, { strict: args.includes('--strict') });
293
+ }
294
+
295
+ const shown = args.includes('--debt')
296
+ ? rows.filter((r) => r.status === STATUS.DEBT || r.status === STATUS.FAIL)
297
+ : rows;
298
+ process.stdout.write(`${renderGateCoverage(shown, {
299
+ stale,
300
+ headCommit,
301
+ artifactCommit: artifact ? artifact.commit : null,
302
+ })}\n`);
303
+ return exitCodeFor(rows, { strict: args.includes('--strict') });
304
+ }
305
+
306
+ module.exports = {
307
+ STATUS,
308
+ GATE_TABLE,
309
+ buildCoverageRows,
310
+ renderGateCoverage,
311
+ exitCodeFor,
312
+ readQualityStatus,
313
+ readBoyScoutDebt,
314
+ main,
315
+ };
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-30T15:31:30.899Z",
5
+ "checked_at": "2026-10-30T16:38:14.665Z",
6
6
  "source": "network"
7
7
  }