loki-mode 7.26.0 → 7.28.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 +15 -13
- package/SKILL.md +11 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +310 -6
- package/autonomy/context-tracker.py +32 -7
- package/autonomy/grill.sh +321 -0
- package/autonomy/lib/trust_metrics.py +636 -0
- package/autonomy/loki +142 -0
- package/autonomy/prd-checklist.sh +248 -14
- package/autonomy/run.sh +283 -32
- package/autonomy/spec.sh +646 -0
- package/autonomy/verify.sh +1130 -0
- package/dashboard/__init__.py +1 -1
- package/dashboard/static/index.html +1 -1
- package/docs/COMPARISON.md +9 -9
- package/docs/COMPETITIVE-ANALYSIS.md +18 -37
- package/docs/INSTALLATION.md +1 -1
- package/docs/auto-claude-comparison.md +9 -6
- package/docs/certification/01-core-concepts/lesson.md +3 -3
- package/docs/competitive/emergence-others-analysis.md +1 -1
- package/docs/competitive/replit-lovable-analysis.md +1 -1
- package/docs/cursor-comparison.md +1 -1
- package/docs/prd-purple-lab-platform.md +1 -1
- package/docs/show-hn-post.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +2 -1
- package/providers/codex.sh +3 -2
- package/references/agent-types.md +9 -9
- package/references/agents.md +8 -8
- package/references/business-ops.md +1 -1
- package/references/competitive-analysis.md +1 -1
- package/skills/agents.md +3 -3
- package/skills/providers.md +3 -3
- package/skills/quality-gates.md +46 -0
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# autonomy/verify.sh - Autonomi Verify (loki verify) deterministic verification core.
|
|
3
|
+
#
|
|
4
|
+
# Verification-as-a-Service MVP, 30-day cut per
|
|
5
|
+
# internal/VERIFICATION-BOT-SPEC-2026-06.md Section 6.
|
|
6
|
+
#
|
|
7
|
+
# This is a STANDALONE module. It deliberately does NOT source run.sh or
|
|
8
|
+
# completion-council.sh: those functions are welded to the autonomous
|
|
9
|
+
# iteration loop (TARGET_DIR / ITERATION_COUNT globals, scattered
|
|
10
|
+
# .loki/quality/* outputs, and the wrong diff base -- HEAD~1 / --cached /
|
|
11
|
+
# run-start SHA). This module FAITHFULLY PORTS the deterministic detection
|
|
12
|
+
# patterns (same runner-detection order, same globs) but re-bases the diff
|
|
13
|
+
# onto PR semantics and consolidates output into one evidence document.
|
|
14
|
+
#
|
|
15
|
+
# Entanglement refactors implemented here (spec Section 2.3):
|
|
16
|
+
# 1. Diff base: verify_diff_base() resolves merge-base(base, HEAD)..HEAD,
|
|
17
|
+
# NOT HEAD~1 (run.sh:6305) or run-start SHA (completion-council.sh:1203).
|
|
18
|
+
# 2. Inverted verdict policy: inconclusive maps to CONCERNS, never VERIFIED.
|
|
19
|
+
# The build loop's council_evidence_gate is pass-through-on-inconclusive
|
|
20
|
+
# by design (completion-council.sh:1216-1221); a verifier wants the
|
|
21
|
+
# opposite.
|
|
22
|
+
# 3. Consolidated output: one evidence.json + one report.md, not the
|
|
23
|
+
# scattered .loki/quality/*.json / .loki/council/*.json artifacts.
|
|
24
|
+
#
|
|
25
|
+
# Reuse map (faithful ports, source cited):
|
|
26
|
+
# - test runner detection: enforce_test_coverage (run.sh:6624)
|
|
27
|
+
# - static analysis detection: enforce_static_analysis (run.sh:6299)
|
|
28
|
+
# - diff+test evidence mechanic: council_evidence_gate
|
|
29
|
+
# (completion-council.sh:1189) -- mechanic reused, policy inverted.
|
|
30
|
+
#
|
|
31
|
+
# NO LLM review in this MVP (deterministic-only first slice). The spec
|
|
32
|
+
# sequences the single-reviewer LLM stage and the blind council later
|
|
33
|
+
# (spec Section 6). This is stated honestly in the help text and the
|
|
34
|
+
# evidence document (llm_review.status = "skipped").
|
|
35
|
+
#
|
|
36
|
+
# Exit codes (per BUILD TASK; see NOTE below):
|
|
37
|
+
# 0 VERIFIED
|
|
38
|
+
# 1 CONCERNS
|
|
39
|
+
# 2 BLOCKED
|
|
40
|
+
# 3 verifier error (could not complete; never silently passes)
|
|
41
|
+
#
|
|
42
|
+
# NOTE on exit-code divergence from the spec: spec Section 1.1 lists
|
|
43
|
+
# 0=VERIFIED, 1=BLOCKED, 2=CONCERNS, 3=error (BLOCKED and CONCERNS swapped
|
|
44
|
+
# vs this implementation). This module follows the explicit BUILD TASK
|
|
45
|
+
# ordering (0/1/2 = VERIFIED/CONCERNS/BLOCKED). A human must reconcile the
|
|
46
|
+
# two before the GitHub App (Phase 2) consumes exit codes. The divergence is
|
|
47
|
+
# surfaced in `loki verify --help`.
|
|
48
|
+
|
|
49
|
+
set -uo pipefail
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Constants
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
VERIFY_EXIT_VERIFIED=0
|
|
55
|
+
VERIFY_EXIT_CONCERNS=1
|
|
56
|
+
VERIFY_EXIT_BLOCKED=2
|
|
57
|
+
VERIFY_EXIT_ERROR=3
|
|
58
|
+
VERIFY_SCHEMA_VERSION="1.0"
|
|
59
|
+
|
|
60
|
+
# Resolve tool version from the VERSION file shipped alongside the repo.
|
|
61
|
+
_verify_tool_version() {
|
|
62
|
+
local here
|
|
63
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
64
|
+
if [ -f "$here/../VERSION" ]; then
|
|
65
|
+
tr -d '[:space:]' <"$here/../VERSION"
|
|
66
|
+
else
|
|
67
|
+
echo "unknown"
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Logging (stderr; stdout is reserved for the human verdict line)
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
_verify_log() { printf '[verify] %s\n' "$*" >&2; }
|
|
75
|
+
_verify_err() { printf '[verify][error] %s\n' "$*" >&2; }
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Findings accumulator
|
|
79
|
+
#
|
|
80
|
+
# Each finding is one TAB-separated record appended to a temp file:
|
|
81
|
+
# severity \t category \t source \t file \t line \t message
|
|
82
|
+
# Severity is one of: Critical High Medium Low Info
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
_VERIFY_FINDINGS_FILE=""
|
|
85
|
+
_VERIFY_GATES_FILE=""
|
|
86
|
+
|
|
87
|
+
_verify_add_finding() {
|
|
88
|
+
# $1 severity $2 category $3 source $4 file $5 line $6 message
|
|
89
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
90
|
+
"$1" "$2" "$3" "$4" "${5:-null}" "$6" >>"$_VERIFY_FINDINGS_FILE"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_verify_add_gate() {
|
|
94
|
+
# $1 gate $2 status(pass|fail|inconclusive|skipped) $3 runner/scanner $4 summary $5 reproducible(true|false)
|
|
95
|
+
printf '%s\t%s\t%s\t%s\t%s\n' \
|
|
96
|
+
"$1" "$2" "${3:-}" "${4:-}" "${5:-true}" >>"$_VERIFY_GATES_FILE"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Entanglement 1: PR-aware diff base.
|
|
101
|
+
#
|
|
102
|
+
# Resolves merge-base(base, HEAD)..HEAD. This is the WHOLE change set vs the
|
|
103
|
+
# target branch, i.e. PR semantics. Replaces the build loop's HEAD~1 /
|
|
104
|
+
# --cached / run-start-SHA bases.
|
|
105
|
+
#
|
|
106
|
+
# Sets globals: VERIFY_BASE_REF VERIFY_HEAD_SHA VERIFY_MERGE_BASE
|
|
107
|
+
# VERIFY_DIFF_FILES VERIFY_DIFF_INS VERIFY_DIFF_DEL
|
|
108
|
+
# VERIFY_DIFF_NAMES (newline list) VERIFY_DIFF_RESOLVED(true|false)
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
verify_diff_base() {
|
|
111
|
+
local base_ref="$1"
|
|
112
|
+
|
|
113
|
+
VERIFY_DIFF_RESOLVED="false"
|
|
114
|
+
VERIFY_BASE_REF="$base_ref"
|
|
115
|
+
VERIFY_HEAD_SHA=""
|
|
116
|
+
VERIFY_MERGE_BASE=""
|
|
117
|
+
VERIFY_DIFF_FILES=0
|
|
118
|
+
VERIFY_DIFF_INS=0
|
|
119
|
+
VERIFY_DIFF_DEL=0
|
|
120
|
+
VERIFY_DIFF_NAMES=""
|
|
121
|
+
|
|
122
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
123
|
+
_verify_log "not a git repository; diff base unresolved (inconclusive)"
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
VERIFY_HEAD_SHA="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
|
128
|
+
if [ -z "$VERIFY_HEAD_SHA" ]; then
|
|
129
|
+
_verify_log "no HEAD commit; diff base unresolved (inconclusive)"
|
|
130
|
+
return 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Resolve the base ref. Prefer origin/<base>, fall back to local <base>.
|
|
134
|
+
local resolved_base=""
|
|
135
|
+
if git rev-parse --verify --quiet "origin/$base_ref" >/dev/null 2>&1; then
|
|
136
|
+
resolved_base="origin/$base_ref"
|
|
137
|
+
elif git rev-parse --verify --quiet "$base_ref" >/dev/null 2>&1; then
|
|
138
|
+
resolved_base="$base_ref"
|
|
139
|
+
else
|
|
140
|
+
_verify_log "base ref '$base_ref' not found (tried origin/$base_ref and $base_ref); diff base unresolved (inconclusive)"
|
|
141
|
+
return 0
|
|
142
|
+
fi
|
|
143
|
+
VERIFY_BASE_REF="$resolved_base"
|
|
144
|
+
|
|
145
|
+
local mb
|
|
146
|
+
mb="$(git merge-base "$resolved_base" HEAD 2>/dev/null || echo "")"
|
|
147
|
+
if [ -z "$mb" ]; then
|
|
148
|
+
_verify_log "no merge-base between $resolved_base and HEAD; diff base unresolved (inconclusive)"
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
VERIFY_MERGE_BASE="$mb"
|
|
152
|
+
|
|
153
|
+
# Diff stats for merge-base..HEAD.
|
|
154
|
+
VERIFY_DIFF_NAMES="$(git diff --name-only "$mb" HEAD 2>/dev/null || echo "")"
|
|
155
|
+
if [ -n "$VERIFY_DIFF_NAMES" ]; then
|
|
156
|
+
VERIFY_DIFF_FILES="$(printf '%s\n' "$VERIFY_DIFF_NAMES" | grep -c . || echo 0)"
|
|
157
|
+
else
|
|
158
|
+
VERIFY_DIFF_FILES=0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
local numstat
|
|
162
|
+
numstat="$(git diff --numstat "$mb" HEAD 2>/dev/null || echo "")"
|
|
163
|
+
if [ -n "$numstat" ]; then
|
|
164
|
+
VERIFY_DIFF_INS="$(printf '%s\n' "$numstat" | awk '$1 ~ /^[0-9]+$/ {s+=$1} END {print s+0}')"
|
|
165
|
+
VERIFY_DIFF_DEL="$(printf '%s\n' "$numstat" | awk '$2 ~ /^[0-9]+$/ {s+=$2} END {print s+0}')"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
VERIFY_DIFF_RESOLVED="true"
|
|
169
|
+
_verify_log "diff base: $resolved_base..HEAD (merge-base $mb) -> $VERIFY_DIFF_FILES files, +$VERIFY_DIFF_INS/-$VERIFY_DIFF_DEL"
|
|
170
|
+
return 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Gate: build (faithful extension of run.sh language detection).
|
|
175
|
+
#
|
|
176
|
+
# A build is run only when a build command is DETECTABLE. If none is
|
|
177
|
+
# detectable, the gate is `skipped` (not-applicable), which does NOT degrade
|
|
178
|
+
# the verdict. A detected build that fails emits a Critical finding.
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
verify_gate_build() {
|
|
181
|
+
local tree="$1"
|
|
182
|
+
local timeout_s="${LOKI_GATE_TIMEOUT:-300}"
|
|
183
|
+
|
|
184
|
+
# package.json with a "build" script.
|
|
185
|
+
if [ -f "$tree/package.json" ] && grep -q '"build"[[:space:]]*:' "$tree/package.json" 2>/dev/null; then
|
|
186
|
+
local out rc=0
|
|
187
|
+
out="$(cd "$tree" && timeout "$timeout_s" npm run build 2>&1)" || rc=$?
|
|
188
|
+
if [ "$rc" -eq 0 ]; then
|
|
189
|
+
_verify_add_gate "build" "pass" "npm" "npm run build succeeded" "true"
|
|
190
|
+
else
|
|
191
|
+
_verify_add_gate "build" "fail" "npm" "npm run build failed (rc=$rc)" "true"
|
|
192
|
+
_verify_add_finding "Critical" "build" "deterministic:npm-build" "package.json" "null" \
|
|
193
|
+
"Build failed: npm run build exited $rc. $(printf '%s' "$out" | tail -2 | tr '\n' ' ')"
|
|
194
|
+
fi
|
|
195
|
+
return 0
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Go.
|
|
199
|
+
if [ -f "$tree/go.mod" ] && command -v go >/dev/null 2>&1; then
|
|
200
|
+
local out rc=0
|
|
201
|
+
out="$(cd "$tree" && timeout "$timeout_s" go build ./... 2>&1)" || rc=$?
|
|
202
|
+
if [ "$rc" -eq 0 ]; then
|
|
203
|
+
_verify_add_gate "build" "pass" "go" "go build ./... succeeded" "true"
|
|
204
|
+
else
|
|
205
|
+
_verify_add_gate "build" "fail" "go" "go build ./... failed (rc=$rc)" "true"
|
|
206
|
+
_verify_add_finding "Critical" "build" "deterministic:go-build" "go.mod" "null" \
|
|
207
|
+
"Build failed: go build exited $rc. $(printf '%s' "$out" | tail -2 | tr '\n' ' ')"
|
|
208
|
+
fi
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# Rust.
|
|
213
|
+
if [ -f "$tree/Cargo.toml" ] && command -v cargo >/dev/null 2>&1; then
|
|
214
|
+
local out rc=0
|
|
215
|
+
out="$(cd "$tree" && timeout "$timeout_s" cargo build 2>&1)" || rc=$?
|
|
216
|
+
if [ "$rc" -eq 0 ]; then
|
|
217
|
+
_verify_add_gate "build" "pass" "cargo" "cargo build succeeded" "true"
|
|
218
|
+
else
|
|
219
|
+
_verify_add_gate "build" "fail" "cargo" "cargo build failed (rc=$rc)" "true"
|
|
220
|
+
_verify_add_finding "Critical" "build" "deterministic:cargo-build" "Cargo.toml" "null" \
|
|
221
|
+
"Build failed: cargo build exited $rc. $(printf '%s' "$out" | tail -2 | tr '\n' ' ')"
|
|
222
|
+
fi
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
_verify_add_gate "build" "skipped" "" "no detectable build command" "true"
|
|
227
|
+
return 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Gate: tests (faithful port of enforce_test_coverage detection, run.sh:6624).
|
|
232
|
+
#
|
|
233
|
+
# Detection order mirrors the source: vitest -> jest -> mocha (package.json),
|
|
234
|
+
# then python (pytest), then go test, then cargo test.
|
|
235
|
+
#
|
|
236
|
+
# skipped = no test framework detected at all (not-applicable).
|
|
237
|
+
# inconclusive = a project is present but no runnable framework
|
|
238
|
+
# (e.g. package.json with no test runner, or a python project
|
|
239
|
+
# with no pytest on PATH). Forces at-least-CONCERNS.
|
|
240
|
+
# fail = tests ran and failed -> High finding.
|
|
241
|
+
# pass = tests ran green.
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
verify_gate_tests() {
|
|
244
|
+
local tree="$1"
|
|
245
|
+
local timeout_s="${LOKI_GATE_TIMEOUT:-300}"
|
|
246
|
+
local runner="none"
|
|
247
|
+
local rc=0
|
|
248
|
+
local out=""
|
|
249
|
+
|
|
250
|
+
if [ -f "$tree/package.json" ]; then
|
|
251
|
+
if grep -q '"vitest"' "$tree/package.json" 2>/dev/null; then
|
|
252
|
+
runner="vitest"
|
|
253
|
+
out="$(cd "$tree" && timeout "$timeout_s" npx vitest run 2>&1)" || rc=$?
|
|
254
|
+
elif grep -q '"jest"' "$tree/package.json" 2>/dev/null; then
|
|
255
|
+
runner="jest"
|
|
256
|
+
out="$(cd "$tree" && timeout "$timeout_s" npx jest --passWithNoTests --forceExit 2>&1)" || rc=$?
|
|
257
|
+
elif grep -q '"mocha"' "$tree/package.json" 2>/dev/null; then
|
|
258
|
+
runner="mocha"
|
|
259
|
+
out="$(cd "$tree" && timeout "$timeout_s" npx mocha 2>&1)" || rc=$?
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
if [ "$runner" = "none" ]; then
|
|
264
|
+
# Python project? (mirrors run.sh:6729-6742 detection)
|
|
265
|
+
local has_python=false
|
|
266
|
+
if [ -f "$tree/setup.py" ] || [ -f "$tree/pyproject.toml" ] \
|
|
267
|
+
|| [ -f "$tree/setup.cfg" ] || [ -f "$tree/pytest.ini" ] \
|
|
268
|
+
|| [ -f "$tree/conftest.py" ]; then
|
|
269
|
+
has_python=true
|
|
270
|
+
elif [ -d "$tree/tests" ]; then
|
|
271
|
+
if find "$tree/tests" -maxdepth 3 -type f \
|
|
272
|
+
\( -name 'test_*.py' -o -name '*_test.py' -o -name 'conftest.py' \) \
|
|
273
|
+
-print -quit 2>/dev/null | grep -q .; then
|
|
274
|
+
has_python=true
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
277
|
+
# Council finding (v7.27.0): bare root-level test files with no
|
|
278
|
+
# pyproject/setup/tests-dir were invisible to detection, letting verify
|
|
279
|
+
# emit VERIFIED while pytest could have discovered and run them. Detect
|
|
280
|
+
# them so the gate runs (or goes inconclusive -> CONCERNS when no
|
|
281
|
+
# runner is installed), never silently skipped.
|
|
282
|
+
if [ "$has_python" = "false" ]; then
|
|
283
|
+
if find "$tree" -maxdepth 1 -type f \
|
|
284
|
+
\( -name 'test_*.py' -o -name '*_test.py' \) \
|
|
285
|
+
-print -quit 2>/dev/null | grep -q .; then
|
|
286
|
+
has_python=true
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
if [ "$has_python" = "true" ]; then
|
|
290
|
+
if command -v pytest >/dev/null 2>&1; then
|
|
291
|
+
runner="pytest"
|
|
292
|
+
out="$(cd "$tree" && timeout "$timeout_s" pytest --tb=short 2>&1)" || rc=$?
|
|
293
|
+
else
|
|
294
|
+
# Applicable but cannot run -> inconclusive (Entanglement 2).
|
|
295
|
+
_verify_add_gate "tests" "inconclusive" "pytest" "python project detected but pytest not on PATH" "true"
|
|
296
|
+
return 0
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
if [ "$runner" = "none" ] && [ -f "$tree/go.mod" ]; then
|
|
302
|
+
if command -v go >/dev/null 2>&1; then
|
|
303
|
+
runner="go-test"
|
|
304
|
+
out="$(cd "$tree" && timeout "$timeout_s" go test ./... 2>&1)" || rc=$?
|
|
305
|
+
else
|
|
306
|
+
_verify_add_gate "tests" "inconclusive" "go-test" "go project detected but go not on PATH" "true"
|
|
307
|
+
return 0
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
if [ "$runner" = "none" ] && [ -f "$tree/Cargo.toml" ]; then
|
|
312
|
+
if command -v cargo >/dev/null 2>&1; then
|
|
313
|
+
runner="cargo-test"
|
|
314
|
+
out="$(cd "$tree" && timeout "$timeout_s" cargo test 2>&1)" || rc=$?
|
|
315
|
+
else
|
|
316
|
+
_verify_add_gate "tests" "inconclusive" "cargo-test" "rust project detected but cargo not on PATH" "true"
|
|
317
|
+
return 0
|
|
318
|
+
fi
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
if [ "$runner" = "none" ]; then
|
|
322
|
+
_verify_add_gate "tests" "skipped" "" "no test framework detected" "true"
|
|
323
|
+
return 0
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
if [ "$rc" -eq 124 ]; then
|
|
327
|
+
_verify_add_gate "tests" "inconclusive" "$runner" "test run timed out after ${timeout_s}s" "true"
|
|
328
|
+
return 0
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
if [ "$rc" -eq 0 ]; then
|
|
332
|
+
_verify_add_gate "tests" "pass" "$runner" "tests passed" "true"
|
|
333
|
+
else
|
|
334
|
+
_verify_add_gate "tests" "fail" "$runner" "tests failed (rc=$rc)" "true"
|
|
335
|
+
_verify_add_finding "High" "tests" "deterministic:$runner" "" "null" \
|
|
336
|
+
"Tests failed under $runner (exit $rc). $(printf '%s' "$out" | tail -2 | tr '\n' ' ')"
|
|
337
|
+
fi
|
|
338
|
+
return 0
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# Gate: static analysis / lint (faithful port of enforce_static_analysis,
|
|
343
|
+
# run.sh:6299). Runs only on files in the PR diff (merge-base..HEAD).
|
|
344
|
+
#
|
|
345
|
+
# skipped = no changed files OR no lint-capable files in the diff.
|
|
346
|
+
# fail = a configured/available linter reported errors -> Medium finding.
|
|
347
|
+
# pass = ran clean.
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
verify_gate_static() {
|
|
350
|
+
local tree="$1"
|
|
351
|
+
local changed="$VERIFY_DIFF_NAMES"
|
|
352
|
+
|
|
353
|
+
if [ -z "$changed" ]; then
|
|
354
|
+
_verify_add_gate "static_analysis" "skipped" "" "no changed files in diff" "true"
|
|
355
|
+
return 0
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
local findings=0
|
|
359
|
+
local checked=0
|
|
360
|
+
local details=""
|
|
361
|
+
|
|
362
|
+
# JavaScript/TypeScript syntax (node --check for .js, tsc/bun for .ts).
|
|
363
|
+
local js_files
|
|
364
|
+
js_files="$(printf '%s\n' "$changed" | grep -E '\.(js|jsx)$' || true)"
|
|
365
|
+
local ts_files
|
|
366
|
+
ts_files="$(printf '%s\n' "$changed" | grep -E '\.(ts|tsx)$' || true)"
|
|
367
|
+
|
|
368
|
+
if [ -n "$js_files" ] && command -v node >/dev/null 2>&1; then
|
|
369
|
+
local f
|
|
370
|
+
while IFS= read -r f; do
|
|
371
|
+
[ -z "$f" ] && continue
|
|
372
|
+
[ -f "$tree/$f" ] || continue
|
|
373
|
+
checked=$((checked + 1))
|
|
374
|
+
node --check "$tree/$f" >/dev/null 2>&1 || {
|
|
375
|
+
findings=$((findings + 1))
|
|
376
|
+
details="${details}JS syntax error: $f. "
|
|
377
|
+
_verify_add_finding "Medium" "lint" "deterministic:node-check" "$f" "null" \
|
|
378
|
+
"JavaScript syntax error in $f (node --check failed)."
|
|
379
|
+
}
|
|
380
|
+
done <<<"$js_files"
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
if [ -n "$ts_files" ]; then
|
|
384
|
+
local tschecker=""
|
|
385
|
+
if command -v tsc >/dev/null 2>&1; then tschecker="tsc"
|
|
386
|
+
elif command -v bun >/dev/null 2>&1; then tschecker="bun"; fi
|
|
387
|
+
if [ -n "$tschecker" ]; then
|
|
388
|
+
local f
|
|
389
|
+
while IFS= read -r f; do
|
|
390
|
+
[ -z "$f" ] && continue
|
|
391
|
+
[ -f "$tree/$f" ] || continue
|
|
392
|
+
checked=$((checked + 1))
|
|
393
|
+
local ok=0
|
|
394
|
+
if [ "$tschecker" = "tsc" ]; then
|
|
395
|
+
(cd "$tree" && tsc --noEmit --allowJs --jsx preserve --target esnext "$f") >/dev/null 2>&1 || ok=1
|
|
396
|
+
else
|
|
397
|
+
(cd "$tree" && bun --check "$f") >/dev/null 2>&1 || ok=1
|
|
398
|
+
fi
|
|
399
|
+
if [ "$ok" -ne 0 ]; then
|
|
400
|
+
findings=$((findings + 1))
|
|
401
|
+
details="${details}TS syntax error: $f. "
|
|
402
|
+
_verify_add_finding "Medium" "lint" "deterministic:$tschecker" "$f" "null" \
|
|
403
|
+
"TypeScript syntax error in $f ($tschecker --check failed)."
|
|
404
|
+
fi
|
|
405
|
+
done <<<"$ts_files"
|
|
406
|
+
fi
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# Python (py_compile).
|
|
410
|
+
local py_files
|
|
411
|
+
py_files="$(printf '%s\n' "$changed" | grep -E '\.py$' || true)"
|
|
412
|
+
if [ -n "$py_files" ] && command -v python3 >/dev/null 2>&1; then
|
|
413
|
+
local f
|
|
414
|
+
while IFS= read -r f; do
|
|
415
|
+
[ -z "$f" ] && continue
|
|
416
|
+
[ -f "$tree/$f" ] || continue
|
|
417
|
+
checked=$((checked + 1))
|
|
418
|
+
python3 -m py_compile "$tree/$f" >/dev/null 2>&1 || {
|
|
419
|
+
findings=$((findings + 1))
|
|
420
|
+
details="${details}Python syntax error: $f. "
|
|
421
|
+
_verify_add_finding "Medium" "lint" "deterministic:py_compile" "$f" "null" \
|
|
422
|
+
"Python syntax error in $f (py_compile failed)."
|
|
423
|
+
}
|
|
424
|
+
done <<<"$py_files"
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
# Shell (bash -n).
|
|
428
|
+
local sh_files
|
|
429
|
+
sh_files="$(printf '%s\n' "$changed" | grep -E '\.(sh|bash)$' || true)"
|
|
430
|
+
if [ -n "$sh_files" ]; then
|
|
431
|
+
local f
|
|
432
|
+
while IFS= read -r f; do
|
|
433
|
+
[ -z "$f" ] && continue
|
|
434
|
+
[ -f "$tree/$f" ] || continue
|
|
435
|
+
checked=$((checked + 1))
|
|
436
|
+
bash -n "$tree/$f" >/dev/null 2>&1 || {
|
|
437
|
+
findings=$((findings + 1))
|
|
438
|
+
details="${details}Shell syntax error: $f. "
|
|
439
|
+
_verify_add_finding "Medium" "lint" "deterministic:bash-n" "$f" "null" \
|
|
440
|
+
"Shell syntax error in $f (bash -n failed)."
|
|
441
|
+
}
|
|
442
|
+
done <<<"$sh_files"
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
if [ "$checked" -eq 0 ]; then
|
|
446
|
+
_verify_add_gate "static_analysis" "skipped" "" "no lint-capable changed files" "true"
|
|
447
|
+
elif [ "$findings" -eq 0 ]; then
|
|
448
|
+
_verify_add_gate "static_analysis" "pass" "syntax" "$checked file(s) checked, no syntax errors" "true"
|
|
449
|
+
else
|
|
450
|
+
_verify_add_gate "static_analysis" "fail" "syntax" "$findings issue(s) in $checked checked file(s): $details" "true"
|
|
451
|
+
fi
|
|
452
|
+
return 0
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
# Gate: secret scan (NET-NEW). gitleaks if available, otherwise a regex
|
|
457
|
+
# fallback over the PR-diff files. A planted credential is a Critical finding.
|
|
458
|
+
#
|
|
459
|
+
# skipped = no changed files to scan.
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
verify_gate_secret_scan() {
|
|
462
|
+
local tree="$1"
|
|
463
|
+
local changed="$VERIFY_DIFF_NAMES"
|
|
464
|
+
|
|
465
|
+
if command -v gitleaks >/dev/null 2>&1; then
|
|
466
|
+
if [ -z "$changed" ]; then
|
|
467
|
+
_verify_add_gate "secret_scan" "skipped" "gitleaks" "no changed files in diff" "true"
|
|
468
|
+
return 0
|
|
469
|
+
fi
|
|
470
|
+
# Scan ONLY the PR-diff files, not the whole tree/history. A verifier
|
|
471
|
+
# attests that THE CHANGE is safe; blocking on pre-existing secrets in
|
|
472
|
+
# untouched files is the #1 false-positive-fatigue failure (spec
|
|
473
|
+
# Section 7.2). Use gitleaks filesystem mode (`detect --no-git --source`)
|
|
474
|
+
# per changed file, which is stable across gitleaks v8 and avoids
|
|
475
|
+
# depending on a specific commit-range flag form.
|
|
476
|
+
local hits=0 detail="" f
|
|
477
|
+
while IFS= read -r f; do
|
|
478
|
+
[ -z "$f" ] && continue
|
|
479
|
+
[ -f "$tree/$f" ] || continue
|
|
480
|
+
if ! gitleaks detect --no-banner --redact --no-git \
|
|
481
|
+
--source "$tree/$f" >/dev/null 2>&1; then
|
|
482
|
+
hits=$((hits + 1))
|
|
483
|
+
detail="${detail}${f} "
|
|
484
|
+
_verify_add_finding "Critical" "security" "deterministic:gitleaks" "$f" "null" \
|
|
485
|
+
"gitleaks detected a secret in $f."
|
|
486
|
+
fi
|
|
487
|
+
done <<<"$changed"
|
|
488
|
+
if [ "$hits" -eq 0 ]; then
|
|
489
|
+
_verify_add_gate "secret_scan" "pass" "gitleaks" "no secrets in changed files" "true"
|
|
490
|
+
else
|
|
491
|
+
_verify_add_gate "secret_scan" "fail" "gitleaks" "$hits changed file(s) with secrets: $detail" "true"
|
|
492
|
+
fi
|
|
493
|
+
return 0
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
# Regex fallback over PR-diff files ONLY (gitleaks not installed). A verifier
|
|
497
|
+
# attests that THE CHANGE is safe; it must not block on pre-existing secrets
|
|
498
|
+
# in untouched files (false-positive fatigue, spec Section 7.2). There is
|
|
499
|
+
# deliberately NO whole-repo fallback: an empty diff short-circuits upstream
|
|
500
|
+
# before this gate runs.
|
|
501
|
+
local changed="$VERIFY_DIFF_NAMES"
|
|
502
|
+
if [ -z "$changed" ]; then
|
|
503
|
+
_verify_add_gate "secret_scan" "skipped" "regex-fallback" "no changed files in diff" "true"
|
|
504
|
+
return 0
|
|
505
|
+
fi
|
|
506
|
+
|
|
507
|
+
# High-confidence credential patterns. Conservative on purpose to limit
|
|
508
|
+
# false positives (spec Section 7.2).
|
|
509
|
+
local patterns=(
|
|
510
|
+
'AKIA[0-9A-Z]{16}' # AWS access key id
|
|
511
|
+
'-----BEGIN [A-Z ]*PRIVATE KEY-----' # PEM private key
|
|
512
|
+
'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub token
|
|
513
|
+
'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack token
|
|
514
|
+
'sk-[A-Za-z0-9]{20,}' # OpenAI-style key
|
|
515
|
+
'AIza[0-9A-Za-z_-]{35}' # Google API key
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
local hits=0
|
|
519
|
+
local detail=""
|
|
520
|
+
local f
|
|
521
|
+
while IFS= read -r f; do
|
|
522
|
+
[ -z "$f" ] && continue
|
|
523
|
+
[ -f "$tree/$f" ] || continue
|
|
524
|
+
# Skip obvious binaries.
|
|
525
|
+
if LC_ALL=C grep -Iq . "$tree/$f" 2>/dev/null; then : ; else continue; fi
|
|
526
|
+
local p
|
|
527
|
+
for p in "${patterns[@]}"; do
|
|
528
|
+
if grep -Eq "$p" "$tree/$f" 2>/dev/null; then
|
|
529
|
+
hits=$((hits + 1))
|
|
530
|
+
detail="${detail}${f} "
|
|
531
|
+
_verify_add_finding "Critical" "security" "deterministic:regex-secret-scan" "$f" "null" \
|
|
532
|
+
"Potential hardcoded secret detected in $f (matched a high-confidence credential pattern)."
|
|
533
|
+
break
|
|
534
|
+
fi
|
|
535
|
+
done
|
|
536
|
+
done <<<"$changed"
|
|
537
|
+
|
|
538
|
+
if [ "$hits" -eq 0 ]; then
|
|
539
|
+
_verify_add_gate "secret_scan" "pass" "regex-fallback" "no secrets matched" "true"
|
|
540
|
+
else
|
|
541
|
+
_verify_add_gate "secret_scan" "fail" "regex-fallback" "$hits file(s) with potential secrets: $detail" "true"
|
|
542
|
+
fi
|
|
543
|
+
return 0
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
# Gate: dependency audit (NET-NEW). npm audit / pip-audit when lockfiles
|
|
548
|
+
# exist. High/Critical CVE -> High finding; moderate -> Medium.
|
|
549
|
+
#
|
|
550
|
+
# skipped = no lockfile (not-applicable).
|
|
551
|
+
# inconclusive = tool needed but not installed, or audit could not complete
|
|
552
|
+
# (e.g. offline) -> forces at-least-CONCERNS.
|
|
553
|
+
# ---------------------------------------------------------------------------
|
|
554
|
+
verify_gate_dependency_audit() {
|
|
555
|
+
local tree="$1"
|
|
556
|
+
local ran=0
|
|
557
|
+
|
|
558
|
+
# npm
|
|
559
|
+
if [ -f "$tree/package-lock.json" ] || [ -f "$tree/npm-shrinkwrap.json" ]; then
|
|
560
|
+
ran=1
|
|
561
|
+
if command -v npm >/dev/null 2>&1; then
|
|
562
|
+
local out rc=0
|
|
563
|
+
out="$(cd "$tree" && npm audit --json 2>/dev/null)" || rc=$?
|
|
564
|
+
# npm audit exits nonzero when vulns are found; rc alone is not an
|
|
565
|
+
# error. Distinguish "ran and found vulns" from "could not run".
|
|
566
|
+
if printf '%s' "$out" | python3 -c "import sys,json; json.load(sys.stdin)" >/dev/null 2>&1; then
|
|
567
|
+
local sev
|
|
568
|
+
sev="$(printf '%s' "$out" | python3 -c '
|
|
569
|
+
import sys, json
|
|
570
|
+
d = json.load(sys.stdin)
|
|
571
|
+
v = d.get("metadata", {}).get("vulnerabilities", {})
|
|
572
|
+
crit = v.get("critical", 0); high = v.get("high", 0)
|
|
573
|
+
mod = v.get("moderate", 0); low = v.get("low", 0)
|
|
574
|
+
print("%d %d %d %d" % (crit, high, mod, low))
|
|
575
|
+
' 2>/dev/null || echo "")"
|
|
576
|
+
if [ -n "$sev" ]; then
|
|
577
|
+
read -r _c _h _m _l <<<"$sev"
|
|
578
|
+
if [ "$_c" -gt 0 ] || [ "$_h" -gt 0 ]; then
|
|
579
|
+
_verify_add_gate "dependency_audit" "fail" "npm-audit" "$_c critical, $_h high CVEs" "true"
|
|
580
|
+
_verify_add_finding "High" "dependencies" "deterministic:npm-audit" "package-lock.json" "null" \
|
|
581
|
+
"npm audit found $_c critical and $_h high severity vulnerabilities."
|
|
582
|
+
elif [ "$_m" -gt 0 ]; then
|
|
583
|
+
_verify_add_gate "dependency_audit" "fail" "npm-audit" "$_m moderate CVEs" "true"
|
|
584
|
+
_verify_add_finding "Medium" "dependencies" "deterministic:npm-audit" "package-lock.json" "null" \
|
|
585
|
+
"npm audit found $_m moderate severity vulnerabilities."
|
|
586
|
+
else
|
|
587
|
+
_verify_add_gate "dependency_audit" "pass" "npm-audit" "no high/critical CVEs ($_l low)" "true"
|
|
588
|
+
fi
|
|
589
|
+
else
|
|
590
|
+
_verify_add_gate "dependency_audit" "inconclusive" "npm-audit" "could not parse npm audit output" "true"
|
|
591
|
+
fi
|
|
592
|
+
else
|
|
593
|
+
_verify_add_gate "dependency_audit" "inconclusive" "npm-audit" "npm audit could not complete (offline or registry error)" "true"
|
|
594
|
+
fi
|
|
595
|
+
else
|
|
596
|
+
_verify_add_gate "dependency_audit" "inconclusive" "npm-audit" "package-lock.json present but npm not on PATH" "true"
|
|
597
|
+
fi
|
|
598
|
+
fi
|
|
599
|
+
|
|
600
|
+
# python (pip-audit)
|
|
601
|
+
if [ -f "$tree/requirements.txt" ] || [ -f "$tree/Pipfile.lock" ] || [ -f "$tree/poetry.lock" ]; then
|
|
602
|
+
ran=1
|
|
603
|
+
if command -v pip-audit >/dev/null 2>&1; then
|
|
604
|
+
local out rc=0
|
|
605
|
+
out="$(cd "$tree" && pip-audit --format json 2>/dev/null)" || rc=$?
|
|
606
|
+
if printf '%s' "$out" | python3 -c "import sys,json; json.load(sys.stdin)" >/dev/null 2>&1; then
|
|
607
|
+
local count
|
|
608
|
+
count="$(printf '%s' "$out" | python3 -c '
|
|
609
|
+
import sys, json
|
|
610
|
+
d = json.load(sys.stdin)
|
|
611
|
+
deps = d.get("dependencies", d if isinstance(d, list) else [])
|
|
612
|
+
n = sum(len(x.get("vulns", [])) for x in deps) if isinstance(deps, list) else 0
|
|
613
|
+
print(n)
|
|
614
|
+
' 2>/dev/null || echo "")"
|
|
615
|
+
if [ -n "$count" ] && [ "$count" -gt 0 ]; then
|
|
616
|
+
_verify_add_gate "dependency_audit" "fail" "pip-audit" "$count known vulnerabilities" "true"
|
|
617
|
+
_verify_add_finding "High" "dependencies" "deterministic:pip-audit" "requirements.txt" "null" \
|
|
618
|
+
"pip-audit found $count known vulnerabilities in Python dependencies."
|
|
619
|
+
elif [ -n "$count" ]; then
|
|
620
|
+
_verify_add_gate "dependency_audit" "pass" "pip-audit" "no known vulnerabilities" "true"
|
|
621
|
+
else
|
|
622
|
+
_verify_add_gate "dependency_audit" "inconclusive" "pip-audit" "could not parse pip-audit output" "true"
|
|
623
|
+
fi
|
|
624
|
+
else
|
|
625
|
+
_verify_add_gate "dependency_audit" "inconclusive" "pip-audit" "pip-audit could not complete (offline or index error)" "true"
|
|
626
|
+
fi
|
|
627
|
+
else
|
|
628
|
+
_verify_add_gate "dependency_audit" "inconclusive" "pip-audit" "python lockfile present but pip-audit not on PATH" "true"
|
|
629
|
+
fi
|
|
630
|
+
fi
|
|
631
|
+
|
|
632
|
+
if [ "$ran" -eq 0 ]; then
|
|
633
|
+
_verify_add_gate "dependency_audit" "skipped" "" "no dependency lockfile found" "true"
|
|
634
|
+
fi
|
|
635
|
+
return 0
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
# ---------------------------------------------------------------------------
|
|
639
|
+
# Verdict computation (Entanglement 2: inconclusive -> CONCERNS, never VERIFIED).
|
|
640
|
+
#
|
|
641
|
+
# Pure function of:
|
|
642
|
+
# - highest severity finding present (Critical/High block by default),
|
|
643
|
+
# - evidence conclusiveness (any inconclusive gate, or unresolved diff,
|
|
644
|
+
# or empty diff -> at least CONCERNS),
|
|
645
|
+
# - block threshold (default Critical,High; configurable via --block-on).
|
|
646
|
+
#
|
|
647
|
+
# Sets globals: VERIFY_VERDICT VERIFY_EXIT
|
|
648
|
+
# ---------------------------------------------------------------------------
|
|
649
|
+
verify_compute_verdict() {
|
|
650
|
+
local block_on="$1" # comma list, lowercased, e.g. "critical,high"
|
|
651
|
+
|
|
652
|
+
local has_critical=false has_high=false has_medium=false has_low=false
|
|
653
|
+
if [ -s "$_VERIFY_FINDINGS_FILE" ]; then
|
|
654
|
+
local sev
|
|
655
|
+
while IFS=$'\t' read -r sev _; do
|
|
656
|
+
case "$sev" in
|
|
657
|
+
Critical) has_critical=true ;;
|
|
658
|
+
High) has_high=true ;;
|
|
659
|
+
Medium) has_medium=true ;;
|
|
660
|
+
Low) has_low=true ;;
|
|
661
|
+
esac
|
|
662
|
+
done <"$_VERIFY_FINDINGS_FILE"
|
|
663
|
+
fi
|
|
664
|
+
|
|
665
|
+
# Determine blocking from threshold.
|
|
666
|
+
local blocked=false
|
|
667
|
+
case ",$block_on," in
|
|
668
|
+
*,critical,*) [ "$has_critical" = "true" ] && blocked=true ;;
|
|
669
|
+
esac
|
|
670
|
+
case ",$block_on," in
|
|
671
|
+
*,high,*) [ "$has_high" = "true" ] && blocked=true ;;
|
|
672
|
+
esac
|
|
673
|
+
case ",$block_on," in
|
|
674
|
+
*,medium,*) [ "$has_medium" = "true" ] && blocked=true ;;
|
|
675
|
+
esac
|
|
676
|
+
case ",$block_on," in
|
|
677
|
+
*,low,*) [ "$has_low" = "true" ] && blocked=true ;;
|
|
678
|
+
esac
|
|
679
|
+
|
|
680
|
+
# Conclusiveness: any inconclusive gate, unresolved diff, or empty diff.
|
|
681
|
+
# Read the gate status (field 2, tab-separated) directly rather than relying
|
|
682
|
+
# on grep -P, which is not portable to BSD grep (e.g. under a constrained
|
|
683
|
+
# PATH on macOS). Each gate record is: gate \t status \t ... .
|
|
684
|
+
local inconclusive=false
|
|
685
|
+
local _g_gate _g_status
|
|
686
|
+
while IFS=$'\t' read -r _g_gate _g_status _; do
|
|
687
|
+
if [ "$_g_status" = "inconclusive" ]; then
|
|
688
|
+
inconclusive=true
|
|
689
|
+
break
|
|
690
|
+
fi
|
|
691
|
+
done <"$_VERIFY_GATES_FILE"
|
|
692
|
+
if [ "${VERIFY_DIFF_RESOLVED:-false}" != "true" ]; then
|
|
693
|
+
inconclusive=true
|
|
694
|
+
elif [ "${VERIFY_DIFF_FILES:-0}" -eq 0 ]; then
|
|
695
|
+
# Empty diff: nothing to verify -> not VERIFIED.
|
|
696
|
+
inconclusive=true
|
|
697
|
+
fi
|
|
698
|
+
|
|
699
|
+
# Any below-threshold finding (Medium/Low present but not blocking) -> CONCERNS.
|
|
700
|
+
local has_nonblock_finding=false
|
|
701
|
+
if [ "$has_medium" = "true" ] || [ "$has_low" = "true" ]; then
|
|
702
|
+
has_nonblock_finding=true
|
|
703
|
+
fi
|
|
704
|
+
# A blocking-severity finding that is NOT in the block list still counts as
|
|
705
|
+
# a non-block finding (e.g. --block-on critical with a High present).
|
|
706
|
+
if [ "$blocked" = "false" ]; then
|
|
707
|
+
if [ "$has_critical" = "true" ] || [ "$has_high" = "true" ]; then
|
|
708
|
+
has_nonblock_finding=true
|
|
709
|
+
fi
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
if [ "$blocked" = "true" ]; then
|
|
713
|
+
VERIFY_VERDICT="BLOCKED"
|
|
714
|
+
VERIFY_EXIT=$VERIFY_EXIT_BLOCKED
|
|
715
|
+
elif [ "$has_nonblock_finding" = "true" ] || [ "$inconclusive" = "true" ]; then
|
|
716
|
+
VERIFY_VERDICT="CONCERNS"
|
|
717
|
+
VERIFY_EXIT=$VERIFY_EXIT_CONCERNS
|
|
718
|
+
else
|
|
719
|
+
VERIFY_VERDICT="VERIFIED"
|
|
720
|
+
VERIFY_EXIT=$VERIFY_EXIT_VERIFIED
|
|
721
|
+
fi
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# ---------------------------------------------------------------------------
|
|
725
|
+
# Evidence emitters: consolidated evidence.json + report.md (Entanglement 3).
|
|
726
|
+
# ---------------------------------------------------------------------------
|
|
727
|
+
verify_emit_evidence() {
|
|
728
|
+
local out_dir="$1"
|
|
729
|
+
local started_at="$2"
|
|
730
|
+
local completed_at="$3"
|
|
731
|
+
local block_on="$4"
|
|
732
|
+
|
|
733
|
+
local tool_version
|
|
734
|
+
tool_version="$(_verify_tool_version)"
|
|
735
|
+
local repo_name
|
|
736
|
+
repo_name="$(git config --get remote.origin.url 2>/dev/null | sed -E 's#.*[:/]([^/]+/[^/]+)(\.git)?$#\1#' || echo "local")"
|
|
737
|
+
[ -z "$repo_name" ] && repo_name="local"
|
|
738
|
+
|
|
739
|
+
_VERIFY_OUT_DIR="$out_dir" \
|
|
740
|
+
_VERIFY_FINDINGS="$_VERIFY_FINDINGS_FILE" \
|
|
741
|
+
_VERIFY_GATES="$_VERIFY_GATES_FILE" \
|
|
742
|
+
_V_VERDICT="$VERIFY_VERDICT" \
|
|
743
|
+
_V_EXIT="$VERIFY_EXIT" \
|
|
744
|
+
_V_SCHEMA="$VERIFY_SCHEMA_VERSION" \
|
|
745
|
+
_V_TOOLVER="$tool_version" \
|
|
746
|
+
_V_REPO="$repo_name" \
|
|
747
|
+
_V_HEAD="${VERIFY_HEAD_SHA:-}" \
|
|
748
|
+
_V_BASE="${VERIFY_BASE_REF:-}" \
|
|
749
|
+
_V_MB="${VERIFY_MERGE_BASE:-}" \
|
|
750
|
+
_V_FILES="${VERIFY_DIFF_FILES:-0}" \
|
|
751
|
+
_V_INS="${VERIFY_DIFF_INS:-0}" \
|
|
752
|
+
_V_DEL="${VERIFY_DIFF_DEL:-0}" \
|
|
753
|
+
_V_START="$started_at" \
|
|
754
|
+
_V_DONE="$completed_at" \
|
|
755
|
+
_V_BLOCKON="$block_on" \
|
|
756
|
+
python3 - <<'PYEOF'
|
|
757
|
+
import json, os, hashlib
|
|
758
|
+
|
|
759
|
+
out_dir = os.environ["_VERIFY_OUT_DIR"]
|
|
760
|
+
findings_file = os.environ["_VERIFY_FINDINGS"]
|
|
761
|
+
gates_file = os.environ["_VERIFY_GATES"]
|
|
762
|
+
|
|
763
|
+
def read_tsv(path, n):
|
|
764
|
+
rows = []
|
|
765
|
+
if os.path.exists(path):
|
|
766
|
+
with open(path) as f:
|
|
767
|
+
for line in f:
|
|
768
|
+
line = line.rstrip("\n")
|
|
769
|
+
if not line:
|
|
770
|
+
continue
|
|
771
|
+
parts = line.split("\t")
|
|
772
|
+
parts += [""] * (n - len(parts))
|
|
773
|
+
rows.append(parts[:n])
|
|
774
|
+
return rows
|
|
775
|
+
|
|
776
|
+
block_on = [s.strip() for s in os.environ["_V_BLOCKON"].split(",") if s.strip()]
|
|
777
|
+
|
|
778
|
+
gates = []
|
|
779
|
+
for gate, status, runner, summary, reproducible in read_tsv(gates_file, 5):
|
|
780
|
+
g = {"gate": gate, "status": status, "reproducible": (reproducible == "true")}
|
|
781
|
+
if runner:
|
|
782
|
+
g["runner" if gate in ("tests",) else "scanner" if gate in ("secret_scan", "dependency_audit") else "runner"] = runner
|
|
783
|
+
if summary:
|
|
784
|
+
g["summary"] = summary
|
|
785
|
+
gates.append(g)
|
|
786
|
+
|
|
787
|
+
findings = []
|
|
788
|
+
for severity, category, source, fpath, line, message in read_tsv(findings_file, 6):
|
|
789
|
+
text = message[:80]
|
|
790
|
+
fid = "%s::%s" % (source, text)
|
|
791
|
+
blocking = severity.lower() in block_on
|
|
792
|
+
findings.append({
|
|
793
|
+
"id": fid,
|
|
794
|
+
"severity": severity,
|
|
795
|
+
"category": category,
|
|
796
|
+
"source": source,
|
|
797
|
+
"file": fpath if fpath else None,
|
|
798
|
+
"line": (int(line) if line.isdigit() else None),
|
|
799
|
+
"message": message,
|
|
800
|
+
"blocking": blocking,
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
doc = {
|
|
804
|
+
"schema_version": os.environ["_V_SCHEMA"],
|
|
805
|
+
"verdict": os.environ["_V_VERDICT"],
|
|
806
|
+
"exit_code": int(os.environ["_V_EXIT"]),
|
|
807
|
+
"subject": {
|
|
808
|
+
"repo": os.environ["_V_REPO"],
|
|
809
|
+
"pr_number": None,
|
|
810
|
+
"head_sha": os.environ["_V_HEAD"],
|
|
811
|
+
"base_branch": os.environ["_V_BASE"],
|
|
812
|
+
"merge_base_sha": os.environ["_V_MB"],
|
|
813
|
+
"diff_stats": {
|
|
814
|
+
"files_changed": int(os.environ["_V_FILES"]),
|
|
815
|
+
"insertions": int(os.environ["_V_INS"]),
|
|
816
|
+
"deletions": int(os.environ["_V_DEL"]),
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
"produced_by": {
|
|
820
|
+
"tool": "loki verify",
|
|
821
|
+
"tool_version": os.environ["_V_TOOLVER"],
|
|
822
|
+
"run_started_at": os.environ["_V_START"],
|
|
823
|
+
"run_completed_at": os.environ["_V_DONE"],
|
|
824
|
+
"runner": "self-hosted",
|
|
825
|
+
"key_source": "none",
|
|
826
|
+
},
|
|
827
|
+
"deterministic_gates": gates,
|
|
828
|
+
"llm_review": {
|
|
829
|
+
"status": "skipped",
|
|
830
|
+
"reason": "deterministic-only MVP (30-day cut); single-reviewer LLM stage and blind council are deferred to Phase 2",
|
|
831
|
+
"reproducible": False,
|
|
832
|
+
},
|
|
833
|
+
"findings": findings,
|
|
834
|
+
"suppressed": [],
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
838
|
+
ev_path = os.path.join(out_dir, "evidence.json")
|
|
839
|
+
with open(ev_path, "w") as f:
|
|
840
|
+
json.dump(doc, f, indent=2)
|
|
841
|
+
f.write("\n")
|
|
842
|
+
|
|
843
|
+
# ----- Markdown report -----
|
|
844
|
+
def sev_rank(s):
|
|
845
|
+
return {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Info": 4}.get(s, 5)
|
|
846
|
+
|
|
847
|
+
lines = []
|
|
848
|
+
lines.append("# Autonomi Verify report")
|
|
849
|
+
lines.append("")
|
|
850
|
+
lines.append("Verdict: **%s** (exit %d)" % (doc["verdict"], doc["exit_code"]))
|
|
851
|
+
lines.append("")
|
|
852
|
+
lines.append("Tool: loki verify %s | deterministic-only MVP (no LLM review)" % doc["produced_by"]["tool_version"])
|
|
853
|
+
lines.append("")
|
|
854
|
+
s = doc["subject"]
|
|
855
|
+
lines.append("## Subject")
|
|
856
|
+
lines.append("")
|
|
857
|
+
lines.append("- Repo: %s" % s["repo"])
|
|
858
|
+
lines.append("- Base: %s" % (s["base_branch"] or "(unresolved)"))
|
|
859
|
+
lines.append("- Head: %s" % (s["head_sha"] or "(none)"))
|
|
860
|
+
lines.append("- Merge base: %s" % (s["merge_base_sha"] or "(none)"))
|
|
861
|
+
ds = s["diff_stats"]
|
|
862
|
+
lines.append("- Diff: %d files, +%d / -%d" % (ds["files_changed"], ds["insertions"], ds["deletions"]))
|
|
863
|
+
lines.append("")
|
|
864
|
+
lines.append("## Deterministic gates")
|
|
865
|
+
lines.append("")
|
|
866
|
+
lines.append("| Gate | Status | Detail |")
|
|
867
|
+
lines.append("|------|--------|--------|")
|
|
868
|
+
for g in gates:
|
|
869
|
+
detail = g.get("summary", "")
|
|
870
|
+
lines.append("| %s | %s | %s |" % (g["gate"], g["status"], detail.replace("|", "\\|")))
|
|
871
|
+
lines.append("")
|
|
872
|
+
lines.append("## Findings")
|
|
873
|
+
lines.append("")
|
|
874
|
+
if findings:
|
|
875
|
+
lines.append("| Severity | Category | Source | File:Line | Blocking | Message |")
|
|
876
|
+
lines.append("|----------|----------|--------|-----------|----------|---------|")
|
|
877
|
+
for fd in sorted(findings, key=lambda x: sev_rank(x["severity"])):
|
|
878
|
+
loc = (fd["file"] or "-")
|
|
879
|
+
if fd["line"] is not None:
|
|
880
|
+
loc += ":%d" % fd["line"]
|
|
881
|
+
lines.append("| %s | %s | %s | %s | %s | %s |" % (
|
|
882
|
+
fd["severity"], fd["category"], fd["source"], loc,
|
|
883
|
+
"yes" if fd["blocking"] else "no",
|
|
884
|
+
fd["message"].replace("|", "\\|")))
|
|
885
|
+
else:
|
|
886
|
+
lines.append("No findings.")
|
|
887
|
+
lines.append("")
|
|
888
|
+
lines.append("## LLM review")
|
|
889
|
+
lines.append("")
|
|
890
|
+
lines.append("Skipped: %s" % doc["llm_review"]["reason"])
|
|
891
|
+
lines.append("")
|
|
892
|
+
lines.append("Evidence JSON: %s" % ev_path)
|
|
893
|
+
lines.append("")
|
|
894
|
+
|
|
895
|
+
with open(os.path.join(out_dir, "report.md"), "w") as f:
|
|
896
|
+
f.write("\n".join(lines))
|
|
897
|
+
|
|
898
|
+
print(ev_path)
|
|
899
|
+
PYEOF
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
# ---------------------------------------------------------------------------
|
|
903
|
+
# Help
|
|
904
|
+
# ---------------------------------------------------------------------------
|
|
905
|
+
verify_help() {
|
|
906
|
+
cat <<'EOF'
|
|
907
|
+
loki verify - Autonomi Verify: deterministic PR verification (MVP)
|
|
908
|
+
|
|
909
|
+
USAGE:
|
|
910
|
+
loki verify [<base-ref>] [options]
|
|
911
|
+
|
|
912
|
+
DESCRIPTION:
|
|
913
|
+
Verifies the CURRENT working tree (HEAD) against a base ref by computing
|
|
914
|
+
the PR-style delta: merge-base(base, HEAD)..HEAD. It runs deterministic
|
|
915
|
+
quality gates on that change set and emits a single auditable verdict plus
|
|
916
|
+
a machine-readable evidence document.
|
|
917
|
+
|
|
918
|
+
This MVP is DETERMINISTIC-ONLY. There is NO LLM code review in this slice.
|
|
919
|
+
The single-reviewer LLM stage and the blind multi-reviewer council are
|
|
920
|
+
sequenced for a later phase (see the verification spec). The evidence
|
|
921
|
+
document records llm_review.status = "skipped" honestly.
|
|
922
|
+
|
|
923
|
+
ARGUMENTS:
|
|
924
|
+
<base-ref> Base branch/ref to diff against. Default: origin/main, then
|
|
925
|
+
main. Tries origin/<ref> before local <ref>.
|
|
926
|
+
|
|
927
|
+
OPTIONS:
|
|
928
|
+
--base <ref> Same as the positional base-ref (explicit form).
|
|
929
|
+
--out <dir> Output directory. Default: .loki/verify
|
|
930
|
+
--block-on <list> Comma list of severities that BLOCK.
|
|
931
|
+
Default: critical,high (one notch looser than the
|
|
932
|
+
Loki build loop, which also blocks on medium).
|
|
933
|
+
--no-llm Accepted for forward-compat; LLM is already off in MVP.
|
|
934
|
+
-h, --help Show this help.
|
|
935
|
+
|
|
936
|
+
GATES (deterministic, reproducible by construction):
|
|
937
|
+
build run if a build command is detectable (npm/go/cargo)
|
|
938
|
+
tests vitest/jest/mocha/pytest/go test/cargo test
|
|
939
|
+
static_analysis syntax check of changed files (node/tsc/py_compile/bash -n)
|
|
940
|
+
secret_scan gitleaks if installed, else high-confidence regex fallback
|
|
941
|
+
dependency_audit npm audit / pip-audit when a lockfile exists
|
|
942
|
+
|
|
943
|
+
VERDICT MODEL:
|
|
944
|
+
VERIFIED zero findings, diff non-empty, all gates conclusive
|
|
945
|
+
CONCERNS below-threshold findings, OR inconclusive evidence
|
|
946
|
+
(inconclusive is NEVER upgraded to VERIFIED)
|
|
947
|
+
BLOCKED one or more findings at/above the block threshold
|
|
948
|
+
|
|
949
|
+
Gate-not-applicable (e.g. no lockfile) = skipped, does NOT affect verdict.
|
|
950
|
+
Gate-applicable-but-could-not-run = inconclusive, forces at-least-CONCERNS.
|
|
951
|
+
|
|
952
|
+
The diff is the COMMITTED delta merge-base(base, HEAD)..HEAD (PR semantics).
|
|
953
|
+
Uncommitted working-tree changes are not verified; commit them first. An
|
|
954
|
+
empty diff yields CONCERNS (nothing to verify), never VERIFIED.
|
|
955
|
+
|
|
956
|
+
EXIT CODES (this implementation):
|
|
957
|
+
0 VERIFIED
|
|
958
|
+
1 CONCERNS
|
|
959
|
+
2 BLOCKED
|
|
960
|
+
3 verifier error (could not complete; never silently passes)
|
|
961
|
+
|
|
962
|
+
NOTE: the verification spec (Section 1.1) lists 1=BLOCKED, 2=CONCERNS.
|
|
963
|
+
This implementation follows the build-task ordering (1=CONCERNS,
|
|
964
|
+
2=BLOCKED). A human must reconcile the two before the GitHub App consumes
|
|
965
|
+
these codes.
|
|
966
|
+
|
|
967
|
+
OUTPUT:
|
|
968
|
+
<out>/evidence.json consolidated machine-readable evidence (schema 1.0)
|
|
969
|
+
<out>/report.md human-readable verdict + findings table
|
|
970
|
+
|
|
971
|
+
EOF
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
# ---------------------------------------------------------------------------
|
|
975
|
+
# Living-spec drift gate (integration with autonomy/spec.sh).
|
|
976
|
+
#
|
|
977
|
+
# When .loki/spec/spec.lock exists, source the spec module and run its
|
|
978
|
+
# verify hook, which emits at most one SPEC_DRIFT finding (Medium -> CONCERNS)
|
|
979
|
+
# in the verify finding TSV shape. Records a gate row either way so the
|
|
980
|
+
# evidence document shows the spec was checked. Fully graceful: no lock, no
|
|
981
|
+
# module, or a hook error all degrade to a skipped gate and never abort verify.
|
|
982
|
+
# ---------------------------------------------------------------------------
|
|
983
|
+
verify_spec_drift_gate() {
|
|
984
|
+
local tree="${1:-.}"
|
|
985
|
+
local spec_dir="$tree/.loki/spec"
|
|
986
|
+
# Normalize a leading "./" so the lock-path probe is clean.
|
|
987
|
+
spec_dir="${spec_dir#./}"
|
|
988
|
+
local lock_file="$spec_dir/spec.lock"
|
|
989
|
+
|
|
990
|
+
if [ ! -f "$lock_file" ]; then
|
|
991
|
+
_verify_add_gate "spec_drift" "skipped" "loki-spec" "no spec lock (.loki/spec/spec.lock); run 'loki spec lock' to enable" "true"
|
|
992
|
+
return 0
|
|
993
|
+
fi
|
|
994
|
+
|
|
995
|
+
local spec_mod
|
|
996
|
+
spec_mod="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/spec.sh"
|
|
997
|
+
if [ ! -f "$spec_mod" ]; then
|
|
998
|
+
_verify_add_gate "spec_drift" "inconclusive" "loki-spec" "spec lock present but spec module not found" "true"
|
|
999
|
+
return 0
|
|
1000
|
+
fi
|
|
1001
|
+
|
|
1002
|
+
# Source the spec module in a guarded subshell-free way: it defines
|
|
1003
|
+
# spec_verify_hook which prints zero or one TSV finding line on stdout.
|
|
1004
|
+
# shellcheck source=/dev/null
|
|
1005
|
+
if ! source "$spec_mod" 2>/dev/null; then
|
|
1006
|
+
_verify_add_gate "spec_drift" "inconclusive" "loki-spec" "could not load spec module" "true"
|
|
1007
|
+
return 0
|
|
1008
|
+
fi
|
|
1009
|
+
|
|
1010
|
+
local finding
|
|
1011
|
+
finding="$(spec_verify_hook "$spec_dir" 2>/dev/null || true)"
|
|
1012
|
+
|
|
1013
|
+
if [ -n "$finding" ]; then
|
|
1014
|
+
# Append the SPEC_DRIFT finding(s) verbatim (already TSV-shaped).
|
|
1015
|
+
printf '%s\n' "$finding" >>"$_VERIFY_FINDINGS_FILE"
|
|
1016
|
+
_verify_add_gate "spec_drift" "fail" "loki-spec" "spec has drifted from its lock (see SPEC_DRIFT finding)" "true"
|
|
1017
|
+
else
|
|
1018
|
+
_verify_add_gate "spec_drift" "pass" "loki-spec" "spec is in sync with its lock" "true"
|
|
1019
|
+
fi
|
|
1020
|
+
return 0
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
# ---------------------------------------------------------------------------
|
|
1024
|
+
# Entry point
|
|
1025
|
+
# ---------------------------------------------------------------------------
|
|
1026
|
+
verify_main() {
|
|
1027
|
+
local base_ref=""
|
|
1028
|
+
local out_dir=".loki/verify"
|
|
1029
|
+
local block_on="critical,high"
|
|
1030
|
+
|
|
1031
|
+
while [ $# -gt 0 ]; do
|
|
1032
|
+
case "$1" in
|
|
1033
|
+
-h|--help) verify_help; return 0 ;;
|
|
1034
|
+
--base)
|
|
1035
|
+
base_ref="${2:-}"; shift 2 ;;
|
|
1036
|
+
--out)
|
|
1037
|
+
out_dir="${2:-}"; shift 2 ;;
|
|
1038
|
+
--block-on)
|
|
1039
|
+
block_on="$(printf '%s' "${2:-}" | tr '[:upper:]' '[:lower:]')"; shift 2 ;;
|
|
1040
|
+
--no-llm)
|
|
1041
|
+
shift ;;
|
|
1042
|
+
--) shift; break ;;
|
|
1043
|
+
-*)
|
|
1044
|
+
_verify_err "unknown option: $1"; verify_help; return $VERIFY_EXIT_ERROR ;;
|
|
1045
|
+
*)
|
|
1046
|
+
if [ -z "$base_ref" ]; then base_ref="$1"; else
|
|
1047
|
+
_verify_err "unexpected argument: $1"; return $VERIFY_EXIT_ERROR
|
|
1048
|
+
fi
|
|
1049
|
+
shift ;;
|
|
1050
|
+
esac
|
|
1051
|
+
done
|
|
1052
|
+
|
|
1053
|
+
# Default base.
|
|
1054
|
+
if [ -z "$base_ref" ]; then
|
|
1055
|
+
if git rev-parse --verify --quiet "origin/main" >/dev/null 2>&1; then
|
|
1056
|
+
base_ref="main"
|
|
1057
|
+
else
|
|
1058
|
+
base_ref="main"
|
|
1059
|
+
fi
|
|
1060
|
+
fi
|
|
1061
|
+
|
|
1062
|
+
local tree="."
|
|
1063
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
1064
|
+
_verify_err "not inside a git repository; cannot resolve a PR diff"
|
|
1065
|
+
# Still emit an evidence doc with inconclusive verdict (never silent pass).
|
|
1066
|
+
fi
|
|
1067
|
+
|
|
1068
|
+
_VERIFY_FINDINGS_FILE="$(mktemp -t loki-verify-findings.XXXXXX)" || { _verify_err "mktemp failed"; return $VERIFY_EXIT_ERROR; }
|
|
1069
|
+
_VERIFY_GATES_FILE="$(mktemp -t loki-verify-gates.XXXXXX)" || { _verify_err "mktemp failed"; return $VERIFY_EXIT_ERROR; }
|
|
1070
|
+
# shellcheck disable=SC2064
|
|
1071
|
+
trap "rm -f '$_VERIFY_FINDINGS_FILE' '$_VERIFY_GATES_FILE'" RETURN
|
|
1072
|
+
|
|
1073
|
+
local started_at completed_at
|
|
1074
|
+
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1075
|
+
|
|
1076
|
+
_verify_log "loki verify (deterministic-only MVP) starting; base=$base_ref out=$out_dir"
|
|
1077
|
+
|
|
1078
|
+
verify_diff_base "$base_ref"
|
|
1079
|
+
|
|
1080
|
+
# Short-circuit on an empty or unresolvable change set. A verifier verifies
|
|
1081
|
+
# a CHANGE; with nothing to verify there is no basis for a VERIFIED verdict.
|
|
1082
|
+
# Recording the gates as skipped (rather than running them against the whole
|
|
1083
|
+
# tree) avoids blocking on pre-existing repo state, and the unresolved/empty
|
|
1084
|
+
# diff is already reclassified as inconclusive -> CONCERNS by the verdict
|
|
1085
|
+
# function (Entanglement 2). A real PR always has a non-empty committed diff,
|
|
1086
|
+
# so this only triggers on the degenerate local case (e.g. uncommitted-only
|
|
1087
|
+
# changes: merge-base..HEAD is committed-only by design, spec Entanglement 1).
|
|
1088
|
+
if [ "${VERIFY_DIFF_RESOLVED:-false}" != "true" ] || [ "${VERIFY_DIFF_FILES:-0}" -eq 0 ]; then
|
|
1089
|
+
local _skip_reason="no resolvable change set vs base"
|
|
1090
|
+
[ "${VERIFY_DIFF_RESOLVED:-false}" = "true" ] && _skip_reason="empty diff vs base (nothing to verify)"
|
|
1091
|
+
_verify_log "$_skip_reason; skipping gates (verdict -> CONCERNS)"
|
|
1092
|
+
local _g
|
|
1093
|
+
for _g in build tests static_analysis secret_scan dependency_audit; do
|
|
1094
|
+
_verify_add_gate "$_g" "skipped" "" "$_skip_reason" "true"
|
|
1095
|
+
done
|
|
1096
|
+
else
|
|
1097
|
+
verify_gate_build "$tree"
|
|
1098
|
+
verify_gate_tests "$tree"
|
|
1099
|
+
verify_gate_static "$tree"
|
|
1100
|
+
verify_gate_secret_scan "$tree"
|
|
1101
|
+
verify_gate_dependency_audit "$tree"
|
|
1102
|
+
fi
|
|
1103
|
+
|
|
1104
|
+
# Living-spec integration: when a spec lock exists, fold a SPEC_DRIFT
|
|
1105
|
+
# finding into the verdict. Graceful no-op when there is no lock or the
|
|
1106
|
+
# spec machinery is unavailable -- verify must never fail to complete
|
|
1107
|
+
# because the optional spec module is missing.
|
|
1108
|
+
verify_spec_drift_gate "$tree"
|
|
1109
|
+
|
|
1110
|
+
verify_compute_verdict "$block_on"
|
|
1111
|
+
|
|
1112
|
+
completed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
1113
|
+
|
|
1114
|
+
verify_emit_evidence "$out_dir" "$started_at" "$completed_at" "$block_on" >/dev/null || {
|
|
1115
|
+
_verify_err "failed to emit evidence document"
|
|
1116
|
+
return $VERIFY_EXIT_ERROR
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
printf 'VERDICT: %s\n' "$VERIFY_VERDICT"
|
|
1120
|
+
printf 'Evidence: %s/evidence.json\n' "$out_dir"
|
|
1121
|
+
printf 'Report: %s/report.md\n' "$out_dir"
|
|
1122
|
+
|
|
1123
|
+
return "$VERIFY_EXIT"
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
# Allow direct execution: bash autonomy/verify.sh [args]
|
|
1127
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
1128
|
+
verify_main "$@"
|
|
1129
|
+
exit $?
|
|
1130
|
+
fi
|