@boyingliu01/xp-gate 0.35.17 → 0.35.19
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/gate-3.sh +94 -9
- package/adapters/gate-4.sh +27 -32
- package/adapters/gate-8.sh +32 -3
- package/adapters/gate-9.sh +14 -2
- package/gate-12-file-hygiene.sh +26 -4
- package/gate-13-compiler-strictness.sh +12 -0
- package/gates/gate-3.ts +77 -21
- package/gates/gate-8.ts +39 -26
- package/gates/gate-9.ts +4 -4
- package/hooks/gate-12-file-hygiene.sh +26 -4
- package/hooks/gate-13-compiler-strictness.sh +12 -0
- package/hooks/pre-commit +75 -11
- package/hooks/pre-push +11 -2
- package/lib/__tests__/doctor-delphi.test.js +114 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +74 -0
- package/lib/__tests__/doctor.test.js +9 -0
- package/lib/__tests__/gate-numbering.test.js +52 -0
- package/lib/__tests__/gate-runner.test.js +59 -14
- package/lib/__tests__/shared-utils.test.js +47 -0
- package/lib/__tests__/update-skill.test.js +77 -0
- package/lib/doctor-tui.js +23 -2
- package/lib/doctor.js +28 -7
- package/lib/gate-runner.js +98 -18
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-utils.js +27 -1
- package/lib/update-skill.js +59 -3
- 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/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/claude-code/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- 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/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/dsh/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-findings.test.ts +32 -6
- package/plugins/dsh/src/exit-code.test.ts +6 -0
- 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/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/opencode/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- 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/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/qoder/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/__tests__/sync-package-content.test.js +114 -46
- package/scripts/sync-package-content.js +32 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -36,13 +36,35 @@ fi
|
|
|
36
36
|
HYGIENE_ISSUES=0
|
|
37
37
|
HYGIENE_FILES=""
|
|
38
38
|
|
|
39
|
+
# Is this staged file binary (so line-based checks do not apply)?
|
|
40
|
+
#
|
|
41
|
+
# This replaces `grep -q $'\x00'`, which was a no-op guard: bash strips NUL from
|
|
42
|
+
# `$'\x00'`, so the pattern was the EMPTY string and `grep -q ""` matches any
|
|
43
|
+
# non-empty input. Every staged file was therefore judged binary and `continue`d,
|
|
44
|
+
# silently disabling Check 1 (trailing whitespace), Check 2 (missing EOF newline)
|
|
45
|
+
# and -- worst -- Check 3 (merge conflict markers, a BLOCK-level check). Checks 4
|
|
46
|
+
# and 5 lacked the guard, which is why the gate looked half-working.
|
|
47
|
+
#
|
|
48
|
+
# Ask git instead of guessing: `--numstat` reports a binary entry as `-\t-\t<path>`
|
|
49
|
+
# because git already made that determination. A byte-level NUL test cannot be
|
|
50
|
+
# written with a shell pattern at all (variables and command substitution both drop
|
|
51
|
+
# NUL), and `grep -I` only suppresses output on a binary input -- it does not make
|
|
52
|
+
# `grep -q` fail, which is why that shortcut still reported text.
|
|
53
|
+
is_binary_staged_file() {
|
|
54
|
+
local _file="$1"
|
|
55
|
+
case "$(git diff --cached --numstat -- "$_file" 2>/dev/null)" in
|
|
56
|
+
-[[:space:]]-*) return 0 ;; # binary: git could not count lines
|
|
57
|
+
*) return 1 ;; # text (or empty -- nothing for the line checks)
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
# --- Check 1: Trailing whitespace ---
|
|
40
62
|
TRAILING_WS_FILES=""
|
|
41
63
|
if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
42
64
|
while IFS= read -r file; do
|
|
43
65
|
[ -z "$file" ] && continue
|
|
44
|
-
# Skip binary files
|
|
45
|
-
if
|
|
66
|
+
# Skip binary files : they have no lines to check
|
|
67
|
+
if is_binary_staged_file "$file"; then
|
|
46
68
|
continue
|
|
47
69
|
fi
|
|
48
70
|
# Check staged content for trailing whitespace (spaces/tabs at end of lines)
|
|
@@ -69,7 +91,7 @@ if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
|
69
91
|
while IFS= read -r file; do
|
|
70
92
|
[ -z "$file" ] && continue
|
|
71
93
|
# Skip binary files
|
|
72
|
-
if
|
|
94
|
+
if is_binary_staged_file "$file"; then
|
|
73
95
|
continue
|
|
74
96
|
fi
|
|
75
97
|
# Check if file content ends with newline
|
|
@@ -108,7 +130,7 @@ else
|
|
|
108
130
|
while IFS= read -r file; do
|
|
109
131
|
[ -z "$file" ] && continue
|
|
110
132
|
# Skip binary files
|
|
111
|
-
if
|
|
133
|
+
if is_binary_staged_file "$file"; then
|
|
112
134
|
continue
|
|
113
135
|
fi
|
|
114
136
|
# Check for conflict markers in staged content
|
|
@@ -527,6 +527,18 @@ gate13_count_lint_findings() {
|
|
|
527
527
|
[ -n "$src" ] || src=$(cat)
|
|
528
528
|
printf '%s\n' "$src" | awk '
|
|
529
529
|
BEGIN { n = 0 }
|
|
530
|
+
# (#673) Maven 3.9.7+ reactor summary appends the packaging type per module:
|
|
531
|
+
# [INFO] whaleaudit-core [jar]
|
|
532
|
+
# `[pom]`/`[jar]` match the open-category rule below, so a 3-module reactor
|
|
533
|
+
# reported 4 javac findings on code with ZERO warnings -- and got worse with
|
|
534
|
+
# every module added. The marker set is closed (packaging types are an enum in
|
|
535
|
+
# Maven itself), so excluding it does not reopen the #659 fail-open hole that
|
|
536
|
+
# the open-category rule was introduced to close.
|
|
537
|
+
#
|
|
538
|
+
# Scoped to `[INFO]` lines: a javac diagnostic is never prefixed with the
|
|
539
|
+
# Maven [INFO] banner, so this cannot mask a real finding even when a category
|
|
540
|
+
# name collides with a packaging type.
|
|
541
|
+
/^\[INFO\].*\[(pom|jar|war|ear|rar|ejb|maven-plugin|par|test-jar|java-source|javadoc|zip|tar\.gz|tar\.bz2)\][[:space:]]*$/ { next }
|
|
530
542
|
# Any bracketed lowercase category: covers the whole javac -Xlint enum and any
|
|
531
543
|
# later addition to it. Restricted to lowercase/digit/hyphen so a Maven
|
|
532
544
|
# `[WARNING]` with no category, or a `[INFO]` banner, is not counted.
|
package/hooks/pre-commit
CHANGED
|
@@ -319,6 +319,32 @@ if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
|
319
319
|
}
|
|
320
320
|
fi
|
|
321
321
|
|
|
322
|
+
# Run a repo-local Node CLI (eslint, vitest, biome, ...) without npx when the
|
|
323
|
+
# binary is already in this repo's node_modules.
|
|
324
|
+
#
|
|
325
|
+
# `npx <tool>` costs ~1.4-1.5 s more than the tool itself on this machine
|
|
326
|
+
# (measured: eslint --version 1654 ms via npx vs 348 ms direct; vitest 1799 ms vs
|
|
327
|
+
# 250 ms). That is npm resolving the package before it ever runs: on a cold
|
|
328
|
+
# cache it may reach the network, which a quality gate must never depend on.
|
|
329
|
+
# pre-commit invokes these tools a handful of times per commit, so the wrapper
|
|
330
|
+
# was paying several seconds to start processes that were already installed.
|
|
331
|
+
#
|
|
332
|
+
# `run_node_bin <tool> <node-entry> [args...]`. Falls back to npx when the local
|
|
333
|
+
# entry is absent (a globally-installed toolchain is still a valid setup).
|
|
334
|
+
if ! declare -F run_node_bin >/dev/null 2>&1; then
|
|
335
|
+
run_node_bin() {
|
|
336
|
+
local _tool="$1" _entry="$2"
|
|
337
|
+
shift 2
|
|
338
|
+
local _root="${PROJECT_ROOT:-$(pwd)}"
|
|
339
|
+
local _local="$_root/node_modules/$_entry"
|
|
340
|
+
if [ -f "$_local" ] && command -v node >/dev/null 2>&1; then
|
|
341
|
+
node "$_local" "$@"
|
|
342
|
+
else
|
|
343
|
+
npx "$_tool" "$@"
|
|
344
|
+
fi
|
|
345
|
+
}
|
|
346
|
+
fi
|
|
347
|
+
|
|
322
348
|
# Gate 5's vitest exit-code judgement (is_runner_infrastructure_error,
|
|
323
349
|
# is_partial_threshold_only_exit, handle_test_failure) is sourced from
|
|
324
350
|
# lib/test-failure.sh below, next to the other hook libraries.
|
|
@@ -1677,9 +1703,9 @@ else
|
|
|
1677
1703
|
# each linted file (found in the subdirectory automatically); flat
|
|
1678
1704
|
# configs are discovered from the CWD, so point at them explicitly.
|
|
1679
1705
|
if [ "$TS_PROJECT_DIR" != "." ] && [ -f "$TS_PROJECT_DIR/eslint.config.js" ]; then
|
|
1680
|
-
ESLINT_OUTPUT=$(
|
|
1706
|
+
ESLINT_OUTPUT=$(run_node_bin eslint eslint/bin/eslint.js --config "$TS_PROJECT_DIR/eslint.config.js" $ESLINT_FILES -f json --no-warn-ignored 2>&1)
|
|
1681
1707
|
else
|
|
1682
|
-
ESLINT_OUTPUT=$(
|
|
1708
|
+
ESLINT_OUTPUT=$(run_node_bin eslint eslint/bin/eslint.js $ESLINT_FILES -f json --no-warn-ignored 2>&1)
|
|
1683
1709
|
fi
|
|
1684
1710
|
ESLINT_EXIT=$?
|
|
1685
1711
|
if [ "$ESLINT_EXIT" -ne 0 ]; then
|
|
@@ -3008,7 +3034,7 @@ else
|
|
|
3008
3034
|
if source "$ADAPTER_FILE" 2>/dev/null; then
|
|
3009
3035
|
# TypeScript: vitest with smart test selection
|
|
3010
3036
|
if [ "$CURRENT_LANG" = "typescript" ] && { [ -f "package.json" ] || [ -f "$PROJECT_ROOT/package.json" ]; }; then
|
|
3011
|
-
if run_without_git_context
|
|
3037
|
+
if run_without_git_context run_node_bin vitest vitest/vitest.mjs --version >/dev/null 2>&1; then
|
|
3012
3038
|
# Count changed test files to decide strategy. `grep -c '.'` prints its
|
|
3013
3039
|
# own zero AND exits 1 on an empty list, so `|| echo "0"` used to yield
|
|
3014
3040
|
# $'0\n0' -- the same two-value bug that false-blocked Gate 8 (#493).
|
|
@@ -3055,7 +3081,7 @@ else
|
|
|
3055
3081
|
if [ "$CHANGED_TEST_FILE_COUNT" -gt 20 ] 2>/dev/null; then
|
|
3056
3082
|
# Too many test files changed → full run
|
|
3057
3083
|
echo "Too many test files changed ($CHANGED_TEST_FILE_COUNT), running full test suite..."
|
|
3058
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3084
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage 2>&1)
|
|
3059
3085
|
TESTS_EXIT_CODE=$?
|
|
3060
3086
|
echo "$TESTS_OUTPUT" | head -60
|
|
3061
3087
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3069,7 +3095,7 @@ else
|
|
|
3069
3095
|
SMART_TARGETS=$(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')
|
|
3070
3096
|
echo "Running ${CHANGED_TEST_FILE_COUNT} changed test file(s) with coverage..."
|
|
3071
3097
|
echo " Target(s): $(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')"
|
|
3072
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3098
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage $SMART_TARGETS 2>&1)
|
|
3073
3099
|
TESTS_EXIT_CODE=$?
|
|
3074
3100
|
echo "$TESTS_OUTPUT" | head -60
|
|
3075
3101
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3114,7 +3140,7 @@ else
|
|
|
3114
3140
|
if [ -n "$RELATED_TESTS" ]; then
|
|
3115
3141
|
echo " Found related test(s): $(echo "$RELATED_TESTS" | tr '\n' ' ')"
|
|
3116
3142
|
PARTIAL_TEST_RUN=true # Mark as partial run for coverage check
|
|
3117
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3143
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage $RELATED_TESTS 2>&1)
|
|
3118
3144
|
TESTS_EXIT_CODE=$?
|
|
3119
3145
|
echo "$TESTS_OUTPUT" | head -60
|
|
3120
3146
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3125,7 +3151,7 @@ else
|
|
|
3125
3151
|
else
|
|
3126
3152
|
# No related tests found → full run is safest
|
|
3127
3153
|
echo " No related test files found, running full test suite..."
|
|
3128
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3154
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage 2>&1)
|
|
3129
3155
|
TESTS_EXIT_CODE=$?
|
|
3130
3156
|
echo "$TESTS_OUTPUT" | head -60
|
|
3131
3157
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3982,6 +4008,8 @@ else
|
|
|
3982
4008
|
cd "$PROJECT_ROOT" || exit 1
|
|
3983
4009
|
if [ "$ARCHLINT_KIND" = "npx" ]; then
|
|
3984
4010
|
npx @archlinter/cli "$@"
|
|
4011
|
+
elif [ "${ARCHLINT_NODE:-0}" = "1" ]; then
|
|
4012
|
+
node "$ARCHLINT_BIN" "$@"
|
|
3985
4013
|
else
|
|
3986
4014
|
"$ARCHLINT_BIN" "$@"
|
|
3987
4015
|
fi
|
|
@@ -4002,8 +4030,17 @@ else
|
|
|
4002
4030
|
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
4003
4031
|
case "$CURRENT_LANG" in
|
|
4004
4032
|
"typescript")
|
|
4005
|
-
#
|
|
4006
|
-
|
|
4033
|
+
# Prefer node on the package's own entry over the .bin shim: the shim's
|
|
4034
|
+
# exec bit and extension vary by platform and package manager (npm writes
|
|
4035
|
+
# archlint, archlint.cmd and archlint.ps1 on Windows), so `-x .bin/...`
|
|
4036
|
+
# is true here while the no-extension shim is what Git Bash would run.
|
|
4037
|
+
# `node <entry>` sidesteps all of that and also avoids npx's ~1.6 s
|
|
4038
|
+
# package resolution (measured: 1909 ms via npx vs 319 ms direct).
|
|
4039
|
+
if [ -f "$PROJECT_ROOT/node_modules/@archlinter/cli/bin/archlint.js" ] && command -v node >/dev/null 2>&1; then
|
|
4040
|
+
ARCHLINT_KIND="binary"
|
|
4041
|
+
ARCHLINT_BIN="$PROJECT_ROOT/node_modules/@archlinter/cli/bin/archlint.js"
|
|
4042
|
+
ARCHLINT_NODE=1
|
|
4043
|
+
elif [ -x "$PROJECT_ROOT/node_modules/.bin/archlint" ]; then
|
|
4007
4044
|
ARCHLINT_KIND="binary"
|
|
4008
4045
|
ARCHLINT_BIN="$PROJECT_ROOT/node_modules/.bin/archlint"
|
|
4009
4046
|
elif command -v archlint >/dev/null 2>&1; then
|
|
@@ -4764,7 +4801,12 @@ if [ -n "$MIRROR_STAGED" ] && printf '%s\n' "$MIRROR_STAGED" | grep -qE '^(githo
|
|
|
4764
4801
|
MIRROR_FAIL=0
|
|
4765
4802
|
while IFS= read -r _mf; do
|
|
4766
4803
|
[ -n "$_mf" ] || continue
|
|
4804
|
+
# `__tests__/` has no mirror BY DESIGN: sync-package-content.js lists it in
|
|
4805
|
+
# SKIP_DIRS so test suites never ship to consumers. Demanding a mirror for
|
|
4806
|
+
# them makes every new BATS file un-committable -- the two rules contradicted
|
|
4807
|
+
# each other, and the only way past was to edit a generated file by hand.
|
|
4767
4808
|
case "$_mf" in
|
|
4809
|
+
*/__tests__/*) continue ;;
|
|
4768
4810
|
githooks/*) _mm="src/npm-package/hooks/${_mf#githooks/}" ;;
|
|
4769
4811
|
plugins/*/src/*) _mm=$(printf '%s' "$_mf" | sed 's|^plugins/\([^/]*\)/src/|src/npm-package/plugins/\1/src/|') ;;
|
|
4770
4812
|
*) continue ;;
|
|
@@ -4880,7 +4922,22 @@ generate_quality_report() {
|
|
|
4880
4922
|
local TIMESTAMP
|
|
4881
4923
|
local BRANCH
|
|
4882
4924
|
local PASSED_COUNT=0
|
|
4883
|
-
|
|
4925
|
+
# #680: the gate list is the single source of truth. TOTAL_GATES, the scoring
|
|
4926
|
+
# loop, the console table and the JSON `overall` block all derive from it.
|
|
4927
|
+
#
|
|
4928
|
+
# Gate 13 was added in v0.35.x with only its execution and JSON write -- these
|
|
4929
|
+
# three sites kept saying "12". A machine with no C/C++ compiler therefore saw
|
|
4930
|
+
# `10.0/10 effective`, `gatesWarned: 0` and a table that stopped at Gate 12,
|
|
4931
|
+
# while the JSON `gates` object held 13 keys. "Compiler strictness was never
|
|
4932
|
+
# checked" and "compiler reported zero warnings" rendered identically, which is
|
|
4933
|
+
# the same silent-success pattern as #561/#570/#585.
|
|
4934
|
+
#
|
|
4935
|
+
# Adding a gate now means editing ALL_GATE_NUMBERS + ONE table row. Nothing
|
|
4936
|
+
# else carries a count.
|
|
4937
|
+
local ALL_GATE_NUMBERS="1 2 3 4 5 6 7 8 9 10 11 12 13"
|
|
4938
|
+
local TOTAL_GATES=0
|
|
4939
|
+
local _g
|
|
4940
|
+
for _g in $ALL_GATE_NUMBERS; do TOTAL_GATES=$((TOTAL_GATES + 1)); done
|
|
4884
4941
|
local SKIP_COUNT=0
|
|
4885
4942
|
local WARN_COUNT=0
|
|
4886
4943
|
local NONPASS_COUNT=0
|
|
@@ -4908,7 +4965,7 @@ generate_quality_report() {
|
|
|
4908
4965
|
local DEGRADED_JSON=""
|
|
4909
4966
|
local FAIL_COUNT=0
|
|
4910
4967
|
local FAILED_JSON=""
|
|
4911
|
-
for gate in
|
|
4968
|
+
for gate in $ALL_GATE_NUMBERS; do
|
|
4912
4969
|
local status_var="GATE_${gate}_STATUS"
|
|
4913
4970
|
# Default mirrors the JSON report rendering below EXACTLY (gates 1-11
|
|
4914
4971
|
# PASS, gate 12 WARN): a gate that never assigned a status is "not
|
|
@@ -4918,6 +4975,10 @@ generate_quality_report() {
|
|
|
4918
4975
|
# Default direction is "did not run", never "passed": an unassigned status
|
|
4919
4976
|
# is a gate that never reached a verdict, and scoring it PASS both claims a
|
|
4920
4977
|
# success that did not happen and hides the gap in the effective rate.
|
|
4978
|
+
#
|
|
4979
|
+
# Gate 13 defaults to SKIP like the rest: it is only WARN when the toolchain
|
|
4980
|
+
# is missing and the gate explicitly says so, and Gate 12 is the one gate
|
|
4981
|
+
# whose "no verdict" is still a warning rather than "not applicable".
|
|
4921
4982
|
if [ "$gate" -eq 12 ]; then gate_default="WARN"; else gate_default="SKIP"; fi
|
|
4922
4983
|
gate_status="${!status_var:-$gate_default}"
|
|
4923
4984
|
case "$gate_status" in
|
|
@@ -5264,6 +5325,9 @@ ENDJSON
|
|
|
5264
5325
|
printf " %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-SKIP}"
|
|
5265
5326
|
printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-SKIP}"
|
|
5266
5327
|
printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
|
|
5328
|
+
# #680: Gate 13 ran and could block, but had no row here -- the table is what a
|
|
5329
|
+
# human reads, so a gate missing from it reads as a gate that does not exist.
|
|
5330
|
+
printf " %-45s %s\n" "Gate 13: Compiler Strictness" "${GATE_13_STATUS:-SKIP}"
|
|
5267
5331
|
echo ""
|
|
5268
5332
|
# #564: both views in one line. The effective score answers "of the gates that
|
|
5269
5333
|
# could run, how many passed"; the all-gates score counts a skip against the
|
package/hooks/pre-push
CHANGED
|
@@ -426,7 +426,16 @@ SPRINT_GATE_SCRIPT=""
|
|
|
426
426
|
# ~/.config/xp-gate/hooks -- a correctly installed machine was told the file was
|
|
427
427
|
# missing here too. Nothing is sourced for this: pre-push resolves its libraries
|
|
428
428
|
# further down (ADAPTER_COMMON), and this gate must decide before that.
|
|
429
|
-
|
|
429
|
+
#
|
|
430
|
+
# `$HOME/.config/xp-gate/adapters` is the tier #678 found missing: `xp-gate
|
|
431
|
+
# setup-global` runs copyAdapters(), which ships every `gate-*.sh` AND
|
|
432
|
+
# `sprint-gate.sh` into the ADAPTERS directory -- never into hooks/. So a globally
|
|
433
|
+
# installed machine had sprint-gate.sh present under adapters/ while this loop
|
|
434
|
+
# searched only hooks/, and Gate MS skipped on every push without complaint. Both
|
|
435
|
+
# tiers are searched now: which directory `init` chose is an install detail this
|
|
436
|
+
# gate should not depend on.
|
|
437
|
+
for _sprint_gate_dir in "$PROJECT_ROOT/githooks" "$(dirname "$0")" \
|
|
438
|
+
"$HOME/.config/xp-gate/hooks" "$HOME/.config/xp-gate/adapters"; do
|
|
430
439
|
if [ -n "$_sprint_gate_dir" ] && [ -f "$_sprint_gate_dir/sprint-gate.sh" ]; then
|
|
431
440
|
SPRINT_GATE_SCRIPT="$_sprint_gate_dir/sprint-gate.sh"
|
|
432
441
|
break
|
|
@@ -440,7 +449,7 @@ if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
|
440
449
|
exit 1
|
|
441
450
|
fi
|
|
442
451
|
else
|
|
443
|
-
echo "⏭️ SKIPPED - Gate MS: Sprint Flow (sprint-gate.sh not found; searched: $PROJECT_ROOT/githooks, $(dirname "$0"), $HOME/.config/xp-gate/hooks)"
|
|
452
|
+
echo "⏭️ SKIPPED - Gate MS: Sprint Flow (sprint-gate.sh not found; searched: $PROJECT_ROOT/githooks, $(dirname "$0"), $HOME/.config/xp-gate/hooks, $HOME/.config/xp-gate/adapters)"
|
|
444
453
|
GATE_S_STATUS="SKIP"
|
|
445
454
|
fi
|
|
446
455
|
echo ""
|
|
@@ -337,3 +337,117 @@ describe('doctor delphi (#414 REQ-3/REQ-6)', () => {
|
|
|
337
337
|
}
|
|
338
338
|
});
|
|
339
339
|
});
|
|
340
|
+
|
|
341
|
+
// ── #684: the documented global-config fallback ──────────────────────────────
|
|
342
|
+
// SKILL.md tells users to keep ONE machine-level config at
|
|
343
|
+
// `~/.config/xp-gate/.delphi-config.json` and carry no repo-local copy. The
|
|
344
|
+
// runner resolves that fallback, so a review works; the doctor did not, and
|
|
345
|
+
// reported FAIL on a layout its own documentation recommends. Worse, the FAIL
|
|
346
|
+
// detail told the user to copy the example into the project — which manufactures
|
|
347
|
+
// exactly the drifted duplicate that #541 added a shadow warning for.
|
|
348
|
+
//
|
|
349
|
+
// The distinction this batch pins: "no config anywhere" is still a FAIL
|
|
350
|
+
// (that is a genuinely broken install); "no project config but a valid global
|
|
351
|
+
// one" is not.
|
|
352
|
+
describe('doctor delphi #684 global config fallback', () => {
|
|
353
|
+
let tmpDirs;
|
|
354
|
+
let tmpHome;
|
|
355
|
+
let originalHome;
|
|
356
|
+
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
tmpDirs = makeTempDirs(['doctor-684-home']);
|
|
359
|
+
tmpHome = tmpDirs['doctor-684-home'];
|
|
360
|
+
originalHome = process.env.HOME;
|
|
361
|
+
process.env.HOME = tmpHome;
|
|
362
|
+
fs.mkdirSync(path.join(tmpHome, '.qoder'), { recursive: true });
|
|
363
|
+
resetLibModules(vi, LIB_MODULES);
|
|
364
|
+
muteConsole(vi);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
afterEach(() => {
|
|
368
|
+
if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome;
|
|
369
|
+
removeTempDirs(tmpDirs);
|
|
370
|
+
vi.restoreAllMocks();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Local copy: the suite above keeps its own writeConfig inside its describe
|
|
374
|
+
// scope, so this block cannot reach it.
|
|
375
|
+
function writeConfig(configPath) {
|
|
376
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
377
|
+
active_profile: 'default',
|
|
378
|
+
profiles: {
|
|
379
|
+
default: {
|
|
380
|
+
providers: {
|
|
381
|
+
gw: { base_url: 'https://gw.example/v1', api_key: 'k' },
|
|
382
|
+
},
|
|
383
|
+
experts: {
|
|
384
|
+
architecture: { provider: 'gw', model: 'model-a' },
|
|
385
|
+
technical: { provider: 'gw', model: 'model-b' },
|
|
386
|
+
feasibility: { provider: 'gw', model: 'model-c' },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
}));
|
|
391
|
+
return configPath;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function writeGlobalConfig() {
|
|
395
|
+
const dir = path.join(tmpHome, '.config', 'xp-gate');
|
|
396
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
397
|
+
return writeConfig(path.join(dir, '.delphi-config.json'));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function captureLogs() {
|
|
401
|
+
const logs = [];
|
|
402
|
+
const spy = vi.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));
|
|
403
|
+
return { logs, restore: () => spy.mockRestore() };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
it('AC-684-01: a valid global config with no project copy PASSes instead of failing', async () => {
|
|
407
|
+
const globalPath = writeGlobalConfig();
|
|
408
|
+
const project = path.join(tmpHome, 'a-project-with-no-config');
|
|
409
|
+
fs.mkdirSync(project, { recursive: true });
|
|
410
|
+
const { logs, restore } = captureLogs();
|
|
411
|
+
const { doctorDelphi } = require('../doctor');
|
|
412
|
+
await doctorDelphi({ projectDir: project, globalConfigPath: globalPath, npmLatest: null });
|
|
413
|
+
restore();
|
|
414
|
+
const cfgLine = logs.find((l) => l.includes('Delphi config'));
|
|
415
|
+
expect(cfgLine).toBeDefined();
|
|
416
|
+
expect(cfgLine).not.toMatch(/FAIL/);
|
|
417
|
+
expect(cfgLine).toMatch(/global/i);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('AC-684-02: no config in either scope still FAILs (fallback must not mask a broken install)', async () => {
|
|
421
|
+
const project = path.join(tmpHome, 'bare-project');
|
|
422
|
+
fs.mkdirSync(project, { recursive: true });
|
|
423
|
+
const { logs, restore } = captureLogs();
|
|
424
|
+
const { doctorDelphi } = require('../doctor');
|
|
425
|
+
const code = await doctorDelphi({
|
|
426
|
+
projectDir: project,
|
|
427
|
+
globalConfigPath: path.join(tmpHome, 'no-such-global.json'),
|
|
428
|
+
npmLatest: null,
|
|
429
|
+
});
|
|
430
|
+
restore();
|
|
431
|
+
expect(code).toBe(1);
|
|
432
|
+
// The rendered line carries a ✗ marker, not the literal word FAIL.
|
|
433
|
+
const cfgLine = logs.find((l) => l.includes('Delphi config (.delphi-config.json)'));
|
|
434
|
+
expect(cfgLine).toBeDefined();
|
|
435
|
+
expect(cfgLine).toContain('✗');
|
|
436
|
+
expect(cfgLine).toMatch(/not found/);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('AC-684-03: a project config still wins over the global one', async () => {
|
|
440
|
+
const globalPath = writeGlobalConfig();
|
|
441
|
+
const project = path.join(tmpHome, 'project-with-own-config');
|
|
442
|
+
fs.mkdirSync(project, { recursive: true });
|
|
443
|
+
const localPath = writeConfig(path.join(project, '.delphi-config.json'));
|
|
444
|
+
const { logs, restore } = captureLogs();
|
|
445
|
+
const { doctorDelphi } = require('../doctor');
|
|
446
|
+
await doctorDelphi({ projectDir: project, globalConfigPath: globalPath, npmLatest: null });
|
|
447
|
+
restore();
|
|
448
|
+
const cfgLine = logs.find((l) => l.includes('Delphi config'));
|
|
449
|
+
expect(cfgLine).toBeDefined();
|
|
450
|
+
expect(cfgLine).toContain(localPath);
|
|
451
|
+
expect(cfgLine).not.toMatch(/global/i);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -294,3 +294,77 @@ describe('doctor-tui installed skills enumerate the filesystem (#635)', () => {
|
|
|
294
294
|
expect(row.status).toBe('PASS');
|
|
295
295
|
});
|
|
296
296
|
});
|
|
297
|
+
|
|
298
|
+
// ── #681: equal version is not proof of equal content ────────────────────────
|
|
299
|
+
// The freshness check short-circuits on `compareSemver(...) === 0` and answers
|
|
300
|
+
// "Up to date" without ever reading the bodies. A skill that gained a whole
|
|
301
|
+
// chapter without a version bump therefore reported healthy while the installed
|
|
302
|
+
// copy was ~20k characters behind — and `update-skill` agreed with it.
|
|
303
|
+
//
|
|
304
|
+
// The version number is a claim the publisher makes; the content is the fact.
|
|
305
|
+
// When the claim says "same" the fact still has to be checked.
|
|
306
|
+
describe('doctor-tui skill freshness #681 equal version, different content', () => {
|
|
307
|
+
let tmpHome;
|
|
308
|
+
let originalHome;
|
|
309
|
+
const bundledSkillsDir = path.join(__dirname, '..', '..', 'skills');
|
|
310
|
+
|
|
311
|
+
beforeEach(() => {
|
|
312
|
+
originalHome = process.env.HOME;
|
|
313
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-fresh681-'));
|
|
314
|
+
process.env.HOME = tmpHome;
|
|
315
|
+
vi.resetModules();
|
|
316
|
+
delete require.cache[require.resolve('../doctor-tui')];
|
|
317
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
afterEach(() => {
|
|
321
|
+
process.env.HOME = originalHome;
|
|
322
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
323
|
+
vi.restoreAllMocks();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
function installSkill(name, content) {
|
|
327
|
+
const dir = path.join(tmpHome, '.config', 'opencode', 'skills', name);
|
|
328
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
329
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), content);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function runFreshness(name) {
|
|
333
|
+
const { diagnoseInstalledSkills } = require('../doctor-tui');
|
|
334
|
+
const checks = [];
|
|
335
|
+
const issues = diagnoseInstalledSkills({ installedSkills: { [name]: {} } }, checks);
|
|
336
|
+
return { issues, check: checks.find((c) => c.name === `Skill: ${name}`) };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
it('AC-681-01: same version but a missing chapter is WARN, not "Up to date"', () => {
|
|
340
|
+
const bundled = fs.readFileSync(path.join(bundledSkillsDir, 'delphi-review', 'SKILL.md'), 'utf8');
|
|
341
|
+
// Keep the frontmatter (version included), drop a large slice of the body.
|
|
342
|
+
const idx = bundled.indexOf('\n---', 4);
|
|
343
|
+
const head = bundled.slice(0, idx + 4);
|
|
344
|
+
const body = bundled.slice(idx + 4);
|
|
345
|
+
const truncated = head + body.slice(0, Math.floor(body.length / 2));
|
|
346
|
+
installSkill('delphi-review', truncated);
|
|
347
|
+
|
|
348
|
+
const { issues, check } = runFreshness('delphi-review');
|
|
349
|
+
expect(check).toBeDefined();
|
|
350
|
+
expect(check.detail).not.toMatch(/Up to date/);
|
|
351
|
+
expect(check.status).toBe('WARN');
|
|
352
|
+
expect(issues).toBe(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('AC-681-02: same version and same content is still "Up to date" (no false alarm)', () => {
|
|
356
|
+
const bundled = fs.readFileSync(path.join(bundledSkillsDir, 'delphi-review', 'SKILL.md'), 'utf8');
|
|
357
|
+
installSkill('delphi-review', bundled);
|
|
358
|
+
const { issues, check } = runFreshness('delphi-review');
|
|
359
|
+
expect(check.detail).toMatch(/Up to date/);
|
|
360
|
+
expect(issues).toBe(0);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('AC-681-03: line endings alone still do not count as a difference', () => {
|
|
364
|
+
const bundled = fs.readFileSync(path.join(bundledSkillsDir, 'delphi-review', 'SKILL.md'), 'utf8');
|
|
365
|
+
installSkill('delphi-review', bundled.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'));
|
|
366
|
+
const { issues, check } = runFreshness('delphi-review');
|
|
367
|
+
expect(check.detail).toMatch(/Up to date/);
|
|
368
|
+
expect(issues).toBe(0);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -13,7 +13,16 @@ const util = require('util');
|
|
|
13
13
|
// tests run in parallel across worker threads.
|
|
14
14
|
const DESCRIBE_ID = String(Math.random().toString(36).slice(2, 8));
|
|
15
15
|
|
|
16
|
+
// Budget for every case in this file. `doctor --fix` walks the whole install
|
|
17
|
+
// surface (hooks, adapters, config, version cache) and the default 5 s budget is
|
|
18
|
+
// a measurement of an idle machine, not of one running a full suite: under
|
|
19
|
+
// concurrent load the same passing case times out, which reads as a doctor
|
|
20
|
+
// defect. 30 s keeps real hangs bounded while leaving the CPU contention room.
|
|
21
|
+
const DOCTOR_TIMEOUT_MS = 30000;
|
|
22
|
+
|
|
16
23
|
describe('doctor', () => {
|
|
24
|
+
// Applies to every `it` below (the per-test arg is only needed when overriding).
|
|
25
|
+
vi.setConfig({ testTimeout: DOCTOR_TIMEOUT_MS });
|
|
17
26
|
let tmpHome;
|
|
18
27
|
let tmpProject;
|
|
19
28
|
let originalHome;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-686
|
|
3
|
+
* @intent 验证 CLI 的门禁编号与钩子一致——钩子把 9 号门叫 Build integrity、
|
|
4
|
+
* 10 号门叫 SAST,而 CLI 注册表把两者对调了;用户按报错提示单跑对应门
|
|
5
|
+
* 会跑到另一道门上去(#677)
|
|
6
|
+
* @covers AC-686-01, AC-686-02, AC-686-03
|
|
7
|
+
*
|
|
8
|
+
* 为什么必须直接断言注册表:这个错误的表现是「跑起来了、报告绿了」。
|
|
9
|
+
* `xp-gate check --gates 9` 在编号错位时不会报错,它跑的是 semgrep 并如实报告
|
|
10
|
+
* semgrep 的结果——只有拿它和钩子里的门名对照才能看出跑错了门。
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { GATE_REGISTRY, resolveAlias } = require('../gate-runner');
|
|
17
|
+
|
|
18
|
+
const REPO_ROOT = path.join(__dirname, '../../../..');
|
|
19
|
+
|
|
20
|
+
describe('gate numbering matches the hook (#677)', () => {
|
|
21
|
+
it('AC-686-01: gate 9 is Build Integrity and gate 10 is SAST Security', () => {
|
|
22
|
+
expect(GATE_REGISTRY['9'].name).toBe('Build Integrity');
|
|
23
|
+
expect(GATE_REGISTRY['10'].name).toBe('SAST Security');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('AC-686-02: the hook agrees — its own output names 9 as Build integrity', () => {
|
|
27
|
+
// Reads the hook rather than restating the constant: if the hook ever
|
|
28
|
+
// renumbers again, this fails and points at which side moved.
|
|
29
|
+
const hook = fs.readFileSync(path.join(REPO_ROOT, 'githooks/pre-commit'), 'utf8');
|
|
30
|
+
expect(hook).toMatch(/→ Gate 9: Build integrity/);
|
|
31
|
+
expect(hook).toMatch(/GATE 10: Semgrep SAST/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('AC-686-03: aliases resolve to the gate the user meant', () => {
|
|
35
|
+
expect(resolveAlias('sast')).toBe('10');
|
|
36
|
+
expect(resolveAlias('semgrep')).toBe('10');
|
|
37
|
+
expect(resolveAlias('build')).toBe('9');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('AC-686-03: gates 12 and 13 are registered instead of "Unknown gate(s)"', () => {
|
|
41
|
+
// Both block commits in the hook. Answering "Unknown gate" for a gate that just
|
|
42
|
+
// blocked the user is the same category of wrong as running the wrong one.
|
|
43
|
+
expect(GATE_REGISTRY['12'].name).toBe('File Hygiene');
|
|
44
|
+
expect(GATE_REGISTRY['13'].name).toBe('Compiler Strictness');
|
|
45
|
+
// They are registered as hook-only, with a stated reason -- not as runnable
|
|
46
|
+
// stubs that would answer PASS without doing the work.
|
|
47
|
+
expect(GATE_REGISTRY['12'].preCommitOnly).toBe(true);
|
|
48
|
+
expect(GATE_REGISTRY['13'].preCommitOnly).toBe(true);
|
|
49
|
+
expect(GATE_REGISTRY['12'].reason).toBeTruthy();
|
|
50
|
+
expect(GATE_REGISTRY['13'].reason).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
});
|