@boyingliu01/xp-gate 0.31.4 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/adapters/java.sh +256 -4
  2. package/adapters/powershell.sh +45 -6
  3. package/bin/xp-gate.js +8 -2
  4. package/build-integrity/__tests__/gate-10.test.ts +88 -1
  5. package/build-integrity/gate-10.ts +51 -16
  6. package/dsh.bundle.patch.yml +15 -0
  7. package/gate-13-compiler-strictness.sh +1031 -0
  8. package/gate-3.sh +48 -13
  9. package/hooks/gate-13-compiler-strictness.sh +1031 -0
  10. package/hooks/gate-3.sh +48 -13
  11. package/hooks/lib/coverage-total.sh +18 -2
  12. package/hooks/lib/java-test-selection.sh +251 -0
  13. package/hooks/lib/jscpd-run.sh +9 -0
  14. package/hooks/lib/validate-code-walkthrough.cjs +186 -29
  15. package/hooks/pre-commit +149 -2
  16. package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
  17. package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
  18. package/lib/__tests__/gate-runner.test.js +29 -2
  19. package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
  20. package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
  21. package/lib/__tests__/shared-utils.test.js +21 -0
  22. package/lib/check.js +6 -2
  23. package/lib/detect-deps.js +42 -10
  24. package/lib/doctor-tui.js +111 -21
  25. package/lib/gate-runner.js +111 -37
  26. package/lib/mattpocock-license-verdict.json +1 -1
  27. package/lib/principles.js +48 -6
  28. package/lib/shared-utils.js +8 -0
  29. package/mock-policy/AGENTS.md +3 -3
  30. package/mock-policy/gate-m3.ts +57 -23
  31. package/mutation/AGENTS.md +3 -3
  32. package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
  33. package/mutation/__tests__/test-filter-parity.test.ts +5 -2
  34. package/mutation/gate-m.ts +30 -24
  35. package/mutation/runners/mutmut-runner.ts +80 -30
  36. package/mutation/runners/pitest-runner.ts +60 -42
  37. package/package.json +14 -2
  38. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  39. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  40. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  41. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  42. package/plugins/dsh/lib/command.d.ts +38 -0
  43. package/plugins/dsh/lib/command.d.ts.map +1 -0
  44. package/plugins/dsh/lib/command.js +87 -0
  45. package/plugins/dsh/lib/credentials.d.ts +39 -0
  46. package/plugins/dsh/lib/credentials.d.ts.map +1 -0
  47. package/plugins/dsh/lib/credentials.js +57 -0
  48. package/plugins/dsh/lib/delphi-config.d.ts +129 -0
  49. package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
  50. package/plugins/dsh/lib/delphi-config.js +369 -0
  51. package/plugins/dsh/lib/delphi-run.d.ts +24 -0
  52. package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
  53. package/plugins/dsh/lib/delphi-run.js +113 -0
  54. package/plugins/dsh/lib/delphi.d.ts +94 -0
  55. package/plugins/dsh/lib/delphi.d.ts.map +1 -0
  56. package/plugins/dsh/lib/delphi.js +207 -0
  57. package/plugins/dsh/lib/dialect.d.ts +24 -0
  58. package/plugins/dsh/lib/dialect.d.ts.map +1 -0
  59. package/plugins/dsh/lib/dialect.js +36 -0
  60. package/plugins/dsh/lib/evidence.d.ts +70 -0
  61. package/plugins/dsh/lib/evidence.d.ts.map +1 -0
  62. package/plugins/dsh/lib/evidence.js +92 -0
  63. package/plugins/dsh/lib/gate-runner.d.ts +49 -0
  64. package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
  65. package/plugins/dsh/lib/gate-runner.js +95 -0
  66. package/plugins/dsh/lib/git-context.d.ts +26 -0
  67. package/plugins/dsh/lib/git-context.d.ts.map +1 -0
  68. package/plugins/dsh/lib/git-context.js +30 -0
  69. package/plugins/dsh/lib/hooks.d.ts +42 -0
  70. package/plugins/dsh/lib/hooks.d.ts.map +1 -0
  71. package/plugins/dsh/lib/hooks.js +113 -0
  72. package/plugins/dsh/lib/index.d.ts +20 -0
  73. package/plugins/dsh/lib/index.d.ts.map +1 -0
  74. package/plugins/dsh/lib/index.js +430 -0
  75. package/plugins/dsh/lib/skill-provider.d.ts +15 -0
  76. package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
  77. package/plugins/dsh/lib/skill-provider.js +205 -0
  78. package/plugins/dsh/lib/sprint.d.ts +60 -0
  79. package/plugins/dsh/lib/sprint.d.ts.map +1 -0
  80. package/plugins/dsh/lib/sprint.js +92 -0
  81. package/plugins/dsh/package.json +14 -8
  82. package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  84. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  85. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/plugins/dsh/src/command-single-form.test.ts +67 -0
  87. package/plugins/dsh/src/command.test.ts +21 -17
  88. package/plugins/dsh/src/command.ts +19 -21
  89. package/plugins/dsh/src/compat.test.ts +200 -0
  90. package/plugins/dsh/src/contract.test.ts +7 -1
  91. package/plugins/dsh/src/credentials.test.ts +93 -0
  92. package/plugins/dsh/src/credentials.ts +71 -0
  93. package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
  94. package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
  95. package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
  96. package/plugins/dsh/src/delphi-config.ts +146 -53
  97. package/plugins/dsh/src/delphi-run.ts +27 -4
  98. package/plugins/dsh/src/delphi.ts +133 -65
  99. package/plugins/dsh/src/evidence.test.ts +74 -0
  100. package/plugins/dsh/src/evidence.ts +141 -0
  101. package/plugins/dsh/src/gate-runner.test.ts +14 -6
  102. package/plugins/dsh/src/gate-runner.ts +68 -5
  103. package/plugins/dsh/src/git-context.test.ts +39 -0
  104. package/plugins/dsh/src/git-context.ts +38 -0
  105. package/plugins/dsh/src/hooks.test.ts +87 -0
  106. package/plugins/dsh/src/hooks.ts +126 -0
  107. package/plugins/dsh/src/index.test.ts +63 -6
  108. package/plugins/dsh/src/index.ts +340 -13
  109. package/plugins/dsh/src/skill-provider.test.ts +194 -0
  110. package/plugins/dsh/src/skill-provider.ts +231 -0
  111. package/plugins/dsh/src/sprint.test.ts +64 -0
  112. package/plugins/dsh/src/sprint.ts +122 -0
  113. package/plugins/dsh/src/tri-state.test.ts +123 -0
  114. package/plugins/opencode/index.ts +192 -58
  115. package/plugins/opencode/package.json +1 -1
  116. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  118. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  119. package/plugins/qoder/plugin.json +1 -1
  120. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  122. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  123. package/principles/AGENTS.md +3 -3
  124. package/principles/__tests__/analyze-read-failure.test.ts +70 -0
  125. package/principles/__tests__/baseline-prune.test.ts +61 -0
  126. package/principles/__tests__/cli-files-separators.test.ts +69 -0
  127. package/principles/__tests__/config-merge-warnings.test.ts +148 -0
  128. package/principles/adapters/java.ts +22 -3
  129. package/principles/adapters/typescript.ts +70 -29
  130. package/principles/analyzer.ts +36 -3
  131. package/principles/baseline-prune.ts +19 -7
  132. package/principles/config.ts +56 -35
  133. package/principles/index.ts +122 -40
  134. package/principles/rules/clean-code/deep-nesting.ts +36 -22
  135. package/principles/rules/clean-code/unused-imports.ts +30 -19
  136. package/principles/rules/solid/dip.ts +30 -20
  137. package/principles/rules/solid/lsp.ts +17 -2
  138. package/principles/rules/solid/ocp.ts +33 -23
  139. package/principles/rules/solid/srp.ts +65 -23
  140. package/scripts/delphi-external-review.cjs +25 -2
  141. package/scripts/sync-package-content.js +28 -1
  142. package/skills/delphi-review/AGENTS.md +3 -3
  143. package/skills/sprint-flow/AGENTS.md +3 -3
  144. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -0,0 +1,1031 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # GATE 13: Compiler Strictness Check
4
+ #
5
+ # The compiler is the first and cheapest static analysis tool for any compiled
6
+ # language, and XP-Gate historically never used it that way: `cpp.sh` only runs
7
+ # clang-tidy/cppcheck over the first 20 files, and Gate 9's build-integrity half
8
+ # is TypeScript-and-Maven only. A measured `g++ -Wall -Wextra` pass over an
9
+ # innocuous-looking translation unit reports -Wnon-virtual-dtor, -Wreorder,
10
+ # -Wparentheses, -Wclass-memaccess, -Wold-style-cast and -Wswitch-default —
11
+ # real defects no linter in the chain is positioned to see. Bare `javac`
12
+ # likewise reports NOTHING for raw types; `-Xlint:all` does, and an identical
13
+ # Java file went from "0 diagnostics" to a hard error under `-Werror`.
14
+ #
15
+ # Design: docs/plans/2026-10-26-gate-13-compiler-strictness-design.md
16
+ # Requirement rounds 1-3 (.sprint-state/) — review findings folded in below are
17
+ # tagged with their review id (CI-xx architecture, MC-xx technical, FC-xx
18
+ # feasibility).
19
+ #
20
+ # ── VERDICT CONTRACT ────────────────────────────────────────────────────────
21
+ # Inherited from pre-commit's established java.sh / powershell.sh protocol (#507):
22
+ # 0 = PASS every compiled file clean or within its baseline
23
+ # 1 = FAIL new warnings on changed files (a real finding) -> commit blocked
24
+ # 2 = BLOCK tool RAN but its output is untrustworthy (fail-closed)
25
+ # 3 = WARN compiler absent / cross-compiling / timeout / warn-only findings
26
+ # -> counted IN the scoring denominator, never PASS
27
+ #
28
+ # A TIMEOUT is always 3, never 2 (architecture MA-03): a timeout is incomplete
29
+ # coverage, not a wrong conclusion, and promoting it to BLOCK would turn machine
30
+ # load into a failed commit. 2 is reserved for the case where the tool finished
31
+ # but contradicted itself or emitted unparseable output.
32
+ #
33
+ # Reasons cross the command-substitution subshell as an `XP_G13_REASON: ...`
34
+ # stdout line — an `export` inside this file can NEVER reach the caller, whose
35
+ # `$(...)` discards the child environment.
36
+ # ============================================================================
37
+
38
+ # ── Verdict codes (pinned by the bats suite) ────────────────────────────────
39
+ # shellcheck disable=SC2034 # public contract: pre-commit reads these by name
40
+ GATE13_PASS=0
41
+ # shellcheck disable=SC2034 # emitted from SLICE-2 onward (C/C++ warning findings)
42
+ GATE13_FAIL=1
43
+ # shellcheck disable=SC2034 # public contract
44
+ GATE13_BLOCK=2
45
+ # shellcheck disable=SC2034 # public contract
46
+ GATE13_WARN=3
47
+
48
+ GATE13_DEFAULT_TIMEOUT=120
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Reason channel. Mirrors java.sh's XP_G1_REASON protocol so pre-commit can
52
+ # surface a SKIP/WARN cause without losing the verdict code.
53
+ #
54
+ # Written to stderr, NOT stdout: the helpers below are also called in command
55
+ # substitution (`budget=$(gate13_timeout_budget)`), and a stdout reason would be
56
+ # captured INTO the value — turning a valid budget into "XP_G13_REASON: ... 120"
57
+ # and breaking every numeric consumer. pre-commit reads the reason with 2>&1.
58
+ # ---------------------------------------------------------------------------
59
+ gate13_reason() {
60
+ printf 'XP_G13_REASON: %s\n' "$*" >&2
61
+ }
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # AC-007-01 — timeout budget.
65
+ # Precedence (feasibility FC-02): XP_GATE_COMPILER_TIMEOUT > .xp-gate/compiler-gate-timeout
66
+ # > 120s. The env var wins so CI can raise the budget for a large changeset
67
+ # without editing a tracked file. An unreadable/invalid override WARNs and falls
68
+ # back — an unreadable override must never silently disable the budget.
69
+ # ---------------------------------------------------------------------------
70
+ gate13_timeout_budget() {
71
+ local file=".xp-gate/compiler-gate-timeout" val=""
72
+
73
+ if [ -n "${XP_GATE_COMPILER_TIMEOUT:-}" ]; then
74
+ val=$(printf '%s' "$XP_GATE_COMPILER_TIMEOUT" | tr -d '[:space:]')
75
+ case "$val" in
76
+ ''|*[!0-9]*|0)
77
+ gate13_reason "invalid XP_GATE_COMPILER_TIMEOUT='${XP_GATE_COMPILER_TIMEOUT}' (expected a positive integer), falling back to ${GATE13_DEFAULT_TIMEOUT}s"
78
+ echo "$GATE13_DEFAULT_TIMEOUT"
79
+ return 0
80
+ ;;
81
+ *) echo "$val"; return 0 ;;
82
+ esac
83
+ fi
84
+
85
+ if [ -f "$file" ]; then
86
+ val=$(tr -d '[:space:]' < "$file")
87
+ fi
88
+
89
+ case "$val" in
90
+ '')
91
+ echo "$GATE13_DEFAULT_TIMEOUT"
92
+ ;;
93
+ *[!0-9]*|0)
94
+ gate13_reason "invalid $file value '${val}' (expected a positive integer), falling back to ${GATE13_DEFAULT_TIMEOUT}s"
95
+ echo "$GATE13_DEFAULT_TIMEOUT"
96
+ ;;
97
+ *) echo "$val" ;;
98
+ esac
99
+ }
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # AC-007-02 / AC-006-03 — timeout classification. Always WARN, and the output
103
+ # must be visually distinguishable from "clean" so a reader never mistakes
104
+ # incomplete coverage for a pass.
105
+ # ---------------------------------------------------------------------------
106
+ gate13_classify_timeout() {
107
+ echo "TIMEOUT — coverage incomplete (the compiler did not finish within the budget); verdict treated as WARN, not a finding"
108
+ return "$GATE13_WARN"
109
+ }
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # AC-006-03 — the ONLY path that yields verdict 2. The tool ran, but produced
113
+ # output that contradicts itself or cannot be interpreted, so no conclusion can
114
+ # be drawn. Missing tools never come here (they are 3).
115
+ # ---------------------------------------------------------------------------
116
+ gate13_classify_unusable() {
117
+ echo "BLOCK (fail-closed) — the compiler ran but its output is untrustworthy; refusing to report success"
118
+ return "$GATE13_BLOCK"
119
+ }
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # AC-001-03 — cross-compilation predicate. Pure: takes a compiler name/command,
123
+ # returns 0 (cross) or 1 (host). Three explicit conditions (architecture CI-01),
124
+ # each independently testable:
125
+ # 1. CROSS_COMPILE is non-empty
126
+ # 2. the basename matches ^(prefix-)?(gcc|g++|cc|c++|clang|clang++)(-ver)?$
127
+ # with a NON-EMPTY prefix (arm-linux-gnueabihf-g++, aarch64-none-elf-gcc)
128
+ # 3. the target triple differs from the host triple (needs -dumpmachine)
129
+ #
130
+ # Explicitly answered in review: an empty CROSS_COMPILE with
131
+ # CC=arm-linux-gnueabihf-gcc IS cross-compilation — condition 2 catches it.
132
+ # ---------------------------------------------------------------------------
133
+ gate13_is_cross_compiling() {
134
+ local cc="${1:-}"
135
+ local base prefix
136
+
137
+ if [ -n "${CROSS_COMPILE:-}" ]; then
138
+ return 0
139
+ fi
140
+
141
+ base=$(basename -- "$cc" 2>/dev/null || printf '%s' "$cc")
142
+ # Strip a trailing -<version> / -<major.minor> so g++-13 is host, not cross.
143
+ base=$(printf '%s' "$base" | sed -E 's/-[0-9]+(\.[0-9]+)*$//')
144
+
145
+ # prefix-<tool>. `gcc` itself has no prefix; `<prefix>-gcc` does.
146
+ if printf '%s' "$base" | grep -Eq '^(.+)-(gcc|g\+\+|cc|c\+\+|clang|clang\+\+)$'; then
147
+ prefix=$(printf '%s' "$base" | sed -E 's/^(.+)-(gcc|g\+\+|cc|c\+\+|clang|clang\+\+)$/\1/')
148
+ # A leading path component was already stripped by basename, so any prefix
149
+ # left here is a toolchain prefix.
150
+ if [ -n "$prefix" ]; then
151
+ return 0
152
+ fi
153
+ fi
154
+
155
+ # Known host-style names with no prefix at all.
156
+ case "$base" in
157
+ gcc|g++|cc|c++|clang|clang++) return 1 ;;
158
+ esac
159
+
160
+ # Anything else (e.g. an absolute path to a foreign toolchain) — cond 3.
161
+ if [ -x "$cc" ] && command -v "$cc" >/dev/null 2>&1; then
162
+ local target host
163
+ target=$("$cc" -dumpmachine 2>/dev/null)
164
+ host=$(uname -m 2>/dev/null)
165
+ if [ -n "$target" ] && [ -n "$host" ]; then
166
+ case "$target" in
167
+ "$host"*) return 1 ;;
168
+ *) return 0 ;;
169
+ esac
170
+ fi
171
+ fi
172
+
173
+ return 1
174
+ }
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Usability probe — `command -v` alone is NOT enough. A broken shim, a missing
178
+ # shared library, or a PATH entry for the wrong architecture all pass `command
179
+ # -v` and then fail at the first real invocation. Treating that as "compiler
180
+ # present" would route to the analysis stage and report a spurious failure (or,
181
+ # worse, no failure at all). Ask the compiler to identify itself; anything but
182
+ # a clean exit means "not usable".
183
+ # ---------------------------------------------------------------------------
184
+ gate13_compiler_usable() {
185
+ local cc="$1"
186
+ "$cc" --version >/dev/null 2>&1
187
+ }
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # AC-001-01 / AC-001-04 — compiler selection.
191
+ # Order (REQ-001): $CXX > Makefile CXX := > PATH (g++ > clang++ > c++).
192
+ # A user-named compiler that cannot be used is REPORTED, never silently replaced
193
+ # (AC-001-04) — a silent swap would compile the wrong code with the wrong flags
194
+ # and still print PASS.
195
+ #
196
+ # Prints the selected compiler path on stdout and the selection basis on stderr
197
+ # so callers can capture the path without the narrative.
198
+ # ---------------------------------------------------------------------------
199
+ gate13_select_cxx() {
200
+ local cand=""
201
+
202
+ # 1. Environment — this is what make itself would use.
203
+ if [ -n "${CXX:-}" ]; then
204
+ if command -v "$CXX" >/dev/null 2>&1 && gate13_compiler_usable "$CXX"; then
205
+ echo "compiler selected: $CXX (basis: \$CXX environment variable)" >&2
206
+ printf '%s\n' "$CXX"
207
+ return 0
208
+ fi
209
+ gate13_reason "explicitly configured CXX='${CXX}' is not runnable; refusing to substitute a different compiler"
210
+ echo "compiler selected: none (basis: \$CXX='${CXX}' is unusable)" >&2
211
+ return 1
212
+ fi
213
+
214
+ # 2. Makefile assignment — the project's own declared compiler.
215
+ if [ -f "Makefile" ]; then
216
+ cand=$(sed -nE 's/^[[:space:]]*CXX[[:space:]]*[:?]?=[[:space:]]*([^[:space:]#]+).*/\1/p' Makefile 2>/dev/null | head -1)
217
+ if [ -n "$cand" ] && command -v "$cand" >/dev/null 2>&1 && gate13_compiler_usable "$cand"; then
218
+ echo "compiler selected: $cand (basis: Makefile CXX assignment)" >&2
219
+ printf '%s\n' "$cand"
220
+ return 0
221
+ fi
222
+ fi
223
+
224
+ # 3. PATH lookup, most standard first.
225
+ for cand in g++ clang++ c++; do
226
+ if command -v "$cand" >/dev/null 2>&1 && gate13_compiler_usable "$cand"; then
227
+ echo "compiler selected: $cand (basis: PATH lookup)" >&2
228
+ printf '%s\n' "$cand"
229
+ return 0
230
+ fi
231
+ done
232
+
233
+ echo "compiler selected: none (basis: no usable C++ compiler on PATH)" >&2
234
+ return 1
235
+ }
236
+
237
+ # Same order for C, so a pure-C project is not forced through a C++ driver.
238
+ gate13_select_cc() {
239
+ local cand=""
240
+
241
+ if [ -n "${CC:-}" ]; then
242
+ if command -v "$CC" >/dev/null 2>&1 && gate13_compiler_usable "$CC"; then
243
+ printf '%s\n' "$CC"
244
+ return 0
245
+ fi
246
+ gate13_reason "explicitly configured CC='${CC}' is not runnable; refusing to substitute a different compiler"
247
+ return 1
248
+ fi
249
+
250
+ if [ -f "Makefile" ]; then
251
+ cand=$(sed -nE 's/^[[:space:]]*CC[[:space:]]*[:?]?=[[:space:]]*([^[:space:]#]+).*/\1/p' Makefile 2>/dev/null | head -1)
252
+ if [ -n "$cand" ] && command -v "$cand" >/dev/null 2>&1 && gate13_compiler_usable "$cand"; then
253
+ printf '%s\n' "$cand"
254
+ return 0
255
+ fi
256
+ fi
257
+
258
+ for cand in gcc clang cc; do
259
+ if command -v "$cand" >/dev/null 2>&1 && gate13_compiler_usable "$cand"; then
260
+ printf '%s\n' "$cand"
261
+ return 0
262
+ fi
263
+ done
264
+
265
+ return 1
266
+ }
267
+
268
+ # ---------------------------------------------------------------------------
269
+ # AC-008-02 — emergency kill switch, orthogonal to the strictness tier
270
+ # (feasibility FC-03). A gate that cannot be disabled during a misreport storm
271
+ # gets disabled permanently by `--no-verify`, which the Code of Conduct forbids —
272
+ # so the sanctioned escape hatch is an explicit, audited SKIP.
273
+ # ---------------------------------------------------------------------------
274
+ gate13_is_disabled() {
275
+ [ "${XP_GATE_DISABLE_13:-}" = "1" ]
276
+ }
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # AC-002-01 — strictness tier resolution.
280
+ # Values: strict | balanced (default) | warn-only. An unrecognised value WARNs
281
+ # and falls back to balanced rather than silently choosing a stricter or looser
282
+ # policy than the user asked for.
283
+ # ---------------------------------------------------------------------------
284
+ gate13_resolve_tier() {
285
+ local tier="${1:-${XP_GATE_COMPILER_STRICTNESS:-balanced}}"
286
+ case "$tier" in
287
+ strict|balanced|warn-only) printf '%s\n' "$tier" ;;
288
+ *)
289
+ gate13_reason "unknown XP_GATE_COMPILER_STRICTNESS='${tier}' (expected strict|balanced|warn-only), falling back to balanced"
290
+ printf 'balanced\n'
291
+ ;;
292
+ esac
293
+ }
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # AC-002-01 — the flag set for a tier and language.
297
+ #
298
+ # balanced covers the highest-value real-bug classes from the GCC manual with a
299
+ # low false-positive cost. strict adds -Wconversion/-Wsign-conversion, which are
300
+ # very noisy in numeric code, hence opt-in. The C++-only flags are separated so a
301
+ # C translation unit is never handed -Wold-style-cast (a C++ diagnostic gcc would
302
+ # reject as unrecognised for C with -Werror).
303
+ # ---------------------------------------------------------------------------
304
+ gate13_cpp_flags() {
305
+ local tier lang base strict common
306
+ tier=$(gate13_resolve_tier "${1:-}")
307
+ lang="${2:-cpp}"
308
+
309
+ base="-Wall -Wextra"
310
+
311
+ if [ "$tier" = "balanced" ] || [ "$tier" = "warn-only" ]; then
312
+ printf '%s\n' "$base"
313
+ return 0
314
+ fi
315
+
316
+ # strict
317
+ common="-Wpedantic -Wshadow -Wformat=2 -Wcast-qual -Wcast-align -Wnull-dereference -Wswitch-enum"
318
+ if [ "$lang" = "c" ]; then
319
+ printf '%s %s\n' "$base" "$common"
320
+ else
321
+ printf '%s %s -Wconversion -Wsign-conversion -Wold-style-cast -Wuseless-cast -Wnon-virtual-dtor -Woverloaded-virtual\n' "$base" "$common"
322
+ fi
323
+ }
324
+
325
+ # ---------------------------------------------------------------------------
326
+ # AC-002-02 / AC-002-03 — capability probe.
327
+ #
328
+ # One clean throwaway translation unit, each candidate flag tried ALONE in the
329
+ # POSITIVE form. Measured: gcc reports `unrecognized command-line option '-Wfoo'`
330
+ # for an unknown positive flag, but accepts an unknown `-Wno-foo` silently (a
331
+ # `cc1plus: note:` appears only when other diagnostics exist). Probing the
332
+ # negative form would therefore "succeed" for every flag and the gate would claim
333
+ # checks it never ran.
334
+ #
335
+ # Caching (architecture MA-01): results live in GATE13_PROBE_CACHE for the
336
+ # lifetime of one hook run — a persisted cache would go stale on compiler upgrade.
337
+ # Emits the ACCEPTED flags on stdout (space separated) and `dropped flag:` lines
338
+ # on stderr.
339
+ # ---------------------------------------------------------------------------
340
+ gate13_flag_supported() {
341
+ local cc="$1" flag="$2" lang="${3:-cpp}" probe_tu out
342
+ # The cache key carries the language (walkthrough R2 MA-05): gcc accepts
343
+ # -Wold-style-cast for C++ and rejects it for C, so a key without the language
344
+ # would let a C probe inherit the C++ answer.
345
+ local key="${cc}|${lang}|${flag}"
346
+
347
+ # Two separate caches, checked separately. A single "seen this key" cache was
348
+ # wrong (walkthrough MA-02): once any flag was dropped, every cached key
349
+ # returned "supported", re-enabling flags the compiler had already rejected.
350
+ case " ${GATE13_PROBE_DROPPED:-} " in
351
+ *" ${key} "*) return 1 ;;
352
+ esac
353
+ case " ${GATE13_PROBE_CACHE:-} " in
354
+ *" ${key} "*) return 0 ;;
355
+ esac
356
+
357
+ probe_tu="${GATE13_PROBE_TU:-}"
358
+ if [ -z "$probe_tu" ] || [ ! -f "$probe_tu" ]; then
359
+ probe_tu="${TMPDIR:-/tmp}/gate13_probe_$$.cpp"
360
+ printf 'int main() { return 0; }\n' > "$probe_tu"
361
+ GATE13_PROBE_TU="$probe_tu"
362
+ fi
363
+
364
+ if out=$("$cc" -fsyntax-only "$flag" "$probe_tu" 2>&1); then
365
+ GATE13_PROBE_CACHE="${GATE13_PROBE_CACHE:-} ${key}"
366
+ return 0
367
+ fi
368
+
369
+ case "$out" in
370
+ *"unrecognized command-line option"*|*"unknown warning option"*|*"unknown argument"*)
371
+ GATE13_PROBE_DROPPED="${GATE13_PROBE_DROPPED:-} ${key}"
372
+ return 1
373
+ ;;
374
+ esac
375
+
376
+ # The flag compiled but failed for some other reason (bad probe TU, missing
377
+ # system headers). Assume support: dropping a working flag silently loses
378
+ # coverage, and a genuinely unsupported flag is caught by the pattern above.
379
+ GATE13_PROBE_CACHE="${GATE13_PROBE_CACHE:-} ${key}"
380
+ return 0
381
+ }
382
+
383
+ gate13_probe_flags() {
384
+ local cc="$1" lang="$2"
385
+ shift 2
386
+ local flag accepted="" dropped=""
387
+
388
+ for flag in "$@"; do
389
+ if gate13_flag_supported "$cc" "$flag" "$lang"; then
390
+ accepted="$accepted $flag"
391
+ else
392
+ dropped="$dropped $flag"
393
+ printf 'dropped flag: %s (unsupported by %s)\n' "$flag" "$(gate13_compiler_id "$cc")" >&2
394
+ fi
395
+ done
396
+
397
+ printf '%s\n' "$(printf '%s' "$accepted" | sed 's/^ *//')"
398
+ }
399
+
400
+ gate13_compiler_id() {
401
+ local cc="$1" v
402
+ v=$("$cc" --version 2>/dev/null | head -1)
403
+ printf '%s' "${v:-$cc}"
404
+ }
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # AC-003-04 — the changed set. Staged files only, matching Gate 1/12.
408
+ # Deleted files are excluded: they cannot be compiled.
409
+ # ---------------------------------------------------------------------------
410
+ gate13_changed_cpp_files() {
411
+ git diff --cached --name-only --diff-filter=d -- '*.c' '*.cc' '*.cpp' '*.cxx' '*.C' 2>/dev/null
412
+ }
413
+
414
+ # Emits "<status>\t<file>" where status is A (added) or M (modified/other).
415
+ gate13_classify_changed() {
416
+ local f status
417
+ while IFS= read -r f; do
418
+ [ -n "$f" ] || continue
419
+ status=$(git diff --cached --name-status --diff-filter=ACMR -- "$f" 2>/dev/null | awk '{print $1}' | head -1)
420
+ case "$status" in
421
+ A) printf 'added\t%s\n' "$f" ;;
422
+ *) printf 'modified\t%s\n' "$f" ;;
423
+ esac
424
+ done <<EOF
425
+ $(gate13_changed_cpp_files)
426
+ EOF
427
+ }
428
+
429
+ # ---------------------------------------------------------------------------
430
+ # AC-003-02 — baseline storage. In-repo, mirroring Gate 6's .warnings-baseline.json
431
+ # precedent (technical CRITICAL-1 / feasibility FC-01). A local cache was
432
+ # rejected because it makes CI and developer verdicts diverge.
433
+ # The HOOK NEVER WRITES THIS FILE — that is the precise meaning of
434
+ # "baseline only grows" (architecture CI-03): a hook that silently rewrote the
435
+ # baseline could excuse its own regressions.
436
+ # ---------------------------------------------------------------------------
437
+ gate13_baseline_path() {
438
+ printf '%s\n' ".xp-gate/compiler-warnings-baseline.json"
439
+ }
440
+
441
+ # Prints the recorded warning count for a file, or empty when there is no
442
+ # baseline / no entry for that file.
443
+ gate13_baseline_count() {
444
+ local file="$1" path count
445
+ path=$(gate13_baseline_path)
446
+ [ -f "$path" ] || return 0
447
+
448
+ if command -v node >/dev/null 2>&1; then
449
+ count=$(node -e '
450
+ const fs = require("fs");
451
+ try {
452
+ const j = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
453
+ const e = (j.files || {})[process.argv[2]];
454
+ if (e && typeof e.count === "number") process.stdout.write(String(e.count));
455
+ } catch (_) { /* unreadable baseline == no baseline */ }
456
+ ' "$path" "$file" 2>/dev/null)
457
+ fi
458
+ printf '%s' "$count"
459
+ }
460
+
461
+ gate13_baseline_exists() {
462
+ [ -f "$(gate13_baseline_path)" ]
463
+ }
464
+
465
+ # ---------------------------------------------------------------------------
466
+ # Compile one translation unit and count its diagnostics.
467
+ # -fsyntax-only: no artifacts, measured to write nothing into the tree.
468
+ # Prints the diagnostics on stderr; the COUNT goes to stdout.
469
+ # ---------------------------------------------------------------------------
470
+ gate13_count_warnings() {
471
+ local cc="$1" flags="$2" file="$3" out
472
+ # shellcheck disable=SC2086 # flags is a deliberately word-split flag list
473
+ out=$("$cc" -fsyntax-only $flags "$file" 2>&1)
474
+ printf '%s\n' "$out" | grep -E 'warning:' | grep -vcE '^\s*$' || true
475
+ }
476
+
477
+ # ============================================================================
478
+ # JAVA PATH (REQ-004)
479
+ #
480
+ # Measured motivation: `List raw = l;` in a Java file produces ZERO diagnostics
481
+ # from bare javac, `[rawtypes]` under -Xlint:all, and a hard error under
482
+ # -Xlint:all -Werror. Gate 9's `mvn -q -DskipTests compile` suppresses the very
483
+ # warnings a strict gate exists to read.
484
+ # ============================================================================
485
+
486
+ # ---------------------------------------------------------------------------
487
+ # AC-004-01 — Maven arguments.
488
+ #
489
+ # `-B` (batch mode) NOT `-q`: -q suppresses non-error diagnostics, so "run mvn
490
+ # quietly" and "parse the warning lines" are mutually exclusive — a real
491
+ # self-contradiction caught in requirement round 2 (architecture CI-02).
492
+ # `-o` keeps a hook from reaching the network (REQ-007).
493
+ # `compilerArgument` is a SINGLE appended argument; -Xlint:all,-processing is
494
+ # one token precisely because of that (technical MC-01).
495
+ # ---------------------------------------------------------------------------
496
+ gate13_maven_args() {
497
+ printf '%s\n' "-B -o -DskipTests compile -Dmaven.compiler.showWarnings=true -Dmaven.compiler.showDeprecation=true -Dmaven.compiler.compilerArgument=-Xlint:all,-processing"
498
+ }
499
+
500
+ gate13_javac_args() {
501
+ printf '%s\n' "-Xlint:all,-processing -d"
502
+ }
503
+
504
+ gate13_java_sources() {
505
+ git diff --cached --name-only --diff-filter=d -- '*.java' 2>/dev/null
506
+ }
507
+
508
+ # ---------------------------------------------------------------------------
509
+ # AC-004-02 — is the command-line Maven injection likely to take effect?
510
+ #
511
+ # Returns 0 = reliable, 1 = unreliable. Three known failure modes, each of which
512
+ # silently drops the lint flags rather than failing loudly:
513
+ # 1. the pom already declares <compilerArgs> — a CLI single-arg append is
514
+ # overridden by the explicit configuration (technical MC-01)
515
+ # 2. maven-compiler-plugin < 3.1 — compilerArgument semantics were not stable
516
+ # (feasibility FC-02)
517
+ # 3. no plugin version declared at all — the effective version is inherited or
518
+ # defaulted and cannot be verified, so do not assume it works
519
+ #
520
+ # On "unreliable" the gate degrades to warn-only with a loud banner. It never
521
+ # fakes a PASS and never emits an unpredictable BLOCK.
522
+ # ---------------------------------------------------------------------------
523
+ # shellcheck disable=SC2016 # this function matches LITERAL ${...} in a pom, not expansions
524
+ gate13_maven_injection_reliable() {
525
+ local pom="${1:-pom.xml}" ver
526
+
527
+ [ -f "$pom" ] || return 1
528
+
529
+ if grep -q '<compilerArgs>' "$pom" 2>/dev/null; then
530
+ printf 'pom already declares <compilerArgs>; a command-line -Dmaven.compiler.compilerArgument may be overridden\n' >&2
531
+ return 1
532
+ fi
533
+
534
+ # Extract the version belonging to the maven-compiler-plugin block.
535
+ # Walkthrough R2 MA-04: a range match to the first </plugin> could pick up a
536
+ # neighbouring plugin's version, and a strict numeric pattern missed
537
+ # `3.13.0-SNAPSHOT`. Anchor on the plugin's own artifactId, then take the FIRST
538
+ # <version> that follows it, and accept a qualifier suffix.
539
+ ver=$(awk '
540
+ /<artifactId>[[:space:]]*maven-compiler-plugin[[:space:]]*<\/artifactId>/ { found=1 }
541
+ found && /<version>/ {
542
+ line=$0
543
+ sub(/.*<version>[[:space:]]*/, "", line)
544
+ sub(/[[:space:]]*<\/version>.*/, "", line)
545
+ print line
546
+ exit
547
+ }
548
+ ' "$pom" 2>/dev/null)
549
+
550
+ if [ -z "$ver" ]; then
551
+ printf 'maven-compiler-plugin version is not declared; injection cannot be verified\n' >&2
552
+ return 1
553
+ fi
554
+
555
+ # A property placeholder cannot be resolved statically — treat as unverified
556
+ # rather than guessing (walkthrough R2 MA-04).
557
+ case "$ver" in
558
+ *'${'*)
559
+ printf 'maven-compiler-plugin version is a property reference (%s); cannot be verified statically\n' "$ver" >&2
560
+ return 1
561
+ ;;
562
+ esac
563
+
564
+ # Drop a qualifier suffix (3.13.0-SNAPSHOT -> 3.13.0) before comparing.
565
+ ver=${ver%%-*}
566
+
567
+ local major minor
568
+ major=${ver%%.*}
569
+ minor=${ver#*.}
570
+ minor=${minor%%.*}
571
+
572
+ if [ "$major" -lt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -lt 1 ]; }; then
573
+ printf 'maven-compiler-plugin %s predates stable compilerArgument support (needs >= 3.1)\n' "$ver" >&2
574
+ return 1
575
+ fi
576
+
577
+ return 0
578
+ }
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # AC-004-03 — count javac diagnostics for a file under the strict lint set.
582
+ # Verified by measurement: bare javac reports nothing for a raw type, this
583
+ # reports [rawtypes].
584
+ # ---------------------------------------------------------------------------
585
+ gate13_javac_warning_count() {
586
+ local file="$1" out tmpd rc=0 budget
587
+ command -v javac >/dev/null 2>&1 || { printf '0\n'; return 0; }
588
+
589
+ tmpd="${TMPDIR:-/tmp}/gate13_javac_$$"
590
+ mkdir -p "$tmpd"
591
+ budget=$(gate13_timeout_budget)
592
+
593
+ # Timeout-bounded (walkthrough MC-03): a pathological file can hang javac, and
594
+ # a pre-commit hook must never hang with it. `timeout` may be absent on some
595
+ # systems, in which case we run unbounded rather than skipping the check.
596
+ if command -v timeout >/dev/null 2>&1; then
597
+ out=$(timeout "$budget" javac -Xlint:all,-processing -d "$tmpd" "$file" 2>&1)
598
+ rc=$?
599
+ else
600
+ out=$(javac -Xlint:all,-processing -d "$tmpd" "$file" 2>&1)
601
+ rc=$?
602
+ fi
603
+ rm -rf "$tmpd"
604
+
605
+ # timeout(1) reports 124 when it fired. Returning 0 here would be
606
+ # indistinguishable from "compiled clean" (walkthrough round 2, CI-02), so
607
+ # signal incompleteness with a dedicated non-numeric marker the caller checks.
608
+ if [ "$rc" -eq 124 ]; then
609
+ printf 'TIMEOUT\n'
610
+ return 0
611
+ fi
612
+
613
+ # javac prefixes each warning with "warning:" (localised builds may differ), so
614
+ # also count the "[category]" marker lines that every -Xlint diagnostic emits.
615
+ printf '%s\n' "$out" | grep -cE 'warning:|\[(rawtypes|unchecked|deprecation|fallthrough|finally|cast|static|try|overrides|overloads|serial|synchronization)\]' || true
616
+ }
617
+
618
+ # ---------------------------------------------------------------------------
619
+ # Java analysis stage. Verdict semantics mirror the C/C++ stage:
620
+ # 0 PASS, 1 FAIL, 2 BLOCK(untrustworthy), 3 WARN (no toolchain / degraded).
621
+ # ---------------------------------------------------------------------------
622
+ gate13_run_java_analysis() {
623
+ local srcs had_tool=0 degraded=0 files f cnt total=0
624
+
625
+ if gate13_is_disabled; then
626
+ gate13_reason "Gate 13 disabled via XP_GATE_DISABLE_13=1"
627
+ echo "SKIPPED - compiler strictness check (XP_GATE_DISABLE_13=1)"
628
+ return "$GATE13_WARN"
629
+ fi
630
+
631
+ srcs=$(gate13_java_sources)
632
+ if [ -z "$srcs" ]; then
633
+ return "$GATE13_PASS"
634
+ fi
635
+
636
+ if [ "${GATE13_FORCE_NO_JAVA_TOOLCHAIN:-}" = "1" ]; then
637
+ echo "WARN - no usable Java toolchain (mvn / gradle / javac); Java compiler checks NOT performed."
638
+ return "$GATE13_WARN"
639
+ fi
640
+
641
+ # ── Maven path ────────────────────────────────────────────────────────────
642
+ if [ -f "pom.xml" ]; then
643
+ if [ -x "./mvnw" ]; then
644
+ had_tool=1
645
+ elif command -v mvn >/dev/null 2>&1; then
646
+ had_tool=1
647
+ fi
648
+
649
+ if [ "$had_tool" -eq 1 ]; then
650
+ local mvn_bin="mvn"
651
+ [ -x "./mvnw" ] && mvn_bin="./mvnw"
652
+
653
+ if ! gate13_maven_injection_reliable "pom.xml" 2>/dev/null; then
654
+ echo " ⚠️ DEGRADED - Maven compiler-arg injection unreliable (see reasons below);"
655
+ echo " Java lint findings are REPORT-ONLY for this run (this is a degraded mode, not a normal warn-only run)."
656
+ degraded=1
657
+ fi
658
+
659
+ local args budget rc mvn_out mvn_log mvn_findings
660
+ args=$(gate13_maven_args)
661
+ budget=$(gate13_timeout_budget)
662
+ mvn_log="${TMPDIR:-/tmp}/gate13_mvn_$$.log"
663
+
664
+ # Capture BOTH streams. This is the whole point of REQ-004: `mvn compile`
665
+ # EXITS 0 even when javac emitted warnings, so a verdict derived from the
666
+ # exit code alone can never see a -Xlint finding. (Walkthrough review CI-01
667
+ # caught exactly that: the first implementation redirected to /dev/null and
668
+ # printed PASSED no matter what javac said.)
669
+ if command -v timeout >/dev/null 2>&1; then
670
+ # shellcheck disable=SC2086
671
+ timeout "$budget" "$mvn_bin" $args >"$mvn_log" 2>&1
672
+ else
673
+ # shellcheck disable=SC2086
674
+ "$mvn_bin" $args >"$mvn_log" 2>&1
675
+ fi
676
+ rc=$?
677
+ mvn_out=$(cat "$mvn_log" 2>/dev/null)
678
+ rm -f "$mvn_log"
679
+
680
+ if [ "$rc" -eq 124 ]; then
681
+ gate13_classify_timeout
682
+ return "$GATE13_WARN"
683
+ fi
684
+ if [ "$rc" -ne 0 ]; then
685
+ if [ "$degraded" -eq 1 ]; then
686
+ # We already know the lint flags were not applied, so a build failure
687
+ # here says nothing about compiler strictness — it is usually missing
688
+ # offline dependencies. Reporting it as FAIL would blame this gate for
689
+ # a build-environment problem (and would be a lie: nothing was linted).
690
+ echo " ⚠️ Java build failed (mvn exit ${rc}) in degraded mode — not attributed to Gate 13"
691
+ return "$GATE13_WARN"
692
+ fi
693
+ # A non-zero exit in a run where the lint flags WERE applied means javac
694
+ # found something (with -Werror in a project config) or the code does not
695
+ # compile. Either way it is a real finding, not an environment problem.
696
+ echo " ❌ Java compilation failed (mvn exit ${rc})"
697
+ printf '%s\n' "$mvn_out" | grep -E 'ERROR|WARNING' | head -10 | sed 's/^/ /'
698
+ return "$GATE13_FAIL"
699
+ fi
700
+
701
+ if [ "$degraded" -eq 1 ]; then
702
+ echo " ⚠️ DEGRADED - Java compiled but lint enforcement was NOT applied; no conclusion drawn"
703
+ return "$GATE13_WARN"
704
+ fi
705
+
706
+ # Count javac's lint output. Maven prefixes it with "[WARNING] ".
707
+ mvn_findings=$(printf '%s\n' "$mvn_out" \
708
+ | grep -cE '\[WARNING\].*\[(rawtypes|unchecked|deprecation|fallthrough|finally|cast|static|try|overrides|overloads|serial|synchronization|varargs|divzero|empty|exports|module|opens|options|path|processing|removal|requires|text-blocks|this-escape)\]' \
709
+ | tr -d '[:space:]')
710
+ mvn_findings=${mvn_findings:-0}
711
+
712
+ if [ "$mvn_findings" -gt 0 ]; then
713
+ printf '%s\n' "$mvn_out" \
714
+ | grep -E '\[WARNING\].*\[[a-z-]+\]' | head -10 | sed 's/^/ /'
715
+ echo " ❌ ${mvn_findings} javac -Xlint finding(s)"
716
+ return "$GATE13_FAIL"
717
+ fi
718
+
719
+ echo " ✅ PASSED - Java compiled with -Xlint:all,-processing (no findings)"
720
+ return "$GATE13_PASS"
721
+ fi
722
+ fi
723
+
724
+ # ── Gradle path ───────────────────────────────────────────────────────────
725
+ if [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
726
+ if [ -x "./gradlew" ] || command -v gradle >/dev/null 2>&1; then
727
+ local g_bin="gradle"
728
+ [ -x "./gradlew" ] && g_bin="./gradlew"
729
+ local budget g_log grc g_out g_findings
730
+ budget=$(gate13_timeout_budget)
731
+ g_log="${TMPDIR:-/tmp}/gate13_gradle_$$.log"
732
+
733
+ # Same reasoning as the Maven path: capture the output, because a
734
+ # successful `compileJava` still carries the javac warnings.
735
+ # NOTE: no -q here — it suppresses exactly the diagnostics we want, the
736
+ # same trap that made -q wrong for Maven.
737
+ if command -v timeout >/dev/null 2>&1; then
738
+ timeout "$budget" "$g_bin" compileJava --offline --warning-mode all >"$g_log" 2>&1
739
+ else
740
+ "$g_bin" compileJava --offline --warning-mode all >"$g_log" 2>&1
741
+ fi
742
+ grc=$?
743
+ g_out=$(cat "$g_log" 2>/dev/null)
744
+ rm -f "$g_log"
745
+
746
+ if [ "$grc" -eq 124 ]; then
747
+ gate13_classify_timeout
748
+ return "$GATE13_WARN"
749
+ fi
750
+ if [ "$grc" -ne 0 ]; then
751
+ echo " ❌ Java compilation failed (gradle exit ${grc})"
752
+ printf '%s\n' "$g_out" | grep -E 'error|warning' | head -10 | sed 's/^/ /'
753
+ return "$GATE13_FAIL"
754
+ fi
755
+
756
+ g_findings=$(printf '%s\n' "$g_out" \
757
+ | grep -cE '\[(rawtypes|unchecked|deprecation|fallthrough|cast|static|overrides|serial|synchronization)\]' \
758
+ | tr -d '[:space:]')
759
+ g_findings=${g_findings:-0}
760
+ if [ "$g_findings" -gt 0 ]; then
761
+ printf '%s\n' "$g_out" | grep -E 'warning:' | head -10 | sed 's/^/ /'
762
+ echo " ❌ ${g_findings} javac lint finding(s) (gradle compileJava)"
763
+ return "$GATE13_FAIL"
764
+ fi
765
+ echo " ✅ PASSED - Java compiled (gradle compileJava, no findings)"
766
+ return "$GATE13_PASS"
767
+ fi
768
+ fi
769
+
770
+ # ── Bare javac path ───────────────────────────────────────────────────────
771
+ if command -v javac >/dev/null 2>&1; then
772
+ local blocking=0 timed_out=0
773
+ while IFS= read -r f; do
774
+ [ -n "$f" ] || continue
775
+ cnt=$(gate13_javac_warning_count "$f")
776
+ # A timed-out file was NOT checked. Treating it as 0 warnings would report
777
+ # a pass for code nobody looked at (walkthrough R2 CI-02).
778
+ if [ "$cnt" = "TIMEOUT" ]; then
779
+ gate13_classify_timeout
780
+ echo " ⚠️ ${f}: javac timed out — not analysed"
781
+ timed_out=$((timed_out + 1))
782
+ continue
783
+ fi
784
+ cnt=${cnt:-0}
785
+ if [ "$cnt" -gt 0 ]; then
786
+ total=$((total + cnt))
787
+ echo " ❌ ${f}: ${cnt} javac -Xlint finding(s)"
788
+ blocking=$((blocking + 1))
789
+ fi
790
+ done <<EOF
791
+ $srcs
792
+ EOF
793
+ if [ "$blocking" -gt 0 ]; then
794
+ echo " ❌ BLOCKED - ${blocking} Java file(s) with -Xlint findings (${total} total)"
795
+ return "$GATE13_FAIL"
796
+ fi
797
+ if [ "$timed_out" -gt 0 ]; then
798
+ echo " ⚠️ WARN - ${timed_out} Java file(s) were not analysed (timeout); coverage incomplete"
799
+ return "$GATE13_WARN"
800
+ fi
801
+ echo " ✅ PASSED - Java compiler strictness (javac -Xlint:all,-processing)"
802
+ return "$GATE13_PASS"
803
+ fi
804
+
805
+ echo "WARN - no usable Java toolchain (mvn / gradle / javac); Java compiler checks NOT performed."
806
+ return "$GATE13_WARN"
807
+ }
808
+
809
+ # ============================================================================
810
+ # `-fanalyzer` PATH (REQ-005) — C files only, opt-in.
811
+ #
812
+ # Path-sensitive analysis that finds use-after-free / double-free / NULL
813
+ # dereference no warning flag reaches. OFF by default for three measured reasons:
814
+ # * speed — seconds per translation unit, not milliseconds
815
+ # * portability— GCC-only; clang errors on the flag
816
+ # * semantics — it emits NOTHING under -fsyntax-only (measured), because the
817
+ # analysis hangs off real optimisation passes. It therefore has
818
+ # to compile for real, which is why artifacts go to a temp dir
819
+ # with -c (never a linked binary, never the work tree).
820
+ # Requirement round 2 (feasibility FC-04) restricted it to .c files: C++ TUs
821
+ # through -fanalyzer are dramatically slower and noisier.
822
+ # ============================================================================
823
+
824
+ gate13_analyzer_enabled() {
825
+ [ "${XP_GATE_COMPILER_ANALYZER:-}" = "1" ]
826
+ }
827
+
828
+ # Print the staged C (not C++) files eligible for analysis.
829
+ gate13_analyzer_files() {
830
+ git diff --cached --name-only --diff-filter=d -- '*.c' 2>/dev/null
831
+ }
832
+
833
+ # Can this compiler do -fanalyzer at all?
834
+ gate13_analyzer_supported() {
835
+ local cc="$1" out tu
836
+ [ -n "$cc" ] || return 1
837
+ command -v "$cc" >/dev/null 2>&1 || return 1
838
+
839
+ tu="${GATE13_ANALYZER_TU:-}"
840
+ if [ -z "$tu" ] || [ ! -f "$tu" ]; then
841
+ tu="${TMPDIR:-/tmp}/gate13_analyzer_probe_$$.c"
842
+ printf 'int main(void) { return 0; }\n' > "$tu"
843
+ GATE13_ANALYZER_TU="$tu"
844
+ fi
845
+
846
+ out=$("$cc" -fanalyzer -fsyntax-only "$tu" 2>&1) && return 0
847
+ case "$out" in
848
+ *"unknown argument"*|*"unrecognized command-line option"*|*"unknown option"*|*"not supported"*)
849
+ return 1
850
+ ;;
851
+ esac
852
+ # It failed for another reason (e.g. no C frontend): treat as unsupported
853
+ # rather than silently claiming the analysis ran.
854
+ return 1
855
+ }
856
+
857
+ gate13_run_analyzer() {
858
+ local cc
859
+ local files=""
860
+ local f cnt total=0 blocking=0
861
+ local workdir
862
+
863
+ gate13_analyzer_enabled || return 0
864
+
865
+ cc=$(gate13_select_cc)
866
+ [ -n "$cc" ] || {
867
+ echo " ⚠️ -fanalyzer requested but no C compiler is usable; analyzer stage skipped"
868
+ return 0
869
+ }
870
+
871
+ if ! gate13_analyzer_supported "$cc"; then
872
+ echo " ℹ️ $(gate13_compiler_id "$cc") has no -fanalyzer support; analyzer stage skipped"
873
+ return 0
874
+ fi
875
+
876
+ files=$(gate13_analyzer_files)
877
+ if [ -z "$files" ]; then
878
+ echo " ℹ️ -fanalyzer: no staged .c files"
879
+ return 0
880
+ fi
881
+
882
+ workdir="${TMPDIR:-/tmp}/gate13_analyzer_$$"
883
+ mkdir -p "$workdir"
884
+
885
+ while IFS= read -r f; do
886
+ [ -n "$f" ] || continue
887
+ cnt=$("$cc" -fanalyzer -c -o "$workdir/$(basename -- "$f").o" "$f" 2>&1 \
888
+ | grep -cE 'warning:|\[-Wanalyzer' || true)
889
+ cnt=${cnt:-0}
890
+ if [ "$cnt" -gt 0 ]; then
891
+ total=$((total + cnt))
892
+ echo " ⚠️ ${f}: ${cnt} -fanalyzer finding(s)"
893
+ blocking=$((blocking + 1))
894
+ fi
895
+ done <<EOF
896
+ $files
897
+ EOF
898
+
899
+ rm -rf "$workdir"
900
+
901
+ if [ "$total" -gt 0 ]; then
902
+ echo " ℹ️ -fanalyzer: ${total} finding(s) across ${blocking} file(s) (advisory, opt-in stage)"
903
+ else
904
+ echo " ✅ -fanalyzer: clean"
905
+ fi
906
+ return 0
907
+ }
908
+
909
+ # ---------------------------------------------------------------------------
910
+ # AC-003-01/02/03/05 — the C/C++ analysis stage.
911
+ #
912
+ # Policy:
913
+ # added file -> -Werror: ANY warning in the active tier blocks
914
+ # modified file -> compare count to baseline; no baseline => report-only
915
+ # untouched file -> never compiled
916
+ # warn-only tier -> nothing blocks; findings surface as WARN
917
+ # ---------------------------------------------------------------------------
918
+ gate13_run_cpp_analysis() {
919
+ local cxx budget tier flags
920
+ local added_list="" modified_list=""
921
+ local blocking=0 findings=0 baseline_seen=0
922
+
923
+ if gate13_is_disabled; then
924
+ gate13_reason "Gate 13 disabled via XP_GATE_DISABLE_13=1"
925
+ echo "SKIPPED - compiler strictness check (XP_GATE_DISABLE_13=1)"
926
+ return "$GATE13_WARN"
927
+ fi
928
+
929
+ budget=$(gate13_timeout_budget)
930
+ cxx=$(gate13_select_cxx)
931
+ local sel_rc=$?
932
+ if [ "$sel_rc" -ne 0 ] || [ -z "$cxx" ]; then
933
+ echo "WARN - no usable C/C++ compiler found; compiler checks NOT performed."
934
+ echo " Install one of: g++ / clang++ (Debian: build-essential, macOS: xcode-select --install)"
935
+ return "$GATE13_WARN"
936
+ fi
937
+
938
+ if gate13_is_cross_compiling "$cxx"; then
939
+ gate13_reason "cross-compilation detected (compiler: ${cxx}, CROSS_COMPILE=${CROSS_COMPILE:-<empty>}); skipping host-side compile checks"
940
+ echo "WARN - cross-compilation detected: ${cxx}"
941
+ echo " Target-architecture code is not compiled with a host compiler; compiler checks NOT performed."
942
+ return "$GATE13_WARN"
943
+ fi
944
+
945
+ tier=$(gate13_resolve_tier)
946
+ flags=$(gate13_cpp_flags "$tier" cpp)
947
+ local probed
948
+ # shellcheck disable=SC2086 # $flags is a deliberately word-split flag list
949
+ probed=$(gate13_probe_flags "$cxx" cpp $flags)
950
+ [ -n "$probed" ] && flags="$probed"
951
+
952
+ echo "compiler: ${cxx} ($(gate13_compiler_id "$cxx"))"
953
+ echo "strictness: ${tier} | flags: ${flags} | budget: ${budget}s"
954
+
955
+ local classified
956
+ classified=$(gate13_classify_changed)
957
+ if [ -z "$classified" ]; then
958
+ echo " ℹ️ No staged C/C++ source files — nothing to compile"
959
+ return "$GATE13_PASS"
960
+ fi
961
+
962
+ if ! gate13_baseline_exists; then
963
+ echo " ⚠️ baseline missing — run 'xp-gate compiler-baseline create' to enable enforcement on modified files"
964
+ echo " (new files are still -Werror; modified files are report-only until then)"
965
+ fi
966
+
967
+ local f kind cnt base
968
+ while IFS=$'\t' read -r kind f; do
969
+ [ -n "$f" ] || continue
970
+
971
+ if [ "$kind" = "added" ]; then
972
+ # AC-003-01: an added file's baseline count is inherently zero, so it is
973
+ # enforced even without a baseline file.
974
+ cnt=$(gate13_count_warnings "$cxx" "$flags" "$f")
975
+ cnt=${cnt:-0}
976
+ if [ "$cnt" -gt 0 ]; then
977
+ findings=$((findings + cnt))
978
+ if [ "$tier" = "warn-only" ]; then
979
+ echo " ⚠️ ${f}: ${cnt} warning(s) (warn-only tier, not blocking)"
980
+ else
981
+ echo " ❌ ${f}: ${cnt} warning(s) in a NEW file"
982
+ blocking=$((blocking + 1))
983
+ fi
984
+ fi
985
+ else
986
+ # AC-003-02/03: modified file.
987
+ base=$(gate13_baseline_count "$f")
988
+ if [ -z "$base" ]; then
989
+ baseline_seen=1
990
+ cnt=$(gate13_count_warnings "$cxx" "$flags" "$f")
991
+ echo " ℹ️ ${f}: ${cnt:-0} warning(s) (no baseline entry, report-only)"
992
+ continue
993
+ fi
994
+ cnt=$(gate13_count_warnings "$cxx" "$flags" "$f")
995
+ cnt=${cnt:-0}
996
+ if [ "$cnt" -gt "$base" ]; then
997
+ findings=$((findings + cnt))
998
+ if [ "$tier" = "warn-only" ]; then
999
+ echo " ⚠️ ${f}: ${base} -> ${cnt} warning(s) (warn-only tier, not blocking)"
1000
+ else
1001
+ echo " ❌ ${f}: warning count rose ${base} -> ${cnt} (baseline exceeded)"
1002
+ blocking=$((blocking + 1))
1003
+ fi
1004
+ fi
1005
+ fi
1006
+ done <<EOF
1007
+ $classified
1008
+ EOF
1009
+
1010
+ if [ "$tier" = "warn-only" ]; then
1011
+ if [ "$findings" -gt 0 ]; then
1012
+ echo " ⚠️ WARN - ${findings} C/C++ compiler finding(s) (warn-only tier)"
1013
+ fi
1014
+ return "$GATE13_WARN"
1015
+ fi
1016
+
1017
+ if [ "$blocking" -gt 0 ]; then
1018
+ echo " ❌ BLOCKED - ${blocking} file(s) with compiler findings (${findings} warning(s) total)"
1019
+ echo " Fix the warnings, or (for pre-existing code) record a baseline:"
1020
+ echo " xp-gate compiler-baseline create"
1021
+ return "$GATE13_FAIL"
1022
+ fi
1023
+
1024
+ echo " ✅ PASSED - C/C++ compiler strictness (${tier} tier)"
1025
+
1026
+ # NOTE: gate13_run_analyzer is deliberately NOT called here. pre-commit drives
1027
+ # the three stages separately and reduces their verdicts itself; calling it in
1028
+ # both places ran -fanalyzer twice (walkthrough MC-01), doubling the slowest
1029
+ # stage for no benefit.
1030
+ return "$GATE13_PASS"
1031
+ }