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.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +85 -2
- package/autonomy/prd-analyzer.py +215 -1
- package/autonomy/prd-checklist.sh +315 -0
- package/autonomy/run.sh +124 -0
- package/autonomy/spec-interrogation.sh +224 -4
- package/autonomy/spec.sh +25 -16
- package/autonomy/verify.sh +108 -26
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_v2.py +258 -12
- package/dashboard/audit.py +202 -21
- package/dashboard/server.py +64 -10
- package/docs/INSTALLATION.md +2 -2
- package/docs/siem-integration.md +102 -0
- package/loki-ts/dist/loki.js +231 -230
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/invariant-checks.md +109 -0
- package/src/audit/crosslink.js +413 -0
- package/src/audit/index.js +32 -0
- package/src/observability/siem-export.js +424 -0
- package/src/policies/cost.js +270 -1
|
@@ -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")}
|