@boyingliu01/xp-gate 0.31.3 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- 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/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- 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/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- 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/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__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const os = require('node:os');
|
|
6
7
|
|
|
7
8
|
const REQUIRED_ROLES = ['architecture', 'technical', 'feasibility'];
|
|
8
9
|
const VALIDITY_MS = 60 * 60 * 1000;
|
|
@@ -45,25 +46,31 @@ function parseTimestamp(value, field) {
|
|
|
45
46
|
: null;
|
|
46
47
|
if (!match) fail(`${field} must be a canonical UTC ISO-8601 timestamp.`);
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const minute =
|
|
54
|
-
const
|
|
55
|
-
const milliseconds = Number((fractionText || '').padEnd(3, '0') || 0);
|
|
49
|
+
// Groups 1..6 are year, month, day, hour, minute, second; group 7 is the
|
|
50
|
+
// optional millisecond fraction. Keeping them as arrays means the "build the
|
|
51
|
+
// date" and "verify round-trip" steps share one ordered definition instead of
|
|
52
|
+
// repeating seven hand-numbered comparisons.
|
|
53
|
+
const parts = match.slice(1, 7).map(Number);
|
|
54
|
+
const [year, month, day, hour, minute, second] = parts;
|
|
55
|
+
const milliseconds = Number((match[7] || '').padEnd(3, '0') || 0);
|
|
56
56
|
const date = new Date(0);
|
|
57
57
|
date.setUTCFullYear(year, month - 1, day);
|
|
58
58
|
date.setUTCHours(hour, minute, second, milliseconds);
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
// A date silently rolls over ("2026-02-30" becomes March 2), so the only way
|
|
61
|
+
// to know the input was a real calendar instant is to read it back and compare
|
|
62
|
+
// every field.
|
|
63
|
+
const expected = [...parts, milliseconds];
|
|
64
|
+
const readBack = [
|
|
65
|
+
date.getUTCFullYear(),
|
|
66
|
+
date.getUTCMonth() + 1,
|
|
67
|
+
date.getUTCDate(),
|
|
68
|
+
date.getUTCHours(),
|
|
69
|
+
date.getUTCMinutes(),
|
|
70
|
+
date.getUTCSeconds(),
|
|
71
|
+
date.getUTCMilliseconds(),
|
|
72
|
+
];
|
|
73
|
+
if (!readBack.every((actual, i) => actual === expected[i])) {
|
|
67
74
|
fail(`${field} must contain a valid UTC calendar date and time.`);
|
|
68
75
|
}
|
|
69
76
|
return date.getTime();
|
|
@@ -142,6 +149,19 @@ function resolveBindingSeats(config) {
|
|
|
142
149
|
return seatsFromExperts(profiles[active].experts);
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Machine-wide Delphi config path: `$XDG_CONFIG_HOME/xp-gate/` or
|
|
154
|
+
* `~/.config/xp-gate/`. Mirrors `defaultGlobalConfigPath()` in
|
|
155
|
+
* `plugins/dsh/src/delphi-config.ts` so the gate and the tool that produces the
|
|
156
|
+
* evidence agree on where seats come from. XDG_CONFIG_HOME is honoured because
|
|
157
|
+
* the plugin honours it — a gate reading a different file than the reviewer
|
|
158
|
+
* used would bind against seats nobody actually ran.
|
|
159
|
+
*/
|
|
160
|
+
function globalConfigPath(env = process.env) {
|
|
161
|
+
const base = env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
162
|
+
return path.join(base, 'xp-gate', DELPHI_CONFIG_FILE);
|
|
163
|
+
}
|
|
164
|
+
|
|
145
165
|
/**
|
|
146
166
|
* Read the binding seats for the project rooted at `projectRoot`.
|
|
147
167
|
*
|
|
@@ -152,23 +172,88 @@ function resolveBindingSeats(config) {
|
|
|
152
172
|
* fixture to xp-gate's seats. The gate's subject is the repository being
|
|
153
173
|
* pushed, so that is what must be named.
|
|
154
174
|
*
|
|
175
|
+
* ## Global fallback (2026-10-27)
|
|
176
|
+
*
|
|
177
|
+
* A project file still WINS, so a repo can override seats deliberately. When
|
|
178
|
+
* the project has none, the machine-wide config is used instead of returning
|
|
179
|
+
* null — and this is load-bearing, not a convenience:
|
|
180
|
+
*
|
|
181
|
+
* Before this fallback existed, DELETING a project-level config silently
|
|
182
|
+
* disabled the entire #574 seat check, because `bindingSeats === null`
|
|
183
|
+
* short-circuits `validateRequestedModel`. The gate kept reporting PASS while
|
|
184
|
+
* any three distinct gateway models — including the expensive overseas ones
|
|
185
|
+
* #574 exists to catch — could satisfy it, with no message anywhere. A user
|
|
186
|
+
* consolidating onto one global config to stop maintaining a copy per repo
|
|
187
|
+
* would have unknowingly traded that protection away.
|
|
188
|
+
*
|
|
189
|
+
* Only ENOENT triggers the fallback: an unreadable project config
|
|
190
|
+
* (EACCES/EISDIR) must surface rather than silently routing this run to a
|
|
191
|
+
* different file. Same semantics as `loadDelphiConfig()` in the DSH plugin.
|
|
192
|
+
*
|
|
193
|
+
* ## No config anywhere is now FAIL-CLOSED (2026-10-27)
|
|
194
|
+
*
|
|
195
|
+
* Returning null when both locations are absent re-opened the exact hole #574
|
|
196
|
+
* closed: measured, `b-claude-4.8-opus` / `gpt-5.5` / `gemini-3-pro` pass the
|
|
197
|
+
* validator when no config exists. That was tolerable while a config shipped
|
|
198
|
+
* inside every repo (deleting one looked like opting out); once the machine-wide
|
|
199
|
+
* file is the single source of truth, "no config" becomes the normal state of a
|
|
200
|
+
* fresh machine or a bare CI runner — i.e. the bypass is the default path.
|
|
201
|
+
*
|
|
202
|
+
* The caller distinguishes two cases it already knows:
|
|
203
|
+
* - no project context at all (no root argument) -> unbound, #423 behaviour;
|
|
204
|
+
* - a real repo with evidence but no config anywhere -> refuse.
|
|
205
|
+
* This function therefore throws rather than returning null for the second
|
|
206
|
+
* case; `main()` only reaches it when a project root was named.
|
|
207
|
+
*
|
|
155
208
|
* @param {string} projectRoot repository root whose config binds the evidence
|
|
156
|
-
* @
|
|
209
|
+
* @param {NodeJS.ProcessEnv} env environment for the global-path lookup
|
|
210
|
+
* @returns {Map<string, string>} seats; never null
|
|
211
|
+
* @throws exits via fail() when neither location has a config
|
|
157
212
|
*/
|
|
158
|
-
function loadBindingSeats(projectRoot) {
|
|
159
|
-
const
|
|
213
|
+
function loadBindingSeats(projectRoot, env = process.env) {
|
|
214
|
+
const projectPath = path.join(projectRoot, DELPHI_CONFIG_FILE);
|
|
215
|
+
const fallbackPath = globalConfigPath(env);
|
|
216
|
+
|
|
160
217
|
let raw;
|
|
218
|
+
let source = projectPath;
|
|
219
|
+
let projectMissing = false;
|
|
161
220
|
try {
|
|
162
|
-
raw = fs.readFileSync(
|
|
163
|
-
} catch (
|
|
164
|
-
if (
|
|
165
|
-
|
|
221
|
+
raw = fs.readFileSync(projectPath, 'utf8');
|
|
222
|
+
} catch (projectError) {
|
|
223
|
+
if (projectError && projectError.code === 'ENOENT') {
|
|
224
|
+
projectMissing = true;
|
|
225
|
+
try {
|
|
226
|
+
raw = fs.readFileSync(fallbackPath, 'utf8');
|
|
227
|
+
source = fallbackPath;
|
|
228
|
+
} catch (globalError) {
|
|
229
|
+
if (globalError && globalError.code === 'ENOENT') {
|
|
230
|
+
// Both absent: this repo has walkthrough evidence but nothing binds
|
|
231
|
+
// it to seats. Fail closed and say exactly where we looked.
|
|
232
|
+
fail(
|
|
233
|
+
`no ${DELPHI_CONFIG_FILE} found, so the reviewer seats cannot be verified. `
|
|
234
|
+
+ `Looked in: ${projectPath}, ${fallbackPath}. `
|
|
235
|
+
+ `Create the machine-wide config or add one to the repository.`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
fail(
|
|
239
|
+
`cannot read ${DELPHI_CONFIG_FILE}: ${globalError && globalError.message
|
|
240
|
+
? globalError.message
|
|
241
|
+
: String(globalError)}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
fail(
|
|
246
|
+
`cannot read ${DELPHI_CONFIG_FILE}: ${projectError && projectError.message
|
|
247
|
+
? projectError.message
|
|
248
|
+
: String(projectError)}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
166
251
|
}
|
|
167
252
|
let parsed;
|
|
168
253
|
try {
|
|
169
254
|
parsed = JSON.parse(raw);
|
|
170
255
|
} catch (error) {
|
|
171
|
-
fail(`${
|
|
256
|
+
fail(`${source} is not valid JSON: ${error.message}`);
|
|
172
257
|
}
|
|
173
258
|
const seats = resolveBindingSeats(parsed);
|
|
174
259
|
for (const role of REQUIRED_ROLES) {
|
|
@@ -176,9 +261,60 @@ function loadBindingSeats(projectRoot) {
|
|
|
176
261
|
fail(`${DELPHI_CONFIG_FILE} declares no seat for the ${role} role; all three roles must be configured.`);
|
|
177
262
|
}
|
|
178
263
|
}
|
|
264
|
+
// A repo-local copy that disagrees with the machine-wide one is exactly how
|
|
265
|
+
// the 2026-10-27 incident happened: a stale per-repo file silently won and the
|
|
266
|
+
// review kept using a seat that had been retired. Winning is still allowed
|
|
267
|
+
// (deliberate overrides exist), but it must never be silent.
|
|
268
|
+
if (!projectMissing) {
|
|
269
|
+
warnOnSeatDrift(projectPath, fallbackPath, seats);
|
|
270
|
+
}
|
|
179
271
|
return seats;
|
|
180
272
|
}
|
|
181
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Warn (never block) when a repository-local config disagrees with the
|
|
276
|
+
* machine-wide one.
|
|
277
|
+
*
|
|
278
|
+
* 2026-10-27 incident: the machine-wide config had been updated to replace a
|
|
279
|
+
* flaky seat, but an older copy survived in the repo root. The project file
|
|
280
|
+
* won, the review kept calling the retired seat, and nothing anywhere said so —
|
|
281
|
+
* the only symptom was intermittent HTTP 500s that looked like a gateway
|
|
282
|
+
* problem. Silence is what made it expensive.
|
|
283
|
+
*
|
|
284
|
+
* Deliberate per-repo overrides are legitimate, so this is a WARN on stderr and
|
|
285
|
+
* never changes the exit code. Absence of either file is not drift.
|
|
286
|
+
*
|
|
287
|
+
* @param {string} projectPath repository config path
|
|
288
|
+
* @param {string} fallbackPath machine-wide config path
|
|
289
|
+
* @param {Map<string, string>} projectSeats seats actually in effect
|
|
290
|
+
*/
|
|
291
|
+
function warnOnSeatDrift(projectPath, fallbackPath, projectSeats) {
|
|
292
|
+
let machineRaw;
|
|
293
|
+
try {
|
|
294
|
+
machineRaw = fs.readFileSync(fallbackPath, 'utf8');
|
|
295
|
+
} catch {
|
|
296
|
+
return; // no machine-wide config -> nothing to disagree with
|
|
297
|
+
}
|
|
298
|
+
let machineSeats;
|
|
299
|
+
try {
|
|
300
|
+
machineSeats = resolveBindingSeats(JSON.parse(machineRaw));
|
|
301
|
+
} catch {
|
|
302
|
+
return; // a malformed machine config is reported by its own path when used
|
|
303
|
+
}
|
|
304
|
+
const drift = REQUIRED_ROLES.filter(
|
|
305
|
+
(role) => machineSeats.has(role) && projectSeats.get(role) !== machineSeats.get(role)
|
|
306
|
+
);
|
|
307
|
+
if (drift.length === 0) return;
|
|
308
|
+
const detail = drift
|
|
309
|
+
.map((role) => `${role}: repo=${projectSeats.get(role)} machine=${machineSeats.get(role)}`)
|
|
310
|
+
.join('; ');
|
|
311
|
+
process.stderr.write(
|
|
312
|
+
`⚠️ ${DELPHI_CONFIG_FILE} in this repository overrides the machine-wide seats `
|
|
313
|
+
+ `(${detail}). The repository copy wins. If the machine-wide config was updated `
|
|
314
|
+
+ `to retire a seat, this repo is still calling the old one.\n`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
182
318
|
/**
|
|
183
319
|
* Validate an expert's identity fields: shape, role, approval status, and
|
|
184
320
|
* channel provenance. Split out of `validateExpert`, which exceeded the
|
|
@@ -284,10 +420,24 @@ function validateExpert(expert, index, roles, models, resolvedModels, bindingSea
|
|
|
284
420
|
|
|
285
421
|
function validateEvidence(evidence, expectedCommit, expectedBranch, now, bindingSeats) {
|
|
286
422
|
if (!isPlainObject(evidence)) fail('top level must be a plain object.');
|
|
423
|
+
validateEvidenceIdentity(evidence, expectedCommit, expectedBranch);
|
|
424
|
+
validateEvidenceTimestamps(evidence, now);
|
|
425
|
+
validateConsensusRatio(evidence.consensus_ratio);
|
|
426
|
+
validateExperts(evidence.experts, bindingSeats);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** The document must describe this exact push, and must approve it. */
|
|
430
|
+
function validateEvidenceIdentity(evidence, expectedCommit, expectedBranch) {
|
|
287
431
|
if (evidence.commit !== expectedCommit) fail('commit does not match HEAD.');
|
|
288
432
|
if (evidence.branch !== expectedBranch) fail('branch does not match the current branch.');
|
|
289
433
|
if (evidence.verdict !== 'APPROVED') fail('top-level verdict must be APPROVED.');
|
|
434
|
+
}
|
|
290
435
|
|
|
436
|
+
/**
|
|
437
|
+
* The evidence must be recent and self-consistent: not from the future, not
|
|
438
|
+
* expired, and valid for exactly one hour from its own timestamp.
|
|
439
|
+
*/
|
|
440
|
+
function validateEvidenceTimestamps(evidence, now) {
|
|
291
441
|
const timestamp = parseTimestamp(evidence.timestamp, 'timestamp');
|
|
292
442
|
const expires = parseTimestamp(evidence.expires, 'expires');
|
|
293
443
|
if (timestamp > now) fail('timestamp is in the future.');
|
|
@@ -295,15 +445,22 @@ function validateEvidence(evidence, expectedCommit, expectedBranch, now, binding
|
|
|
295
445
|
if (expires - timestamp !== VALIDITY_MS) {
|
|
296
446
|
fail('expires must be exactly one hour after timestamp.');
|
|
297
447
|
}
|
|
448
|
+
}
|
|
298
449
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|| evidence.consensus_ratio > 1) {
|
|
450
|
+
/** The three experts must have agreed at 90% or better. */
|
|
451
|
+
function validateConsensusRatio(ratio) {
|
|
452
|
+
if (typeof ratio !== 'number' || !Number.isFinite(ratio) || ratio < 0.90 || ratio > 1) {
|
|
303
453
|
fail('consensus_ratio must be a finite number from 0.90 through 1.');
|
|
304
454
|
}
|
|
455
|
+
}
|
|
305
456
|
|
|
306
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Validate each of the three expert records, then confirm every required role
|
|
459
|
+
* appeared. `roles` accumulates across the records, so it is checked once after
|
|
460
|
+
* the loop rather than per expert.
|
|
461
|
+
*/
|
|
462
|
+
function validateExperts(experts, bindingSeats) {
|
|
463
|
+
if (!Array.isArray(experts) || experts.length !== 3) {
|
|
307
464
|
fail('experts must contain exactly three records.');
|
|
308
465
|
}
|
|
309
466
|
const roles = new Set();
|
|
@@ -311,7 +468,7 @@ function validateEvidence(evidence, expectedCommit, expectedBranch, now, binding
|
|
|
311
468
|
// Scoped to this validation run, so one document's models cannot leak into the
|
|
312
469
|
// next (a long-lived process validating several pushes).
|
|
313
470
|
const resolvedModels = new Set();
|
|
314
|
-
|
|
471
|
+
experts.forEach((expert, index) => {
|
|
315
472
|
validateExpert(expert, index, roles, models, resolvedModels, bindingSeats);
|
|
316
473
|
});
|
|
317
474
|
if (roles.size !== REQUIRED_ROLES.length) fail('all required expert roles must be present.');
|
package/hooks/pre-commit
CHANGED
|
@@ -1506,9 +1506,32 @@ else
|
|
|
1506
1506
|
fi
|
|
1507
1507
|
|
|
1508
1508
|
# Debug statement detection (debugger statement in JS/TS)
|
|
1509
|
-
|
|
1509
|
+
#
|
|
1510
|
+
# Dependencies and build output are filtered out first (#648). The list
|
|
1511
|
+
# comes from `git diff --cached`, so a project (or a CI fixture) that
|
|
1512
|
+
# vendors or commits its dependencies gets the dependency's OWN source
|
|
1513
|
+
# scanned as if it were project code. TypeScript 7.x ships a bare
|
|
1514
|
+
# `debugger;` in dist/internal/utils.js, which blocked commits that changed
|
|
1515
|
+
# nothing of their own:
|
|
1516
|
+
# ❌ BLOCKED - 'debugger' statements detected:
|
|
1517
|
+
# node_modules/typescript/dist/internal/utils.js:18: debugger;
|
|
1518
|
+
# Pinning the dependency's version only postpones that: any package may
|
|
1519
|
+
# carry a debugger statement, and which one lands changes with every
|
|
1520
|
+
# install. The rule the gate actually wants is "no debugger in OUR code",
|
|
1521
|
+
# so the ignore set Gate 2 already uses decides what counts as ours — one
|
|
1522
|
+
# notion of build output across the gates, overridable through the same
|
|
1523
|
+
# XP_GATE_JSCPD_DEFAULT_IGNORE switch.
|
|
1524
|
+
TS_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' | {
|
|
1525
|
+
while IFS= read -r _df; do
|
|
1526
|
+
[ -n "$_df" ] || continue
|
|
1527
|
+
if ! jscpd_is_default_ignored "$_df"; then
|
|
1528
|
+
printf '%s\n' "$_df"
|
|
1529
|
+
fi
|
|
1530
|
+
done
|
|
1531
|
+
} || true)
|
|
1510
1532
|
if [ -n "$TS_DEBUG_FILES" ]; then
|
|
1511
|
-
|
|
1533
|
+
# NUL-separated so paths with spaces survive; a bare $VAR would word-split.
|
|
1534
|
+
DEBUGGER_HITS=$(printf '%s\n' "$TS_DEBUG_FILES" | tr '\n' '\0' | xargs -0 grep -n '^\s*debugger\s*;\?\s*$' 2>/dev/null || true)
|
|
1512
1535
|
if [ -n "$DEBUGGER_HITS" ]; then
|
|
1513
1536
|
echo ""
|
|
1514
1537
|
echo "❌ BLOCKED - 'debugger' statements detected:"
|
|
@@ -2885,9 +2908,33 @@ else
|
|
|
2885
2908
|
echo "ℹ️ Shell coverage not typically measured"
|
|
2886
2909
|
;;
|
|
2887
2910
|
*)
|
|
2911
|
+
# #640: hand the staged set to the adapter. Java's run_coverage uses it
|
|
2912
|
+
# for three-tier smart selection (changed-tests -> related -> full);
|
|
2913
|
+
# without it the adapter deliberately runs the full suite rather than
|
|
2914
|
+
# guessing, so this export failing closed is safe.
|
|
2915
|
+
# Colon-separated: newlines do not survive every adapter's word
|
|
2916
|
+
# splitting, and paths with spaces stay intact inside the value.
|
|
2917
|
+
GATE_CHANGED_FILES=$(printf '%s' "$CHANGED_FILES" | tr '\n' ':')
|
|
2918
|
+
export GATE_CHANGED_FILES
|
|
2919
|
+
# The adapter runs inside $(...), so it cannot set PARTIAL_TEST_RUN
|
|
2920
|
+
# for us. It writes the chosen strategy to this file and we raise the
|
|
2921
|
+
# flag here, in the shell that later reads it. Without this a Java
|
|
2922
|
+
# subset run under 80% coverage would take the hard-BLOCK branch
|
|
2923
|
+
# instead of the WARN branch, exactly the bug #556 fixed for Python.
|
|
2924
|
+
_java_partial_file=$(mktemp 2>/dev/null || echo "")
|
|
2925
|
+
if [ -n "$_java_partial_file" ]; then
|
|
2926
|
+
export XP_GATE_PARTIAL_RUN_FILE="$_java_partial_file"
|
|
2927
|
+
fi
|
|
2888
2928
|
default_cov_output=$(run_coverage 2>&1)
|
|
2889
2929
|
COV_EXIT=$((COV_EXIT || $?))
|
|
2890
2930
|
echo "$default_cov_output" | head -30
|
|
2931
|
+
unset XP_GATE_PARTIAL_RUN_FILE GATE_CHANGED_FILES
|
|
2932
|
+
if [ -n "$_java_partial_file" ]; then
|
|
2933
|
+
if [ -s "$_java_partial_file" ]; then
|
|
2934
|
+
PARTIAL_TEST_RUN=true
|
|
2935
|
+
fi
|
|
2936
|
+
rm -f "$_java_partial_file" 2>/dev/null || true
|
|
2937
|
+
fi
|
|
2891
2938
|
;;
|
|
2892
2939
|
esac
|
|
2893
2940
|
else
|
|
@@ -3008,19 +3055,53 @@ else
|
|
|
3008
3055
|
# JaCoCo writes target/site/jacoco/ (Maven) or build/reports/jacoco/test/
|
|
3009
3056
|
# (Gradle), i.e. neither path probed by the default branch below, so Java
|
|
3010
3057
|
# needs its own branch to be measurable at all.
|
|
3058
|
+
#
|
|
3059
|
+
# #603: a MULTI-MODULE Maven repo writes one report per module --
|
|
3060
|
+
# whaleaudit-core/target/site/jacoco/jacoco.csv, whaleaudit-demo/... --
|
|
3061
|
+
# and nothing at the repo root. Probing only the root paths therefore
|
|
3062
|
+
# concluded "No JaCoCo report found, so the 80% floor cannot be verified"
|
|
3063
|
+
# and BLOCKED every Java commit of a project that had a complete report
|
|
3064
|
+
# sitting one directory down. A gate that cannot see the report is not
|
|
3065
|
+
# enforcing a floor, it is enforcing an accident of build layout.
|
|
3066
|
+
# Fix: probe the root paths first (single-module projects, and the
|
|
3067
|
+
# historical behaviour), then fall back to the module reports and
|
|
3068
|
+
# aggregate across them, so the floor covers every class that was built.
|
|
3011
3069
|
jacoco_csv=""
|
|
3070
|
+
jacoco_all=""
|
|
3012
3071
|
for jacoco_candidate in \
|
|
3013
3072
|
"target/site/jacoco/jacoco.csv" \
|
|
3014
3073
|
"build/reports/jacoco/test/jacocoTestReport.csv"; do
|
|
3015
3074
|
[ -f "$jacoco_candidate" ] && { jacoco_csv="$jacoco_candidate"; break; }
|
|
3016
3075
|
done
|
|
3076
|
+
if [ -z "$jacoco_csv" ]; then
|
|
3077
|
+
# Module reports. -mindepth-free walk so ./module/... is found; sorted
|
|
3078
|
+
# for a stable, reproducible aggregate order.
|
|
3079
|
+
jacoco_all=$(find . -path ./target -prune -o \
|
|
3080
|
+
-type f \( -name 'jacoco.csv' -path '*/target/site/jacoco/*' \) -print 2>/dev/null \
|
|
3081
|
+
| sort | tr '\n' ' ')
|
|
3082
|
+
# First module report doubles as the "report exists" witness for the
|
|
3083
|
+
# messages below; the aggregate sums over every one of them.
|
|
3084
|
+
jacoco_csv=$(echo "$jacoco_all" | awk '{print $1}')
|
|
3085
|
+
fi
|
|
3086
|
+
if [ -n "$jacoco_csv" ] && [ -n "$jacoco_all" ]; then
|
|
3087
|
+
_jc_n=$(echo "$jacoco_all" | awk '{print NF}')
|
|
3088
|
+
echo " ℹ️ Multi-module JaCoCo: aggregating $_jc_n module report(s)."
|
|
3089
|
+
fi
|
|
3017
3090
|
|
|
3018
3091
|
if [ -n "$jacoco_csv" ]; then
|
|
3019
3092
|
# Aggregate INSTRUCTION_MISSED / INSTRUCTION_COVERED over every class row.
|
|
3020
3093
|
# Columns are located by header name because their position is not part of
|
|
3021
3094
|
# the CSV's stable contract.
|
|
3095
|
+
#
|
|
3096
|
+
# #603: with module reports, each file repeats the header row, so the
|
|
3097
|
+
# header detection is per-file (FNR == 1) rather than per-stream
|
|
3098
|
+
# (NR == 1). Otherwise module 2's header is summed as data, or worse,
|
|
3099
|
+
# the single header found in module 1 leaves the columns unset for it.
|
|
3100
|
+
_jc_inputs="$jacoco_csv"
|
|
3101
|
+
[ -n "$jacoco_all" ] && _jc_inputs="$jacoco_all"
|
|
3022
3102
|
COVERAGE_PERCENT=$(awk -F, '
|
|
3023
|
-
|
|
3103
|
+
FNR == 1 {
|
|
3104
|
+
missed_col = 0; covered_col = 0;
|
|
3024
3105
|
for (i = 1; i <= NF; i++) {
|
|
3025
3106
|
if ($i == "INSTRUCTION_MISSED") missed_col = i;
|
|
3026
3107
|
if ($i == "INSTRUCTION_COVERED") covered_col = i;
|
|
@@ -3036,7 +3117,7 @@ else
|
|
|
3036
3117
|
if (total > 0) printf "%.2f", (covered / total) * 100;
|
|
3037
3118
|
else print "";
|
|
3038
3119
|
}
|
|
3039
|
-
'
|
|
3120
|
+
' $_jc_inputs 2>/dev/null) || COVERAGE_PERCENT=""
|
|
3040
3121
|
|
|
3041
3122
|
if [ -n "$COVERAGE_PERCENT" ]; then
|
|
3042
3123
|
# Compare on the unrounded value so 79.5% cannot pass as 80%.
|
|
@@ -3484,18 +3565,77 @@ else
|
|
|
3484
3565
|
fi
|
|
3485
3566
|
;;
|
|
3486
3567
|
"java")
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3568
|
+
# #602: this branch used to be a guaranteed PASS no matter what happened.
|
|
3569
|
+
# 1) The test-dir probe hardcoded `src/test/java/architecture` at the
|
|
3570
|
+
# repo root. A multi-module Maven repo (core/ + demo/) puts the arch
|
|
3571
|
+
# tests under <module>/src/test/java/..., so the probe missed them
|
|
3572
|
+
# and the gate took the SKIP path -- reporting architecture as
|
|
3573
|
+
# "not configured" for a repo that HAS such tests.
|
|
3574
|
+
# 2) Even when it did run, `mvn ... | head -20` yields head's exit
|
|
3575
|
+
# status (0) as the pipeline result, so the `||` fallback never
|
|
3576
|
+
# fired and `echo "✅ ... completed"` printed unconditionally. A
|
|
3577
|
+
# failing architecture test was reported as success. Verified:
|
|
3578
|
+
# `false | head -20` exits 0; with `set -o pipefail` it exits 1.
|
|
3579
|
+
# Fix: discover the test source roots instead of guessing one path, and
|
|
3580
|
+
# judge the build by PIPESTATUS[0] (the pattern run_java_build_integrity
|
|
3581
|
+
# already uses), so the verdict comes from the build, not from `head`.
|
|
3582
|
+
_java_arch_roots=""
|
|
3583
|
+
if [ -f "pom.xml" ] || [ -x "./mvnw" ]; then
|
|
3584
|
+
# Any */src/test/java dir (module-relative root), including the repo
|
|
3585
|
+
# root itself, which find prints as ./src/test/java -> root ".".
|
|
3586
|
+
while IFS= read -r _root; do
|
|
3587
|
+
[ -n "$_root" ] && _java_arch_roots="${_java_arch_roots} ${_root}"
|
|
3588
|
+
done <<JAVAARCHROOTS
|
|
3589
|
+
$(find . -path ./target -prune -o -type d -path '*/src/test/java' -print 2>/dev/null \
|
|
3590
|
+
| sed 's|^\./||; s|/src/test/java$||; s|^src/test/java$|.|')
|
|
3591
|
+
JAVAARCHROOTS
|
|
3592
|
+
elif [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
|
|
3593
|
+
_java_arch_roots="."
|
|
3594
|
+
fi
|
|
3595
|
+
|
|
3596
|
+
# Only the roots that actually contain an *Architecture* test count.
|
|
3597
|
+
_java_arch_hit=""
|
|
3598
|
+
for _r in $_java_arch_roots; do
|
|
3599
|
+
_probe_dir="$_r/src/test/java"
|
|
3600
|
+
[ "$_r" = "." ] && _probe_dir="src/test/java"
|
|
3601
|
+
if find "$_probe_dir" -name '*Architecture*' -type f 2>/dev/null | head -1 | grep -q .; then
|
|
3602
|
+
_java_arch_hit="${_java_arch_hit} ${_r}"
|
|
3603
|
+
fi
|
|
3604
|
+
done
|
|
3605
|
+
|
|
3606
|
+
if [ -n "$_java_arch_hit" ]; then
|
|
3607
|
+
echo " Running Java architecture tests (roots:$_java_arch_hit)..."
|
|
3608
|
+
_java_arch_rc=0
|
|
3609
|
+
if [ -f "pom.xml" ] || [ -x "./mvnw" ]; then
|
|
3610
|
+
_mvn_bin="mvn"
|
|
3611
|
+
[ -x "./mvnw" ] && _mvn_bin="./mvnw"
|
|
3612
|
+
# -pl <module> -am runs only the module that holds the tests (plus
|
|
3613
|
+
# what it needs); quoting the whole root list into one -Dtest works
|
|
3614
|
+
# for Surefire because it accepts comma-separated patterns.
|
|
3615
|
+
for _r in $_java_arch_hit; do
|
|
3616
|
+
if [ "$_r" = "." ]; then
|
|
3617
|
+
"$_mvn_bin" -q test -Dtest='*Architecture*' 2>&1 | tail -20
|
|
3618
|
+
else
|
|
3619
|
+
"$_mvn_bin" -q -pl "$_r" -am test -Dtest='*Architecture*' 2>&1 | tail -20
|
|
3620
|
+
fi
|
|
3621
|
+
_rc="${PIPESTATUS[0]}"
|
|
3622
|
+
[ "$_rc" -ne 0 ] && _java_arch_rc="$_rc"
|
|
3623
|
+
done
|
|
3624
|
+
elif [ -x "./gradlew" ]; then
|
|
3625
|
+
./gradlew test --tests '*Architecture*' 2>&1 | tail -20
|
|
3626
|
+
_java_arch_rc="${PIPESTATUS[0]}"
|
|
3627
|
+
fi
|
|
3628
|
+
|
|
3629
|
+
if [ "$_java_arch_rc" -ne 0 ]; then
|
|
3630
|
+
echo ""
|
|
3631
|
+
echo "❌ BLOCKED - Java architecture test(s) failed (exit $_java_arch_rc)"
|
|
3632
|
+
echo "Fix the layering violation(s) above before committing."
|
|
3633
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_tests_failed"
|
|
3634
|
+
exit 1
|
|
3495
3635
|
fi
|
|
3496
3636
|
echo " ✅ Java architecture validation completed."
|
|
3497
3637
|
else
|
|
3498
|
-
echo " ℹ️ No
|
|
3638
|
+
echo " ℹ️ No *Architecture* test sources found under */src/test/java - skipping"
|
|
3499
3639
|
echo " ⏭️ SKIPPED - Architecture validation (no tests)"
|
|
3500
3640
|
# #537: no test files means Java architecture was NOT measured.
|
|
3501
3641
|
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
@@ -3547,6 +3687,22 @@ else
|
|
|
3547
3687
|
else
|
|
3548
3688
|
GATE_6_REASON="$_g6_seen"
|
|
3549
3689
|
fi
|
|
3690
|
+
|
|
3691
|
+
# #602 (status): a language that was really measured and passed leaves NO skip
|
|
3692
|
+
# reason behind (see the #537 comment in the typescript arm: "archlint
|
|
3693
|
+
# actually ran here, so there is no skip reason"). The verdict was never
|
|
3694
|
+
# upgraded to match, so Gate 6 reported SKIP even when every language in
|
|
3695
|
+
# the project had been validated and passed -- a measured dimension
|
|
3696
|
+
# rendered as unmeasured. That is the mirror image of the bug #537 fixed,
|
|
3697
|
+
# and the report could not tell the two apart.
|
|
3698
|
+
#
|
|
3699
|
+
# Upgrade to PASS only when the measurement actually happened for EVERY
|
|
3700
|
+
# language: no language may carry a skip reason. A single skipped language
|
|
3701
|
+
# keeps the gate non-PASS, so "some languages measured" cannot be scored
|
|
3702
|
+
# as a full success.
|
|
3703
|
+
if [ -z "$GATE_6_LANG_REASONS" ] && [ -n "$PROJECT_LANGS" ]; then
|
|
3704
|
+
GATE_6_STATUS="PASS"
|
|
3705
|
+
fi
|
|
3550
3706
|
else
|
|
3551
3707
|
# Architecture config is REQUIRED — BLOCK if missing
|
|
3552
3708
|
if [ -f "$PROJECT_ROOT/.architecture-skip" ]; then
|
|
@@ -4020,6 +4176,90 @@ else
|
|
|
4020
4176
|
fi
|
|
4021
4177
|
# ============================================================================
|
|
4022
4178
|
|
|
4179
|
+
# ============================================================================
|
|
4180
|
+
# GATE 13: Compiler Strictness Check
|
|
4181
|
+
# The compiler is the first and cheapest static analyser for a compiled language
|
|
4182
|
+
# and XP-Gate never used it as one: cpp.sh only runs clang-tidy/cppcheck over the
|
|
4183
|
+
# first 20 files, and Gate 9's build-integrity half is TypeScript-and-Maven only.
|
|
4184
|
+
# A measured `g++ -Wall -Wextra` pass reports -Wnon-virtual-dtor, -Wreorder,
|
|
4185
|
+
# -Wparentheses and -Wclass-memaccess on an innocuous-looking file; bare `javac`
|
|
4186
|
+
# reports NOTHING for a raw type where `-Xlint:all` reports [rawtypes].
|
|
4187
|
+
#
|
|
4188
|
+
# Verdicts: 0 PASS, 1 FAIL (new findings -> blocked), 2 BLOCK (ran but output
|
|
4189
|
+
# untrustworthy), 3 WARN (tool absent / cross-compiling / timeout / warn-only).
|
|
4190
|
+
# A missing compiler is a SKIP-class WARN per the project convention, NOT a block.
|
|
4191
|
+
#
|
|
4192
|
+
# Escape hatches: XP_GATE_DISABLE_13=1 (kill switch),
|
|
4193
|
+
# XP_GATE_COMPILER_STRICTNESS=strict|balanced|warn-only,
|
|
4194
|
+
# XP_GATE_COMPILER_ANALYZER=1 (gcc -fanalyzer, C only, opt-in).
|
|
4195
|
+
# Design: docs/plans/2026-10-26-gate-13-compiler-strictness-design.md
|
|
4196
|
+
#
|
|
4197
|
+
# NOTE: this block is a TAIL APPEND after Gate 12 and intentionally touches no
|
|
4198
|
+
# line of the gates above it.
|
|
4199
|
+
# ============================================================================
|
|
4200
|
+
GATE_13_STATUS="SKIP"
|
|
4201
|
+
if [ "${XP_GATE_DISABLE_13:-}" = "1" ]; then
|
|
4202
|
+
echo "⏭️ SKIPPED - Gate 13: Compiler Strictness (XP_GATE_DISABLE_13=1)"
|
|
4203
|
+
GATE_13_STATUS="SKIP"
|
|
4204
|
+
elif [ -f "$GATE_DIR/gate-13-compiler-strictness.sh" ] || [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/gate-13-compiler-strictness.sh" ]; then
|
|
4205
|
+
echo "⚙️ Gate 13: Compiler Strictness Check..."
|
|
4206
|
+
GATE13_START=$(gate_start_ms)
|
|
4207
|
+
(
|
|
4208
|
+
# Each stage runs inside the subshell so its shell state cannot leak back
|
|
4209
|
+
# into pre-commit. Verdicts are reduced to the WORST code seen:
|
|
4210
|
+
# FAIL/BLOCK (1/2) always win; WARN (3) only beats PASS (0).
|
|
4211
|
+
_g13_script="$GATE_DIR/gate-13-compiler-strictness.sh"
|
|
4212
|
+
[ -f "$_g13_script" ] || _g13_script="$(git rev-parse --show-toplevel)/githooks/gate-13-compiler-strictness.sh"
|
|
4213
|
+
# shellcheck source=/dev/null
|
|
4214
|
+
. "$_g13_script"
|
|
4215
|
+
|
|
4216
|
+
_g13_worst=0
|
|
4217
|
+
_g13_rc=0
|
|
4218
|
+
_g13_note() {
|
|
4219
|
+
case "$1" in
|
|
4220
|
+
1|2) _g13_worst=$1 ;;
|
|
4221
|
+
3) [ "$_g13_worst" -eq 0 ] && _g13_worst=3 ;;
|
|
4222
|
+
esac
|
|
4223
|
+
return 0
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
gate13_run_cpp_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
4227
|
+
gate13_run_java_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
4228
|
+
gate13_run_analyzer; _g13_rc=$?; _g13_note "$_g13_rc"
|
|
4229
|
+
|
|
4230
|
+
exit "$_g13_worst"
|
|
4231
|
+
)
|
|
4232
|
+
GATE_13_RC=$?
|
|
4233
|
+
GATE13_END=$(now_ms)
|
|
4234
|
+
GATE_13_DURATION=$(( (GATE13_END - GATE13_START) / 1000 ))
|
|
4235
|
+
case "$GATE_13_RC" in
|
|
4236
|
+
0) GATE_13_STATUS="PASS" ;;
|
|
4237
|
+
1) GATE_13_STATUS="FAIL" ;;
|
|
4238
|
+
2) GATE_13_STATUS="BLOCK" ;;
|
|
4239
|
+
*) GATE_13_STATUS="WARN" ;;
|
|
4240
|
+
esac
|
|
4241
|
+
echo " Gate 13 ($GATE_13_STATUS) completed in ${GATE_13_DURATION}s"
|
|
4242
|
+
record_gate_audit "gate-13" "compiler-strictness" "$GATE_13_STATUS" "0" "$GATE13_START"
|
|
4243
|
+
# Every other gate blocks by exiting immediately (e.g. Gate 1's
|
|
4244
|
+
# "❌ BLOCKED ... exit 1"); there is no aggregate-then-exit shared with
|
|
4245
|
+
# gates 0-12, so Gate 13 must do the same or its FAIL would only be cosmetic
|
|
4246
|
+
# in the report while the commit sails through.
|
|
4247
|
+
if [ "$GATE_13_STATUS" = "FAIL" ] || [ "$GATE_13_STATUS" = "BLOCK" ]; then
|
|
4248
|
+
echo ""
|
|
4249
|
+
echo "❌ BLOCKED - Gate 13: Compiler Strictness (${GATE_13_STATUS})"
|
|
4250
|
+
echo " Fix the compiler findings above. To record pre-existing warnings:"
|
|
4251
|
+
echo " xp-gate compiler-baseline create"
|
|
4252
|
+
echo " To relax strictness: XP_GATE_COMPILER_STRICTNESS=warn-only"
|
|
4253
|
+
echo " Emergency only: XP_GATE_DISABLE_13=1"
|
|
4254
|
+
exit 1
|
|
4255
|
+
fi
|
|
4256
|
+
else
|
|
4257
|
+
echo "⏭️ SKIPPED - Gate 13: Compiler Strictness (gate-13-compiler-strictness.sh not found)"
|
|
4258
|
+
GATE_13_STATUS="SKIP"
|
|
4259
|
+
fi
|
|
4260
|
+
echo ""
|
|
4261
|
+
# ============================================================================
|
|
4262
|
+
|
|
4023
4263
|
generate_quality_report() {
|
|
4024
4264
|
local HISTORY_FILE=".quality-history.jsonl"
|
|
4025
4265
|
local COMMIT_HASH
|
|
@@ -4316,6 +4556,22 @@ generate_quality_report() {
|
|
|
4316
4556
|
}
|
|
4317
4557
|
ENDJSON
|
|
4318
4558
|
|
|
4559
|
+
# ── Gate 13 injection (tail append; no existing line above is modified) ────
|
|
4560
|
+
# The heredoc is closed deliberately before Gate 13 so this sprint ADDS lines
|
|
4561
|
+
# instead of editing gates 0-12's report block, keeping the two branches'
|
|
4562
|
+
# hunks disjoint. Scores are recomputed over 13 gates afterwards.
|
|
4563
|
+
if [ -n "${GATE_13_STATUS:-}" ] && [ -f "$REPORT_FILE" ]; then
|
|
4564
|
+
_XP_G13="$REPORT_FILE" _XP_G13S="$GATE_13_STATUS" node -e "
|
|
4565
|
+
const fs=require('fs'),p=process.env._XP_G13;
|
|
4566
|
+
try{
|
|
4567
|
+
const j=JSON.parse(fs.readFileSync(p,'utf8'));
|
|
4568
|
+
j.gates=j.gates||{};
|
|
4569
|
+
j.gates.gate13_compiler_strictness={name:'Compiler Strictness',status:process.env._XP_G13S,tool:'gate-13-compiler-strictness.sh'};
|
|
4570
|
+
fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');
|
|
4571
|
+
}catch(e){}
|
|
4572
|
+
" 2>/dev/null || true
|
|
4573
|
+
fi
|
|
4574
|
+
|
|
4319
4575
|
# ── Persistent per-run report (Issue #101) ────────────────────────────────────────
|
|
4320
4576
|
local REPORTS_DIR=".xp-gate/reports/pre-commit"
|
|
4321
4577
|
local REPORT_TS
|