forgedock 1.29.1 → 1.31.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/bin/engine/outcome-codes.mjs +5 -2
- package/bin/engine/phases.mjs +20 -3
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +453 -66
- package/bin/engine.mjs +50 -38
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +356 -2
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +278 -22
- package/bin/lean/conformance.test.mjs +251 -5
- package/bin/lean/context.mjs +133 -55
- package/bin/lean/context.test.mjs +175 -9
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +55 -19
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +799 -127
- package/bin/lean/lean.test.mjs +1749 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +341 -99
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +180 -0
- package/bin/lean/precheck.test.mjs +205 -0
- package/bin/lean/prefilter.mjs +180 -9
- package/bin/lean/prefilter.test.mjs +175 -3
- package/bin/lean/preflight.mjs +238 -0
- package/bin/lean/preflight.test.mjs +108 -0
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +134 -12
- package/bin/lean/recovery.test.mjs +75 -0
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +385 -0
- package/bin/lean/sequence.test.mjs +373 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +77 -10
- package/bin/lean/verify.test.mjs +74 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- package/commands/orchestrate.md +6 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +88 -6
- package/docs/OUTCOME-CODES.md +7 -4
- package/docs/RELEASE-NOTES.md +10 -0
- package/docs/STATS.md +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -0
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
- package/scripts/trusted-comments.test.sh +1 -1
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
set -uo pipefail
|
|
9
9
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
10
10
|
SPEC="$ROOT/commands/review-pr-staging.md"
|
|
11
|
+
# Since #4359 the spec calls `forgedock review-staging`: the 0A/0B.1 blocks live in review-staging-pre.sh, the panel guard,
|
|
12
|
+
# 6.75 delivery and 6.5 verdict handling in review-staging-post.sh, and the 7A-7C wiring in bin/review-staging.mjs.
|
|
13
|
+
PRE="$ROOT/scripts/review-staging-pre.sh"
|
|
14
|
+
POST="$ROOT/scripts/review-staging-post.sh"
|
|
15
|
+
CLI="$ROOT/bin/review-staging.mjs"
|
|
16
|
+
blk() { awk -v n="$2" '$0 == "# BEGIN " n {f=1; next} $0 == "# END " n {f=0} f' "$1"; }
|
|
11
17
|
TC="$ROOT/scripts/trusted-comments.sh"
|
|
12
18
|
PASS=0; FAILN=0
|
|
13
19
|
ok() { PASS=$((PASS+1)); }
|
|
@@ -24,10 +30,10 @@ chk HIGH 0; chk high 0; chk " High " 0; chk CRITICAL 0; chk critical 0
|
|
|
24
30
|
chk MEDIUM 1; chk medium 1; chk LOW 1; chk INFO 1
|
|
25
31
|
chk "" 0; chk "SEVERE" 0; chk "HIGH/MEDIUM" 0
|
|
26
32
|
|
|
27
|
-
# (2) full-sha reviewed test: the _AGENT regex from the
|
|
28
|
-
LINE=$(grep -E '^[[:space:]]*_AGENT=' "$
|
|
33
|
+
# (2) full-sha reviewed test: the _AGENT regex from the 0B.1 scope loop, run through the real trusted-comments predicate
|
|
34
|
+
LINE=$(grep -E '^[[:space:]]*_AGENT=' "$PRE" | head -1)
|
|
29
35
|
RE=$(printf '%s' "$LINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p')
|
|
30
|
-
if [ -n "$RE" ]; then ok; else bad "_AGENT regex not found in review-
|
|
36
|
+
if [ -n "$RE" ]; then ok; else bad "_AGENT regex not found in review-staging-pre.sh"; fi
|
|
31
37
|
FULL=0123456789abcdef0123456789abcdef01234567; SHORT=${FULL:0:7}
|
|
32
38
|
RE=${RE//\\\\/\\}; RE=${RE//\$\{_HEAD\}/$FULL}
|
|
33
39
|
FB=$'<!-- REVIEW-FINDINGS-START -->\nNo findings.\n<!-- REVIEW-FINDINGS-END -->'
|
|
@@ -41,7 +47,7 @@ cnt() { bash "$TC" count "$RE"; }
|
|
|
41
47
|
[ "$(mk $'<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: '"$FULL"$'\n\nno findings block here' Bot | cnt)" = 0 ] && ok || bad "comment without a findings block must not count (panel integrity)"
|
|
42
48
|
|
|
43
49
|
# (3) _ROUTE regex: real mode on the final head counts, spec-evolution-blocked and a stale sha do not
|
|
44
|
-
RLINE=$(grep -E '^[[:space:]]*_ROUTE=' "$
|
|
50
|
+
RLINE=$(grep -E '^[[:space:]]*_ROUTE=' "$PRE" | head -1)
|
|
45
51
|
RRE=$(printf '%s' "$RLINE" | sed -n 's/.*count "\(.*\)") ||.*/\1/p'); RRE=${RRE//\\\\/\\}; RRE=${RRE//\$\{_HEAD7\}/$SHORT}
|
|
46
52
|
rcnt() { bash "$TC" count "$RRE"; }
|
|
47
53
|
[ "$(mk '<!-- FORGE:REVIEW_ROUTE mode=single-pr spec=review-pr.md sha='"$SHORT"' -->' Bot | rcnt)" = 1 ] && ok || bad "route marker on the final head should count"
|
|
@@ -50,14 +56,14 @@ rcnt() { bash "$TC" count "$RRE"; }
|
|
|
50
56
|
|
|
51
57
|
# (4) spec text guards: agents may not be told to suppress findings; no $PWD tier in the classifier resolver; git reads are status-checked
|
|
52
58
|
if grep -q 'MUST NOT report a defect' "$SPEC"; then bad "Phase 2 must not tell agents to suppress findings (7B owns demotion)"; else ok; fi
|
|
53
|
-
if grep -qE "^DEPLOY_WIRING_FILES=.*git diff" "$SPEC"; then bad "DEPLOY_WIRING_FILES must not pipe git diff directly"; else ok; fi
|
|
54
|
-
if grep -q 'git diff-tree .*|| { SCOPE_MODE=full' "$
|
|
59
|
+
if grep -qE "^DEPLOY_WIRING_FILES=.*git diff" "$SPEC" "$PRE"; then bad "DEPLOY_WIRING_FILES must not pipe git diff directly"; else ok; fi
|
|
60
|
+
if grep -q 'git diff-tree .*|| { SCOPE_MODE=full' "$PRE"; then ok; else bad "git diff-tree reads must fall back to full scope"; fi
|
|
55
61
|
# the staging triage is the shared script: scoped and prior-PR arguments reach `classify`, the scope file and member PR list are built from the persisted 0B.1 state, no classifier logic is copied into the spec
|
|
56
|
-
if grep -qF
|
|
62
|
+
if grep -qF '"--scope-files", scopeFile' "$CLI" && grep -qF '"--prior-prs", priorPrs' "$CLI" && grep -qF '_scope.json' "$CLI" && grep -qF '"review-finalize.sh"' "$CLI"; then ok; else bad "the staging CLI must call review-finalize.sh classify with --scope-files and --prior-prs from the persisted 0B.1 scope"; fi
|
|
57
63
|
if grep -qE "^(classify_one|cap_pass|prior_findings_tsv)\(\) \{" "$SPEC"; then bad "review-pr-staging.md must not carry a copy of the classifier, cap or re-flag logic"; else ok; fi
|
|
58
64
|
if grep -qF 'FINDING_TEXT_FILE="$WORKDIR/finding-text.$I"; jq -r' "$ROOT/scripts/review-finalize.sh" && grep -qF -- '--text-file "$FINDING_TEXT_FILE"' "$ROOT/scripts/review-finalize.sh"; then ok; else bad "FINDING_TEXT_FILE must be written before the classifier call in review-finalize.sh"; fi
|
|
59
65
|
grep -q "skip-dedup" "$ROOT/scripts/review-finalize.sh" && ok || bad "review-finalize.sh must skip a deduped finding"
|
|
60
|
-
grep -q "review-prepare.sh" "$
|
|
66
|
+
grep -q "review-prepare.sh" "$CLI" && grep -q -- "--no-delta" "$CLI" && ok || bad "staging must call review-prepare.sh --no-delta in 0C"
|
|
61
67
|
# (7) fail-closed guard: BLOCKED when the classifier is unresolvable, no manual classifier mode, no classifier-unavailable fallback
|
|
62
68
|
SPEC_FIN="$ROOT/scripts/review-finalize.sh" # review-pr.md delegates classification to review-finalize.sh, which owns the fail-closed BLOCKED line (#3841)
|
|
63
69
|
if grep -qE 'classifier=manual|echo script \|\| echo manual|classifier-unavailable' "$SPEC"; then bad "review-pr-staging.md must carry no manual classifier mode"; else ok; fi
|
|
@@ -68,7 +74,7 @@ done
|
|
|
68
74
|
# (forge#3778) the spec carries a byte-identical stub per fence; the body lives in scripts/lib/fence-bootstrap.sh, which is SOURCED
|
|
69
75
|
# in a fresh shell (the stub's locator + source line is executed for real, so the stub itself is covered too).
|
|
70
76
|
[ "$(grep '# FENCE_BOOTSTRAP$' "$SPEC" | sort -u | wc -l | tr -d ' ')" -eq 1 ] && ok || bad "FENCE_BOOTSTRAP stubs must be byte-identical"
|
|
71
|
-
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by
|
|
77
|
+
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by the 7A, 7C and Phase 8 fences"
|
|
72
78
|
grep '^type forge_resolve >/dev/null' "$SPEC" | sed 1q > "$TMP/boot.sh"
|
|
73
79
|
grep '# FENCE_BOOTSTRAP$' "$SPEC" | sed 1q >> "$TMP/boot.sh"
|
|
74
80
|
[ "$(wc -l < "$TMP/boot.sh")" -eq 2 ] && ok || bad "bootstrap stub (locator + source line) not found"
|
|
@@ -81,17 +87,17 @@ UNCOVERED=$(awk '
|
|
|
81
87
|
infence { body = body "\n" $0 }' "$SPEC")
|
|
82
88
|
[ -z "$UNCOVERED" ] && ok || bad "bash fences starting at line(s) ${UNCOVERED}consume cross-fence state without the FENCE_BOOTSTRAP block"
|
|
83
89
|
# the persisted Phase 6.5 / 6.75 values: producers write them, consumers reload them validated
|
|
84
|
-
grep -q 'test_gate_verdict' "$
|
|
90
|
+
grep -q 'test_gate_verdict' "$POST" && grep -q 'launched_review_agents' "$POST" && ok || bad "TEST_GATE_VERDICT and LAUNCHED_REVIEW_AGENTS must be persisted to the scratchpad"
|
|
85
91
|
# PR-keyed (concurrent runs in one cwd must not share state) and reset by an executable command, not prose
|
|
86
|
-
if grep -qE '\$_SD/(test_gate_verdict|test_gate_reason|launched_review_agents)|\.forge-scratch\}/launched_review_agents' "$SPEC"; then bad "gate/launched-agent scratch files must be keyed by \${PR_NUMBER}_"; else ok; fi
|
|
87
|
-
grep -qF '
|
|
92
|
+
if grep -qE '\$_SD/(test_gate_verdict|test_gate_reason|launched_review_agents)|\.forge-scratch\}/launched_review_agents' "$SPEC" "$POST"; then bad "gate/launched-agent scratch files must be keyed by \${PR_NUMBER}_"; else ok; fi
|
|
93
|
+
grep -qF 'io.writeFile(launchedFile, "")' "$CLI" && grep -qF 'rm -f "${_SD:?}/${PR_NUMBER:?}_test_gate_verdict" "${_SD:?}/${PR_NUMBER:?}_test_gate_reason"' "$POST" && ok || bad "the pre-dispatch must truncate the launched-agent file and the test gate must rm -f the PR-keyed verdict files"
|
|
88
94
|
if awk '/^### 7E:/,/^### 7F:/' "$SPEC" | grep -qF '{GH_REPO}'; then bad "7E must not use the {GH_REPO} template token"; else ok; fi
|
|
89
95
|
# (8b) forge#3733: Phase 6.75 fails closed on an empty/absent launched-agent list or empty PR_NUMBER; Phase 6 fails closed on a bad SELECTED_AGENT_COUNT
|
|
90
|
-
F675=$(
|
|
96
|
+
F675=$(blk "$POST" delivery-check)
|
|
91
97
|
GUARD_LN=$(printf '%s\n' "$F675" | grep -n 'launched-agent list is empty or unreadable' | head -1 | cut -d: -f1)
|
|
92
98
|
LOOP_LN=$(printf '%s\n' "$F675" | grep -n '^for AGENT_DOMAIN in' | head -1 | cut -d: -f1)
|
|
93
99
|
[ -n "$GUARD_LN" ] && [ -n "$LOOP_LN" ] && [ "$GUARD_LN" -lt "$LOOP_LN" ] && ok || bad "6.75 empty-list guard must precede the for AGENT_DOMAIN loop"
|
|
94
|
-
printf '%s\n' "$F675" | sed -n "1,$((LOOP_LN-1))p" | grep -v '^[[:space:]]*gh ' > "$TMP/f675.sh"
|
|
100
|
+
{ echo '_SD="$FORGE_SCRATCHPAD"'; printf '%s\n' "$F675" | sed -n "1,$((LOOP_LN-1))p" | grep -v '^[[:space:]]*gh '; } > "$TMP/f675.sh"
|
|
95
101
|
mkdir -p "$TMP/sd9/empty" "$TMP/sd9/full" "$TMP/bin9"
|
|
96
102
|
printf '#!/bin/sh\necho stub-value\n' > "$TMP/bin9/yq"; printf '#!/bin/sh\nexit 0\n' > "$TMP/bin9/gh"; chmod +x "$TMP/bin9/yq" "$TMP/bin9/gh"
|
|
97
103
|
: > "$TMP/sd9/empty/42_launched_review_agents"
|
|
@@ -104,7 +110,7 @@ done
|
|
|
104
110
|
OUT9=$(run675 "$TMP/sd9/full" 42)
|
|
105
111
|
case "$OUT9" in *"launched-agent list is empty"*) bad "6.75 guard must not fire for a non-empty list (got '$OUT9')" ;; *) ok ;; esac
|
|
106
112
|
# Phase 6 SELECTED_AGENT_COUNT validation: static order + behaviour of the reload/validate lines
|
|
107
|
-
F6=$(
|
|
113
|
+
F6=$(blk "$POST" panel-guard)
|
|
108
114
|
printf '%s' "$F6" | grep -qF 'SELECTED_COUNT_INVALID' && ok || bad "Phase 6 fence must validate SELECTED_AGENT_COUNT (SELECTED_COUNT_INVALID)"
|
|
109
115
|
printf '%s' "$F6" | grep -F 'SELECTED_AGENT_COUNT=' | grep -F 'launched_review_agents' > "$TMP/sel.sh"
|
|
110
116
|
printf '%s\n' 'SELECTED_COUNT_INVALID=false' "$(printf '%s' "$F6" | grep -F 'case "${SELECTED_AGENT_COUNT:-}"')" > "$TMP/selcase.sh"
|
|
@@ -140,20 +146,20 @@ case "$OUT8" in *"PR=|"*) ok ;; *) bad "non-numeric staging_pr_number must be re
|
|
|
140
146
|
OUT8=$( ( unset ALL_PR_NUMBERS; PR_NUMBER=99; export PATH="$TMP/bin:$PATH" FORGE_SCRATCHPAD="$TMP/sd8/full" FORGEDOCK_HOME="$ROOT"; . "$TMP/boot.sh" >/dev/null 2>&1; echo "PR=$PR_NUMBER|ALL=${ALL_PR_NUMBERS:-}" ) )
|
|
141
147
|
[ "$OUT8" = "PR=99|ALL=" ] && ok || bad "in-shell PR_NUMBER must not be overridden by the scratch file (got '$OUT8')"
|
|
142
148
|
# producers persist, and 0B.1 / 7B.5 no longer use the unsubstituted-template repo token or a bare unquoted PR number in gh pr comment
|
|
143
|
-
grep -q 'staging_pr_number' "$
|
|
144
|
-
if
|
|
149
|
+
grep -q 'staging_pr_number' "$CLI" && grep -q '_bundle-prs.txt' "$PRE" && ok || bad "Phase -1 / 0A must persist staging_pr_number and _bundle-prs.txt"
|
|
150
|
+
if grep -v '^[[:space:]]*#' "$PRE" "$POST" | grep -qE '(^|[^$]){GH_REPO}'; then bad "the 0A/0B.1 and post-dispatch scripts must not use the {GH_REPO} template token"; else ok; fi
|
|
145
151
|
if awk '/^### 7A:/,/^### 7D:/' "$SPEC" | grep -qE '\{GH_REPO\}|gh pr comment \$\{PR_NUMBER\}'; then bad "7A-7C must use quoted \$GH_REPO / \$PR_NUMBER"; else ok; fi
|
|
146
152
|
# (7) reviewer templates bind the full head SHA via the dispatch placeholder, not an orchestrator-only shell var
|
|
147
153
|
if grep -qE '^Reviewed-SHA: \$\{REVIEW_SHA_FULL\}$' "$SPEC"; then bad "reviewer template still uses \${REVIEW_SHA_FULL} (unbound in a fresh shell)"; else ok; fi
|
|
148
154
|
[ "$(grep -cE '^Reviewed-SHA: \[REVIEW_SHA\]$' "$SPEC")" -ge 5 ] && ok || bad "all five reviewer templates must use Reviewed-SHA: [REVIEW_SHA]"
|
|
149
155
|
if grep -E '^Reviewed-SHA:' "$SPEC" | grep -qvE '^Reviewed-SHA: \[REVIEW_SHA\]$'; then bad "every Reviewed-SHA template line must be [REVIEW_SHA]"; else ok; fi
|
|
150
|
-
grep -
|
|
156
|
+
grep -qiF 'substitute `[REVIEW_SHA]`' "$SPEC" && ok || bad "dispatch text must bind [REVIEW_SHA]"
|
|
151
157
|
# (8) cross-PR re-flag (forge#3641) and the per-review cap (forge#3642) now run inside review-finalize.sh classify: scripts/review-finalize.test.sh covers them (bundle re-flag, scope demotion, cap)
|
|
152
158
|
# (9) stale-head delta credit (forge#3638): the spec's bundle_delta_credit runs against real git fixtures and the REAL review-delta.sh / trusted-comments.sh.
|
|
153
159
|
# Fail-closed cases first: every FULL / error / untrusted path must leave the PR's commits UNREVIEWED (empty credit).
|
|
154
160
|
if command -v git >/dev/null 2>&1; then
|
|
155
|
-
awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$
|
|
156
|
-
[ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in
|
|
161
|
+
awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$PRE" > "$TMP/delta-fn.sh"
|
|
162
|
+
[ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in review-staging-pre.sh"
|
|
157
163
|
RDS="$ROOT/scripts/review-delta.sh"
|
|
158
164
|
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
|
|
159
165
|
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
|
|
@@ -227,25 +233,25 @@ mkrepo9; commit9 pr.txt pr1b pr1b; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"
|
|
|
227
233
|
OUT9=$(credit9 "$H"); N9=$(printf '%s' "${OUT9#*|}" | wc -w)
|
|
228
234
|
[ "${OUT9%%|*}" = 0 ] && [ "$N9" -eq 2 ] && case "$OUT9" in *"$(short9 HEAD)"*|*"$(short9 HEAD~1)"*) false ;; *) true ;; esac && ok || bad "partial review must credit exactly the 2 reviewed commits (got '$OUT9')"
|
|
229
235
|
# spec text guards: the resolver is reused, never copied; carried record accepted; the unchanged cross-PR and merge-resolution paths are still present
|
|
230
|
-
grep -q 'review-delta.sh' "$
|
|
231
|
-
if grep -qE 'merge-base --is-ancestor.*REVIEWED_SHA_STALE|diff_shape\(\)' "$SPEC"; then bad "spec must not carry a second copy of the resolver logic"; else ok; fi
|
|
232
|
-
grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$
|
|
236
|
+
grep -q 'review-delta.sh' "$PRE" && grep -q 'REVIEW-CARRIED' "$PRE" && ok || bad "review-staging-pre.sh must call review-delta.sh and accept the REVIEW-CARRIED record"
|
|
237
|
+
if grep -qE 'merge-base --is-ancestor.*REVIEWED_SHA_STALE|diff_shape\(\)' "$SPEC" "$PRE"; then bad "spec must not carry a second copy of the resolver logic"; else ok; fi
|
|
238
|
+
grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$PRE" && grep -qF 'git diff-tree --no-commit-id --name-only -r --cc "$c"' "$PRE" && ok || bad "CROSS_PR_FILES accumulation and --cc merge scoping must stay intact"
|
|
233
239
|
[ "$(grep -c '# FENCE_BOOTSTRAP$' "$SPEC" | head -1)" -ge 3 ] && ok || bad "FENCE_BOOTSTRAP stubs must remain"
|
|
234
240
|
# an unresolvable helper fails the fence closed (exit 1), never an unsourced bootstrap
|
|
235
241
|
OUT8=$( ( unset FORGEDOCK_HOME FORGE_HOME FORGE_ROOT CLAUDE_PLUGIN_ROOT CLAUDE_CONFIG_DIR CODEX_HOME FORGE_SOURCED_ROOT FORGEDOCK_MARKETPLACE; export HOME="$TMP/nohome"; cd "$TMP" && bash -c "$(cat "$TMP/boot.sh"); echo reached" ) 2>&1 )
|
|
236
242
|
case "$OUT8" in *BLOCKED*) case "$OUT8" in *reached*) bad "unresolved bootstrap helper must stop the fence" ;; *) ok ;; esac ;; *) bad "unresolved bootstrap helper must print BLOCKED (got '$OUT8')" ;; esac
|
|
237
243
|
# a stale-head PR must not credit its own merge commit (it contains the post-review commits): the DELTA branch only appends DELTA_COMMITS
|
|
238
|
-
if awk '/^bundle_delta_credit\(\)/,/^\}/' "$
|
|
244
|
+
if awk '/^bundle_delta_credit\(\)/,/^\}/' "$PRE" | grep -q 'mergeCommit'; then bad "the DELTA path must not credit the PR merge commit"; else ok; fi
|
|
239
245
|
# (10) fail-closed gate verdict (forge#3734): an empty/missing/invalid persisted verdict must never become SKIP
|
|
240
|
-
|
|
246
|
+
blk "$POST" test-gate-verdict > "$TMP/vh.sh"
|
|
241
247
|
[ -s "$TMP/vh.sh" ] && ok || bad "could not extract the Phase 6.5 verdict-handling fence"
|
|
242
|
-
if awk '/^ \*\)/,/;;/' "$
|
|
243
|
-
grep -qF 'missing/invalid' "$SPEC" && ok || bad "spec must document missing/invalid gate-verdict handling"
|
|
244
|
-
if grep -F 'FORGE:TEST_GATE:RESULT=' "$
|
|
248
|
+
if awk '/^ \*\)/,/;;/' "$POST" | grep -q 'TEST_GATE_VERDICT="SKIP"'; then bad "catch-all arm must not set TEST_GATE_VERDICT=SKIP"; else ok; fi
|
|
249
|
+
grep -qF 'missing/invalid' "$SPEC" && grep -qF 'missing/invalid' "$POST" && ok || bad "spec must document missing/invalid gate-verdict handling"
|
|
250
|
+
if grep -F 'FORGE:TEST_GATE:RESULT=' "$POST" | grep -q 'tail -1 || echo "SKIP"'; then bad "producer must not coerce an absent marker to SKIP"; else ok; fi
|
|
245
251
|
vh10() { # $1 posture, $2 persisted verdict content ('-' = no file), $3 env verdict (optional)
|
|
246
252
|
local sd="$TMP/vh10.$RANDOM"; mkdir -p "$sd"
|
|
247
253
|
[ "$2" = "-" ] || printf '%s\n' "$2" > "$sd/5_test_gate_verdict"
|
|
248
|
-
( export _SD="$sd" PR_NUMBER=5 GH_REPO=o/r DEFAULT_BRANCH=main ALL_PR_NUMBERS="5" CONFIG_FILE=/dev/null P10="$1"
|
|
254
|
+
( export _SD="$sd" PR_NUMBER=5 GH_REPO=o/r DEFAULT_BRANCH=main ALL_PR_NUMBERS="5" CONFIG_FILE=/dev/null P10="$1" GATE_POSTURE="$1" OVERRIDE_PHRASE="OVERRIDE: x" GH_FLAG="-R o/r"
|
|
249
255
|
[ -z "${3:-}" ] || export TEST_GATE_VERDICT="$3"
|
|
250
256
|
forge_resolve() { :; }; yq() { case "$*" in *posture*) echo "$P10" ;; *) echo "OVERRIDE: x" ;; esac; }; gh() { return 0; }
|
|
251
257
|
. "$TMP/vh.sh" >"$sd/out" 2>&1; echo "rc=0" >> "$sd/out" ) ; local rc=$?
|
|
@@ -280,12 +286,12 @@ gh11() {
|
|
|
280
286
|
*) return 1 ;;
|
|
281
287
|
esac
|
|
282
288
|
}
|
|
283
|
-
[ "$(grep -c '^# BEGIN bundle-pr-kind$' "$
|
|
284
|
-
awk '/^# BEGIN bundle-pr-set$/{f=1;next} /^# END bundle-pr-set$/{f=0} f' "$
|
|
289
|
+
[ "$(grep -c '^# BEGIN bundle-pr-kind$' "$PRE")" -eq 2 ] && [ "$(awk '/^# BEGIN bundle-pr-kind$/{f=1;n++} f{print > ("'"$TMP"'/kind" n ".sh")} /^# END bundle-pr-kind$/{f=0}' "$PRE"; cmp -s "$TMP/kind1.sh" "$TMP/kind2.sh" && echo same)" = same ] && ok || bad "0A and 0B.1 must carry identical bundle_pr_kind blocks"
|
|
290
|
+
awk '/^# BEGIN bundle-pr-set$/{f=1;next} /^# END bundle-pr-set$/{f=0} f' "$PRE" | sed 's/{GH_REPO}/o\/r/g' > "$TMP/prset.sh"
|
|
285
291
|
[ -s "$TMP/prset.sh" ] && ok || bad "bundle-pr-set block not found in Phase 0A"
|
|
286
|
-
OUT11=$( gh() { gh11 "$@"; }; BUNDLE_PRS="5 6 28668 77 90"; MERGE_PRS="90"; . "$TMP/prset.sh"; echo $ALL_PR_NUMBERS "|$SKIPPED_ISSUES" )
|
|
292
|
+
OUT11=$( gh() { gh11 "$@"; }; GH_REPO=o/r; BUNDLE_PRS="5 6 28668 77 90"; MERGE_PRS="90"; . "$TMP/prset.sh"; echo $ALL_PR_NUMBERS "|$SKIPPED_ISSUES" )
|
|
287
293
|
[ "$OUT11" = "5 6 77 90 | 28668" ] && ok || bad "0A must drop the confirmed issue and keep PRs, merge PRs and unclassifiable numbers (got '$OUT11')"
|
|
288
|
-
awk '/^# BEGIN bundle-scope-loop$/{f=1;next} /^# END bundle-scope-loop$/{f=0} f' "$
|
|
294
|
+
awk '/^# BEGIN bundle-scope-loop$/{f=1;next} /^# END bundle-scope-loop$/{f=0} f' "$PRE" > "$TMP/loop.sh"
|
|
289
295
|
[ -s "$TMP/loop.sh" ] && ok || bad "bundle-scope-loop block not found in 0B.1"
|
|
290
296
|
scope11() { # $1 = ALL_PR_NUMBERS; prints "<SCOPE_MODE> <REVIEWED_PR_COUNT>" then the loop output
|
|
291
297
|
( gh() { gh11 "$@"; }; bundle_delta_credit() { DELTA_COMMITS=""; DELTA_FULL=0; }; . "$TMP/kind1.sh"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# the PR checkout, a SPEC_CLI persona file, forge.yaml paths.root) as data.
|
|
10
10
|
#
|
|
11
11
|
# Usage: review-prepare.sh --pr N --repo owner/name [--merge-issue N] [--thorough true|false] [--out FILE] [--out-dir DIR]
|
|
12
|
-
# [--checkout-pr] [--no-delta] [--build-gate block|advisory]
|
|
12
|
+
# [--checkout-pr] [--no-delta] [--delta-from SHA] [--build-gate block|advisory]
|
|
13
13
|
# --pr PR number (numeric, required) --repo owner/name (required)
|
|
14
14
|
# --merge-issue issue the PR closes, from --issue (numeric; non-numeric is ignored)
|
|
15
15
|
# --thorough true restores the full-union roster --out write the context JSON here (default: stdout)
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
# --no-delta skip the FULL/DELTA/BASE_SYNC_ONLY resolver: the review mode is always FULL (a staging bundle review always reviews the
|
|
24
24
|
# whole bundle), without widening the roster to the full union the way --thorough does; it also enables the pinned
|
|
25
25
|
# local-git diff fallback when `gh pr diff` fails (e.g. HTTP 406 over 20,000 lines), which a milestone->staging PR gets without it
|
|
26
|
+
# --delta-from explicit DELTA from a caller-supplied earlier reviewed head (forge#4360, `forgedock review-staging --delta`): skips the
|
|
27
|
+
# resolver (even with --no-delta), narrows diff, slices and roster to SHA..review sha (the roster is the domains the delta
|
|
28
|
+
# files touch). BLOCKED when SHA is not a full commit id in the checkout, equals or is not an ancestor of the review sha,
|
|
29
|
+
# or has no trusted FORGE:REVIEW-AGENT body with `Reviewed-SHA: SHA` (no earlier pass to credit). An empty file delta
|
|
30
|
+
# is FULL. Not combinable with --thorough true (exit 2).
|
|
26
31
|
# Env: FORGE_CONFIG (forge.yaml path; default: the main checkout's forge.yaml, then the git top-level one),
|
|
27
32
|
# REVIEW_PREPARE_SLEEP (seconds between mergeability retries, default 5),
|
|
28
33
|
# REVIEW_PREPARE_INDEX_TIMEOUT (seconds the code-index build may take, default 120; on timeout or failure the slices say
|
|
@@ -61,10 +66,10 @@
|
|
|
61
66
|
|
|
62
67
|
set -u
|
|
63
68
|
|
|
64
|
-
PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"; NO_DELTA="false"; BUILD_GATE="block"
|
|
69
|
+
PR_NUMBER=""; REPO=""; MERGE_ISSUE=""; THOROUGH="false"; OUT=""; OUT_DIR=""; CHECKOUT_PR="false"; NO_DELTA="false"; BUILD_GATE="block"; DELTA_FROM_OPT=""
|
|
65
70
|
while [ $# -gt 0 ]; do
|
|
66
71
|
case "$1" in
|
|
67
|
-
--pr|--repo|--merge-issue|--thorough|--out|--out-dir)
|
|
72
|
+
--pr|--repo|--merge-issue|--thorough|--out|--out-dir|--delta-from)
|
|
68
73
|
[ $# -ge 2 ] || { echo "review-prepare: $1 requires a value" >&2; exit 2; } ;;
|
|
69
74
|
esac
|
|
70
75
|
case "$1" in
|
|
@@ -76,6 +81,7 @@ while [ $# -gt 0 ]; do
|
|
|
76
81
|
--out-dir) OUT_DIR="${2:-}"; shift 2 ;;
|
|
77
82
|
--checkout-pr) CHECKOUT_PR="true"; shift ;;
|
|
78
83
|
--no-delta) NO_DELTA="true"; shift ;;
|
|
84
|
+
--delta-from) DELTA_FROM_OPT="${2:-}"; [ -n "$DELTA_FROM_OPT" ] || DELTA_FROM_OPT="-"; shift 2 ;;
|
|
79
85
|
--build-gate) case "${2:-}" in block|advisory) BUILD_GATE="$2" ;; *) echo "review-prepare: --build-gate must be block or advisory" >&2; exit 2 ;; esac; shift 2 ;;
|
|
80
86
|
*) echo "review-prepare: unknown argument: $1" >&2; exit 2 ;;
|
|
81
87
|
esac
|
|
@@ -85,6 +91,7 @@ case "$REPO" in */*) ;; *) echo "review-prepare: --repo must be owner/name" >&2;
|
|
|
85
91
|
case "$REPO" in *[!A-Za-z0-9._/-]*|*/*/*|/*|*/) echo "review-prepare: invalid --repo" >&2; exit 2 ;; esac
|
|
86
92
|
case "$MERGE_ISSUE" in *[!0-9]*) MERGE_ISSUE="" ;; esac
|
|
87
93
|
[ "$THOROUGH" = "true" ] || THOROUGH="false"
|
|
94
|
+
if [ -n "$DELTA_FROM_OPT" ] && [ "$THOROUGH" = "true" ]; then echo "review-prepare: --delta-from cannot be combined with --thorough true" >&2; exit 2; fi
|
|
88
95
|
|
|
89
96
|
# --- trusted helper resolution (install roots only; never cwd) ---
|
|
90
97
|
# shellcheck source=/dev/null # the resolver lib is found at runtime in the trusted install root
|
|
@@ -125,7 +132,7 @@ cleanup() { restore_ref; rm -rf "$TMP_DIR"; }
|
|
|
125
132
|
trap cleanup EXIT
|
|
126
133
|
|
|
127
134
|
# --- Phase 1A: fetch ---
|
|
128
|
-
PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,isCrossRepository,additions,deletions,changedFiles 2>/dev/null) || PR_INFO=""
|
|
135
|
+
PR_INFO=$(gh pr view "$PR_NUMBER" -R "$REPO" --json number,title,author,baseRefName,headRefName,headRefOid,isCrossRepository,additions,deletions,changedFiles,labels 2>/dev/null) || PR_INFO=""
|
|
129
136
|
printf '%s' "$PR_INFO" | jq -e . >/dev/null 2>&1 || PR_INFO=""
|
|
130
137
|
if [ -z "$PR_INFO" ]; then block "gh pr view failed for #${PR_NUMBER}"; fi
|
|
131
138
|
pj() { printf '%s' "$PR_INFO" | jq -r "$1 // empty" 2>/dev/null; }
|
|
@@ -452,7 +459,53 @@ delta_roster() {
|
|
|
452
459
|
printf '%s' "$out"
|
|
453
460
|
}
|
|
454
461
|
REVIEW_MODE="FULL"; DELTA_FROM=""; CARRIED_FROM=""; FULL_ROUNDS=0; DELTA_FILES=""
|
|
455
|
-
if [
|
|
462
|
+
if [ -n "$DELTA_FROM_OPT" ]; then
|
|
463
|
+
# forge#4360: explicit delta. Every check fails closed (BLOCKED); only an empty file delta widens to FULL (below).
|
|
464
|
+
_dtop="${TOP:-.}"
|
|
465
|
+
case "$DELTA_FROM_OPT" in *[!0-9a-f]*|"") _dok=false ;; *) { [ "${#DELTA_FROM_OPT}" -eq 40 ] || [ "${#DELTA_FROM_OPT}" -eq 64 ]; } && _dok=true || _dok=false ;; esac
|
|
466
|
+
if [ "$_dok" != "true" ]; then block "--delta-from is not a full commit id: $(printf '%s' "$DELTA_FROM_OPT" | tr -cd '0-9A-Za-z' | cut -c1-64)"
|
|
467
|
+
elif [ "$DELTA_FROM_OPT" = "$REVIEW_SHA" ]; then block "--delta-from ${DELTA_FROM_OPT} is the review head: nothing to review"
|
|
468
|
+
elif ! git -C "$_dtop" cat-file -e "${DELTA_FROM_OPT}^{commit}" 2>/dev/null; then block "--delta-from ${DELTA_FROM_OPT} is not a commit in this checkout (fetch it first)"
|
|
469
|
+
elif ! git -C "$_dtop" merge-base --is-ancestor "$DELTA_FROM_OPT" "$REVIEW_SHA" 2>/dev/null; then block "--delta-from ${DELTA_FROM_OPT} is not an ancestor of ${REVIEW_SHA} (force-push or rebase): run a full review"
|
|
470
|
+
elif printf '%s' "$PR_INFO" | jq -e '[.labels[]?.name] | index("review-degraded") != null' >/dev/null 2>&1; then block "--delta-from ${DELTA_FROM_OPT}: the PR is labelled review-degraded (an earlier panel was incomplete): run a full review"
|
|
471
|
+
else
|
|
472
|
+
# forge#4471: the earlier pass must be complete and non-degraded, as scripts/review-delta.sh requires of a checkpoint:
|
|
473
|
+
# an agent body with a findings block for <from>, a post-panel-guard proof naming <from> (staging summary, synthesized
|
|
474
|
+
# block, or a # PR Review Summary whose Agents count matches) and no degraded marker after its first agent body.
|
|
475
|
+
_dv="error"
|
|
476
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$COMMENTS_JSON" ]; then
|
|
477
|
+
_db=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->|^<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->|# PR Review Summary|^<!-- FORGE:GATE_(PASS|FAILURE) -->|<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED' 2>/dev/null) \
|
|
478
|
+
&& _dv=$(printf '%s\n' "$_db" | jq -s -r --arg f "$DELTA_FROM_OPT" '
|
|
479
|
+
def shas($t; $re): [ $t | scan($re) | .[0] ];
|
|
480
|
+
def one($a): if ($a | length) == 1 then $a[0] else null end;
|
|
481
|
+
def rsha: "(?:^|\\n)Reviewed-SHA: ([0-9a-f]{40}|[0-9a-f]{64})\\r?(?:\\n|$)";
|
|
482
|
+
to_entries as $E
|
|
483
|
+
| [ $E[] | select((.value | test("\\A<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->")) and (.value | test("<!-- REVIEW-FINDINGS-START -->")))
|
|
484
|
+
| select(one(shas(.value; rsha)) == $f)
|
|
485
|
+
| {i: .key, d: (.value | capture("\\A<!-- FORGE:REVIEW-AGENT:(?<d>[a-z0-9-]+) -->").d)} ] as $AG
|
|
486
|
+
| if ($AG | length) == 0 then "no-agent" else
|
|
487
|
+
($AG | map(.i) | min) as $first | ($AG | map(.d) | unique | length) as $nd
|
|
488
|
+
| ([ $E[] | .value as $t | select(
|
|
489
|
+
(($t | test("\\A<!-- FORGE:GATE_(?:PASS|FAILURE) -->\\r?\\n## Staging Review:"))
|
|
490
|
+
and one(shas($t; "\\*\\*Head\\*\\*: `([0-9a-f]{40}|[0-9a-f]{64})`")) == $f)
|
|
491
|
+
or (($t | startswith("<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->")) and one(shas($t; rsha)) == $f)
|
|
492
|
+
or (($t | test("# PR Review Summary"))
|
|
493
|
+
and one(shas($t; "\\*\\*Reviewed commit\\*\\*: `?([0-9a-f]{40}|[0-9a-f]{64})`?(?:[ |\\r\\n]|$)")) == $f
|
|
494
|
+
and one(shas($t; "\\*\\*Agents\\*\\*: ([0-9]+)")) == ($nd | tostring))) ] | length) as $proof
|
|
495
|
+
| ([ $E[] | select(.key > $first)
|
|
496
|
+
| select(.value | test("<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity|<!-- FORGE:REVIEW_DEGRADED|<!-- FORGE:REVIEW_BLOCKED")) ] | length) as $deg
|
|
497
|
+
| if $deg > 0 then "degraded" elif $proof == 0 then "no-proof" else "ok" end
|
|
498
|
+
end' 2>/dev/null) || _dv="error"
|
|
499
|
+
fi
|
|
500
|
+
case "$_dv" in
|
|
501
|
+
ok) REVIEW_MODE="DELTA"; DELTA_FROM="$DELTA_FROM_OPT" ;;
|
|
502
|
+
no-agent) block "--delta-from ${DELTA_FROM_OPT}: no trusted FORGE:REVIEW-AGENT comment with a findings block for that head, no earlier pass to credit: run a full review" ;;
|
|
503
|
+
no-proof) block "--delta-from ${DELTA_FROM_OPT}: no trusted proof that the earlier panel completed (staging summary, synthesized block or PR Review Summary for that head): run a full review" ;;
|
|
504
|
+
degraded) block "--delta-from ${DELTA_FROM_OPT}: the earlier pass is degraded (review-panel-integrity / REVIEW_DEGRADED / REVIEW_BLOCKED after its agents): run a full review" ;;
|
|
505
|
+
*) block "--delta-from ${DELTA_FROM_OPT}: could not read the trusted review comments, no earlier pass to credit: run a full review" ;;
|
|
506
|
+
esac
|
|
507
|
+
fi
|
|
508
|
+
elif [ "$THOROUGH" != "true" ] && [ "$IS_MILESTONE_TO_STAGING" != "true" ] && [ "$NO_DELTA" != "true" ]; then
|
|
456
509
|
read -r RD_KIND RD_ARG RD_ROUNDS <<< "$(review_delta_mode)"
|
|
457
510
|
FULL_ROUNDS="${RD_ROUNDS:-0}"
|
|
458
511
|
case "$RD_KIND" in
|
|
@@ -463,7 +516,21 @@ fi
|
|
|
463
516
|
if [ "$REVIEW_MODE" = "DELTA" ]; then
|
|
464
517
|
DELTA_FILES=$(git -C "${TOP:-.}" -c core.quotepath=off diff --name-only --no-renames "${DELTA_FROM}" "${REVIEW_SHA}" 2>/dev/null) || DELTA_FILES=""
|
|
465
518
|
if [ -z "$DELTA_FILES" ]; then REVIEW_MODE="FULL"; DELTA_FROM=""
|
|
466
|
-
else
|
|
519
|
+
else
|
|
520
|
+
# an explicit --delta-from always narrows the roster to the domains the delta touches (no full-rounds gate)
|
|
521
|
+
_drounds="$FULL_ROUNDS"
|
|
522
|
+
# forge#4471: an explicit delta that changes the review specs/config cannot credit the earlier pass (same list as
|
|
523
|
+
# scripts/review-delta.sh, which answers FULL there; review-prepare.test.sh checks the two lists match)
|
|
524
|
+
if [ -n "$DELTA_FROM_OPT" ]; then
|
|
525
|
+
while IFS= read -r _df; do
|
|
526
|
+
case "$_df" in
|
|
527
|
+
forge.yaml|commands/review-pr*.md|commands/review-pr-agents/*|scripts/review-delta.sh|scripts/trusted-comments.sh|scripts/verify-phase-trail.sh|docs/spec/review-protocol.md|scripts/classify-finding.sh) block "--delta-from ${DELTA_FROM}: the delta changes ${_df} (review spec/config), the earlier pass cannot be credited: run a full review"; break ;;
|
|
528
|
+
esac
|
|
529
|
+
done <<< "$DELTA_FILES"
|
|
530
|
+
fi
|
|
531
|
+
[ -n "$DELTA_FROM_OPT" ] && { _drounds=2; ROSTER_REASON="delta ${DELTA_FROM:0:7}..${REVIEW_SHA:0:7}: domains the delta touches"; }
|
|
532
|
+
SELECTED_AGENTS=$(canonical_roster "$(delta_roster "$_drounds" "$SELECTED_AGENTS" "$DELTA_FILES")")
|
|
533
|
+
fi
|
|
467
534
|
fi
|
|
468
535
|
[ "$REVIEW_MODE" = "BASE_SYNC_ONLY" ] && SELECTED_AGENTS=""
|
|
469
536
|
|
|
@@ -199,6 +199,91 @@ DELTA_KIND=DELTA prep "$WORK/root-delta" --thorough true; eq "thorough never goe
|
|
|
199
199
|
setup feat/x staging "$FIXFILES" "$DIFF_AUTH_BILL" "notasha"
|
|
200
200
|
DELTA_KIND=DELTA prep "$WORK/root-delta"; eq "unvalidated sha is FULL" .delta.mode FULL
|
|
201
201
|
|
|
202
|
+
# --- 5b. explicit --delta-from (forge#4360: forgedock review-staging --delta): no resolver involved ---
|
|
203
|
+
cmt() { # cmt <body> [association]: one comment object
|
|
204
|
+
jq -n --arg b "$1" --arg a "${2:-OWNER}" '{body: $b, author_association: $a, user: {login: "r", type: "User"}}'
|
|
205
|
+
}
|
|
206
|
+
agent_body() { printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->' "$1"; }
|
|
207
|
+
# shellcheck disable=SC2016 # the backticks are literal markdown
|
|
208
|
+
staging_summary() { printf '<!-- FORGE:GATE_FAILURE -->\n## Staging Review: NEEDS FIXES FIRST\n\n**Head**: `%s`\n\n<!-- FORGE:GATE_FAILURE:TYPE=staging-review|BUNDLE= -->' "$1"; }
|
|
209
|
+
comments_of() { jq -s . > "$FIX/comments.json"; } # comments_of < <cmt ...>...
|
|
210
|
+
earlier_pass() { # earlier_pass <sha> [association]: a complete earlier pass (agent body + post-guard staging summary)
|
|
211
|
+
{ cmt "$(agent_body "$1")" "${2:-OWNER}"; cmt "$(staging_summary "$1")" "${2:-OWNER}"; } | comments_of
|
|
212
|
+
}
|
|
213
|
+
git -C "$REPO_DIR" checkout -q -b pr-empty pr-branch; git -C "$REPO_DIR" commit -q --allow-empty -m C; # own branch: pr-branch stays at B (pinned-fallback tests below)
|
|
214
|
+
SHA_C=$(git -C "$REPO_DIR" rev-parse HEAD)
|
|
215
|
+
git -C "$REPO_DIR" checkout -q -b side main; echo d > "$REPO_DIR/d.txt"; git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm D
|
|
216
|
+
SHA_D=$(git -C "$REPO_DIR" rev-parse HEAD); git -C "$REPO_DIR" checkout -q main
|
|
217
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL"; earlier_pass "$SHA_A"
|
|
218
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"
|
|
219
|
+
rc_is "--delta-from ok" 0; eq "--delta-from mode" .delta.mode DELTA; eq "--delta-from from" .delta.from "$SHA_A"
|
|
220
|
+
eq "--delta-from files" '.delta.files | join(" ")' "services/api/auth.py"
|
|
221
|
+
eq "--delta-from roster is the touched domains, even before 2 full rounds" '.roster.agents | join(" ")' "security auth api"
|
|
222
|
+
eq "--delta-from reason names the delta" '.roster.reason | startswith("delta ")' true
|
|
223
|
+
eq "--delta-from slices narrow to the delta" '.slices.security.paths | join(" ")' "services/api/auth.py"
|
|
224
|
+
eq "--delta-from full_files stays whole" '.full_files | length' 2
|
|
225
|
+
prep "$WORK/root-nodelta" --no-delta; eq "--no-delta without --delta-from stays FULL" .delta.mode FULL
|
|
226
|
+
earlier_pass "$SHA_A" NONE
|
|
227
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "untrusted earlier pass" 3
|
|
228
|
+
eq "untrusted earlier pass blocker" '.blockers | join(" ") | test("no earlier pass to credit")' true
|
|
229
|
+
earlier_pass "$SHA_B"
|
|
230
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "no earlier pass at the from sha" 3
|
|
231
|
+
earlier_pass "$SHA_D"
|
|
232
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_D"; rc_is "non-ancestor from sha" 3
|
|
233
|
+
eq "non-ancestor blocker" '.blockers | join(" ") | test("not an ancestor")' true
|
|
234
|
+
earlier_pass "$SHA_B"
|
|
235
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "from sha == head" 3
|
|
236
|
+
eq "head blocker" '.blockers | join(" ") | test("is the review head")' true
|
|
237
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "0123456789012345678901234567890123456789"; rc_is "unknown commit" 3
|
|
238
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "abc123"; rc_is "short sha" 3
|
|
239
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL" "$SHA_C"; earlier_pass "$SHA_B"
|
|
240
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "empty delta" 0; eq "empty delta is FULL" .delta.mode FULL
|
|
241
|
+
prep "$WORK/root-nodelta" --delta-from "$SHA_B" --thorough true; rc_is "--delta-from with --thorough" 2
|
|
242
|
+
# forge#4471: the earlier pass must be complete and non-degraded, and the delta must not change review specs/config
|
|
243
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL"
|
|
244
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "<!-- FORGE:GATE_FAILURE:TYPE=review-panel-integrity -->
|
|
245
|
+
panel degraded: 1 of 4 reviewers completed"; } | comments_of
|
|
246
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "degraded earlier pass blocks --delta-from" 3
|
|
247
|
+
eq "degraded earlier pass blocker" '.blockers | join(" ") | test("earlier pass is degraded")' true
|
|
248
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")"; cmt "<!-- FORGE:REVIEW_DEGRADED -->"; } | comments_of
|
|
249
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "REVIEW_DEGRADED after a summarised pass blocks" 3
|
|
250
|
+
{ cmt "<!-- FORGE:REVIEW_DEGRADED -->"; cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")"; } | comments_of
|
|
251
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "degraded marker before the earlier pass does not disqualify it" 0
|
|
252
|
+
{ cmt "$(agent_body "$SHA_A")"; } | comments_of
|
|
253
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "partial earlier pass (no post-guard proof) blocks" 3
|
|
254
|
+
eq "partial earlier pass blocker" '.blockers | join(" ") | test("no trusted proof that the earlier panel completed")' true
|
|
255
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "$(staging_summary "$SHA_A")" NONE; } | comments_of
|
|
256
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "untrusted staging summary is no proof" 3
|
|
257
|
+
{ cmt "$(staging_summary "$SHA_B")"; cmt "$(agent_body "$SHA_A")"; } | comments_of
|
|
258
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "summary for another head is no proof" 3
|
|
259
|
+
{ cmt "<!-- FORGE:REVIEW-AGENT:security -->
|
|
260
|
+
Reviewed-SHA: $SHA_A
|
|
261
|
+
|
|
262
|
+
no findings block"; cmt "$(staging_summary "$SHA_A")"; } | comments_of
|
|
263
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "agent body without a findings block blocks" 3
|
|
264
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->
|
|
265
|
+
Reviewed-SHA: $SHA_A
|
|
266
|
+
<!-- REVIEW-FINDINGS-SYNTHESIZED-END -->"; } | comments_of
|
|
267
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "synthesized block is a post-guard proof" 0
|
|
268
|
+
{ cmt "$(agent_body "$SHA_A")"; cmt "# PR Review Summary
|
|
269
|
+
**Reviewed commit**: \`$SHA_A\`
|
|
270
|
+
**Agents**: 2"; } | comments_of
|
|
271
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "PR Review Summary with an Agents count above the domains blocks" 3
|
|
272
|
+
earlier_pass "$SHA_A"
|
|
273
|
+
sed 's/"changedFiles":1}/"changedFiles":1,"labels":[{"name":"review-degraded"}]}/' "$FIX/prinfo.json" > "$FIX/prinfo.tmp" && mv "$FIX/prinfo.tmp" "$FIX/prinfo.json"
|
|
274
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_A"; rc_is "review-degraded label blocks --delta-from" 3
|
|
275
|
+
eq "review-degraded label blocker" '.blockers | join(" ") | test("labelled review-degraded")' true
|
|
276
|
+
for _sf in forge.yaml scripts/trusted-comments.sh; do
|
|
277
|
+
git -C "$REPO_DIR" checkout -q -b "pr-spec-$(basename "$_sf")" pr-branch; mkdir -p "$REPO_DIR/$(dirname "$_sf")"; echo x > "$REPO_DIR/$_sf"
|
|
278
|
+
git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm "touch $_sf"; _sh=$(git -C "$REPO_DIR" rev-parse HEAD); git -C "$REPO_DIR" checkout -q main
|
|
279
|
+
setup staging main "$FIXFILES" "$DIFF_AUTH_BILL" "$_sh"; earlier_pass "$SHA_B"
|
|
280
|
+
prep "$WORK/root-nodelta" --no-delta --delta-from "$SHA_B"; rc_is "delta touching $_sf blocks --delta-from" 3
|
|
281
|
+
eq "delta touching $_sf blocker" '.blockers | join(" ") | test("review spec/config")' true
|
|
282
|
+
done
|
|
283
|
+
# the spec/config list must stay identical to the resolver's FULL list
|
|
284
|
+
_pl() { grep -oE 'forge\.yaml\|commands/review-pr[^)]*\)' "$1" | head -n 1; }
|
|
285
|
+
if [ -n "$(_pl "$ROOT/scripts/review-delta.sh")" ] && [ "$(_pl "$ROOT/scripts/review-prepare.sh")" = "$(_pl "$ROOT/scripts/review-delta.sh")" ]; then ok; else bad "review-prepare.sh spec/config list drifted from scripts/review-delta.sh"; fi
|
|
286
|
+
|
|
202
287
|
# --- 6. full-build lane: empty trusted config is BLOCKED, never a silent skip ---
|
|
203
288
|
TS_DIFF='diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
|
|
204
289
|
+x'
|