loki-mode 7.48.0 → 7.50.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.
@@ -48,6 +48,13 @@ CHECKLIST_DIR=""
48
48
  CHECKLIST_FILE=""
49
49
  CHECKLIST_RESULTS_FILE=""
50
50
  CHECKLIST_LAST_VERIFY_ITERATION=0
51
+ # Path to the spec/PRD that drove this checklist. Used by the acceptance-oracle
52
+ # triangulation (checklist_oracle_triangulate) to compare what the spec ASSERTS
53
+ # against actual codebase reality. Empty in codebase-analysis mode (no PRD).
54
+ CHECKLIST_PRD_PATH=""
55
+
56
+ # Acceptance-oracle triangulation toggle (default-on, opt-out).
57
+ CHECKLIST_ORACLE_ENABLED=${LOKI_CHECKLIST_ORACLE:-1}
51
58
 
52
59
  #===============================================================================
53
60
  # Initialization
@@ -66,13 +73,310 @@ checklist_init() {
66
73
 
67
74
  mkdir -p "$CHECKLIST_DIR"
68
75
 
76
+ # Remember the spec path so verify-time triangulation can read what the spec
77
+ # asserts. Only store a real, readable file (codebase-analysis mode has none).
69
78
  if [ -n "$prd_path" ] && [ -f "$prd_path" ]; then
79
+ CHECKLIST_PRD_PATH="$prd_path"
70
80
  log_info "PRD checklist initialized for: $prd_path"
71
81
  fi
72
82
 
73
83
  return 0
74
84
  }
75
85
 
86
+ #===============================================================================
87
+ # Acceptance-Oracle Triangulation (P2-3)
88
+ #===============================================================================
89
+ # The PRD checklist derives acceptance criteria from the SPEC alone. That is a
90
+ # single oracle: if the spec is wrong, every check can pass while the product is
91
+ # wrong. This function adds a SECOND independent oracle -- actual codebase
92
+ # reality -- and triangulates the two:
93
+ #
94
+ # (1) spec : what the PRD/spec ASSERTS (e.g. "uses PostgreSQL")
95
+ # (2) reality : what the codebase actually wires (deps + import/usage signals)
96
+ #
97
+ # When the spec asserts a stack the codebase CONTRADICTS (reality shows a
98
+ # DIFFERENT, mutually-exclusive choice and NOT the spec's), we EMIT A FINDING
99
+ # rather than silently letting the spec win. The finding is written to
100
+ # .loki/checklist/oracle-findings.json and surfaced to the completion council via
101
+ # checklist_as_evidence(), and a best-effort trust event is recorded.
102
+ #
103
+ # Honest scope (v7.x, P2-3 first slice):
104
+ # IMPLEMENTED: the spec-vs-reality CONFLICT dimension for the database/datastore
105
+ # choice, which is the highest-signal, lowest-false-positive triangulation
106
+ # (the choices are mutually exclusive and have unambiguous dependency markers).
107
+ #
108
+ # DEFERRED (documented follow-up, NOT faked): the third leg of full
109
+ # triangulation -- DOMAIN INVARIANTS (auth must hash, money must not be float,
110
+ # PII must be encrypted) -- and additional stack axes (web framework, language
111
+ # runtime). These need an invariant catalog plus per-language AST-aware
112
+ # detection to avoid false positives; a regex grep would be noisy. They are
113
+ # intentionally left out of this slice. Backlog: P2-3 follow-up "domain-
114
+ # invariant oracle". See the conflict-detection design here as the template.
115
+ #
116
+ # Non-conflict cases that must NOT fire (guarded + tested):
117
+ # - spec asserts DB X, codebase has nothing wired yet (greenfield): ABSENT is
118
+ # not a contradiction -> no finding.
119
+ # - spec asserts DB X, codebase wires DB X: agreement -> no finding.
120
+ # - spec asserts nothing about a datastore: no spec oracle -> no finding.
121
+ #
122
+ # Opt-out: LOKI_CHECKLIST_ORACLE=0 (default on). When off, this is a no-op.
123
+
124
+ # Emit oracle findings JSON for the current project. Pure detection in python3
125
+ # (env-var inputs only, never positional args inside the heredoc), so the bash
126
+ # wrapper just decides whether anything was written.
127
+ checklist_oracle_triangulate() {
128
+ if [ "$CHECKLIST_ORACLE_ENABLED" != "1" ]; then
129
+ return 0
130
+ fi
131
+
132
+ # Need a spec oracle to triangulate against. No spec -> nothing to do.
133
+ if [ -z "$CHECKLIST_PRD_PATH" ] || [ ! -f "$CHECKLIST_PRD_PATH" ]; then
134
+ return 0
135
+ fi
136
+
137
+ local findings_file="${CHECKLIST_DIR:-".loki/checklist"}/oracle-findings.json"
138
+ local project_dir
139
+ project_dir="$(pwd)"
140
+
141
+ # Feed the detector to python3 via a quoted heredoc (delimiter in quotes) so
142
+ # bash performs NO interpolation: no dollar-digit footgun, no quote-escaping
143
+ # hazards. All inputs arrive through _ORACLE_* env vars.
144
+ local status_token
145
+ status_token=$(_ORACLE_SPEC="$CHECKLIST_PRD_PATH" \
146
+ _ORACLE_OUT="$findings_file" \
147
+ _ORACLE_PROJECT="$project_dir" \
148
+ python3 - <<'ORACLE_PY' 2>/dev/null || echo "NOOP"
149
+ import json, os, re, sys, tempfile, glob
150
+
151
+ spec_path = os.environ["_ORACLE_SPEC"]
152
+ out_path = os.environ["_ORACLE_OUT"]
153
+ project = os.environ["_ORACLE_PROJECT"]
154
+
155
+ # --- Datastore catalog: canonical name -> (spec regex, reality dependency/usage
156
+ # markers). Choices are mutually exclusive enough that a different one being
157
+ # wired while the spec names another is a genuine contradiction. ---
158
+ DATASTORES = {
159
+ "postgresql": {
160
+ "spec": r"(?i)\b(postgres(?:ql)?|psql)\b",
161
+ # python deps / node deps / connection markers
162
+ "deps": [r"(?i)\bpsycopg2?\b", r"(?i)\basyncpg\b",
163
+ r'(?i)"pg"\s*:', r"(?i)\bpg-promise\b", r"(?i)\bpostgres\b",
164
+ r"(?i)\bsequelize\b.*postgres", r"(?i)postgresql://"],
165
+ },
166
+ "mysql": {
167
+ "spec": r"(?i)\b(mysql|mariadb)\b",
168
+ "deps": [r"(?i)\bpymysql\b", r"(?i)\bmysqlclient\b",
169
+ r'(?i)"mysql2?"\s*:', r"(?i)mysql://"],
170
+ },
171
+ "mongodb": {
172
+ "spec": r"(?i)\b(mongo(?:db)?)\b",
173
+ "deps": [r"(?i)\bpymongo\b", r"(?i)\bmongoengine\b", r"(?i)\bmotor\b",
174
+ r'(?i)"mongoose"\s*:', r'(?i)"mongodb"\s*:', r"(?i)mongodb(?:\+srv)?://"],
175
+ },
176
+ "sqlite": {
177
+ "spec": r"(?i)\bsqlite\b",
178
+ "deps": [r"(?i)\bsqlite3\b", r"(?i)\baiosqlite\b",
179
+ r'(?i)"better-sqlite3"\s*:', r"(?i)sqlite://"],
180
+ },
181
+ "redis": {
182
+ "spec": r"(?i)\bredis\b",
183
+ "deps": [r"(?i)\bredis\b", r"(?i)\bioredis\b", r"(?i)redis://"],
184
+ },
185
+ "dynamodb": {
186
+ "spec": r"(?i)\bdynamo(?:db)?\b",
187
+ "deps": [r"(?i)\bboto3\b.*dynamo", r"(?i)dynamodb", r'(?i)"@aws-sdk/client-dynamodb"'],
188
+ },
189
+ }
190
+
191
+ # Datastores that are caches/secondary by nature: their presence alongside a
192
+ # primary DB is NOT a contradiction (apps routinely use both). Treat them as
193
+ # spec-only signals but never as the "reality contradicts" side.
194
+ SECONDARY = {"redis"}
195
+
196
+ def read_text(path, limit=400000):
197
+ try:
198
+ with open(path, "r", errors="replace") as f:
199
+ return f.read(limit)
200
+ except Exception:
201
+ return ""
202
+
203
+ spec_text = read_text(spec_path)
204
+ if not spec_text.strip():
205
+ print("NOOP")
206
+ sys.exit(0)
207
+
208
+ # --- Spec oracle: which datastores does the spec ASSERT? ---
209
+ spec_asserts = set()
210
+ for name, cfg in DATASTORES.items():
211
+ if re.search(cfg["spec"], spec_text):
212
+ spec_asserts.add(name)
213
+
214
+ # If the spec names exactly one PRIMARY datastore, we can triangulate it. If it
215
+ # names several primaries, the spec itself is ambiguous about the datastore;
216
+ # triangulation of "the" choice is unsound, so skip (spec-interrogation owns
217
+ # spec-internal ambiguity, not this oracle).
218
+ spec_primary = sorted(spec_asserts - SECONDARY)
219
+ if len(spec_primary) != 1:
220
+ print("NO_SPEC_DB")
221
+ sys.exit(0)
222
+ spec_db = spec_primary[0]
223
+
224
+ # --- Reality oracle: scan a bounded set of dependency/lock/source manifests for
225
+ # datastore markers. We scan manifests first (highest signal, lowest noise). ---
226
+ MANIFEST_GLOBS = [
227
+ "package.json", "requirements.txt", "requirements/*.txt", "pyproject.toml",
228
+ "Pipfile", "poetry.lock", "go.mod", "Gemfile", "pom.xml", "build.gradle",
229
+ "composer.json", "Cargo.toml",
230
+ ".env.example", ".env.sample", "docker-compose.yml", "docker-compose.yaml",
231
+ ]
232
+ SKIP_DIRS = {".git", "node_modules", ".loki", "__pycache__", ".venv", "venv",
233
+ "dist", "build", ".next", "vendor"}
234
+
235
+ manifest_text_parts = []
236
+ for pat in MANIFEST_GLOBS:
237
+ for p in glob.glob(os.path.join(project, pat)):
238
+ if os.path.isfile(p):
239
+ manifest_text_parts.append(read_text(p, 200000))
240
+ manifest_text = "\n".join(manifest_text_parts)
241
+
242
+ # Light source scan for connection-string usage as a secondary reality signal,
243
+ # bounded to a small number of files to stay fast and avoid scanning artifacts.
244
+ def source_scan_hit(markers, cap_files=400):
245
+ seen = 0
246
+ for root, dirs, files in os.walk(project):
247
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
248
+ for fn in files:
249
+ if not fn.endswith((".py", ".js", ".ts", ".tsx", ".jsx", ".go",
250
+ ".rb", ".java", ".rs", ".php", ".env")):
251
+ continue
252
+ seen += 1
253
+ if seen > cap_files:
254
+ return False
255
+ txt = read_text(os.path.join(root, fn), 80000)
256
+ for m in markers:
257
+ if re.search(m, txt):
258
+ return True
259
+ return False
260
+
261
+ def reality_has(name):
262
+ cfg = DATASTORES[name]
263
+ for m in cfg["deps"]:
264
+ if re.search(m, manifest_text):
265
+ return True
266
+ # Only fall back to a source scan for the cheap connection-string style
267
+ # markers (URLs); dependency-name markers belong in manifests.
268
+ url_markers = [m for m in cfg["deps"] if "://" in m]
269
+ if url_markers and source_scan_hit(url_markers):
270
+ return True
271
+ return False
272
+
273
+ reality_dbs = set()
274
+ for name in DATASTORES:
275
+ if name in SECONDARY:
276
+ continue
277
+ if reality_has(name):
278
+ reality_dbs.add(name)
279
+
280
+ findings = []
281
+
282
+ # Conflict iff: spec asserts spec_db, AND reality shows a DIFFERENT primary db,
283
+ # AND reality does NOT also show the spec db. Absent reality (greenfield) and
284
+ # agreement both yield no finding.
285
+ contradicting = sorted(d for d in reality_dbs if d != spec_db)
286
+ if contradicting and spec_db not in reality_dbs:
287
+ findings.append({
288
+ "id": "oracle-datastore-conflict",
289
+ "dimension": "spec_vs_reality",
290
+ "axis": "datastore",
291
+ "severity": "high",
292
+ "spec_asserts": spec_db,
293
+ "codebase_reality": contradicting,
294
+ "title": "Spec asserts %s but codebase wires %s" % (
295
+ spec_db, ", ".join(contradicting)),
296
+ "detail": ("The spec/PRD names %s as the datastore, but the codebase "
297
+ "dependencies/usage indicate %s is wired and %s is not. "
298
+ "Acceptance criteria derived from the spec alone would pass "
299
+ "against the wrong datastore. Resolve which oracle is correct "
300
+ "(update the spec or the implementation) before completion."
301
+ % (spec_db, ", ".join(contradicting), spec_db)),
302
+ })
303
+
304
+ result = {
305
+ "version": 1,
306
+ "spec_path": spec_path,
307
+ "spec_datastore": spec_db,
308
+ "reality_datastores": sorted(reality_dbs),
309
+ "findings": findings,
310
+ "deferred": [
311
+ "domain-invariant oracle (auth-hashing, money-not-float, PII-encryption)",
312
+ "additional stack axes (web framework, language runtime)",
313
+ ],
314
+ }
315
+
316
+ # Always write the result so evidence/council can read both findings AND a clean
317
+ # "checked, no conflict" record (honest: absence of a finding is itself signal).
318
+ d = os.path.dirname(out_path) or "."
319
+ os.makedirs(d, exist_ok=True)
320
+ fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
321
+ with os.fdopen(fd, "w") as f:
322
+ json.dump(result, f, indent=2)
323
+ f.write("\n")
324
+ os.replace(tmp, out_path)
325
+
326
+ if findings:
327
+ print("CONFLICT %d" % len(findings))
328
+ else:
329
+ print("CLEAN")
330
+ ORACLE_PY
331
+ )
332
+
333
+ # Honest logging + best-effort trust event on a real conflict only.
334
+ local tok rest
335
+ read -r tok rest <<< "$status_token"
336
+ case "$tok" in
337
+ CONFLICT)
338
+ log_warn "[checklist] acceptance-oracle conflict: spec disagrees with codebase reality (${rest:-1} finding(s)); see ${findings_file}"
339
+ if type record_trust_event_bash &>/dev/null; then
340
+ record_trust_event_bash "oracle_conflict" \
341
+ "dimension=spec_vs_reality" \
342
+ "axis=datastore" \
343
+ "findings=${rest:-1}" \
344
+ >/dev/null 2>&1 || true
345
+ fi
346
+ ;;
347
+ esac
348
+
349
+ return 0
350
+ }
351
+
352
+ # Echo the oracle findings as a formatted block for council evidence. Empty when
353
+ # no findings (or oracle disabled / not run).
354
+ checklist_oracle_evidence() {
355
+ local findings_file="${CHECKLIST_DIR:-".loki/checklist"}/oracle-findings.json"
356
+ if [ ! -f "$findings_file" ]; then
357
+ return 0
358
+ fi
359
+ _ORACLE_OUT="$findings_file" python3 -c '
360
+ import json, os
361
+ try:
362
+ with open(os.environ["_ORACLE_OUT"]) as f:
363
+ data = json.load(f)
364
+ except Exception:
365
+ raise SystemExit(0)
366
+ findings = data.get("findings", [])
367
+ if not findings:
368
+ raise SystemExit(0)
369
+ print("")
370
+ print("### Acceptance-Oracle Triangulation (spec vs codebase reality)")
371
+ for fnd in findings:
372
+ sev = str(fnd.get("severity", "high")).upper()
373
+ print(" [FAIL] [%s] %s" % (sev, fnd.get("title", fnd.get("id", "?"))))
374
+ detail = fnd.get("detail", "")
375
+ if detail:
376
+ print(" %s" % detail)
377
+ ' 2>/dev/null || true
378
+ }
379
+
76
380
  #===============================================================================
77
381
  # Interval Control
78
382
  #===============================================================================
@@ -314,6 +618,12 @@ checklist_verify() {
314
618
  # first verification-results.json summary already excludes held-out items.
315
619
  checklist_select_heldout
316
620
 
621
+ # Acceptance-oracle triangulation: compare what the spec ASSERTS against
622
+ # actual codebase reality and emit a finding on conflict. Runs at verify-time
623
+ # (not init) so codebase reality actually exists by now even on a greenfield
624
+ # build. Best-effort, default-on, never blocks verification itself.
625
+ checklist_oracle_triangulate 2>/dev/null || true
626
+
317
627
  local script_dir
318
628
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
319
629
  local verify_script="${script_dir}/checklist-verify.py"
@@ -477,6 +787,11 @@ try:
477
787
  except Exception:
478
788
  print('Checklist data unavailable')
479
789
  " 2>/dev/null || echo "Checklist data unavailable"
790
+
791
+ # Append acceptance-oracle triangulation findings (spec vs codebase
792
+ # reality) so the completion council sees a spec-vs-reality conflict as
793
+ # first-class evidence. Emits nothing when there are no findings.
794
+ checklist_oracle_evidence
480
795
  } >> "${evidence_file:-/dev/stdout}"
481
796
  }
482
797
 
package/autonomy/run.sh CHANGED
@@ -7038,6 +7038,130 @@ enforce_static_analysis() {
7038
7038
  }
7039
7039
  fi
7040
7040
 
7041
+ # C / C++ (P1-6: cppcheck is a standalone static analyzer that needs no
7042
+ # build system, headers, or compile flags, so it does not false-block on
7043
+ # missing includes the way a per-file `clang` compile would. The exit gate
7044
+ # fires only on `error` severity; style/warning/portability findings on WIP
7045
+ # code do not block. When cppcheck is absent we pass through honestly
7046
+ # (log, no block) rather than silently skipping.)
7047
+ local cfiles
7048
+ cfiles=$(echo "$changed_files" | grep -E '\.(c|cc|cpp|cxx|h|hpp|hxx)$' || true)
7049
+ if [ -n "$cfiles" ]; then
7050
+ local cabs=""
7051
+ for f in $cfiles; do
7052
+ [ -f "${TARGET_DIR:-.}/$f" ] && cabs="$cabs ${TARGET_DIR:-.}/$f"
7053
+ done
7054
+ if [ -n "$cabs" ]; then
7055
+ if command -v cppcheck &>/dev/null; then
7056
+ total_checked=$((total_checked + $(echo "$cabs" | wc -w)))
7057
+ # Default cppcheck reports ONLY error severity, so with
7058
+ # --error-exitcode=2 the gate returns 2 exclusively on an
7059
+ # error-severity finding. We deliberately do NOT pass
7060
+ # --enable=warning: that would make warning/style/portability
7061
+ # findings on incomplete WIP code block the iteration (verified:
7062
+ # a deref-then-null-check warning returns 2 under --enable=warning
7063
+ # but 0 under the default ruleset). Error severity only = honest
7064
+ # parity with the TS/shell `-S error` gates above.
7065
+ local cpp_out cpp_rc=0
7066
+ # shellcheck disable=SC2086
7067
+ cpp_out=$(cppcheck --quiet --error-exitcode=2 $cabs 2>&1) || cpp_rc=$?
7068
+ if [ "$cpp_rc" -eq 2 ]; then
7069
+ findings=$((findings + 1))
7070
+ details="${details}cppcheck (error severity): $(echo "$cpp_out" | tail -3 | tr '\n' ' '). "
7071
+ fi
7072
+ else
7073
+ log_info "Static analysis: cppcheck not on PATH, skipping C/C++ check (pass-through)"
7074
+ fi
7075
+ fi
7076
+ fi
7077
+
7078
+ # Kotlin (P1-6: ktlint and detekt are standalone, build-system-free linters.
7079
+ # Prefer ktlint; fall back to detekt. Absent -> honest pass-through.)
7080
+ #
7081
+ # ADVISORY ONLY (not blocking): unlike cppcheck/checkstyle which expose an
7082
+ # error-vs-style severity distinction, ktlint is a pure formatter -- every
7083
+ # finding it reports is a style/formatting issue and it exits nonzero on ANY
7084
+ # violation, with no CLI mode to fail only on error severity. detekt's failure
7085
+ # threshold is config-driven (maxIssues) and its findings are code smells, not
7086
+ # compiler errors; there is no stable CLI flag to fail only on error severity.
7087
+ # Per the gate principle (a new-language arm must NOT block on style/formatting,
7088
+ # consistent with cppcheck's error-exitcode-only and the JS/TS/Py `-S error`
7089
+ # gates), we run these linters as ADVISORY: report findings via log_warn and
7090
+ # the details string, but do NOT increment `findings` (no BLOCK). This avoids
7091
+ # false-blocking a WIP build on formatting. Absent -> honest pass-through.
7092
+ local kt_files
7093
+ kt_files=$(echo "$changed_files" | grep -E '\.(kt|kts)$' || true)
7094
+ if [ -n "$kt_files" ]; then
7095
+ local kt_abs=""
7096
+ for f in $kt_files; do
7097
+ [ -f "${TARGET_DIR:-.}/$f" ] && kt_abs="$kt_abs ${TARGET_DIR:-.}/$f"
7098
+ done
7099
+ if [ -n "$kt_abs" ]; then
7100
+ if command -v ktlint &>/dev/null; then
7101
+ total_checked=$((total_checked + $(echo "$kt_abs" | wc -w)))
7102
+ local kt_out
7103
+ # shellcheck disable=SC2086
7104
+ kt_out=$(cd "${TARGET_DIR:-.}" && ktlint $kt_files 2>&1) || {
7105
+ # Advisory: ktlint reports only style/formatting; warn, do not block.
7106
+ details="${details}ktlint advisory (style, non-blocking): $(echo "$kt_out" | tail -3 | tr '\n' ' '). "
7107
+ log_warn "Static analysis: ktlint reported style findings (advisory, non-blocking)"
7108
+ }
7109
+ elif command -v detekt &>/dev/null; then
7110
+ total_checked=$((total_checked + $(echo "$kt_abs" | wc -w)))
7111
+ local dt_out dt_input
7112
+ dt_input=$(echo "$kt_files" | tr ' \n' ',,' | sed 's/,*$//;s/^,*//')
7113
+ dt_out=$(cd "${TARGET_DIR:-.}" && detekt --input "$dt_input" 2>&1) || {
7114
+ # Advisory: detekt threshold is config-driven, findings are code
7115
+ # smells (no error-severity-only CLI mode); warn, do not block.
7116
+ details="${details}detekt advisory (code smell, non-blocking): $(echo "$dt_out" | tail -3 | tr '\n' ' '). "
7117
+ log_warn "Static analysis: detekt reported findings (advisory, non-blocking)"
7118
+ }
7119
+ else
7120
+ log_info "Static analysis: ktlint/detekt not on PATH, skipping Kotlin check (pass-through)"
7121
+ fi
7122
+ fi
7123
+ fi
7124
+
7125
+ # Java (P1-6: checkstyle is a pure static linter that needs no compile or
7126
+ # classpath, but it REQUIRES a config file. A per-file `javac` would
7127
+ # false-block on unresolved imports/classpath the way per-file tsc did, so
7128
+ # Java is gated on checkstyle-with-config only. Without a config we pass
7129
+ # through honestly. C# is deferred: roslyn analyzers and `dotnet build` need
7130
+ # a full project + restore, which cannot be auto-detected cleanly per-file.)
7131
+ local java_files
7132
+ java_files=$(echo "$changed_files" | grep -E '\.java$' || true)
7133
+ if [ -n "$java_files" ]; then
7134
+ local java_abs=""
7135
+ for f in $java_files; do
7136
+ [ -f "${TARGET_DIR:-.}/$f" ] && java_abs="$java_abs ${TARGET_DIR:-.}/$f"
7137
+ done
7138
+ if [ -n "$java_abs" ]; then
7139
+ local _cs_config=""
7140
+ for cfg in checkstyle.xml .checkstyle.xml config/checkstyle/checkstyle.xml google_checks.xml sun_checks.xml; do
7141
+ if [ -f "${TARGET_DIR:-.}/$cfg" ]; then _cs_config="${TARGET_DIR:-.}/$cfg"; break; fi
7142
+ done
7143
+ if command -v checkstyle &>/dev/null && [ -n "$_cs_config" ]; then
7144
+ total_checked=$((total_checked + $(echo "$java_abs" | wc -w)))
7145
+ local cs_out
7146
+ # checkstyle's exit code equals the count of audit events at
7147
+ # severity=error; warning/info violations are printed but do NOT
7148
+ # bump the exit code (verified against checkstyle CLI behavior).
7149
+ # So a nonzero exit means error-severity findings only -- this is
7150
+ # already error-gated like cppcheck (--error-exitcode) and the
7151
+ # JS/TS/Py `-S error` gates, and does NOT block on style/warning.
7152
+ # Whether a given rule is error vs warning is the user's explicit
7153
+ # choice in their checkstyle config, which we respect.
7154
+ # shellcheck disable=SC2086
7155
+ cs_out=$(cd "${TARGET_DIR:-.}" && checkstyle -c "$_cs_config" $java_files 2>&1) || {
7156
+ findings=$((findings + 1))
7157
+ details="${details}checkstyle (error severity): $(echo "$cs_out" | tail -3 | tr '\n' ' '). "
7158
+ }
7159
+ else
7160
+ log_info "Static analysis: checkstyle+config not available, skipping Java check (pass-through)"
7161
+ fi
7162
+ fi
7163
+ fi
7164
+
7041
7165
  # Write results
7042
7166
  cat > "$quality_dir/static-analysis.json" << SAFEOF
7043
7167
  {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","files_checked":$total_checked,"findings":$findings,"summary":"$details","pass":$([ $findings -eq 0 ] && echo "true" || echo "false")}