@boyingliu01/xp-gate 0.28.1 → 0.30.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/gate-3.sh +18 -1
- package/hooks/gate-3.sh +18 -1
- package/hooks/hook-module-resolution.sh +39 -0
- package/hooks/pre-commit +315 -38
- package/hooks/pre-push +26 -2
- package/lib/__tests__/doctor-module-resolution-parity.test.js +79 -11
- package/lib/__tests__/sprint-state-manager.test.js +31 -0
- package/lib/doctor.js +60 -27
- package/lib/hook-module-resolution.js +127 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/retro.js +24 -7
- package/lib/sprint-state-manager.js +14 -6
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +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/grill-with-docs/SKILL.md +1 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +1 -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/grill-with-docs/SKILL.md +1 -0
- package/plugins/dsh/skills/grilling/SKILL.md +1 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/opencode/skills/grilling/SKILL.md +1 -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/batch-grill-me/SKILL.md +9 -2
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/grill-with-docs/SKILL.md +2 -2
- package/plugins/qoder/skills/grilling/SKILL.md +23 -5
- 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/scripts/sync-package-content.js +3 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/gate-3.sh
CHANGED
|
@@ -77,7 +77,24 @@ else
|
|
|
77
77
|
|
|
78
78
|
# Get changed source files for complexity check (includes test files)
|
|
79
79
|
CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
|
|
80
|
-
|
|
80
|
+
|
|
81
|
+
# Grade exactly the population the baseline was written from. The two must
|
|
82
|
+
# match or the ratchet reports pre-existing code as "new", which is a false
|
|
83
|
+
# BLOCK no repair can clear (the exact failure #495 Round 3 called out, in a
|
|
84
|
+
# different place). scripts/ccn-report.cjs --write-baseline records only
|
|
85
|
+
# bucket === 'production' rows; its bucketFor() classifies two other buckets:
|
|
86
|
+
# mirror src/npm-package/**, plugins/*/src/** — regenerated by prepack
|
|
87
|
+
# from a canonical source that IS graded here. Editing a mirror
|
|
88
|
+
# is reverted at the next publish, so grading it cannot be acted
|
|
89
|
+
# on (doctorDelphi CCN=106 blocked every src/npm-package commit,
|
|
90
|
+
# on HEAD too — no baseline entry could ever exist for it).
|
|
91
|
+
# test __tests__/, *.test.*, *_test.{py,go}, *Test(s).java — recorded
|
|
92
|
+
# in the report's test bucket, never in the baseline.
|
|
93
|
+
# Both prefixes and the test pattern mirror ccn-report.cjs so the gate and
|
|
94
|
+
# the writer cannot drift apart again (pinned by __tests__/gate-3-*.bats).
|
|
95
|
+
CC_FILES=$(echo "$CC_FILES" | grep -vE \
|
|
96
|
+
'^"?src/npm-package/|^"?plugins/[^/]+/src/|(^|/)__tests__?/|\.test\.[cm]?[jt]sx?$|_test\.(py|go)$|Test(s)?\.java$' || true)
|
|
97
|
+
|
|
81
98
|
if [ -n "$CC_FILES" ]; then
|
|
82
99
|
echo "Checking complexity for source files..."
|
|
83
100
|
|
package/hooks/gate-3.sh
CHANGED
|
@@ -77,7 +77,24 @@ else
|
|
|
77
77
|
|
|
78
78
|
# Get changed source files for complexity check (includes test files)
|
|
79
79
|
CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
|
|
80
|
-
|
|
80
|
+
|
|
81
|
+
# Grade exactly the population the baseline was written from. The two must
|
|
82
|
+
# match or the ratchet reports pre-existing code as "new", which is a false
|
|
83
|
+
# BLOCK no repair can clear (the exact failure #495 Round 3 called out, in a
|
|
84
|
+
# different place). scripts/ccn-report.cjs --write-baseline records only
|
|
85
|
+
# bucket === 'production' rows; its bucketFor() classifies two other buckets:
|
|
86
|
+
# mirror src/npm-package/**, plugins/*/src/** — regenerated by prepack
|
|
87
|
+
# from a canonical source that IS graded here. Editing a mirror
|
|
88
|
+
# is reverted at the next publish, so grading it cannot be acted
|
|
89
|
+
# on (doctorDelphi CCN=106 blocked every src/npm-package commit,
|
|
90
|
+
# on HEAD too — no baseline entry could ever exist for it).
|
|
91
|
+
# test __tests__/, *.test.*, *_test.{py,go}, *Test(s).java — recorded
|
|
92
|
+
# in the report's test bucket, never in the baseline.
|
|
93
|
+
# Both prefixes and the test pattern mirror ccn-report.cjs so the gate and
|
|
94
|
+
# the writer cannot drift apart again (pinned by __tests__/gate-3-*.bats).
|
|
95
|
+
CC_FILES=$(echo "$CC_FILES" | grep -vE \
|
|
96
|
+
'^"?src/npm-package/|^"?plugins/[^/]+/src/|(^|/)__tests__?/|\.test\.[cm]?[jt]sx?$|_test\.(py|go)$|Test(s)?\.java$' || true)
|
|
97
|
+
|
|
81
98
|
if [ -n "$CC_FILES" ]; then
|
|
82
99
|
echo "Checking complexity for source files..."
|
|
83
100
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GENERATED FILE — DO NOT EDIT.
|
|
3
|
+
# Source of truth: src/npm-package/lib/hook-module-resolution.js
|
|
4
|
+
# Regenerate: node scripts/generate-hook-resolution.cjs
|
|
5
|
+
#
|
|
6
|
+
# The module resolution order (#505) lives in one place, as data. This file
|
|
7
|
+
# is its shell projection. Editing it by hand makes the hook disagree with
|
|
8
|
+
# doctor.js, which reads the JS module directly — the exact divergence
|
|
9
|
+
# #495/#500 were about.
|
|
10
|
+
|
|
11
|
+
# Probe file used to pick the gate tier.
|
|
12
|
+
HOOK_GATE_PROBE="gate-3.sh"
|
|
13
|
+
|
|
14
|
+
# ADAPTER_DIR cascade, highest priority first.
|
|
15
|
+
# Each entry is "tier_id|base_var|probe_file".
|
|
16
|
+
HOOK_ADAPTER_DIR_TIERS=(
|
|
17
|
+
'global|GLOBAL_ADAPTER_DIR|adapter-common.sh'
|
|
18
|
+
'project|PROJECT_GITHOOKS|adapter-common.sh'
|
|
19
|
+
'script|SCRIPT_DIR|adapter-common.sh'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Adapter resolution tiers, highest priority first.
|
|
23
|
+
# Each entry is "tier_id|base_var|template" with {name} as the basename.
|
|
24
|
+
HOOK_ADAPTER_TIERS=(
|
|
25
|
+
'adapter_flat|ADAPTER_DIR|{name}.sh'
|
|
26
|
+
'adapter_nested|ADAPTER_DIR|adapters/{name}.sh'
|
|
27
|
+
'project_nested|PROJECT_GITHOOKS|adapters/{name}.sh'
|
|
28
|
+
'script_nested|SCRIPT_DIR|adapters/{name}.sh'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Gate resolution tiers, highest priority first.
|
|
32
|
+
# Semantics differ from adapter tiers: each entry names the DIRECTORY that
|
|
33
|
+
# would hold the gate probe, so a match yields the directory (GATE_DIR), not
|
|
34
|
+
# the probe file itself. Entry format: "tier_id|base_var|(empty suffix)".
|
|
35
|
+
HOOK_GATE_TIERS=(
|
|
36
|
+
'gate_adapter_dir|ADAPTER_DIR|'
|
|
37
|
+
'gate_project|PROJECT_GITHOOKS|'
|
|
38
|
+
'gate_script_dir|SCRIPT_DIR|'
|
|
39
|
+
)
|
package/hooks/pre-commit
CHANGED
|
@@ -15,6 +15,89 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
15
15
|
ADAPTER_DIR=""
|
|
16
16
|
GLOBAL_ADAPTER_DIR="$HOME/.config/xp-gate/adapters"
|
|
17
17
|
|
|
18
|
+
# #505: load the resolution-order data BEFORE any tier decision runs. The file is
|
|
19
|
+
# generated from src/npm-package/lib/hook-module-resolution.js, which doctor.js
|
|
20
|
+
# also reads, so the hook and the diagnostic share one order instead of two
|
|
21
|
+
# hand-written copies that a parity test had to keep honest.
|
|
22
|
+
#
|
|
23
|
+
# It is looked up beside the hook (SCRIPT_DIR — always present, since this file
|
|
24
|
+
# IS the hook) and beside adapter-common.sh (ADAPTER_DIR — where `xp-gate init`
|
|
25
|
+
# and setup-global install the supporting modules). Either location works; the
|
|
26
|
+
# hook-dir copy is what `doctor --sync-hooks` and sync-package-content maintain.
|
|
27
|
+
# Note this runs BEFORE ADAPTER_DIR is resolved, which is why the script dir is
|
|
28
|
+
# the primary location: reading the order cannot depend on the order.
|
|
29
|
+
HOOK_RESOLUTION_SH="${SCRIPT_DIR}/hook-module-resolution.sh"
|
|
30
|
+
if [ ! -f "$HOOK_RESOLUTION_SH" ] && [ -f "$GLOBAL_ADAPTER_DIR/hook-module-resolution.sh" ]; then
|
|
31
|
+
HOOK_RESOLUTION_SH="$GLOBAL_ADAPTER_DIR/hook-module-resolution.sh"
|
|
32
|
+
fi
|
|
33
|
+
if [ ! -f "$HOOK_RESOLUTION_SH" ]; then
|
|
34
|
+
# Fail closed: a hook that cannot load its resolution order must not fall back
|
|
35
|
+
# to an invented one — that would silently resurrect the duplication removed
|
|
36
|
+
# here, and doctor would report a different surface than the hook executes.
|
|
37
|
+
echo "FATAL: cannot load hook-module-resolution.sh (module resolution order is unavailable)" >&2
|
|
38
|
+
echo "Fix: reinstall the hooks (xp-gate init, or xp-gate doctor --sync-hooks in the xp-gate repo)." >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
# shellcheck source=/dev/null
|
|
42
|
+
. "$HOOK_RESOLUTION_SH"
|
|
43
|
+
|
|
44
|
+
# Look up one tier's directory variable by name without eval-ing arbitrary input:
|
|
45
|
+
# the base names are a closed set baked into the generated file.
|
|
46
|
+
_hook_tier_base() {
|
|
47
|
+
case "$1" in
|
|
48
|
+
ADAPTER_DIR) printf '%s' "$ADAPTER_DIR" ;;
|
|
49
|
+
GLOBAL_ADAPTER_DIR) printf '%s' "$GLOBAL_ADAPTER_DIR" ;;
|
|
50
|
+
PROJECT_GITHOOKS) printf '%s' "$PROJECT_GITHOOKS" ;;
|
|
51
|
+
SCRIPT_DIR) printf '%s' "$SCRIPT_DIR" ;;
|
|
52
|
+
*) printf '%s' "" ;;
|
|
53
|
+
esac
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Walk a "tier_id|base|suffix" table in order and print the first existing match.
|
|
57
|
+
# Usage: _hook_resolve <module_filename> <tiers_array_name>
|
|
58
|
+
_hook_resolve() {
|
|
59
|
+
local module_name="$1"
|
|
60
|
+
local tiers_var="$2"
|
|
61
|
+
local entry base suffix base_dir candidate
|
|
62
|
+
eval "set -- \"\${${tiers_var}[@]}\""
|
|
63
|
+
for entry in "$@"; do
|
|
64
|
+
entry="${entry#*|}"
|
|
65
|
+
base="${entry%%|*}"
|
|
66
|
+
suffix="${entry#*|}"
|
|
67
|
+
base_dir="$(_hook_tier_base "$base")"
|
|
68
|
+
[ -n "$base_dir" ] || continue
|
|
69
|
+
candidate="$base_dir/${suffix/\{name\}/$module_name}"
|
|
70
|
+
if [ -f "$candidate" ]; then
|
|
71
|
+
printf '%s' "$candidate"
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
return 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Walk a gate tier table and print the first DIRECTORY that contains the probe.
|
|
79
|
+
# The adapter table yields a file; this one yields the directory the gate suite
|
|
80
|
+
# lives in (GATE_DIR), which is why it probes rather than substitutes.
|
|
81
|
+
# Usage: _hook_resolve_dir <probe_filename> <tiers_array_name>
|
|
82
|
+
_hook_resolve_dir() {
|
|
83
|
+
local probe="$1"
|
|
84
|
+
local tiers_var="$2"
|
|
85
|
+
local entry base suffix base_dir
|
|
86
|
+
eval "set -- \"\${${tiers_var}[@]}\""
|
|
87
|
+
for entry in "$@"; do
|
|
88
|
+
entry="${entry#*|}"
|
|
89
|
+
base="${entry%%|*}"
|
|
90
|
+
suffix="${entry#*|}"
|
|
91
|
+
base_dir="$(_hook_tier_base "$base")"
|
|
92
|
+
[ -n "$base_dir" ] || continue
|
|
93
|
+
if [ -f "$base_dir${suffix}/$probe" ]; then
|
|
94
|
+
printf '%s' "$base_dir${suffix}"
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
18
101
|
# BEGIN GIT CONTEXT FALLBACK
|
|
19
102
|
if ! declare -F run_without_git_context >/dev/null 2>&1; then
|
|
20
103
|
run_without_git_context() (
|
|
@@ -51,39 +134,25 @@ source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
|
|
|
51
134
|
# ADAPTER_DIR points to the base directory that contains either
|
|
52
135
|
# adapter-common.sh directly (global) or adapter-common.sh in a flat layout.
|
|
53
136
|
# Gate scripts live alongside adapter-common.sh.
|
|
137
|
+
#
|
|
138
|
+
# #505: the ORDER is not written here. HOOK_ADAPTER_TIERS is generated from
|
|
139
|
+
# src/npm-package/lib/hook-module-resolution.js, which doctor.js reads directly,
|
|
140
|
+
# so the hook and the diagnostic cannot disagree about which file wins.
|
|
54
141
|
resolve_adapter_path() {
|
|
55
|
-
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
echo "$ADAPTER_DIR/${lang}.sh"
|
|
59
|
-
return 0
|
|
60
|
-
fi
|
|
61
|
-
# Try nested layout (project: adapters/typescript.sh)
|
|
62
|
-
if [ -f "$ADAPTER_DIR/adapters/${lang}.sh" ]; then
|
|
63
|
-
echo "$ADAPTER_DIR/adapters/${lang}.sh"
|
|
64
|
-
return 0
|
|
65
|
-
fi
|
|
66
|
-
# Try project githooks as fallback
|
|
67
|
-
if [ -f "$PROJECT_GITHOOKS/adapters/${lang}.sh" ]; then
|
|
68
|
-
echo "$PROJECT_GITHOOKS/adapters/${lang}.sh"
|
|
69
|
-
return 0
|
|
70
|
-
fi
|
|
71
|
-
# Try script dir as last resort
|
|
72
|
-
if [ -f "$SCRIPT_DIR/adapters/${lang}.sh" ]; then
|
|
73
|
-
echo "$SCRIPT_DIR/adapters/${lang}.sh"
|
|
74
|
-
return 0
|
|
75
|
-
fi
|
|
76
|
-
return 1
|
|
142
|
+
# ADAPTER templates carry the extension ("{name}.sh"), so pass the bare language
|
|
143
|
+
# name here. GATE templates are "{name}", so their call passes the full filename.
|
|
144
|
+
_hook_resolve "${1}" HOOK_ADAPTER_TIERS
|
|
77
145
|
}
|
|
78
146
|
|
|
79
|
-
# Gate scripts directory — falls back to project githooks if ADAPTER_DIR doesn't have them
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
147
|
+
# Gate scripts directory — falls back to project githooks if ADAPTER_DIR doesn't have them.
|
|
148
|
+
# #505: the probe file and the tier order come from the generated data file, so
|
|
149
|
+
# this cascade cannot drift from what doctor.js reports.
|
|
150
|
+
GATE_DIR="$(_hook_resolve_dir "$HOOK_GATE_PROBE" HOOK_GATE_TIERS)" || GATE_DIR=""
|
|
151
|
+
# Deliberate last resort: when no tier carries the gate suite, point at
|
|
152
|
+
# ADAPTER_DIR anyway rather than leaving GATE_DIR empty. The gate-N.sh sources
|
|
153
|
+
# further down check for the file before sourcing, so an absent suite still
|
|
154
|
+
# degrades to a reported skip instead of a path built from an empty string.
|
|
155
|
+
if [ -z "$GATE_DIR" ]; then
|
|
87
156
|
GATE_DIR="$ADAPTER_DIR"
|
|
88
157
|
fi
|
|
89
158
|
|
|
@@ -285,6 +354,104 @@ gate_start_ms() {
|
|
|
285
354
|
now_ms
|
|
286
355
|
}
|
|
287
356
|
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# #537: per-language coverage classification.
|
|
359
|
+
#
|
|
360
|
+
# A gate that SKIPs tells the reader nothing on its own — "documentation
|
|
361
|
+
# project", "tool not installed" and "nothing changed" are wildly different
|
|
362
|
+
# facts that used to collapse into one word. A PowerShell-primary repo could
|
|
363
|
+
# therefore look fully covered while 3.3k lines were measured by nothing.
|
|
364
|
+
#
|
|
365
|
+
# classify_coverage_reason REASON GATE_STATUS
|
|
366
|
+
# -> covered | not_applicable | no_changes | tool_missing | unknown
|
|
367
|
+
#
|
|
368
|
+
# FAIL CLOSED: an unrecognised reason is `unknown`, never `covered`. `covered`
|
|
369
|
+
# is reachable ONLY by a gate that actually passed (PASS / WARN /
|
|
370
|
+
# PASS_WITH_WARNINGS) with no skip reason attached.
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
classify_coverage_reason() {
|
|
373
|
+
local reason="${1:-}"
|
|
374
|
+
local gate_status="${2:-}"
|
|
375
|
+
|
|
376
|
+
case "$reason" in
|
|
377
|
+
documentation_project) echo "not_applicable" ;;
|
|
378
|
+
architecture_skip_exemption) echo "not_applicable" ;;
|
|
379
|
+
no_source_files_changed) echo "no_changes" ;;
|
|
380
|
+
# Gate 4 names the same concept "no_matching_files_changed"; Gate 3/6 use
|
|
381
|
+
# the shorter form. Both mean "the tool exists, nothing to look at".
|
|
382
|
+
no_matching_files_changed) echo "no_changes" ;;
|
|
383
|
+
tool_missing|tool_not_installed|no_powershell_tool|no_archlint_detector|checker_not_found)
|
|
384
|
+
echo "tool_missing" ;;
|
|
385
|
+
# archlint / import-linter / arch-go / Java arch tests exist as concepts but
|
|
386
|
+
# this repo carries no config for the language, so nothing was measured.
|
|
387
|
+
architecture_not_configured) echo "tool_missing" ;;
|
|
388
|
+
# Gate 4's checker ran but crashed. Not covered, not "tool missing" — the
|
|
389
|
+
# measurement was attempted and failed, so the honest answer is unknown.
|
|
390
|
+
checker_execution_error) echo "unknown" ;;
|
|
391
|
+
"")
|
|
392
|
+
case "$gate_status" in
|
|
393
|
+
PASS|WARN|PASS_WITH_WARNINGS|PASSED) echo "covered" ;;
|
|
394
|
+
*) echo "unknown" ;;
|
|
395
|
+
esac
|
|
396
|
+
;;
|
|
397
|
+
*) echo "unknown" ;;
|
|
398
|
+
esac
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# #537: record where the language key set came from. PROJECT_LANGS is derived
|
|
402
|
+
# from the CURRENTLY CHANGED files, not from a static inventory of the repo, so
|
|
403
|
+
# a docs-only commit legitimately yields an empty set. Reporting the matrix
|
|
404
|
+
# without this provenance would repeat the sin the matrix exists to fix.
|
|
405
|
+
resolve_languages_source() {
|
|
406
|
+
local override_langs="${1:-}"
|
|
407
|
+
local config_langs="${2:-}"
|
|
408
|
+
local resolved_langs="${3:-}"
|
|
409
|
+
if [ -n "$override_langs" ]; then
|
|
410
|
+
echo "override"
|
|
411
|
+
elif [ -n "$config_langs" ]; then
|
|
412
|
+
echo "config"
|
|
413
|
+
elif [ -n "$resolved_langs" ]; then
|
|
414
|
+
echo "changed_files"
|
|
415
|
+
else
|
|
416
|
+
echo "none"
|
|
417
|
+
fi
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# #537: resolve Gate 6's reason for ONE language.
|
|
421
|
+
#
|
|
422
|
+
# Reads the flattened GATE_6_LANG_REASONS ("lang=reason,lang=reason") because
|
|
423
|
+
# the associate array that produced it is function-local to the gate and
|
|
424
|
+
# invisible to generate_quality_report().
|
|
425
|
+
#
|
|
426
|
+
# The scalar GATE_6_REASON is a SUMMARY across languages and must NOT be used
|
|
427
|
+
# as a per-language fallback when per-language detail exists: with langs
|
|
428
|
+
# "typescript iac" where only iac lacks a detector, the scalar reads
|
|
429
|
+
# no_archlint_detector and would relabel typescript — the exact language
|
|
430
|
+
# archlint had just measured. A real probe run exposed this:
|
|
431
|
+
# langs=[typescript iac] reasons=[iac=no_archlint_detector]
|
|
432
|
+
#
|
|
433
|
+
# So: an explicit pair wins; otherwise there is no skip reason for this
|
|
434
|
+
# language, and "no reason" + PASS status is precisely what `covered` means.
|
|
435
|
+
# The scalar only speaks for every language when there is no detail at all
|
|
436
|
+
# (config-missing / bash<4 paths).
|
|
437
|
+
coverage_g6_reason_for() {
|
|
438
|
+
local lang="${1:-}"
|
|
439
|
+
local detail="${GATE_6_LANG_REASONS:-}"
|
|
440
|
+
local scalar="${GATE_6_REASON:-}"
|
|
441
|
+
local pairs=",${detail},"
|
|
442
|
+
local reason=""
|
|
443
|
+
case "$pairs" in
|
|
444
|
+
*",${lang}="*)
|
|
445
|
+
reason="${pairs#*",${lang}="}"
|
|
446
|
+
reason="${reason%%,*}"
|
|
447
|
+
;;
|
|
448
|
+
esac
|
|
449
|
+
if [ -z "$reason" ] && [ -z "$detail" ] && [ -n "$scalar" ]; then
|
|
450
|
+
reason="$scalar"
|
|
451
|
+
fi
|
|
452
|
+
echo "$reason"
|
|
453
|
+
}
|
|
454
|
+
|
|
288
455
|
# Audit helper — records gate execution to .xp-gate/audit.jsonl
|
|
289
456
|
# NEVER blocks commit on failure (try/catch around npx tsx).
|
|
290
457
|
# Usage: record_gate_audit <gate_id> <gate_name> <passed> <issues_found> <start_ms>
|
|
@@ -2902,6 +3069,15 @@ else
|
|
|
2902
3069
|
|
|
2903
3070
|
# Check if architecture config exists (use $PROJECT_ROOT — hook may have cd'd to a subdir via ARCH-03)
|
|
2904
3071
|
if [ -f "$PROJECT_ROOT/architecture.yaml" ] || [ -f "$PROJECT_ROOT/.architecturerc" ] || [ -f "$PROJECT_ROOT/.archlint.yaml" ]; then
|
|
3072
|
+
# #537: per-language reason bookkeeping. Gate 3/4 are single-language gates
|
|
3073
|
+
# and get away with one scalar reason; Gate 6 loops, so a scalar would lose
|
|
3074
|
+
# information ("TS passed, PS has no detector" cannot be one string).
|
|
3075
|
+
# Bash 4 associative array, with a scalar aggregate for the legacy field.
|
|
3076
|
+
GATE_6_REASON=""
|
|
3077
|
+
GATE_6_LANG_REASONS=""
|
|
3078
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
3079
|
+
declare -A GATE_6_REASON_BY_LANG=()
|
|
3080
|
+
fi
|
|
2905
3081
|
# Different architecture tools per language
|
|
2906
3082
|
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
2907
3083
|
case "$CURRENT_LANG" in
|
|
@@ -2947,6 +3123,8 @@ else
|
|
|
2947
3123
|
else
|
|
2948
3124
|
echo " ✅ TypeScript architecture validation completed (zero-tolerance mode). no baseline found."
|
|
2949
3125
|
fi
|
|
3126
|
+
# #537: archlint actually ran here, so there is no skip reason. The
|
|
3127
|
+
# matrix reads an absent reason + PASS status as `covered`.
|
|
2950
3128
|
;;
|
|
2951
3129
|
"python")
|
|
2952
3130
|
# detect any import-linter carrier (incl. pyproject.toml [tool.importlinter])
|
|
@@ -2972,6 +3150,12 @@ else
|
|
|
2972
3150
|
else
|
|
2973
3151
|
echo " ℹ️ No import-linter config found (.import-linter.yml/.importlinter/[tool.importlinter]) - skipping"
|
|
2974
3152
|
echo " ⏭️ SKIPPED - Python architecture validation (no config)"
|
|
3153
|
+
# #537: no config means Python architecture was NOT measured. Without
|
|
3154
|
+
# this the matrix would call it `covered` simply because the branch
|
|
3155
|
+
# recorded nothing — the exact false-confidence the matrix prevents.
|
|
3156
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
3157
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_not_configured"
|
|
3158
|
+
fi
|
|
2975
3159
|
fi
|
|
2976
3160
|
;;
|
|
2977
3161
|
"go")
|
|
@@ -2988,6 +3172,10 @@ else
|
|
|
2988
3172
|
else
|
|
2989
3173
|
echo " ℹ️ No arch-go.yaml found - skipping Go architecture"
|
|
2990
3174
|
echo " ⏭️ SKIPPED - Go architecture validation (no config)"
|
|
3175
|
+
# #537: same reasoning as the Python branch above.
|
|
3176
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
3177
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_not_configured"
|
|
3178
|
+
fi
|
|
2991
3179
|
fi
|
|
2992
3180
|
;;
|
|
2993
3181
|
"java")
|
|
@@ -3004,19 +3192,63 @@ else
|
|
|
3004
3192
|
else
|
|
3005
3193
|
echo " ℹ️ No architecture test files found - skipping"
|
|
3006
3194
|
echo " ⏭️ SKIPPED - Architecture validation (no tests)"
|
|
3195
|
+
# #537: no test files means Java architecture was NOT measured.
|
|
3196
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
3197
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_not_configured"
|
|
3198
|
+
fi
|
|
3007
3199
|
fi
|
|
3008
3200
|
;;
|
|
3009
3201
|
*)
|
|
3010
3202
|
echo " ℹ️ Architecture validation not configured for $CURRENT_LANG"
|
|
3011
3203
|
echo " ⏭️ SKIPPED - Architecture validation (not configured for $CURRENT_LANG)"
|
|
3204
|
+
# #537: archlint ships no detector for this language. Say so in a way
|
|
3205
|
+
# the quality report can read, instead of leaving the dimension silent.
|
|
3206
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
3207
|
+
GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="no_archlint_detector"
|
|
3208
|
+
fi
|
|
3012
3209
|
;;
|
|
3013
3210
|
esac
|
|
3014
3211
|
done # end for CURRENT_LANG (architecture)
|
|
3212
|
+
|
|
3213
|
+
# #537: fold the per-language reasons into the legacy scalar. Homogeneous
|
|
3214
|
+
# reasons survive as-is; a genuine mix becomes the `mixed` sentinel, which
|
|
3215
|
+
# classify_coverage_reason() maps to `unknown` (fail closed) rather than
|
|
3216
|
+
# pretending one language's verdict covers all of them.
|
|
3217
|
+
#
|
|
3218
|
+
# `declare -A` inside a function is FUNCTION-LOCAL in bash, and the report
|
|
3219
|
+
# is written by generate_quality_report() — a different function. So the
|
|
3220
|
+
# assoc array is flattened into this global string, which the report parses.
|
|
3221
|
+
# (Found by running the hook: the matrix reported tool_missing for
|
|
3222
|
+
# typescript because the lookup saw an empty array.)
|
|
3223
|
+
GATE_6_LANG_REASONS=""
|
|
3224
|
+
_g6_seen=""
|
|
3225
|
+
_g6_mixed=0
|
|
3226
|
+
for _g6_lang in $PROJECT_LANGS; do
|
|
3227
|
+
_g6_r=""
|
|
3228
|
+
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
|
|
3229
|
+
_g6_r="${GATE_6_REASON_BY_LANG[$_g6_lang]:-}"
|
|
3230
|
+
fi
|
|
3231
|
+
if [ -n "$_g6_r" ]; then
|
|
3232
|
+
GATE_6_LANG_REASONS="${GATE_6_LANG_REASONS:+$GATE_6_LANG_REASONS,}${_g6_lang}=${_g6_r}"
|
|
3233
|
+
if [ -z "$_g6_seen" ]; then
|
|
3234
|
+
_g6_seen="$_g6_r"
|
|
3235
|
+
elif [ "$_g6_seen" != "$_g6_r" ]; then
|
|
3236
|
+
_g6_mixed=1
|
|
3237
|
+
fi
|
|
3238
|
+
fi
|
|
3239
|
+
done
|
|
3240
|
+
if [ "$_g6_mixed" = "1" ]; then
|
|
3241
|
+
GATE_6_REASON="mixed"
|
|
3242
|
+
else
|
|
3243
|
+
GATE_6_REASON="$_g6_seen"
|
|
3244
|
+
fi
|
|
3015
3245
|
else
|
|
3016
3246
|
# Architecture config is REQUIRED — BLOCK if missing
|
|
3017
3247
|
if [ -f "$PROJECT_ROOT/.architecture-skip" ]; then
|
|
3018
3248
|
echo " ⚠️ Architecture config missing but .architecture-skip present — allowed to skip"
|
|
3019
3249
|
echo " ⏭️ SKIPPED - Architecture validation (.architecture-skip exemption)"
|
|
3250
|
+
# #537: an explicit exemption is "not applicable", not "tool missing".
|
|
3251
|
+
GATE_6_REASON="architecture_skip_exemption"
|
|
3020
3252
|
else
|
|
3021
3253
|
echo ""
|
|
3022
3254
|
echo "❌ ARCHITECTURE CONFIG MISSING - COMMIT BLOCKED"
|
|
@@ -3133,7 +3365,14 @@ else
|
|
|
3133
3365
|
echo " ⏭️ SKIPPED - Boy Scout Rule (not available in project)"
|
|
3134
3366
|
fi
|
|
3135
3367
|
fi
|
|
3136
|
-
|
|
3368
|
+
# A PASS here must mean 'the checks ran and found nothing'. The two
|
|
3369
|
+
# branches above print 'SKIPPED - not configured/available'; stamping PASS
|
|
3370
|
+
# after them turned 'nothing was checked' into a scored success.
|
|
3371
|
+
if [ -n "${GATE_6_STATUS:-}" ]; then
|
|
3372
|
+
: # a branch that really ran already decided the verdict
|
|
3373
|
+
else
|
|
3374
|
+
GATE_6_STATUS="SKIP"
|
|
3375
|
+
fi
|
|
3137
3376
|
record_gate_audit "gate-6" "architecture-boy-scout" "$GATE_6_STATUS" "${BS_BLOCKED:-0}" "$GATE_6_START"
|
|
3138
3377
|
|
|
3139
3378
|
# ============================================================================
|
|
@@ -3490,7 +3729,10 @@ generate_quality_report() {
|
|
|
3490
3729
|
# applicable" and must render and score identically. A previous scoring
|
|
3491
3730
|
# default of FAIL produced self-contradictory reports -- the gate showed
|
|
3492
3731
|
# PASS in JSON but scored as FAIL (#507 Delphi round-1 A-MAJOR-2).
|
|
3493
|
-
|
|
3732
|
+
# Default direction is "did not run", never "passed": an unassigned status
|
|
3733
|
+
# is a gate that never reached a verdict, and scoring it PASS both claims a
|
|
3734
|
+
# success that did not happen and hides the gap in the effective rate.
|
|
3735
|
+
if [ "$gate" -eq 12 ]; then gate_default="WARN"; else gate_default="SKIP"; fi
|
|
3494
3736
|
gate_status="${!status_var:-$gate_default}"
|
|
3495
3737
|
case "$gate_status" in
|
|
3496
3738
|
PASS) PASSED_COUNT=$((PASSED_COUNT + 1)) ;;
|
|
@@ -3563,6 +3805,37 @@ generate_quality_report() {
|
|
|
3563
3805
|
local REPORT_FILE="${STATUS_DIR}/${BRANCH}.json"
|
|
3564
3806
|
mkdir -p "$(dirname "$REPORT_FILE")"
|
|
3565
3807
|
|
|
3808
|
+
# ── #537: per-language coverage matrix ───────────────────────────────────────
|
|
3809
|
+
# Answers "was this dimension actually measured for this language?" instead of
|
|
3810
|
+
# letting every SKIP read as a pass. Built by whitelist splicing (the same
|
|
3811
|
+
# technique as DEGRADED_JSON above) rather than jq, because jq is an OPTIONAL
|
|
3812
|
+
# dependency here (see the `command -v jq` guard earlier in this file) and a
|
|
3813
|
+
# missing jq must not silently cost us the whole matrix.
|
|
3814
|
+
#
|
|
3815
|
+
# Safety: every spliced value is one of the five enum literals below — no
|
|
3816
|
+
# quotes, backslashes or newlines can reach the JSON. Language names are
|
|
3817
|
+
# whitelisted too, and a name that fails the whitelist is reported in
|
|
3818
|
+
# coverageWarnings instead of being silently dropped.
|
|
3819
|
+
local COVERAGE_MATRIX_JSON=""
|
|
3820
|
+
local COVERAGE_WARNINGS_JSON=""
|
|
3821
|
+
local _cm_lang _cm_g3 _cm_g4 _cm_g6 _cm_entry
|
|
3822
|
+
for _cm_lang in $PROJECT_LANGS; do
|
|
3823
|
+
if ! echo "$_cm_lang" | grep -qE '^[a-z][a-z0-9+]*$'; then
|
|
3824
|
+
COVERAGE_WARNINGS_JSON="${COVERAGE_WARNINGS_JSON:+$COVERAGE_WARNINGS_JSON, }\"unsupported_language_name\""
|
|
3825
|
+
continue
|
|
3826
|
+
fi
|
|
3827
|
+
_cm_g3=$(classify_coverage_reason "${GATE_3_REASON:-}" "${GATE_3_STATUS:-PASS}")
|
|
3828
|
+
_cm_g4=$(classify_coverage_reason "${GATE_4_REASON:-}" "${GATE_4_STATUS:-PASS}")
|
|
3829
|
+
# Gate 6 reason for this language — extracted so the rule is testable
|
|
3830
|
+
# rather than inlined (see coverage_g6_reason_for below).
|
|
3831
|
+
_cm_g6_reason=$(coverage_g6_reason_for "$_cm_lang")
|
|
3832
|
+
_cm_g6=$(classify_coverage_reason "$_cm_g6_reason" "${GATE_6_STATUS:-SKIP}")
|
|
3833
|
+
_cm_entry="\"${_cm_lang}\": {\"gate3_complexity\": \"${_cm_g3}\", \"gate4_principles\": \"${_cm_g4}\", \"gate6_architecture\": \"${_cm_g6}\"}"
|
|
3834
|
+
COVERAGE_MATRIX_JSON="${COVERAGE_MATRIX_JSON:+$COVERAGE_MATRIX_JSON, }${_cm_entry}"
|
|
3835
|
+
done
|
|
3836
|
+
local LANGUAGES_SOURCE
|
|
3837
|
+
LANGUAGES_SOURCE=$(resolve_languages_source "${_RESOLVED_LANG_OVERRIDE:-}" "${_CONFIG_LANGS:-}" "${PROJECT_LANGS:-}")
|
|
3838
|
+
|
|
3566
3839
|
cat > "$REPORT_FILE" << ENDJSON
|
|
3567
3840
|
{
|
|
3568
3841
|
"reportVersion": "2.0",
|
|
@@ -3581,6 +3854,9 @@ generate_quality_report() {
|
|
|
3581
3854
|
"score": $SCORE_JSON,
|
|
3582
3855
|
"verdict": "$VERDICT"
|
|
3583
3856
|
},
|
|
3857
|
+
"languagesSource": "${LANGUAGES_SOURCE}",
|
|
3858
|
+
"coverageMatrix": {${COVERAGE_MATRIX_JSON}},
|
|
3859
|
+
"coverageWarnings": [${COVERAGE_WARNINGS_JSON}],
|
|
3584
3860
|
"gates": {
|
|
3585
3861
|
"gate1_static_analysis": {
|
|
3586
3862
|
"name": "Code Quality (Static + Lint + Shell)",
|
|
@@ -3589,7 +3865,7 @@ generate_quality_report() {
|
|
|
3589
3865
|
},
|
|
3590
3866
|
"gate2_dup_code": {
|
|
3591
3867
|
"name": "Duplicate Code",
|
|
3592
|
-
"status": "${GATE_2_STATUS:-
|
|
3868
|
+
"status": "${GATE_2_STATUS:-SKIP}",
|
|
3593
3869
|
"metric": "similarity <= 5%"
|
|
3594
3870
|
},
|
|
3595
3871
|
"gate3_complexity": {
|
|
@@ -3615,7 +3891,8 @@ generate_quality_report() {
|
|
|
3615
3891
|
},
|
|
3616
3892
|
"gate6_arch_boyscout": {
|
|
3617
3893
|
"name": "Architecture + Boy Scout Rule",
|
|
3618
|
-
"status": "${GATE_6_STATUS:-
|
|
3894
|
+
"status": "${GATE_6_STATUS:-SKIP}",
|
|
3895
|
+
"reason": "${GATE_6_REASON:-}",
|
|
3619
3896
|
"boyScoutPassed": $BS_PASSED,
|
|
3620
3897
|
"boyScoutBlocked": $BS_BLOCKED
|
|
3621
3898
|
},
|
|
@@ -3667,11 +3944,11 @@ ENDJSON
|
|
|
3667
3944
|
# ── History append (unchanged — append-only for trend) ────────────────────────────
|
|
3668
3945
|
local GATES_JSON="{"
|
|
3669
3946
|
GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-PASS}\",\"name\":\"Code Quality\"},"
|
|
3670
|
-
GATES_JSON="${GATES_JSON}\"gate2\":{\"status\":\"${GATE_2_STATUS:-
|
|
3947
|
+
GATES_JSON="${GATES_JSON}\"gate2\":{\"status\":\"${GATE_2_STATUS:-SKIP}\",\"name\":\"Duplicate Code\"},"
|
|
3671
3948
|
GATES_JSON="${GATES_JSON}\"gate3\":{\"status\":\"${GATE_3_STATUS:-PASS}\",\"name\":\"Complexity\",\"warnings\":${CC_WARNINGS:-0}},"
|
|
3672
3949
|
GATES_JSON="${GATES_JSON}\"gate4\":{\"status\":\"${GATE_4_STATUS:-PASS}\",\"name\":\"Principles\",\"warnings\":${WARNING_COUNT:-0}},"
|
|
3673
3950
|
GATES_JSON="${GATES_JSON}\"gate5\":{\"status\":\"${GATE_5_STATUS:-PASS}\",\"name\":\"Tests+Coverage\",\"coverage\":\"${COV_PCT}\"},"
|
|
3674
|
-
GATES_JSON="${GATES_JSON}\"gate6\":{\"status\":\"${GATE_6_STATUS:-
|
|
3951
|
+
GATES_JSON="${GATES_JSON}\"gate6\":{\"status\":\"${GATE_6_STATUS:-SKIP}\",\"name\":\"Architecture+BoyScout\",\"bsBlocked\":${BS_BLOCKED}},"
|
|
3675
3952
|
GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-PASS}\",\"name\":\"IaC Security\"},"
|
|
3676
3953
|
GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-PASS}\",\"name\":\"Secret Scanning\"},"
|
|
3677
3954
|
GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"Build Integrity\"},"
|
|
@@ -3688,11 +3965,11 @@ ENDJSON
|
|
|
3688
3965
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
3689
3966
|
echo ""
|
|
3690
3967
|
printf " %-45s %s\n" "Gate 1: Code Quality" "${GATE_1_STATUS:-PASS}"
|
|
3691
|
-
printf " %-45s %s\n" "Gate 2: Duplicate Code" "${GATE_2_STATUS:-
|
|
3968
|
+
printf " %-45s %s\n" "Gate 2: Duplicate Code" "${GATE_2_STATUS:-SKIP}"
|
|
3692
3969
|
printf " %-45s %s (warnings: ${CC_WARNINGS:-0})\n" "Gate 3: Complexity" "${GATE_3_STATUS:-PASS}"
|
|
3693
3970
|
printf " %-45s %s (warnings: ${WARNING_COUNT:-0})\n" "Gate 4: Principles" "${GATE_4_STATUS:-PASS}"
|
|
3694
3971
|
printf " %-45s %s (coverage: ${COV_PCT}%%)\n" "Gate 5: Tests + Coverage" "${GATE_5_STATUS:-PASS}"
|
|
3695
|
-
printf " %-45s %s\n" "Gate 6: Architecture + Boy Scout" "${GATE_6_STATUS:-
|
|
3972
|
+
printf " %-45s %s\n" "Gate 6: Architecture + Boy Scout" "${GATE_6_STATUS:-SKIP}"
|
|
3696
3973
|
printf " %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-PASS}"
|
|
3697
3974
|
printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-PASS}"
|
|
3698
3975
|
printf " %-45s %s\n" "Gate 9: Build Integrity" "${GATE_9_STATUS:-PASS}"
|
package/hooks/pre-push
CHANGED
|
@@ -51,7 +51,7 @@ report = {
|
|
|
51
51
|
'gates': [
|
|
52
52
|
{'id': 'MP', 'name': 'Gate-Script Mirror Parity', 'status': '${GATE_MP_STATUS:-SKIP}', 'details': {'duration_ms': '${MP_DURATION_MS:-0}'}},
|
|
53
53
|
{'id': '10', 'name': 'Build Integrity', 'status': '${GATE_10_STATUS:-SKIP}', 'details': {}},
|
|
54
|
-
{'id': 'M', 'name': 'Mutation Testing', 'status': '${GATE_M_STATUS:-SKIP}', 'details': {}},
|
|
54
|
+
{'id': 'M', 'name': 'Mutation Testing', 'status': '${GATE_M_STATUS:-SKIP}', 'details': {'skip_reason': '${GATE_M_SKIP_REASON:-}', 'duration_ms': '${M_DURATION_MS:-0}', 'mutation_score': '${M_SCORE:-}'}},
|
|
55
55
|
{'id': 'M-Python', 'name': 'Mutation Testing (Python)', 'status': '${GATE_M_PYTHON_STATUS:-SKIP}', 'details': {}},
|
|
56
56
|
{'id': 'M-Go', 'name': 'Mutation Testing (Go)', 'status': '${GATE_M_GO_STATUS:-SKIP}', 'details': {}},
|
|
57
57
|
{'id': 'M-Java', 'name': 'Mutation Testing (Java)', 'status': '${GATE_M_JAVA_STATUS:-SKIP}', 'details': {}},
|
|
@@ -373,16 +373,22 @@ fi
|
|
|
373
373
|
# shellcheck source=githooks/adapter-common.sh
|
|
374
374
|
source "$ADAPTER_COMMON" 2>/dev/null || {
|
|
375
375
|
echo "⚠️ Could not source adapter-common.sh. SKIP — Gate M."
|
|
376
|
+
GATE_M_SKIP_REASON="adapter-common.sh could not be sourced"
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
#
|
|
379
|
+
# #404 P0: every Gate M skip records WHY. Without this the journals carried
|
|
380
|
+
# `details: {}` on 62 of 68 runs, so the RFC's "SKIP rate" question was
|
|
381
|
+
# unanswerable — a missing tool and "no mutable file in this push" are opposite
|
|
382
|
+
# findings that looked identical after the fact.
|
|
379
383
|
if [[ ! -f "package.json" ]] || [[ ! -f "tsconfig.json" ]]; then
|
|
380
384
|
echo "📚 Not a TypeScript project. SKIP — Gate M."
|
|
385
|
+
GATE_M_SKIP_REASON="not a TypeScript project (package.json+tsconfig.json required)"
|
|
381
386
|
else
|
|
382
387
|
# Check if mutation testing is configured
|
|
383
388
|
if ! detect_mutation_testable 2>/dev/null; then
|
|
384
389
|
echo "⚠️ Stryker config not found or @stryker-mutator not installed."
|
|
385
390
|
echo " SKIP — Gate M mutation testing."
|
|
391
|
+
GATE_M_SKIP_REASON="stryker config missing or @stryker-mutator not installed"
|
|
386
392
|
else
|
|
387
393
|
# Filter to source files (exclude tests). The tree-segment grep excludes
|
|
388
394
|
# test infrastructure (helpers/fixtures under tests/, test/, __tests__/)
|
|
@@ -392,6 +398,7 @@ else
|
|
|
392
398
|
|
|
393
399
|
if [ -z "$CHANGED_SOURCE_FILES" ]; then
|
|
394
400
|
echo "📚 No changed TypeScript source files. SKIP — Gate M."
|
|
401
|
+
GATE_M_SKIP_REASON="no changed TypeScript source files in this push"
|
|
395
402
|
else
|
|
396
403
|
echo "🧬 Running mutation tests on changed files..."
|
|
397
404
|
echo "$CHANGED_SOURCE_FILES"
|
|
@@ -408,15 +415,28 @@ else
|
|
|
408
415
|
|
|
409
416
|
if [[ -z "$MUTATION_SCRIPT" ]]; then
|
|
410
417
|
echo "⚠️ Gate M script not found. SKIP — Gate M."
|
|
418
|
+
GATE_M_SKIP_REASON="gate-m.ts not found in .xp-gate/modules, src/mutation, or global modules"
|
|
411
419
|
else
|
|
412
420
|
# Run mutation gate with timeout matching stryker.conf.json (timeoutMS: 600000 = 10min)
|
|
413
421
|
MUTATION_OUTPUT=$(mktemp)
|
|
422
|
+
M_START_MS=$(date +%s%3N 2>/dev/null || python -c 'import time;print(int(time.time()*1000))' 2>/dev/null || echo 0)
|
|
414
423
|
RUN_TSX_TIMEOUT=600s run_tsx $MUTATION_SCRIPT --changed-files "$CHANGED_SOURCE_FILES" > "$MUTATION_OUTPUT" 2>&1
|
|
415
424
|
MUTATION_EXIT=$?
|
|
425
|
+
M_END_MS=$(date +%s%3N 2>/dev/null || python -c 'import time;print(int(time.time()*1000))' 2>/dev/null || echo 0)
|
|
426
|
+
if [ "$M_START_MS" != "0" ] && [ "$M_END_MS" != "0" ]; then
|
|
427
|
+
M_DURATION_MS=$((M_END_MS - M_START_MS))
|
|
428
|
+
[ "$M_DURATION_MS" -ge 0 ] || M_DURATION_MS=0
|
|
429
|
+
else
|
|
430
|
+
M_DURATION_MS=0
|
|
431
|
+
fi
|
|
432
|
+
# #404: the mutation score is the number the RFC wants a distribution for.
|
|
433
|
+
# It is printed by gate-m.ts; take the last percentage it reported.
|
|
434
|
+
M_SCORE=$(sed -n 's/.*[Mm]utation score[: ]*\([0-9][0-9]*\(\.[0-9][0-9]*\)\?\)%.*/\1/p' "$MUTATION_OUTPUT" 2>/dev/null | tail -1)
|
|
416
435
|
|
|
417
436
|
case $MUTATION_EXIT in
|
|
418
437
|
0)
|
|
419
438
|
echo "✅ Gate M: PASS"
|
|
439
|
+
GATE_M_STATUS="PASS"
|
|
420
440
|
# Update baseline after successful mutation test
|
|
421
441
|
if [ -f ".stryker-baseline.json" ]; then
|
|
422
442
|
echo "📝 Updating mutation baseline..."
|
|
@@ -437,11 +457,15 @@ else
|
|
|
437
457
|
124)
|
|
438
458
|
echo "⏱️ Gate M: TIMEOUT (120s). Mutation testing incomplete."
|
|
439
459
|
echo " Allowing push with warning — review mutation coverage manually."
|
|
460
|
+
GATE_M_STATUS="TIMEOUT"
|
|
461
|
+
GATE_M_SKIP_REASON="mutation run exceeded the 600s timeout; score is unknown"
|
|
440
462
|
;;
|
|
441
463
|
*)
|
|
442
464
|
echo "⚠️ Gate M: Unexpected exit code $MUTATION_EXIT"
|
|
443
465
|
cat "$MUTATION_OUTPUT"
|
|
444
466
|
echo " Allowing push with warning."
|
|
467
|
+
GATE_M_STATUS="WARN"
|
|
468
|
+
GATE_M_SKIP_REASON="gate-m.ts exited $MUTATION_EXIT (unexpected); score is unknown"
|
|
445
469
|
;;
|
|
446
470
|
esac
|
|
447
471
|
|