forgedock 1.14.0 → 1.15.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.
@@ -51,7 +51,9 @@
51
51
  # earlier head of this PR or by a bundle's member PRs, one per line, columns
52
52
  # file<TAB>line-start<TAB>line-end<TAB>title<TAB>prior-disposition<TAB>issue#.
53
53
  # prior-disposition is `filed` (its issue is still open), `filed-closed` (issue closed:
54
- # the row is IGNORED so a regressed defect is classified normally) or `note`.
54
+ # the row is IGNORED so a regressed defect is classified normally), `note` (a recorded NOTE) or
55
+ # `unknown` (a reviewer-body row whose issue linkage failed: it is IGNORED for a MEDIUM
56
+ # CONFIRMED/LIKELY finding so it can never suppress one, and behaves like `note` otherwise).
55
57
  # Line columns may be empty (NOTE-sourced rows carry no range). A finding re-flags a row
56
58
  # when it is the same file AND ((line IoU >= 0.6 and >= 1 shared title token), or >= 3 shared title tokens). Output is
57
59
  # `NOTE reflag-filed #N` (row has an issue number) or `NOTE reflag-note`; this gate never
@@ -180,7 +182,7 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
180
182
  case "$_rls" in ''|*[!0-9]*) _rls=""; _rle="" ;; esac
181
183
  case "$_rle" in ''|*[!0-9]*) _rle="$_rls" ;; esac
182
184
  _dmap="$DIFF_MAP"; { [ -n "$_dmap" ] && [ -r "$_dmap" ]; } || _dmap=""
183
- _reflag=$(awk -F '\t' -v file="$_rpath" -v ls="$_rls" -v le="$_rle" -v title="$TITLE" -v dmap="$_dmap" '
185
+ _reflag=$(awk -F '\t' -v file="$_rpath" -v ls="$_rls" -v le="$_rle" -v title="$TITLE" -v dmap="$_dmap" -v sev="$SEV" -v conf="$CONF" '
184
186
  function toks(str, arr, n, i, parts, t, c) {
185
187
  c = 0; str = tolower(str); gsub(/[^a-z0-9]+/, " ", str)
186
188
  n = split(str, parts, " ")
@@ -202,18 +204,23 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
202
204
  }
203
205
  close(dmap)
204
206
  }
207
+ # Generic words count toward the token set but never as topical similarity (#3719).
208
+ ns = split("the and with for from before after write read when that this into not are but can has have via use using does should may all any only also than then over under without within while where which what its it was were been being each both one two", sw, " ")
209
+ for (i = 1; i <= ns; i++) stop[sw[i]] = 1
205
210
  ntitle = toks(title, ft)
206
211
  }
207
212
  {
208
213
  pf = $1; sub(/^\.\//, "", pf)
209
214
  if (pf != file) next
210
215
  if ($5 == "filed-closed") next
216
+ if ($5 == "unknown" && sev == "MEDIUM" && (conf == "CONFIRMED" || conf == "LIKELY")) next
211
217
  hit = 0
212
- delete pt; toks($4, pt); shared = 0
213
- for (t in pt) if (t in ft) shared++
218
+ delete pt; toks($4, pt); shared = 0; cshared = 0
219
+ for (t in pt) if (t in ft) { shared++; if (!(t in stop)) cshared++ }
214
220
  # Line overlap alone is not a match (an unrelated finding can land on the same line): it also
215
- # needs at least one shared title token. Title-only matching still needs >= 3.
216
- if (shared >= 1 && ls != "" && $2 ~ /^[0-9]+$/) {
221
+ # needs at least one shared non-stopword title token. Title-only matching needs >= 3 shared
222
+ # tokens, of which >= 2 are non-stopword (generic words never count as topical similarity).
223
+ if (cshared >= 1 && ls != "" && $2 ~ /^[0-9]+$/) {
217
224
  ps = $2 + 0; pe = ($3 ~ /^[0-9]+$/) ? $3 + 0 : ps
218
225
  if (pe < ps) pe = ps
219
226
  d = shift(pf, ps, pe); ps += d; pe += d
@@ -222,7 +229,7 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
222
229
  uni = (pe - ps + 1) + (le - ls + 1) - inter
223
230
  if (uni > 0 && inter / uni >= 0.6) hit = 1
224
231
  }
225
- if (!hit && ntitle >= 3 && shared >= 3) hit = 1
232
+ if (!hit && ntitle >= 3 && shared >= 3 && cshared >= 2) hit = 1
226
233
  if (!hit) next
227
234
  if ($5 == "filed" && $6 ~ /^#?[0-9]+$/) { n = $6; sub(/^#/, "", n); print "NOTE reflag-filed #" n; found = 1; exit }
228
235
  if (!note) note = "NOTE reflag-note"
@@ -166,6 +166,7 @@ DMAP=$(mktemp "${TMPDIR:-/tmp}/classify-finding-dmap.XXXXXX")
166
166
  printf 'src/api.py\t10\t20\tmissing retry on timeout handler\tfiled\t123\n'
167
167
  printf 'src/closed.py\t5\t15\tstale cache entry never evicted\tfiled-closed\t77\n'
168
168
  printf 'src/notes.py\t\t\tduplicated predicate counted twice\tnote\t\n'
169
+ printf 'src/unlinked.py\t30\t40\tunvalidated redirect target in login handler\tunknown\t\n'
169
170
  } > "$PRIOR"
170
171
  PF=(--prior-findings "$PRIOR")
171
172
  M=(--severity MEDIUM --confidence CONFIRMED)
@@ -182,9 +183,17 @@ expect_out "NOTE reflag-note" "NOTE row with empty line columns matches by title
182
183
  expect ISSUE "same line, different topic MEDIUM CONFIRMED is not swallowed (SEC-1)" -- "${M[@]}" --agent security "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "SQL injection via unsanitized token"
183
184
  expect ISSUE "same line with no title at all is not swallowed" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50
184
185
  expect_out "NOTE reflag-note" "same line, same topic (one shared token) still NOTE" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "Lock handling is racy"
186
+ expect ISSUE "same line, only a stopword-class word shared is not swallowed (#3719)" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 42 --line-end 50 --title "Path traversal in the write target"
187
+ expect ISSUE "far line, only generic words shared is not swallowed (#3719)" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 300 --title "Command injection before write with the lock"
185
188
  expect ISSUE "non-overlapping same-file finding is classified normally" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 200 --line-end 210 --title "missing bounds check"
186
189
  expect ISSUE "two shared tokens are not enough" -- "${M[@]}" "${PF[@]}" --file src/sync.sh --line-start 200 --title "lock released late"
187
190
  expect ISSUE "same title on a different file is classified normally" -- "${M[@]}" "${PF[@]}" --file src/other.sh --line-start 45 --title "lock released before write completes"
191
+ expect ISSUE "unknown row does not demote MEDIUM CONFIRMED (unlinked prior)" -- "${M[@]}" "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
192
+ expect ISSUE "unknown row does not demote MEDIUM CONFIRMED by line IoU" -- "${M[@]}" "${PF[@]}" --file src/unlinked.py --line-start 32 --line-end 40 --title "redirect target unchecked"
193
+ expect ISSUE "unknown row does not demote MEDIUM LIKELY" -- --severity MEDIUM --confidence LIKELY "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
194
+ expect_out "NOTE reflag-note" "unknown row still demotes LOW" -- --severity LOW --confidence CONFIRMED "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
195
+ expect_out "NOTE reflag-note" "unknown row still demotes MEDIUM POSSIBLE" -- --severity MEDIUM --confidence POSSIBLE "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
196
+ expect ISSUE "HIGH unaffected by unknown row" -- --severity HIGH --confidence CONFIRMED "${PF[@]}" --file src/unlinked.py --line-start 300 --title "Unvalidated redirect target in the login handler"
188
197
  expect_out "NOTE reflag-filed #123" "re-flag of an already-filed issue links it" -- "${M[@]}" "${PF[@]}" --file src/api.py --line-start 12 --line-end 18 --title "retry x"
189
198
  expect_out "NOTE reflag-filed #123" "filed re-flag by title tokens" -- "${M[@]}" "${PF[@]}" --file src/api.py --line-start 99 --title "Missing retry on timeout"
190
199
  expect ISSUE "filed-closed row is ignored (regression not swallowed)" -- "${M[@]}" "${PF[@]}" --file src/closed.py --line-start 5 --line-end 15 --title "stale cache entry never evicted"
@@ -13,7 +13,7 @@ ok() { PASS=$((PASS+1)); }
13
13
  bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
14
14
  expect() { [ "$2" = "$3" ] && ok || bad "$1 (got '$3' want '$2')"; }
15
15
 
16
- SITES="commands/work-on.md commands/work-on/build/validate.md commands/quality-gate.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/review-pr.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
16
+ SITES="commands/work-on.md commands/work-on/build/validate.md commands/quality-gate.md commands/work-on/review.md commands/work-on/investigate.md commands/work-on/decompose.md commands/work-on/build.md commands/work-on/close.md commands/review-pr.md commands/work-on/remediate.md commands/orchestrate/phase-1-resolve.md commands/orchestrate/phase-4-execution.md"
17
17
 
18
18
  # extract the bootstrap block (comment line .. closing top-level fi), indentation stripped
19
19
  # Block ends at the first line (after the marker) that starts with `fi` at the marker's own indentation.
@@ -111,6 +111,23 @@ else
111
111
  echo "SKIP: symlink cases (this platform cannot create symlinks)"; SKIPPED=$((SKIPPED+1))
112
112
  fi
113
113
 
114
+ # forge#3758 (hgate is a fixture of its own: the loop runs once per shell and h14 is reused below with other versions): deploy-gate script resolves on a plugin-only install (cache only, no symlink, no FORGE_* env).
115
+ # The gate-script line is taken verbatim from the spec so the test exercises exactly what ships.
116
+ mkscripts "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0"; : > "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh"
117
+ grep -hE '^[[:space:]]*GATE_SCRIPT=""; \[ -n "\$FORGE_ROOT" \]' "$ROOT/commands/work-on/remediate.md" "$ROOT/commands/review-pr.md" | sed 's/^[[:space:]]*//' | sort -u > "$T/gs"
118
+ expect "gate-script resolver line is identical across sites" 1 "$(wc -l < "$T/gs" | tr -d ' ')"
119
+ gate_run() { ( cd "$T/consumer" && env -i PATH="$PATH" HOME="$1" "$SH" -c "$(cat "$T/canon"); $(cat "$T/gs"); printf %s \"\$GATE_SCRIPT\"" ); }
120
+ expect "plugin-only install resolves resolve-deploy-gate.sh" "$T/hgate/.claude/plugins/cache/forgedock/forgedock/1.0.0/scripts/resolve-deploy-gate.sh" "$(gate_run "$T/hgate")"
121
+ expect "unresolved gate script stays empty (strict)" "" "$(gate_run "$T/empty")"
122
+ # cwd/consumer repo is never a candidate even when it carries the gate script
123
+ : > "$T/consumer/scripts/resolve-deploy-gate.sh"
124
+ expect "consumer repo gate script never used" "" "$(gate_run "$T/empty")"
125
+ rm -f "$T/consumer/scripts/resolve-deploy-gate.sh"
126
+ # no spec iterates CLAUDE_PLUGIN_ROOT as an env var for the gate script
127
+ for f in commands/work-on/remediate.md commands/review-pr.md; do
128
+ grep -qF '"${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}"' "$ROOT/$f" && bad "env-style CLAUDE_PLUGIN_ROOT gate loop in $f" || ok
129
+ done
130
+
114
131
  # newest cached version wins (1.10.0 must beat 1.9.0)
115
132
  for v in 1.9.0 1.10.0 1.2.0; do mkscripts "$T/h3/.claude/plugins/cache/forgedock/forgedock/$v"; done
116
133
  expect "newest cached version wins" "$T/h3/.claude/plugins/cache/forgedock/forgedock/1.10.0" "$(run "$T/h3")"
@@ -325,6 +342,8 @@ hr_check_block() { # label file var script
325
342
  hr_check_block review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
326
343
  hr_check_block review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
327
344
  hr_check_block review-pr "$HR_CLASSIFY" CODE_INDEX_SCRIPT code-index.sh
345
+ hr_check_block review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
346
+ hr_check_block issue "$ROOT/commands/issue.md" DEDUP_SCRIPT issue-dedup.sh
328
347
  hr_check_block review-pr-staging "$HR_STG" DERIVE_MILESTONE_SCRIPT derive-finding-milestone.sh
329
348
  hr_check_block review-pr-staging "$HR_STG" PRIORITY_SCRIPT severity-to-priority.sh
330
349
  extract_helper_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
@@ -334,6 +353,9 @@ extract_helper_block "$HR_STG" PRIORITY_SCRIPT > "$T/hr_pr_stg.sh"
334
353
  # only the variable-name-specific text may differ, and review-pr/review-pr-staging must agree
335
354
  cmp -s "$T/hr_dm.sh" "$T/hr_dm_stg.sh" && ok || bad 'DERIVE_MILESTONE_SCRIPT resolver differs between review-pr and review-pr-staging'
336
355
  cmp -s "$T/hr_pr.sh" "$T/hr_pr_stg.sh" && ok || bad 'PRIORITY_SCRIPT resolver differs between review-pr and review-pr-staging'
356
+ extract_helper_block "$HR_CLASSIFY" DEDUP_SCRIPT > "$T/hr_dd.sh"
357
+ extract_helper_block "$ROOT/commands/issue.md" DEDUP_SCRIPT > "$T/hr_dd_issue.sh"
358
+ cmp -s "$T/hr_dd.sh" "$T/hr_dd_issue.sh" && ok || bad 'DEDUP_SCRIPT resolver differs between review-pr and issue'
337
359
  # no bare cwd-relative helper call in either review spec
338
360
  if grep -nE 'bash scripts/(derive-finding-milestone|severity-to-priority)\.sh' "$HR_CLASSIFY" "$HR_STG" >/dev/null 2>&1; then bad 'review spec still calls a helper by a bare scripts/ path'; else ok; fi
339
361
 
@@ -342,10 +364,25 @@ if grep -nE 'bash scripts/(derive-finding-milestone|severity-to-priority)\.sh' "
342
364
  # design-bench.md, design-render-critique-loop.md, pipeline-health/phase-2-metrics.md, phase-3-dependency.md:
343
365
  # node scripts/*.mjs run inside the ForgeDock repo itself or printed as user hints (not run under a PR checkout cwd).
344
366
  # work-on/investigate.md, work-on/build/architect.md: {REPO_PATH}/scripts/code-index.sh, deferred to #3670.
367
+ # orchestrate/phase-3-dependency.md: scripts/worktree-lifecycle.sh lines sit in a roadmap/proposal fence, not executed.
345
368
  # deploy-info.md: read-only grep of decrypt-secrets.sh, never executed.
346
- SCRIPTS_ALLOW='commands/(design-bench|design-render-critique-loop|deploy-info|pipeline-health/phase-2-metrics|pipeline-health/phase-3-dependency|work-on/investigate|work-on/build/architect)\.md'
347
- guard_hits=$(grep -rnE '(^|[^A-Za-z0-9_-])(bash|sh|source) +(\./)?scripts/[A-Za-z0-9_.-]+|REPO_PATH\}?/scripts/' "$ROOT/commands" 2>/dev/null \
348
- | sed "s#^$ROOT/##" | grep -vE "^($SCRIPTS_ALLOW):" | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' || true) # comment lines are prose
369
+ SCRIPTS_ALLOW='commands/(design-bench|design-render-critique-loop|deploy-info|pipeline-health/phase-2-metrics|pipeline-health/phase-3-dependency|orchestrate/phase-3-dependency|work-on/investigate|work-on/build/architect)\.md'
370
+ # Executed forms: a bash/sh/source word, a command substitution `$(scripts/x.sh`, or a line-start/after-;&&| direct `scripts/x.sh` / `./scripts/x.sh`.
371
+ GUARD_RE='(^|[^A-Za-z0-9_-])(bash|sh|source) +(\./)?scripts/[A-Za-z0-9_.-]+|REPO_PATH\}?/scripts/|\$\( *(\./)?scripts/[A-Za-z0-9_.-]+\.sh|(^|:[0-9]+:|[;&(] *)[[:space:]]*(\./)?scripts/[A-Za-z0-9_.-]+\.sh'
372
+ guard_scan() { # dir -> offending file:line:text rows (comment lines are prose)
373
+ grep -rnE "$GUARD_RE" "$1" 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' || true
374
+ }
375
+ guard_hits=$(guard_scan "$ROOT/commands" | sed "s#^$ROOT/##" | grep -vE "^($SCRIPTS_ALLOW):" || true)
376
+ # Negative fixtures (#3679): a bare direct call must be flagged; backticked prose and comments must not.
377
+ mkdir -p "$T/guardfx/bad1" "$T/guardfx/bad2" "$T/guardfx/bad3" "$T/guardfx/good"
378
+ printf 'X=$(scripts/foo.sh a b)\n' > "$T/guardfx/bad1/x.md"
379
+ printf 'scripts/foo.sh arg\n' > "$T/guardfx/bad2/x.md"
380
+ printf 'ok && ./scripts/foo.sh\n' > "$T/guardfx/bad3/x.md"
381
+ printf 'See `scripts/foo.sh` for details, forwarded to scripts/foo.sh --exclude.\n# $(scripts/foo.sh) in a comment\n' > "$T/guardfx/good/x.md"
382
+ for f in bad1 bad2 bad3; do
383
+ [ -n "$(guard_scan "$T/guardfx/$f")" ] && ok || bad "widened guard missed direct execution fixture $f"
384
+ done
385
+ [ -z "$(guard_scan "$T/guardfx/good")" ] && ok || bad 'widened guard flags prose/comment fixture'
349
386
  if [ -n "$guard_hits" ]; then bad "commands/ has a cwd-relative or REPO_PATH helper call outside the allowlist: $(printf '%s' "$guard_hits" | head -5 | tr '\n' '|')"; else ok; fi
350
387
 
351
388
  # Behavior (#3596): planted cwd copies of the two review helpers must never be selected.
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # remediate-approved-head.test.sh — the M7 CI-gated merge is bound to APPROVED_HEAD on every path (forge#3686)
3
+ # Extracts the guard from commands/work-on/remediate.md and exercises it. No network. Usage: bash scripts/remediate-approved-head.test.sh
4
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
5
+ # SPDX-License-Identifier: AGPL-3.0-or-later
6
+
7
+ set -uo pipefail
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
10
+ SPEC="$ROOT/commands/work-on/remediate.md"
11
+
12
+ PASS=0; FAILN=0
13
+ ok() { PASS=$((PASS+1)); echo "ok - $1"; }
14
+ bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
15
+ eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
16
+
17
+ grep -qE 'GATED_HEAD" != "\$\{?APPROVED_HEAD' "$SPEC" && ok "spec compares GATED_HEAD to APPROVED_HEAD" || bad "spec compares GATED_HEAD to APPROVED_HEAD"
18
+ grep -qF 'if [ "${RESUME_AT_M7:-false}" = "true" ] && [ "$CI_GATE_RC" -eq 0 ]' "$SPEC" && bad "guard is no longer resume-only" || ok "guard is no longer resume-only"
19
+
20
+ # Extract the guard fence body (from the scratch reload through the closing fi) and run it per case.
21
+ GUARD=$(awk '/^_SD="\$\{FORGE_SCRATCHPAD/ && !g {g=1} g {print} g && /^fi$/ {exit}' "$SPEC")
22
+ [ -n "$GUARD" ] && ok "guard block extracted" || bad "guard block extracted"
23
+
24
+ # run_guard <persisted head or NONE> <GATED_HEAD> <CI_GATE_RC> <RESUME_AT_M7>
25
+ run_guard() {
26
+ local d; d=$(mktemp -d)
27
+ [ "$1" = NONE ] || printf '%s\n' "$1" > "$d/remediate_approved_head_7"
28
+ FORGE_SCRATCHPAD="$d" GATED_HEAD="$2" CI_GATE_RC="$3" RESUME_AT_M7="$4" CI_GATE_OUT="" \
29
+ bash -c "${GUARD//\{PR_NUMBER\}/7}"$'\nprintf %s "$CI_GATE_RC"'
30
+ rm -rf "$d"
31
+ }
32
+ A=$(printf 'a%.0s' {1..40}); B=$(printf 'b%.0s' {1..40})
33
+ for r in true false; do
34
+ eq "match merges (resume=$r)" "$(run_guard "$A" "$A" 0 $r)" 0
35
+ eq "mismatch refuses (resume=$r)" "$(run_guard "$A" "$B" 0 $r)" 4
36
+ eq "missing approved head refuses (resume=$r)" "$(run_guard NONE "$A" 0 $r)" 4
37
+ eq "malformed approved head refuses (resume=$r)" "$(run_guard "zz" "$A" 0 $r)" 4
38
+ eq "empty gated head refuses (resume=$r)" "$(run_guard "$A" "" 0 $r)" 4
39
+ eq "nonzero rc is unchanged (resume=$r)" "$(run_guard "$A" "$B" 3 $r)" 3
40
+ done
41
+
42
+ echo "passed=$PASS failed=$FAILN"
43
+ [ "$FAILN" -eq 0 ]
@@ -28,6 +28,8 @@ INTERIM=$'<!-- FORGE:REMEDIATION -->\n## Remediation (in progress)'
28
28
  QUOTED_REVIEW=$'## Review\nThe prior run posted FORGE:REMEDIATION:COMPLETE on this PR.\n<!-- FORGE:REMEDIATION:COMPLETE -->'
29
29
  QUOTED_WORK_ORDER=$'<!-- FORGE:INPR_FIX: head=abc1234 -->\nquoting <!-- FORGE:REMEDIATION -->\n<!-- FORGE:REMEDIATION:COMPLETE -->'
30
30
 
31
+ HELD=$'<!-- FORGE:REMEDIATION_HELD -->\n**Re-gate outcome**: HELD-AWAITING-MERGE'
32
+ eq "held-resume record matches neither COMPLETE nor TRAIL (forge#3685)" "$(complete_n "[$(c NONE Bot b "$HELD")]")/$(trail_n "[$(c NONE Bot b "$HELD")]")" "0/0"
31
33
  eq "genuine M8 trail by bot matches COMPLETE" "$(complete_n "[$(c NONE Bot 'app[bot]' "$REAL")]")" 1
32
34
  eq "review comment quoting COMPLETE mid-body does not match" "$(complete_n "[$(c OWNER User u "$QUOTED_REVIEW")]")" 0
33
35
  eq "INPR_FIX work order quoting the markers does not match COMPLETE" "$(complete_n "[$(c NONE Bot b "$QUOTED_WORK_ORDER")]")" 0
@@ -47,5 +49,9 @@ for f in commands/work-on/remediate.md commands/work-on.md commands/orchestrate/
47
49
  done
48
50
  grep -qF "count '$COMPLETE_RE'" "$ROOT/commands/work-on/remediate.md" && ok "remediate.md uses the tested COMPLETE regex" || bad "remediate.md COMPLETE regex drifted from this test"
49
51
 
52
+ grep -qF 'FORGE:REMEDIATION_HELD' "$ROOT/commands/work-on/remediate.md" && ok "remediate.md documents FORGE:REMEDIATION_HELD" || bad "remediate.md lacks FORGE:REMEDIATION_HELD"
53
+ ! grep -qE 'PR_OPEN_STATE = .?MERGED.? → EXIT .REMEDIATE_RESULT: status: ALREADY_DONE. \(nothing to remediate' "$ROOT/commands/work-on/remediate.md" && ok "M0 has no unconditional merged ALREADY_DONE exit" || bad "M0 still exits ALREADY_DONE unconditionally on MERGED"
54
+ grep -qiE 'merged.*(skip|exclusive|do not fall through)' "$ROOT/commands/work-on/remediate.md" && ok "M7 merged-first branch is exclusive" || bad "M7 merged-first exclusivity missing"
55
+
50
56
  echo "passed=$PASS failed=$FAILN"
51
57
  [ "$FAILN" -eq 0 ]
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # resolve-deploy-gate.sh — trusted deploy-gate resolver (forge#3753)
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ #
6
+ # Usage: resolve-deploy-gate.sh <base> <owner/repo>
7
+ #
8
+ # Prints exactly one line: `true` (deploy gate, strict human bar) or `false`
9
+ # (fast lane). Always exits 0; every failure path prints `true` (fail closed).
10
+ #
11
+ # A base is the deploy gate when it is empty/`null`, the literal `main` (hard
12
+ # floor), the repository's GitHub default branch (`default_branch`), or the
13
+ # `branches.default` of the default branch's COMMITTED forge.yaml
14
+ # (`origin/<default>:forge.yaml`, falling back to the contents API) or of an
15
+ # explicit trusted config (FORGE_TRUSTED_CONFIG, orchestrator-supplied).
16
+ #
17
+ # It NEVER reads forge.yaml from $PWD: the PR worktree is author-controlled, so
18
+ # a PR could otherwise redefine which branch counts as the deploy gate.
19
+ # Config sources can only ADD gate branches; main and the GitHub default are
20
+ # unconditional, so no source relaxes the bar below the literal-main floor.
21
+ set -uo pipefail
22
+
23
+ BASE="${1:-}"
24
+ REPO="${2:-}"
25
+
26
+ emit() { printf '%s\n' "$1"; exit 0; }
27
+
28
+ { [ -z "$BASE" ] || [ "$BASE" = "null" ] || [ "$BASE" = "main" ]; } && emit true
29
+ [ -n "$REPO" ] || emit true
30
+
31
+ GH_DEFAULT=$(gh api "repos/${REPO}" --jq .default_branch 2>/dev/null || echo "")
32
+ { [ -z "$GH_DEFAULT" ] || [ "$GH_DEFAULT" = "null" ]; } && emit true
33
+ [ "$BASE" = "$GH_DEFAULT" ] && emit true
34
+
35
+ _cfg_default() { yq '.branches.default // ""' 2>/dev/null || true; }
36
+
37
+ CFG_DEFAULT=""
38
+ # Committed default-branch config only (a ref, never the working tree).
39
+ CFG_DEFAULT=$(git show "origin/${GH_DEFAULT}:forge.yaml" 2>/dev/null | _cfg_default)
40
+ if [ -z "$CFG_DEFAULT" ]; then
41
+ CFG_DEFAULT=$(gh api "repos/${REPO}/contents/forge.yaml?ref=${GH_DEFAULT}" --jq .content 2>/dev/null \
42
+ | base64 -d 2>/dev/null | _cfg_default)
43
+ fi
44
+ [ -n "$CFG_DEFAULT" ] && [ "$CFG_DEFAULT" != "null" ] && [ "$BASE" = "$CFG_DEFAULT" ] && emit true
45
+
46
+ if [ -n "${FORGE_TRUSTED_CONFIG:-}" ] && [ -f "$FORGE_TRUSTED_CONFIG" ]; then
47
+ TRUSTED=$(_cfg_default < "$FORGE_TRUSTED_CONFIG")
48
+ [ -n "$TRUSTED" ] && [ "$TRUSTED" != "null" ] && [ "$BASE" = "$TRUSTED" ] && emit true
49
+ fi
50
+
51
+ emit false
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ # resolve-deploy-gate.test.sh — cases for scripts/resolve-deploy-gate.sh (forge#3753).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ set -uo pipefail
6
+ S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resolve-deploy-gate.sh"
7
+ PASS=0; FAILN=0
8
+ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
9
+ ok() { PASS=$((PASS+1)); }
10
+ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
11
+
12
+ command -v yq >/dev/null 2>&1 || { echo "SKIP: yq not installed"; exit 0; }
13
+
14
+ # Stub gh: GH_DEFAULT_STUB drives `repos/<r> --jq .default_branch`; contents API unused (fails).
15
+ mkdir -p "$TMP/bin"
16
+ cat > "$TMP/bin/gh" <<'EOF'
17
+ #!/usr/bin/env bash
18
+ case "$*" in
19
+ *"--jq .default_branch"*) [ -n "${GH_FAIL:-}" ] && exit 1; printf '%s\n' "${GH_DEFAULT_STUB:-}" ;;
20
+ *) exit 1 ;;
21
+ esac
22
+ EOF
23
+ chmod +x "$TMP/bin/gh"
24
+
25
+ # Repo fixture: origin/<default> committed forge.yaml, plus a PR worktree dir with its own forge.yaml.
26
+ ORIGIN="$TMP/origin.git"; REPO="$TMP/repo"
27
+ git init -q --bare "$ORIGIN"
28
+ git init -q "$REPO"; git -C "$REPO" config user.email t@t; git -C "$REPO" config user.name t
29
+ printf 'branches:\n default: develop\n' > "$REPO/forge.yaml"
30
+ git -C "$REPO" add forge.yaml; git -C "$REPO" commit -qm init
31
+ git -C "$REPO" branch -M trunk
32
+ git -C "$REPO" remote add origin "$ORIGIN"; git -C "$REPO" push -q origin trunk 2>/dev/null
33
+ git -C "$REPO" fetch -q origin
34
+ # PR worktree content: attacker redefines the deploy gate.
35
+ printf 'branches:\n default: release\n' > "$REPO/forge.yaml"
36
+
37
+ run() { # base default_stub [env...]
38
+ local base="$1" def="$2"; shift 2
39
+ (cd "$REPO" && env PATH="$TMP/bin:$PATH" GH_DEFAULT_STUB="$def" "$@" bash "$S" "$base" owner/repo)
40
+ }
41
+ expect() { [ "$1" = "$2" ] && ok || bad "$3: got '$1' want '$2'"; }
42
+
43
+ expect "$(run main trunk)" true "main with worktree release"
44
+ expect "$(run release trunk)" false "worktree-declared branch is not a gate"
45
+ expect "$(run staging trunk)" false "staging is fast lane"
46
+ expect "$(run trunk trunk)" true "github default"
47
+ expect "$(run master master)" true "github default master"
48
+ expect "$(run develop trunk)" true "committed origin config default"
49
+ expect "$(run '' trunk)" true "empty base"
50
+ expect "$(run null trunk)" true "null base"
51
+ expect "$(run staging '')" true "empty gh default fails closed"
52
+ expect "$(run staging trunk GH_FAIL=1)" true "gh failure fails closed"
53
+ expect "$(run main '' GH_FAIL=1)" true "main on gh failure"
54
+ printf 'branches:\n default: prod\n' > "$TMP/trusted.yaml"
55
+ expect "$(run prod trunk FORGE_TRUSTED_CONFIG="$TMP/trusted.yaml")" true "trusted config default"
56
+ expect "$(run prod trunk)" false "prod without trusted config"
57
+
58
+ echo "resolve-deploy-gate.test.sh: $PASS passed, $FAILN failed"
59
+ [ "$FAILN" -eq 0 ]
@@ -120,8 +120,16 @@ expect "round 2 narrows to security by default" "security" "$(roster 2 'security
120
120
  expect "round 2 adds a path-triggered domain" "security auth" "$(roster 2 'security auth billing' $'README.md\nsrc/auth/login.py')"
121
121
  expect "round 3 cap holds" "security" "$(roster 3 'security auth billing database' 'docs/notes.txt')"
122
122
  expect "claim/lease paths trigger concurrency" "security concurrency" "$(roster 2 'security' 'engine/claim-lease.txt')"
123
+ export REPO_PATH="$ROOT" # ForgeDock itself: the persona file exists
123
124
  expect "commands/** triggers spec-cli" "security spec-cli" "$(roster 2 'security' 'commands/foo.md')"
124
125
  expect "bin/** triggers spec-cli" "security spec-cli" "$(roster 2 'security' 'bin/foo.mjs')"
126
+ # consumer repo layout: no persona file, so no spec-cli
127
+ mkdir -p "$TMP/consumer/bin" "$TMP/consumer/commands"
128
+ export REPO_PATH="$TMP/consumer"
129
+ expect "consumer bin/ gets no spec-cli" "security" "$(roster 2 'security' 'bin/foo.mjs')"
130
+ expect "consumer commands/ gets no spec-cli" "security" "$(roster 2 'security' 'commands/foo.md')"
131
+ expect "consumer with another trigger keeps it, no spec-cli" "security auth" "$(roster 2 'security' $'bin/foo.mjs\nsrc/auth/login.py')"
132
+ unset REPO_PATH
125
133
  expect "non-numeric rounds fail to the 3B roster" "security auth" "$(roster 'x' 'security auth' 'README.md')"
126
134
 
127
135
  # ---- 3. mode resolution through the real fence ----------------------------------------------------------------------
@@ -83,5 +83,15 @@ done
83
83
  # Test 6: the protocol defines the marker domain through the mapping
84
84
  if grep -qF 'marker domain' "$PERSONAS/protocols.md"; then ok; else bad "protocols.md does not define the marker domain"; fi
85
85
 
86
+ # Test 7: the roster regexes in delta_roster must equal the DOMAIN_FILES_* regexes, and every domain needs an empty-slice fallback
87
+ for pair in AUTH:auth BILLING:billing CONCURRENCY:concurrency DATABASE:database INFRA:infra SCRAPING:scraper FRONTEND:frontend API:api; do
88
+ var=${pair%%:*}; dom=${pair#*:}
89
+ a=$(grep -E "^DOMAIN_FILES_$var=" "$SPEC" | head -n 1 | sed -n 's/.*grep -iE "\([^"]*\)".*/\1/p')
90
+ b=$(grep -F "&& out=\"\$out $dom\"" "$SPEC" | grep 'grep -qiE' | head -n 1 | sed -n 's/.*grep -qiE "\([^"]*\)".*/\1/p')
91
+ if [ -z "$a" ] || [ -z "$b" ]; then bad "could not extract $var pattern (DOMAIN_FILES='$a' delta_roster='$b')"
92
+ elif [ "$a" = "$b" ]; then ok; else bad "$var regex drifted: DOMAIN_FILES='$a' delta_roster='$b'"; fi
93
+ if grep -qF "[ -z \"\$DOMAIN_FILES_$var\" ] && DOMAIN_FILES_$var=\"\$FILES\"" "$SPEC"; then ok; else bad "no empty-slice fallback for DOMAIN_FILES_$var"; fi
94
+ done
95
+
86
96
  echo "review-pr-domain-map: $pass passed, $fail failed"
87
97
  [ "$fail" -eq 0 ]
@@ -99,6 +99,20 @@ done
99
99
  awk '/^[^ `#].* # FENCE_BOOTSTRAP/ && !seen[$0]++' "$SPEC" > "$TMP/boot.sh"
100
100
  [ "$(wc -l < "$TMP/boot.sh")" -eq 8 ] && ok || bad "FENCE_BOOTSTRAP block expected 8 distinct lines (got $(wc -l < "$TMP/boot.sh"))"
101
101
  [ "$(grep -c '^# Fence bootstrap' "$SPEC")" -ge 3 ] && ok || bad "bootstrap must be carried by 0B.1 and both 7B.5 fences"
102
+ # forge#3681: EVERY bash fence after Phase 0B that reads cross-fence state must carry the bootstrap (a fresh Bash call loses it)
103
+ UNCOVERED=$(awk '
104
+ /^```bash/ { infence=1; body=""; start=NR; next }
105
+ /^```/ && infence { infence=0
106
+ if (start > 400 && body ~ /PR_NUMBER|ALL_PR_NUMBERS|GH_REPO|DEFAULT_BRANCH|STAGING_BRANCH/ && body !~ /# FENCE_BOOTSTRAP/) printf "%d ", start
107
+ next }
108
+ infence { body = body "\n" $0 }' "$SPEC")
109
+ [ -z "$UNCOVERED" ] && ok || bad "bash fences starting at line(s) ${UNCOVERED}consume cross-fence state without the FENCE_BOOTSTRAP block"
110
+ # the persisted Phase 6.5 / 6.75 values: producers write them, consumers reload them validated
111
+ grep -q 'test_gate_verdict' "$SPEC" && grep -q 'launched_review_agents' "$SPEC" && ok || bad "TEST_GATE_VERDICT and LAUNCHED_REVIEW_AGENTS must be persisted to the scratchpad"
112
+ # PR-keyed (concurrent runs in one cwd must not share state) and reset by an executable command, not prose
113
+ 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
114
+ grep -qF ': > "$_SD/${PR_NUMBER}_launched_review_agents"' "$SPEC" && grep -qF 'rm -f "$_SD/${PR_NUMBER}_test_gate_verdict" "$_SD/${PR_NUMBER}_test_gate_reason"' "$SPEC" && ok || bad "Phase 2 must truncate and Phase 6.5 must rm -f the PR-keyed scratch files in executable commands"
115
+ if awk '/^### 7E:/,/^### 7F:/' "$SPEC" | grep -qF '{GH_REPO}'; then bad "7E must not use the {GH_REPO} template token"; else ok; fi
102
116
  mkdir -p "$TMP/sd8/full" "$TMP/sd8/empty" "$TMP/sd8/bad" "$TMP/bin"
103
117
  printf '#!/bin/sh\necho stub-value\n' > "$TMP/bin/yq"; chmod +x "$TMP/bin/yq"
104
118
  printf '42\n' > "$TMP/sd8/full/staging_pr_number"; printf '7\n8\n' > "$TMP/sd8/full/42_bundle-prs.txt"
@@ -155,5 +169,90 @@ run_dup() { # finding-file finding-line -> "DISPOSITION|FILED|REFLAG|LISTED"
155
169
  [ "$(run_dup src/other.py:12)" = "ISSUE MEDIUM-CONFIRMED|1|0|0" ] && ok || bad "a non-duplicate on another file must classify normally (got '$(run_dup src/other.py:12)')"
156
170
  run_dup src/api.py:12 >/dev/null
157
171
  grep -q 'reflag-filed #123' "$TMP/dup-notes.md" && ok || bad "a re-flag NOTE must be listed, not dropped"
172
+ # (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.
173
+ # Fail-closed cases first: every FULL / error / untrusted path must leave the PR's commits UNREVIEWED (empty credit).
174
+ if command -v git >/dev/null 2>&1; then
175
+ awk '/^# BEGIN bundle-delta-credit$/{f=1;next} /^# END bundle-delta-credit$/{f=0} f' "$SPEC" > "$TMP/delta-fn.sh"
176
+ [ -s "$TMP/delta-fn.sh" ] && ok || bad "bundle-delta-credit block not found in the spec"
177
+ RDS="$ROOT/scripts/review-delta.sh"
178
+ export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null GIT_CONFIG_NOSYSTEM=1
179
+ export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
180
+ g9() { git -C "$W9" -c commit.gpgsign=false -c core.hooksPath=/dev/null "$@"; }
181
+ commit9() { mkdir -p "$W9/$(dirname "$1")"; printf '%s\n' "$2" > "$W9/$1"; g9 add -A >/dev/null; g9 commit -q -m "$3" >/dev/null; }
182
+ C9=0
183
+ mkrepo9() { # W9 = clone with origin/main, branch feat with one PR commit
184
+ C9=$((C9+1)); local o="$TMP/do$C9"; W9="$TMP/dw$C9"
185
+ git init -q --bare -b main "$o"; git clone -q "$o" "$W9" 2>/dev/null
186
+ g9 checkout -q -b main 2>/dev/null; commit9 a.txt base base; g9 push -q origin main; g9 checkout -q feat 2>/dev/null || g9 checkout -q -b feat; commit9 pr.txt pr1 pr1
187
+ }
188
+ agent9() { printf '<!-- FORGE:REVIEW-AGENT:%s -->\nReviewed-SHA: %s\n\n<!-- REVIEW-FINDINGS-START -->\nNo findings.\n<!-- REVIEW-FINDINGS-END -->' "$1" "$2"; }
189
+ synth9() { printf '<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->\nReviewed-SHA: %s\n\nsynth' "$1"; }
190
+ carried9() { printf '<!-- FORGE:REVIEW-CARRIED -->\nReviewed-SHA: %s\nCarried-From-SHA: %s\n' "$1" "$2"; }
191
+ cm9() { jq -n --arg b "$1" --arg t "${2:-Bot}" '{"body":$b,"author_association":"NONE","user":{"type":$t,"login":"x"}}'; }
192
+ panel9() { CM9=$(printf '%s\n' "$(cm9 "$(agent9 security "$1")")" "$(cm9 "$(agent9 api "$1")")" "$(cm9 "$(synth9 "$1")")" | jq -s '.'); }
193
+ credit9() { # args: head, labels-csv (default none); needs W9 CM9; prints "<DELTA_FULL>|<credited oids, short>"
194
+ local head="$1" labels="${2:-}" J
195
+ J=$(git -C "$W9" log --format=%H origin/main..HEAD | jq -R . | jq -s --arg h "$head" --arg l "$labels" '{headRefOid:$h,baseRefName:"main",mergeCommit:null,labels:($l|split(",")|map(select(.!="")|{name:.})),commits:(map({oid:.})|reverse)}')
196
+ ( cd "$W9"; pr=7; PR_NUMBER=9; GH_REPO=o/r; _SD="$TMP"; _HEAD="$head"; _J="$J"; _CM="$CM9"; TRUSTED_SCRIPT="${T9_TRUSTED:-$TC}"; REVIEW_DELTA_SCRIPT="${T9_RD-$RDS}"
197
+ . "$TMP/delta-fn.sh"; bundle_delta_credit; echo "$DELTA_FULL|$(for o in $DELTA_COMMITS; do printf '%s ' "${o:0:7}"; done)" )
198
+ }
199
+ short9() { git -C "$W9" rev-parse --short=7 "$1"; }
200
+ # stale-SHA PR, but no trusted panel: stays UNREVIEWED
201
+ mkrepo9; R=$(git -C "$W9" rev-parse HEAD); commit9 pr.txt pr2 pr2; H=$(git -C "$W9" rev-parse HEAD); CM9='[]'
202
+ [ "$(credit9 "$H")" = "0|" ] && ok || bad "stale head with no trusted panel must credit nothing (got '$(credit9 "$H")')"
203
+ # untrusted reviewer comments (User type): stays UNREVIEWED
204
+ CM9=$(printf '%s\n' "$(cm9 "$(agent9 security "$R")" User)" "$(cm9 "$(agent9 api "$R")" User)" "$(cm9 "$(synth9 "$R")" User)" | jq -s '.')
205
+ [ "$(credit9 "$H")" = "0|" ] && ok || bad "untrusted panel comments must credit nothing (got '$(credit9 "$H")')"
206
+ # resolver script missing / garbage / error: UNREVIEWED
207
+ panel9 "$R"
208
+ [ "$(T9_RD="" credit9 "$H")" = "0|" ] && ok || bad "unresolved review-delta.sh must credit nothing"
209
+ printf '#!/bin/sh\necho "DELTA garbage"\necho FULL_ROUNDS=1\n' > "$TMP/rd-garbage.sh"
210
+ [ "$(T9_RD="$TMP/rd-garbage.sh" credit9 "$H")" = "0|" ] && ok || bad "malformed resolver output must credit nothing"
211
+ printf '#!/bin/sh\nexit 3\n' > "$TMP/rd-error.sh"
212
+ [ "$(T9_RD="$TMP/rd-error.sh" credit9 "$H")" = "0|" ] && ok || bad "a failing resolver must credit nothing"
213
+ printf '#!/bin/sh\necho FULL\necho FULL_ROUNDS=0\n' > "$TMP/rd-full.sh"
214
+ [ "$(T9_RD="$TMP/rd-full.sh" credit9 "$H")" = "0|" ] && ok || bad "resolver FULL must credit nothing"
215
+ printf '#!/bin/sh\necho "DELTA %s..%s"\necho FULL_ROUNDS=1\n' "$H" "$H" > "$TMP/rd-same.sh"
216
+ [ "$(T9_RD="$TMP/rd-same.sh" credit9 "$H")" = "0|" ] && ok || bad "a resolver DELTA whose from equals head must credit nothing"
217
+ printf '#!/bin/sh\necho "DELTA %s..%s"\necho FULL_ROUNDS=1\n' "$R" "$R" > "$TMP/rd-wrongto.sh"
218
+ [ "$(T9_RD="$TMP/rd-wrongto.sh" credit9 "$H")" = "0|" ] && ok || bad "a resolver DELTA whose to is not the PR head must credit nothing"
219
+ # degraded label: the resolver forces FULL
220
+ [ "$(credit9 "$H" review-degraded)" = "0|" ] && ok || bad "a review-degraded PR must credit nothing"
221
+ # force-pushed PR (reviewed sha no longer an ancestor): all UNREVIEWED
222
+ mkrepo9; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"; g9 reset -q --hard HEAD~1; commit9 pr.txt rewritten rewritten; H=$(git -C "$W9" rev-parse HEAD)
223
+ [ "$(credit9 "$H")" = "0|" ] && ok || bad "force-pushed PR must keep every commit UNREVIEWED (got '$(credit9 "$H")')"
224
+ # resolver says DELTA from a sha that is not an ancestor of the PR commits (stub): per-commit ancestor test credits nothing
225
+ mkrepo9; R=$(git -C "$W9" rev-parse HEAD); commit9 pr.txt pr2 pr2; H=$(git -C "$W9" rev-parse HEAD); panel9 "$R"
226
+ g9 checkout -q -b other main; commit9 o.txt o other; O=$(git -C "$W9" rev-parse HEAD); g9 checkout -q feat
227
+ printf '#!/bin/sh\necho "DELTA %s..%s"\necho FULL_ROUNDS=1\n' "$O" "$H" > "$TMP/rd-unrelated.sh"
228
+ [ "$(T9_RD="$TMP/rd-unrelated.sh" credit9 "$H")" = "0|" ] && ok || bad "a DELTA from an unrelated sha must credit no PR commit"
229
+ # carried base-sync: untrusted / absent record stays UNREVIEWED
230
+ mkrepo9; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"; g9 checkout -q main; commit9 b.txt b1 b1; g9 push -q origin main; g9 checkout -q feat; g9 fetch -q origin
231
+ g9 merge -q --no-ff -m "merge main" origin/main >/dev/null 2>&1; H=$(git -C "$W9" rev-parse HEAD)
232
+ PANEL9=$CM9
233
+ [ "$(credit9 "$H")" = "0|" ] && ok || bad "BASE_SYNC_ONLY with no carried record must credit nothing (got '$(credit9 "$H")')"
234
+ CM9=$(printf '%s\n%s\n' "$(printf '%s' "$PANEL9" | jq -c '.[]')" "$(cm9 "$(carried9 "$H" "$R")" User)" | jq -s '.')
235
+ [ "$(credit9 "$H")" = "0|" ] && ok || bad "an untrusted REVIEW-CARRIED record must credit nothing (got '$(credit9 "$H")')"
236
+ CM9=$(printf '%s\n%s\n' "$(printf '%s' "$PANEL9" | jq -c '.[]')" "$(cm9 "$(carried9 "$H" "$(printf 'c%.0s' $(seq 1 40))")")" | jq -s '.')
237
+ [ "$(credit9 "$H")" = "0|" ] && ok || bad "a carried record naming another reviewed sha must credit nothing"
238
+ # --- happy paths
239
+ CM9=$(printf '%s\n%s\n' "$(printf '%s' "$PANEL9" | jq -c '.[]')" "$(cm9 "$(carried9 "$H" "$R")")" | jq -s '.')
240
+ [ "$(credit9 "$H")" = "1|" ] && ok || bad "carried base-sync with a trusted record must credit the whole PR (got '$(credit9 "$H")')"
241
+ # stale-SHA PR: older trusted complete panel at R, one real new commit -> only the old commit is credited
242
+ mkrepo9; R=$(git -C "$W9" rev-parse HEAD); R7=$(short9 "$R"); commit9 pr.txt pr2 pr2; H=$(git -C "$W9" rev-parse HEAD); H7=$(short9 "$H"); panel9 "$R"
243
+ OUT9=$(credit9 "$H")
244
+ [ "$OUT9" = "0|$R7 " ] && ok || bad "stale-SHA PR must credit only commits up to the reviewed sha, not the new one (got '$OUT9', new commit $H7)"
245
+ # partially reviewed PR: two commits reviewed, two new -> only the reviewed two credited
246
+ mkrepo9; commit9 pr.txt pr1b pr1b; R=$(git -C "$W9" rev-parse HEAD); panel9 "$R"; commit9 pr.txt pr3 pr3; commit9 pr.txt pr4 pr4; H=$(git -C "$W9" rev-parse HEAD)
247
+ OUT9=$(credit9 "$H"); N9=$(printf '%s' "${OUT9#*|}" | wc -w)
248
+ [ "${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')"
249
+ # spec text guards: the resolver is reused, never copied; carried record accepted; the unchanged cross-PR and merge-resolution paths are still present
250
+ grep -q 'review-delta.sh' "$SPEC" && grep -q 'REVIEW-CARRIED' "$SPEC" && ok || bad "spec must call review-delta.sh and accept the REVIEW-CARRIED record"
251
+ 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
252
+ grep -qF 'printf '"'"'%s\n'"'"' "$_FILES" | sort -u | sed "s|^|$pr |" >> "$_PF"' "$SPEC" && grep -qF 'git diff-tree --no-commit-id --name-only -r --cc "$c"' "$SPEC" && ok || bad "CROSS_PR_FILES accumulation and --cc merge scoping must stay intact"
253
+ [ "$(grep -c '# FENCE_BOOTSTRAP' "$SPEC" | head -1)" -ge 8 ] && ok || bad "FENCE_BOOTSTRAP lines must remain"
254
+ # a stale-head PR must not credit its own merge commit (it contains the post-review commits): the DELTA branch only appends DELTA_COMMITS
255
+ if awk '/^bundle_delta_credit\(\)/,/^\}/' "$SPEC" | grep -q 'mergeCommit'; then bad "the DELTA path must not credit the PR merge commit"; else ok; fi
256
+ else echo "review-pr-staging-scope.test.sh: git missing, delta section skipped"; fi
158
257
  echo "review-pr-staging-scope.test.sh: passed=$PASS failed=$FAILN"
159
258
  [ "$FAILN" -eq 0 ]