@boyingliu01/xp-gate 0.27.6 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapter-common.sh +0 -0
- package/adapters/cpp.sh +0 -0
- package/adapters/dart.sh +0 -0
- package/adapters/flutter.sh +0 -0
- package/adapters/gate-9.sh +13 -2
- package/adapters/go.sh +0 -0
- package/adapters/iac.sh +0 -0
- package/adapters/java.sh +0 -0
- package/adapters/kotlin.sh +0 -0
- package/adapters/objectivec.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
- package/adapters/powershell.sh +0 -0
- package/adapters/python.sh +0 -0
- package/adapters/shell.sh +0 -0
- package/adapters/swift.sh +0 -0
- package/adapters/typescript.sh +0 -0
- package/bin/xp-gate.js +0 -0
- package/build-integrity/gate-10.ts +290 -263
- package/gate-10.sh +25 -6
- package/gate-12-file-hygiene.sh +35 -1
- package/gate-3.sh +78 -2
- package/gate-4.sh +0 -0
- package/gate-7.sh +0 -0
- package/gate-8.sh +0 -0
- package/gate-9.sh +13 -2
- package/hooks/adapter-common.sh +0 -0
- package/hooks/gate-10.sh +25 -6
- package/hooks/gate-12-file-hygiene.sh +35 -1
- package/hooks/gate-3.sh +78 -2
- package/hooks/gate-4.sh +0 -0
- package/hooks/gate-7.sh +0 -0
- package/hooks/gate-8.sh +0 -0
- package/hooks/gate-9.sh +13 -2
- package/hooks/lib/audit-sanitize.sh +51 -0
- package/hooks/lib/ccn-ratchet.sh +183 -0
- package/hooks/lib/docs-only.sh +49 -0
- package/hooks/post-merge +0 -0
- package/hooks/pre-commit +74 -15
- package/hooks/pre-push +36 -5
- package/hooks/sprint-gate.sh +0 -0
- package/lib/__tests__/sprint-audit.test.js +207 -207
- package/lib/gate-audit.ts +9 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/retro.js +27 -6
- package/lib/sprint-audit.js +235 -235
- 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/bin/delphi-review-guard.sh +0 -0
- package/plugins/claude-code/bin/xp-gate-check +0 -0
- 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/package.json +1 -1
- 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/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/bin/sprint-flow-guard.sh +0 -0
- package/plugins/qoder/bin/xp-gate-check +0 -0
- 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__/many-exports-reexport.test.ts +40 -0
- package/principles/adapters/typescript.ts +61 -30
- package/principles/boy-scout.ts +22 -17
- package/scripts/delphi-external-review.cjs +195 -132
- 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
- package/sprint-gate.sh +0 -0
- package/plugins/dsh/lib/command.d.ts +0 -32
- package/plugins/dsh/lib/command.d.ts.map +0 -1
- package/plugins/dsh/lib/command.js +0 -81
- package/plugins/dsh/lib/delphi-config.d.ts +0 -75
- package/plugins/dsh/lib/delphi-config.d.ts.map +0 -1
- package/plugins/dsh/lib/delphi-config.js +0 -183
- package/plugins/dsh/lib/delphi-run.d.ts +0 -24
- package/plugins/dsh/lib/delphi-run.d.ts.map +0 -1
- package/plugins/dsh/lib/delphi-run.js +0 -92
- package/plugins/dsh/lib/delphi.d.ts +0 -88
- package/plugins/dsh/lib/delphi.d.ts.map +0 -1
- package/plugins/dsh/lib/delphi.js +0 -161
- package/plugins/dsh/lib/dialect.d.ts +0 -24
- package/plugins/dsh/lib/dialect.d.ts.map +0 -1
- package/plugins/dsh/lib/dialect.js +0 -36
- package/plugins/dsh/lib/gate-runner.d.ts +0 -18
- package/plugins/dsh/lib/gate-runner.d.ts.map +0 -1
- package/plugins/dsh/lib/gate-runner.js +0 -46
- package/plugins/dsh/lib/index.d.ts +0 -5
- package/plugins/dsh/lib/index.d.ts.map +0 -1
- package/plugins/dsh/lib/index.js +0 -162
- package/plugins/dsh/node_modules/.vite/vitest/results.json +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# ccn-ratchet.sh — Gate 3 baseline ratchet (sprint-20261018-quality REQ-D)
|
|
4
|
+
#
|
|
5
|
+
# The repo carries years of complex-but-alive functions. Blocking them today
|
|
6
|
+
# would freeze the codebase; ignoring them lets complexity grow silently. The
|
|
7
|
+
# ratchet splits the difference: .ccn-baseline.json records the CCN each
|
|
8
|
+
# known function has TODAY, and the gate only fires when a recorded function
|
|
9
|
+
# gets WORSE (strictly greater — equal passes). Functions with no entry are
|
|
10
|
+
# new code and face the live warn/block thresholds unchanged. When a function
|
|
11
|
+
# improves we say so and suggest refreshing the baseline, because a stale
|
|
12
|
+
# ceiling invites nobody to finish the job.
|
|
13
|
+
#
|
|
14
|
+
# Key = "<relative/path>::<functionName>". Rename or move = new key = treated
|
|
15
|
+
# as a new function. That is deliberate: fuzzy matching would hide debt moves.
|
|
16
|
+
# `node scripts/ccn-report.cjs --write-baseline` regenerates the file after a
|
|
17
|
+
# legitimate refactor.
|
|
18
|
+
#
|
|
19
|
+
# evaluate_ccn_ratchet <baseline-json> reads lizard --csv rows on stdin and
|
|
20
|
+
# prints "OK|WARN|BLOCK|LOWER <fn> ..." verdict lines for every function row.
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
# Convert lizard CSV rows to unit-separator-delimited fields.
|
|
24
|
+
#
|
|
25
|
+
# The CSV is only ever parsed once, here, with the quote rules written out in
|
|
26
|
+
# one place; downstream code then reads positional fields with no quoting to
|
|
27
|
+
# worry about. gawk arrays are pass-by-value, so a helper function CANNOT fill
|
|
28
|
+
# a caller-visible array, and `split($0, F, ",")` tears apart lizard's quoted
|
|
29
|
+
# signature column (signatures contain commas). Both of those were live bugs:
|
|
30
|
+
# every lookup fell through to the "new function" branch, so recorded
|
|
31
|
+
# functions were graded against the flat threshold instead of their baseline.
|
|
32
|
+
#
|
|
33
|
+
# Output columns (this is real lizard 1.21.3 --csv output, 11 columns):
|
|
34
|
+
# 1 NLOC, 2 CCN, 3 token_len, 4 param_count, 5 48 (location size),
|
|
35
|
+
# 6 fn@start-end@path, 7 path, 8 name, 9 signature, 10 start, 11 end
|
|
36
|
+
#
|
|
37
|
+
# The separator is ASCII Unit Separator written via sprintf so the byte never
|
|
38
|
+
# has to survive a shell/awk escape round trip: -F'\001' is NOT a reliable way
|
|
39
|
+
# to name a control character (gawk treats the argument as a regex, and the
|
|
40
|
+
# literal backslash path silently yields a separator that matches nothing, so
|
|
41
|
+
# every row is read as a single field).
|
|
42
|
+
CSV_FS=$'\x1f'
|
|
43
|
+
|
|
44
|
+
csv_to_unit_separated() {
|
|
45
|
+
awk '
|
|
46
|
+
function emit(line, cur, i, ch, q, nf) {
|
|
47
|
+
nf = 1; cur = ""; q = 0
|
|
48
|
+
for (i = 1; i <= length(line); i++) {
|
|
49
|
+
ch = substr(line, i, 1)
|
|
50
|
+
if (q) {
|
|
51
|
+
if (ch == "\"") { if (substr(line, i + 1, 1) == "\"") { cur = cur "\""; i++ } else q = 0 }
|
|
52
|
+
else cur = cur ch
|
|
53
|
+
} else if (ch == "\"") q = 1
|
|
54
|
+
else if (ch == ",") { f[nf++] = cur; cur = "" }
|
|
55
|
+
else cur = cur ch
|
|
56
|
+
}
|
|
57
|
+
f[nf] = cur
|
|
58
|
+
out = f[1]
|
|
59
|
+
for (i = 2; i <= nf; i++) out = out FS_OUT f[i]
|
|
60
|
+
print out
|
|
61
|
+
}
|
|
62
|
+
# Build the 0x1F byte from its numeric code: shell escapes into awk strings
|
|
63
|
+
# are fragile across gawk/mawk/Windows builds, but printf to an awk variable
|
|
64
|
+
# is unambiguous.
|
|
65
|
+
BEGIN { FS_OUT = sprintf("%c", 0x1F) }
|
|
66
|
+
{ emit($0) }
|
|
67
|
+
'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
evaluate_ccn_ratchet() {
|
|
71
|
+
local baseline_file="$1"
|
|
72
|
+
local warn_threshold="${CCN_WARN_THRESHOLD:-5}"
|
|
73
|
+
local block_threshold="${CCN_BLOCK_THRESHOLD:-10}"
|
|
74
|
+
|
|
75
|
+
# Capture stdin up front: it is consumed twice (baseline rows and verdicts).
|
|
76
|
+
local awk_input
|
|
77
|
+
awk_input=$(cat)
|
|
78
|
+
|
|
79
|
+
local baseline_json=""
|
|
80
|
+
if [ -f "$baseline_file" ]; then
|
|
81
|
+
baseline_json=$(cat "$baseline_file")
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Flatten {"path::fn": 12, ...} to "path::fn=12" lines without spawning a
|
|
85
|
+
# JSON parser per commit (every spawn is expensive on Windows AV filters).
|
|
86
|
+
local flat
|
|
87
|
+
flat=$(printf '%s' "$baseline_json" \
|
|
88
|
+
| tr -d '\n' \
|
|
89
|
+
| sed 's/^[^{]*{//; s/}[^}]*$//' \
|
|
90
|
+
| grep -oE '"[^"]+"[[:space:]]*:[[:space:]]*[0-9]+' \
|
|
91
|
+
| sed 's/[[:space:]]//g; s/":/=/; s/"//g' || true)
|
|
92
|
+
|
|
93
|
+
# An empty or unparseable baseline must not read as "everything is new code,
|
|
94
|
+
# nothing is protected". `{}` and a truncated file both flatten to zero lines,
|
|
95
|
+
# and the grader then silently degrades to flat thresholds for a repository
|
|
96
|
+
# that explicitly opted into the ratchet. Say so on stderr (verdict lines stay
|
|
97
|
+
# clean for the caller) rather than passing quietly — the caller decides
|
|
98
|
+
# whether an empty baseline is fatal, but it must be able to tell.
|
|
99
|
+
if [ -z "$flat" ]; then
|
|
100
|
+
echo "WARN - complexity baseline '${baseline_file}' is empty or unparseable; grading against flat thresholds only (no function is protected by the ratchet)" >&2
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Convert the CSV rows once into position-only fields, then grade them. The
|
|
104
|
+
# CSV quote rules live in csv_to_unit_separated and nowhere else; see the
|
|
105
|
+
# comment there for why parsing in-place inside awk does not work.
|
|
106
|
+
# Columns: $1 NLOC, $2 CCN, $3 token_len, $4 param_count, $5 location size,
|
|
107
|
+
# $6 fn@start-end@path, $7 path, $8 name, $9 signature,
|
|
108
|
+
# $10 start, $11 end
|
|
109
|
+
local csv_rows
|
|
110
|
+
csv_rows=$(printf '%s\n' "$awk_input" | csv_to_unit_separated || true)
|
|
111
|
+
|
|
112
|
+
# Only function rows survive the conversion with a numeric CCN, which is what
|
|
113
|
+
# the $2 guard below keys on.
|
|
114
|
+
#
|
|
115
|
+
# Duplicate-name tally: baselineKey() in scripts/ccn-report.cjs keys a name by
|
|
116
|
+
# line when it occurs more than once in a file, because two rows can otherwise
|
|
117
|
+
# share one entry and each regresses against the other. The rule is mirrored
|
|
118
|
+
# here so the hook and CI cannot disagree; the count is computed in shell
|
|
119
|
+
# because awk cannot see the whole file before grading its first row.
|
|
120
|
+
local dup_names
|
|
121
|
+
dup_names=$(printf '%s\n' "$csv_rows" | awk -F"$CSV_FS" '
|
|
122
|
+
$2 ~ /^[0-9]+$/ {
|
|
123
|
+
n = split($6, p, "@")
|
|
124
|
+
name = p[1]
|
|
125
|
+
path = (n >= 3) ? p[n] : $7
|
|
126
|
+
gsub(/\\/, "/", path)
|
|
127
|
+
count[path "::" name]++
|
|
128
|
+
}
|
|
129
|
+
END {
|
|
130
|
+
for (k in count) if (count[k] > 1) print k
|
|
131
|
+
}
|
|
132
|
+
' || true)
|
|
133
|
+
|
|
134
|
+
printf '%s\n' "$csv_rows" | awk -F"$CSV_FS" -v flat="$flat" -v warn="$warn_threshold" -v block="$block_threshold" -v dups="$dup_names" -v anon_marker="(anonymous)" -v anon_short="(anon)" '
|
|
135
|
+
BEGIN {
|
|
136
|
+
n = split(flat, kv, "\n")
|
|
137
|
+
for (i = 1; i <= n; i++) {
|
|
138
|
+
eq = index(kv[i], "=")
|
|
139
|
+
if (eq > 0) base[substr(kv[i], 1, eq - 1)] = substr(kv[i], eq + 1) + 0
|
|
140
|
+
}
|
|
141
|
+
m = split(dups, duprow, "\n")
|
|
142
|
+
for (i = 1; i <= m; i++) if (length(duprow[i]) > 0) dup[duprow[i]] = 1
|
|
143
|
+
}
|
|
144
|
+
# FIELD INDEXING: $1 NLOC, $2 CCN, $3 token_len, $4 param_count,
|
|
145
|
+
# $5 location size, $6 fn@start-end@path, $7 path, $8 name,
|
|
146
|
+
# $9 signature, $10 start, $11 end.
|
|
147
|
+
# Those two lines are the entire column contract. Earlier revisions read
|
|
148
|
+
# CCN out of the identity column and name/path out of a shifted column, so
|
|
149
|
+
# nothing ever matched its baseline entry and every recorded function was
|
|
150
|
+
# graded as new code against the flat thresholds.
|
|
151
|
+
# NOTE: this awk program is single-quoted in shell. The two hazards that
|
|
152
|
+
# silently broke earlier revisions: an apostrophe anywhere below, and a
|
|
153
|
+
# bare double-quote pair that the shell re-pairs across the awk text. The
|
|
154
|
+
# anonymous marker is therefore passed in as a -v value, not written inline.
|
|
155
|
+
$2 ~ /^[0-9]+$/ {
|
|
156
|
+
ccn = $2 + 0
|
|
157
|
+
n2 = split($6, parts, "@")
|
|
158
|
+
name = parts[1]
|
|
159
|
+
path = (n2 >= 3) ? parts[n2] : $7
|
|
160
|
+
gsub(/\\/, "/", path)
|
|
161
|
+
# Anonymous callbacks are keyed by position: a file holds many of them and
|
|
162
|
+
# the baseline would otherwise keep only the last one parsed, so every
|
|
163
|
+
# other callback was graded against the number of a different function.
|
|
164
|
+
is_anon = 0
|
|
165
|
+
if (name == anon_marker) is_anon = 1
|
|
166
|
+
if (name == anon_short) is_anon = 1
|
|
167
|
+
start = (n2 >= 3) ? parts[2] : $10
|
|
168
|
+
sub(/-.*/, "", start)
|
|
169
|
+
key = path "::" name
|
|
170
|
+
if (is_anon) key = path "::" anon_marker "@" start
|
|
171
|
+
else if ((path "::" name) in dup) key = path "::" name "@" start
|
|
172
|
+
if (key in base) {
|
|
173
|
+
if (ccn > base[key]) printf "BLOCK %s CCN=%d baseline=%d (ratchet: worse than recorded)\n", name, ccn, base[key]
|
|
174
|
+
else if (ccn < base[key]) printf "LOWER %s CCN=%d baseline=%d (improved — refresh baseline when convenient)\n", name, ccn, base[key]
|
|
175
|
+
else printf "OK %s CCN=%d at baseline\n", name, ccn
|
|
176
|
+
} else {
|
|
177
|
+
if (ccn > block) printf "BLOCK %s CCN=%d (%s exceeds block threshold)\n", name, ccn, path
|
|
178
|
+
else if (ccn > warn) printf "WARN %s CCN=%d (%s new/over warn threshold)\n", name, ccn, path
|
|
179
|
+
else printf "OK %s CCN=%d\n", name, ccn
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
'
|
|
183
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# docs-only.sh — commit-level documentation-only detection (REQ-F2)
|
|
4
|
+
#
|
|
5
|
+
# The pre-commit hook already knows about PROJECT-level documentation repos
|
|
6
|
+
# (PROJECT_LANG=documentation-only: the repo has no code files at all). This
|
|
7
|
+
# is the COMMIT-level sibling: xp-gate's own docs churn — typo fixes, changelog
|
|
8
|
+
# entries, plan docs — used to pay the full ~46s gate bill even though tests,
|
|
9
|
+
# jscpd, tsc and file hygiene cannot observe a markdown edit. When every
|
|
10
|
+
# staged file is documentation, those heavy gates SKIP with reason
|
|
11
|
+
# docs_only_change; light gates (secrets, version, principles) still run.
|
|
12
|
+
#
|
|
13
|
+
# Fail-closed by construction: an empty list or any single non-matching path
|
|
14
|
+
# prints "false" and the gates run as before.
|
|
15
|
+
#
|
|
16
|
+
# detect_docs_only_commit reads newline-separated staged paths on stdin and
|
|
17
|
+
# prints true|false.
|
|
18
|
+
# ============================================================================
|
|
19
|
+
|
|
20
|
+
detect_docs_only_commit() {
|
|
21
|
+
local f ext saw=0
|
|
22
|
+
while IFS= read -r f || [ -n "$f" ]; do
|
|
23
|
+
[ -n "$f" ] || continue
|
|
24
|
+
saw=1
|
|
25
|
+
# Any recognizable source extension is code, wherever it lives — a .ts
|
|
26
|
+
# under docs/ (sample snippets, doc tooling) still deserves its gates.
|
|
27
|
+
ext="${f##*.}"
|
|
28
|
+
case "$ext" in
|
|
29
|
+
ts|tsx|js|jsx|mjs|cjs|py|go|java|kt|kts|swift|cpp|cc|c|h|hpp|m|mm|rs|rb|php|cs|dart|sh|bash|ps1|psm1)
|
|
30
|
+
printf 'false'
|
|
31
|
+
return 0
|
|
32
|
+
;;
|
|
33
|
+
esac
|
|
34
|
+
case "$f" in
|
|
35
|
+
*.md|*.mdx|*.rst|*.txt|*.adoc) continue ;;
|
|
36
|
+
docs/*|docs-*/**|.github/ISSUE_TEMPLATE/**) continue ;;
|
|
37
|
+
*)
|
|
38
|
+
printf 'false'
|
|
39
|
+
return 0
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
if [ "$saw" = 1 ]; then
|
|
44
|
+
printf 'true'
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
printf 'false'
|
|
48
|
+
return 0
|
|
49
|
+
}
|
package/hooks/post-merge
CHANGED
|
File without changes
|
package/hooks/pre-commit
CHANGED
|
@@ -243,6 +243,12 @@ source_gate_lib() {
|
|
|
243
243
|
|
|
244
244
|
# Issue #370: source cross-platform now_ms() helper (node-preferred, regex-validated)
|
|
245
245
|
source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/now-ms.sh"
|
|
246
|
+
# REQ-B: duration bookkeeping sanitizer used by record_gate_audit below.
|
|
247
|
+
source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/audit-sanitize.sh"
|
|
248
|
+
# REQ-F2 (sprint-20261018-quality): commit-level docs-only detection used to
|
|
249
|
+
# short-circuit the heavy, code-blind gates below. Distinct from the
|
|
250
|
+
# PROJECT-level documentation_project detection further down.
|
|
251
|
+
source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/docs-only.sh"
|
|
246
252
|
|
|
247
253
|
# Issue #473 (+ #454): Gate 5's vitest exit-code judgement, sourced as a library
|
|
248
254
|
# so the fail-closed contract is unit-testable without executing this hook.
|
|
@@ -293,16 +299,33 @@ record_gate_audit() {
|
|
|
293
299
|
local detail="${6:-}"
|
|
294
300
|
local end_ms
|
|
295
301
|
end_ms=$(now_ms)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
302
|
+
# REQ-B: sanitize BEFORE subtracting. A poisoned start (empty -> arithmetic
|
|
303
|
+
# 0, or a seconds-unit value from an old code path) used to survive as an
|
|
304
|
+
# epoch-sized duration_ms that only the anomaly flag papered over. Now the
|
|
305
|
+
# record carries null + reason so every consumer can exclude it honestly.
|
|
306
|
+
local clean_start duration_reason=""
|
|
307
|
+
clean_start=$(sanitize_start "$start_ms")
|
|
308
|
+
local duration_ms
|
|
309
|
+
if [ "$clean_start" = "null" ]; then
|
|
310
|
+
duration_ms="null"
|
|
311
|
+
duration_reason="poisoned_start_ms"
|
|
312
|
+
else
|
|
313
|
+
duration_ms=$((end_ms - clean_start))
|
|
314
|
+
if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
|
|
315
|
+
duration_ms="null"
|
|
316
|
+
duration_reason="clock_skew"
|
|
317
|
+
fi
|
|
299
318
|
fi
|
|
300
319
|
|
|
301
320
|
# Issue #370: detect anomalous duration (poisoned timestamp arithmetic)
|
|
302
321
|
local duration_anomaly_flag=""
|
|
303
|
-
if [ "$duration_ms" -gt "$_MAX_DURATION_MS" ] 2>/dev/null; then
|
|
322
|
+
if [ "$duration_ms" != "null" ] && [ "$duration_ms" -gt "$_MAX_DURATION_MS" ] 2>/dev/null; then
|
|
304
323
|
duration_anomaly_flag="--duration-anomaly true"
|
|
305
324
|
fi
|
|
325
|
+
local duration_reason_flag=""
|
|
326
|
+
if [ -n "$duration_reason" ]; then
|
|
327
|
+
duration_reason_flag="--duration-reason $duration_reason"
|
|
328
|
+
fi
|
|
306
329
|
|
|
307
330
|
# Determine gate result: PASS or FAIL based on status variable
|
|
308
331
|
local gate_result="true"
|
|
@@ -312,13 +335,14 @@ record_gate_audit() {
|
|
|
312
335
|
|
|
313
336
|
# Fire-and-forget: audit must never block the commit
|
|
314
337
|
{
|
|
315
|
-
|
|
338
|
+
run_tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
|
|
316
339
|
--gate-id "$gate_id" \
|
|
317
340
|
--gate-name "$gate_name" \
|
|
318
341
|
--passed "$gate_result" \
|
|
319
342
|
--issues-found "$issues_found" \
|
|
320
343
|
--duration-ms "$duration_ms" \
|
|
321
344
|
$duration_anomaly_flag \
|
|
345
|
+
$duration_reason_flag \
|
|
322
346
|
--trigger commit \
|
|
323
347
|
${detail:+--detail "$detail"} \
|
|
324
348
|
2>/dev/null
|
|
@@ -538,6 +562,14 @@ DASHBOARD_PATH_FILTER='^dashboard/'
|
|
|
538
562
|
RAW_CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
539
563
|
CHANGED_FILES=$(printf '%s\n' "$RAW_CHANGED_FILES" | grep -v "$MIRROR_PATH_FILTER" | grep -v "$DASHBOARD_PATH_FILTER" || true)
|
|
540
564
|
|
|
565
|
+
# REQ-F2: one commit-level docs-only verdict, computed once and reused by the
|
|
566
|
+
# heavy gates (2/5/9/12). Light gates never consult it — a secret in a README
|
|
567
|
+
# is still a secret.
|
|
568
|
+
DOCS_ONLY_COMMIT=false
|
|
569
|
+
if printf '%s\n' "$CHANGED_FILES" | detect_docs_only_commit | grep -q '^true$'; then
|
|
570
|
+
DOCS_ONLY_COMMIT=true
|
|
571
|
+
fi
|
|
572
|
+
|
|
541
573
|
if [ -z "$RAW_CHANGED_FILES" ]; then
|
|
542
574
|
echo "No files changed. Skipping gates."
|
|
543
575
|
exit 0
|
|
@@ -1031,14 +1063,24 @@ fi
|
|
|
1031
1063
|
# ============================================================================
|
|
1032
1064
|
2>&1 echo ""
|
|
1033
1065
|
2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
|
|
1066
|
+
# REQ-B: the start stamp is taken OUTSIDE the branches below. It used to be
|
|
1067
|
+
# assigned only on the lint path, so every skipped Gate 1 recorded an empty
|
|
1068
|
+
# GATE_1_START -> duration_ms=null with reason=poisoned_start_ms. That null is
|
|
1069
|
+
# honest but it answers the wrong question: those commits never ran the gate, so
|
|
1070
|
+
# the log should say SKIP, not "measurement failed".
|
|
1071
|
+
GATE_1_START=$(gate_start_ms)
|
|
1072
|
+
GATE_1_STATUS="PASS"
|
|
1073
|
+
GATE_1_DETAIL=""
|
|
1034
1074
|
if ! any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|mm|dart|sh)$" "\.ya?ml$" "\.json$" "Dockerfile" "\.tf$"; then
|
|
1035
1075
|
echo "✅ PASSED - No code files changed, skipping lint."
|
|
1076
|
+
GATE_1_STATUS="SKIP"
|
|
1077
|
+
GATE_1_DETAIL="no_matching_files"
|
|
1036
1078
|
else
|
|
1037
|
-
GATE_1_START=$(gate_start_ms)
|
|
1038
|
-
|
|
1039
1079
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
1040
1080
|
# Documentation-only project - skip static analysis
|
|
1041
1081
|
echo "⏭️ SKIPPED - Code quality (documentation project)."
|
|
1082
|
+
GATE_1_STATUS="SKIP"
|
|
1083
|
+
GATE_1_DETAIL="documentation_project"
|
|
1042
1084
|
|
|
1043
1085
|
else
|
|
1044
1086
|
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
@@ -1596,7 +1638,6 @@ GATE_1_TOOL="${PROJECT_LANGS}"
|
|
|
1596
1638
|
# trail via the S2 detail plumbing so a WARN verdict is diagnosable, not
|
|
1597
1639
|
# silent. WARN stays IN the scoring denominator (Delphi round-2 A-MAJOR-1).
|
|
1598
1640
|
record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START" "${GATE_1_DETAIL:-}"
|
|
1599
|
-
|
|
1600
1641
|
# ============================================================================
|
|
1601
1642
|
# GATE 2: Duplicate Code Detection (NEW gate)
|
|
1602
1643
|
# Uses jscpd, or similar tools, specific to language
|
|
@@ -1606,8 +1647,14 @@ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START" "
|
|
|
1606
1647
|
GATE_2_START=$(gate_start_ms)
|
|
1607
1648
|
GATE_2_STATUS="PASS"
|
|
1608
1649
|
|
|
1609
|
-
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
1610
|
-
echo "⏭️ SKIPPED - Duplicate code (documentation
|
|
1650
|
+
if [ "$PROJECT_LANG" = "documentation-only" ] || [ "$DOCS_ONLY_COMMIT" = true ]; then
|
|
1651
|
+
echo "⏭️ SKIPPED - Duplicate code (documentation-only ${DOCS_ONLY_COMMIT:+commit})."
|
|
1652
|
+
GATE_2_STATUS="SKIP"
|
|
1653
|
+
# GATE_2_DETAIL feeds record_gate_audit's detail field. The previous
|
|
1654
|
+
# GATE_2_REASON was assigned and never read by anything, which is worse than
|
|
1655
|
+
# useless: it looks like the skip reason reaches the audit journal when it does
|
|
1656
|
+
# not (no set -u, so nothing complained).
|
|
1657
|
+
GATE_2_DETAIL="docs_only_change"
|
|
1611
1658
|
|
|
1612
1659
|
else
|
|
1613
1660
|
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
@@ -1715,7 +1762,7 @@ else
|
|
|
1715
1762
|
fi
|
|
1716
1763
|
# Set at the top of this gate; a branch may already have marked it SKIP (#477). The
|
|
1717
1764
|
# audit must record what happened, not what the gate defaults to.
|
|
1718
|
-
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
|
|
1765
|
+
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START" "${GATE_2_DETAIL:-}"
|
|
1719
1766
|
|
|
1720
1767
|
# ============================================================================
|
|
1721
1768
|
# GATE 3: Cyclomatic Complexity Check
|
|
@@ -1744,8 +1791,10 @@ source "$GATE_DIR/gate-4.sh"
|
|
|
1744
1791
|
2>&1 echo "→ Gate 5: Tests & coverage..."
|
|
1745
1792
|
GATE_5_START=$(gate_start_ms)
|
|
1746
1793
|
|
|
1747
|
-
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
1748
|
-
echo "
|
|
1794
|
+
if [ "$PROJECT_LANG" = "documentation-only" ] || [ "$DOCS_ONLY_COMMIT" = true ]; then
|
|
1795
|
+
echo "⏭️ SKIPPED - Tests & coverage (documentation-only commit — no code changed to test)."
|
|
1796
|
+
GATE_5_STATUS="SKIP"
|
|
1797
|
+
GATE_5_DETAIL="docs_only_change"
|
|
1749
1798
|
|
|
1750
1799
|
else
|
|
1751
1800
|
# ========================================================================
|
|
@@ -2822,7 +2871,7 @@ else
|
|
|
2822
2871
|
fi
|
|
2823
2872
|
fi
|
|
2824
2873
|
GATE_5_STATUS="PASS"
|
|
2825
|
-
record_gate_audit "gate-5" "tests-coverage" "$GATE_5_STATUS" "0" "$GATE_5_START"
|
|
2874
|
+
record_gate_audit "gate-5" "tests-coverage" "$GATE_5_STATUS" "0" "$GATE_5_START" "${GATE_5_DETAIL:-}"
|
|
2826
2875
|
|
|
2827
2876
|
# ============================================================================
|
|
2828
2877
|
# GATE 6: Architecture & Tech Debt (Combines Old Gate 8 - Boy Scout and Gate 9 - Architecture)
|
|
@@ -3183,7 +3232,17 @@ GATE_9_TS_STATUS="SKIP"
|
|
|
3183
3232
|
GATE_9_JAVA_STATUS="SKIP"
|
|
3184
3233
|
GATE_9_STATUS="SKIP"
|
|
3185
3234
|
|
|
3186
|
-
if [
|
|
3235
|
+
if [ "$DOCS_ONLY_COMMIT" = true ]; then
|
|
3236
|
+
# REQ-F2: tsc cannot observe a markdown edit. The commit-level docs-only
|
|
3237
|
+
# verdict was already consulted by Gates 2 and 5 but not here, so a pure docs
|
|
3238
|
+
# commit still paid ~11s for a compile that had nothing new to compile.
|
|
3239
|
+
# Recorded as SKIP (not PASS): nothing was verified.
|
|
3240
|
+
GATE_9_TS_STATUS="SKIP"
|
|
3241
|
+
GATE_9_JAVA_STATUS="SKIP"
|
|
3242
|
+
echo " ⏭️ SKIPPED - Build integrity (docs-only commit)."
|
|
3243
|
+
elif [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
3244
|
+
echo " ⏭️ SKIPPED - Build integrity (documentation project)."
|
|
3245
|
+
elif [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
|
|
3187
3246
|
GATE_9_SCRIPT=""
|
|
3188
3247
|
if [ -f "$PROJECT_ROOT/src/build-integrity/gate-10.ts" ]; then
|
|
3189
3248
|
GATE_9_SCRIPT="$PROJECT_ROOT/src/build-integrity/gate-10.ts"
|
package/hooks/pre-push
CHANGED
|
@@ -153,8 +153,15 @@ fi
|
|
|
153
153
|
DOC_ONLY=true
|
|
154
154
|
SOURCE_EXTENSIONS="\.py$|\.js$|\.cjs$|\.mjs$|\.ts$|\.tsx$|\.java$|\.go$|\.rs$|\.cpp$|\.c$|\.swift$|\.kt$|\.sh$|\.dart$|\.ps1$|\.m$|\.mm$|\.h$|\.hpp$"
|
|
155
155
|
|
|
156
|
+
# Extensionless hook scripts and test files are CODE, not documentation.
|
|
157
|
+
# Measured consequence of the old list: a commit touching githooks/pre-push and
|
|
158
|
+
# its .bats suite was classified "Documentation-only push (no source code
|
|
159
|
+
# files)" and the walkthrough was skipped — the single most safety-critical
|
|
160
|
+
# script in the repo, exempt from review by a filename-shape accident.
|
|
161
|
+
NON_EXT_SOURCE_RE='^(githooks/(pre-commit|pre-push|post-merge|install\.sh|verify\.sh)|[^/]+\.bats)$|/__tests__/'
|
|
162
|
+
|
|
156
163
|
for file in $PUSHED_FILES; do
|
|
157
|
-
if echo "$file" | grep -qE "$SOURCE_EXTENSIONS"; then
|
|
164
|
+
if echo "$file" | grep -qE "$SOURCE_EXTENSIONS" || echo "$file" | grep -qE "$NON_EXT_SOURCE_RE"; then
|
|
158
165
|
DOC_ONLY=false
|
|
159
166
|
break
|
|
160
167
|
fi
|
|
@@ -954,6 +961,20 @@ else
|
|
|
954
961
|
# GATE MW: VALIDATE CODE WALKTHROUGH RESULT FILE
|
|
955
962
|
# ============================================================================
|
|
956
963
|
RESULT_FILE=".code-walkthrough-result.json"
|
|
964
|
+
MW_SKIPPED=0
|
|
965
|
+
|
|
966
|
+
# Skipped on main/master by design, exactly like Gate ML and Gate UI above.
|
|
967
|
+
# Work is already walkthrough-reviewed on the feature branch before it lands, so
|
|
968
|
+
# demanding a fresh three-model sweep against the merge commit would only
|
|
969
|
+
# re-review identical code — and it made every release commit unpushable, since
|
|
970
|
+
# the release commit itself changes nothing anyone reviewed. githooks/AGENTS.md
|
|
971
|
+
# documented this skip; the code did not implement it (drift fixed here).
|
|
972
|
+
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
|
|
973
|
+
MW_SKIPPED=1
|
|
974
|
+
echo ""
|
|
975
|
+
echo " ℹ️ Gate MW skipped (pushing to ${CURRENT_BRANCH} — pre-reviewed via branch walkthrough)"
|
|
976
|
+
echo ""
|
|
977
|
+
else
|
|
957
978
|
|
|
958
979
|
|
|
959
980
|
if [ ! -f "$RESULT_FILE" ]; then
|
|
@@ -1019,22 +1040,32 @@ if ! VALIDATION_ERROR=$(node "$MW_VALIDATOR" "$RESULT_FILE" "$EXPECTED_COMMIT" "
|
|
|
1019
1040
|
echo ""
|
|
1020
1041
|
exit 1
|
|
1021
1042
|
fi
|
|
1043
|
+
fi
|
|
1022
1044
|
|
|
1023
1045
|
# ============================================================================
|
|
1024
1046
|
# ALL CHECKS PASSED
|
|
1025
1047
|
# ============================================================================
|
|
1026
1048
|
echo ""
|
|
1027
1049
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1050
|
+
# Say what actually happened. Printing "VERIFIED" on a skipped gate would be a
|
|
1051
|
+
# claim the hook cannot support — the same dishonesty (evidence asserting an
|
|
1052
|
+
# unperformed check) that Gate MW exists to prevent.
|
|
1053
|
+
if [ "$MW_SKIPPED" = "1" ]; then
|
|
1054
|
+
echo " ℹ️ GATE MW: CODE WALKTHROUGH SKIPPED (main-branch push)"
|
|
1055
|
+
else
|
|
1028
1056
|
echo " ✅ GATE MW: CODE WALKTHROUGH VERIFIED"
|
|
1057
|
+
fi
|
|
1029
1058
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1030
1059
|
echo ""
|
|
1031
1060
|
echo "Branch: $CURRENT_BRANCH"
|
|
1032
1061
|
echo "Files changed: $FILES_CHANGED"
|
|
1033
1062
|
echo "Lines: +$LINES_ADDED -$LINES_DELETED"
|
|
1034
|
-
|
|
1035
|
-
echo "
|
|
1036
|
-
echo "
|
|
1037
|
-
echo "
|
|
1063
|
+
if [ "$MW_SKIPPED" != "1" ] && [ -f "$RESULT_FILE" ]; then
|
|
1064
|
+
echo ""
|
|
1065
|
+
echo "Code walkthrough result:"
|
|
1066
|
+
echo " Commit: $EXPECTED_COMMIT"
|
|
1067
|
+
echo " Verdict: APPROVED"
|
|
1068
|
+
fi
|
|
1038
1069
|
|
|
1039
1070
|
echo ""
|
|
1040
1071
|
echo "Proceeding with push..."
|
package/hooks/sprint-gate.sh
CHANGED
|
File without changes
|