forgedock 1.7.0 → 1.8.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.
Files changed (49) hide show
  1. package/README.md +2 -1
  2. package/bin/engine/admission.mjs +242 -7
  3. package/bin/engine/admission.test.mjs +183 -2
  4. package/bin/engine/phases.mjs +31 -22
  5. package/bin/engine/resolve.mjs +8 -1
  6. package/bin/engine/resolve.test.mjs +5 -0
  7. package/bin/engine-cli.mjs +2 -0
  8. package/bin/engine.mjs +26 -17
  9. package/bin/forgedock.mjs +124 -48
  10. package/bin/hooks/pre-tool-use.mjs +573 -2
  11. package/bin/journey.mjs +11 -7
  12. package/bin/labels.json +5 -0
  13. package/bin/opencode-adapter.mjs +652 -92
  14. package/bin/orchestrate-preflight.mjs +434 -0
  15. package/bin/runner.mjs +77 -9
  16. package/commands/audit-agents.md +75 -26
  17. package/commands/autopilot.md +4 -4
  18. package/commands/cleanup.md +10 -5
  19. package/commands/issue.md +95 -35
  20. package/commands/orchestrate/phase-1-resolve.md +70 -32
  21. package/commands/orchestrate/phase-3-dependency.md +167 -22
  22. package/commands/orchestrate/phase-4-execution.md +691 -62
  23. package/commands/orchestrate/phase-5-cleanup.md +5 -11
  24. package/commands/orchestrate/phase-6-report.md +27 -1
  25. package/commands/orchestrate/safety.md +1 -1
  26. package/commands/orchestrate.md +39 -1
  27. package/commands/quality-gate.md +30 -10
  28. package/commands/review-pr-agents/protocols.md +13 -0
  29. package/commands/review-pr-agents.md +8 -0
  30. package/commands/review-pr-staging.md +117 -17
  31. package/commands/review-pr.md +95 -23
  32. package/commands/work-on/build/validate.md +25 -4
  33. package/commands/work-on/close.md +40 -10
  34. package/commands/work-on/decompose.md +33 -1
  35. package/commands/work-on/investigate.md +33 -0
  36. package/commands/work-on/remediate.md +40 -5
  37. package/commands/work-on/review.md +27 -0
  38. package/commands/work-on.md +48 -5
  39. package/docs/CONFIG.md +51 -15
  40. package/docs/OPENCODE.md +128 -25
  41. package/docs/spec/review-protocol.md +7 -0
  42. package/package.json +1 -1
  43. package/scripts/check-admission-parity.mjs +1 -0
  44. package/scripts/check-command-side-effects.sh +14 -13
  45. package/scripts/check-shell-portability.sh +36 -0
  46. package/scripts/check-spec-markers.sh +21 -11
  47. package/scripts/checkbox-sections.test.sh +67 -0
  48. package/scripts/extract-affected-files.sh +85 -5
  49. package/scripts/extract-affected-files.test.sh +154 -2
@@ -87,9 +87,11 @@ MARKER_REGISTRY="
87
87
  BATCH_MEMBERS
88
88
  BENCH_SCORECARD
89
89
  BLOCKED_ON_HUMAN_MERGE
90
+ BODY
90
91
  BUILDER
91
92
  CALIBRATION_CHECK
92
93
  CARD
94
+ CLASS
93
95
  CHECKPOINT
94
96
  CLAIM
95
97
  CLAIM_RELEASED
@@ -100,6 +102,7 @@ MARKER_REGISTRY="
100
102
  DECISION_RECORD
101
103
  DECOMPOSED
102
104
  DESIGN
105
+ DISPATCH
103
106
  DISPATCHER
104
107
  DOSSIER_UPDATED
105
108
  ENGINE_FALLBACK
@@ -187,28 +190,29 @@ while IFS= read -r file; do
187
190
  content="${markerline#*:}"
188
191
 
189
192
  # Skip allowlisted lines
190
- if echo "$content" | grep -qF "$ALLOWLIST_TOKEN"; then
193
+ if [[ "$content" == *"$ALLOWLIST_TOKEN"* ]]; then
191
194
  continue
192
195
  fi
193
196
 
194
197
  # Skip markers that are inside code spans (backtick-quoted) — those are docs examples
195
198
  # Heuristic: if the FORGE:TYPE appears between backticks on the same line, skip it
196
199
  # We check for backtick on either side of the FORGE: marker
197
- if echo "$content" | grep -qE "\`[^\`]*FORGE:[A-Z_]"; then
200
+ if [[ "$content" =~ \`[^\`]*FORGE:[A-Z_] ]]; then
198
201
  continue
199
202
  fi
200
203
 
201
204
  # Extract the full marker string after FORGE: — everything up to a space, -->, or end
202
205
  # This captures compound markers like REVIEW-AGENT:material-change
203
- full_marker=$(echo "$content" | grep -oE 'FORGE:[A-Z][A-Z0-9_:-]*' | head -1 | sed 's/^FORGE://')
206
+ [[ "$content" =~ FORGE:([A-Z][A-Z0-9_:-]*) ]] || continue
207
+ full_marker="${BASH_REMATCH[1]}"
204
208
 
205
209
  [ -z "$full_marker" ] && continue
206
210
 
207
211
  # Extract PRIMARY type: part before the first - or : separator
208
- primary_type=$(echo "$full_marker" | sed 's/[-:].*//')
212
+ primary_type="${full_marker%%[-:]*}"
209
213
 
210
214
  # Validate primary type against registry
211
- if ! echo "$primary_type" | grep -qE "$REGISTRY_PATTERN"; then
215
+ if ! [[ "$primary_type" =~ $REGISTRY_PATTERN ]]; then
212
216
  echo "HIGH | $file:$lineno | unknown FORGE: marker 'FORGE:${full_marker}' (primary type '${primary_type}') — not in registry" >&2
213
217
  VIOLATIONS=$((VIOLATIONS + 1))
214
218
  fi
@@ -242,21 +246,27 @@ if [ -d "$WORKFLOWS_DIR" ]; then
242
246
  LINENO=$((LINENO + 1))
243
247
 
244
248
  # Skip allowlisted lines
245
- if echo "$line" | grep -qF "$ALLOWLIST_TOKEN"; then
249
+ if [[ "$line" == *"$ALLOWLIST_TOKEN"* ]]; then
246
250
  continue
247
251
  fi
248
252
 
249
253
  # Skip comment lines
250
- if echo "$line" | grep -qE '^[[:space:]]*#'; then
254
+ if [[ "$line" =~ ^[[:space:]]*# ]]; then
251
255
  continue
252
256
  fi
253
257
 
254
258
  # Strip bash ${VAR} and $VAR patterns before checking for template placeholders
255
259
  # so that ELAPSED in "${ELAPSED}s" doesn't false-positive as {ELAPSED}
256
- stripped=$(echo "$line" | sed 's/\${\([^}]*\)}/BASH_VAR/g; s/\$[A-Za-z_][A-Za-z0-9_]*/BASH_VAR/g')
257
-
258
- if echo "$stripped" | grep -qE "$PLACEHOLDER_PATTERN"; then
259
- placeholder=$(echo "$stripped" | grep -oE "$PLACEHOLDER_PATTERN" | head -1)
260
+ stripped="$line"
261
+ while [[ "$stripped" =~ \$\{[^}]*\} ]]; do
262
+ stripped="${stripped/"${BASH_REMATCH[0]}"/BASH_VAR}"
263
+ done
264
+ while [[ "$stripped" =~ \$[A-Za-z_][A-Za-z0-9_]* ]]; do
265
+ stripped="${stripped/"${BASH_REMATCH[0]}"/BASH_VAR}"
266
+ done
267
+
268
+ if [[ "$stripped" =~ $PLACEHOLDER_PATTERN ]]; then
269
+ placeholder="${BASH_REMATCH[0]}"
260
270
  echo "HIGH | $wf_file:$LINENO | unsubstituted placeholder '$placeholder' in workflow" >&2
261
271
  VIOLATIONS=$((VIOLATIONS + 1))
262
272
  fi
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # checkbox-sections.test.sh — Regression tests for the close-path classifier.
3
+ #
4
+ # Usage: bash scripts/checkbox-sections.test.sh
5
+
6
+ set -euo pipefail
7
+
8
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
9
+
10
+ extract_block() {
11
+ awk '/^FENCE_COUNT=/{printing=1} printing{print} /^SUBISSUE_ITEMS=/{exit}' "$1"
12
+ }
13
+
14
+ extract_classifier() {
15
+ awk '
16
+ /^CHECKBOX_SECTIONS=.*awk .$/ { printing=1; next }
17
+ printing && /^\047\)$/ { exit }
18
+ printing { print }
19
+ ' "$1"
20
+ }
21
+
22
+ CLOSE_BLOCK=$(extract_block "$ROOT/commands/work-on/close.md")
23
+ INLINE_BLOCK=$(extract_block "$ROOT/commands/work-on.md")
24
+ [[ "$CLOSE_BLOCK" == "$INLINE_BLOCK" ]] || {
25
+ printf 'FAIL: mirrored classifier blocks differ\n' >&2
26
+ exit 1
27
+ }
28
+
29
+ CLOSE_CLASSIFIER=$(extract_classifier "$ROOT/commands/work-on/close.md")
30
+ INLINE_CLASSIFIER=$(extract_classifier "$ROOT/commands/work-on.md")
31
+ [[ "$CLOSE_CLASSIFIER" == "$INLINE_CLASSIFIER" ]] || {
32
+ printf 'FAIL: mirrored classifier implementations differ\n' >&2
33
+ exit 1
34
+ }
35
+
36
+ if grep -q 'echo "\$BODY\|echo "\$BODY_STRIPPED' \
37
+ "$ROOT/commands/work-on/close.md" "$ROOT/commands/work-on.md"; then
38
+ printf 'FAIL: arbitrary issue bodies must be piped with printf, not echo\n' >&2
39
+ exit 1
40
+ fi
41
+
42
+ grep -Fq 'Keep the structural computation above and its consuming multi-phase guard' "$ROOT/commands/work-on.md" || {
43
+ printf 'FAIL: Phase 6A sync invariant must cover the consuming guard\n' >&2
44
+ exit 1
45
+ }
46
+ grep -Fq 'The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`' "$ROOT/commands/work-on.md" || {
47
+ printf 'FAIL: Phase 6A must document the Phase C1 guard\n' >&2
48
+ exit 1
49
+ }
50
+ grep -Fq 'if [ "${CHECKBOX_SECTIONS:-0}" -ge 2 ] || [ "${SUBISSUE_ITEMS:-0}" -gt 0 ]; then' "$ROOT/commands/work-on/close.md" || {
51
+ printf 'FAIL: Phase C1 multi-phase guard changed unexpectedly\n' >&2
52
+ exit 1
53
+ }
54
+
55
+ SETEXT_BODY=$'Phase One\n=========\n- [x] complete\n\nPhase Two\n---------\n- [ ] remaining'
56
+ [[ "$(printf '%s\n' "$SETEXT_BODY" | awk "$CLOSE_CLASSIFIER")" == "2" ]] || {
57
+ printf 'FAIL: setext phases must count as two sections\n' >&2
58
+ exit 1
59
+ }
60
+
61
+ SINGLE_BODY=$'## Acceptance Criteria\n- [ ] complete this work'
62
+ [[ "$(printf '%s\n' "$SINGLE_BODY" | awk "$CLOSE_CLASSIFIER")" == "1" ]] || {
63
+ printf 'FAIL: one checkbox-bearing section must remain single-phase\n' >&2
64
+ exit 1
65
+ }
66
+
67
+ printf 'PASS: mirrored classifier supports setext section boundaries\n'
@@ -7,7 +7,7 @@
7
7
  # extract-affected-files.sh <issue_number> "-R <owner/repo>" (single pre-joined token also accepted)
8
8
  #
9
9
  # Output (stdout):
10
- # Line 1: PROVENANCE=affected-files-section | body-fallback | none | error
10
+ # Line 1: PROVENANCE=contract-deliverables | affected-files-section | body-fallback | none | error
11
11
  # Line 2+: one extracted file path per line (zero lines when PROVENANCE=none or error)
12
12
  #
13
13
  # Exit codes:
@@ -16,12 +16,39 @@
16
16
  # that files were found).
17
17
  # 2 — Usage error (missing issue number, malformed -R value).
18
18
  #
19
- # Extraction rules (forge#2436):
19
+ # Extraction rules (forge#2436, extended forge#2848):
20
+ # 0. Highest-confidence path — the issue's FORGE:CONTRACT comment (if one exists),
21
+ # scoped to ONLY its own "### Deliverables" section. A contract deliverables table
22
+ # states *intent to change* ("I will edit this file"), whereas an investigation's
23
+ # Affected Files list and especially a raw issue body routinely name files as
24
+ # *context* ("this interacts with X", "similar to the check in Y"). Preferring the
25
+ # contract is what stops a cohort of issues that merely cite the same file from
26
+ # being serialized against each other (forge#2848).
27
+ # -> PROVENANCE=contract-deliverables
28
+ #
29
+ # TEMPORAL CAVEAT — do not over-claim this source: FORGE:CONTRACT is posted at
30
+ # *build* time (work-on/build.md Phase B2), which is AFTER /orchestrate Phase 3
31
+ # builds the DAG. On a cold first-pass plan no contract exists yet, so this path is
32
+ # inert by construction. It pays off only on the paths that re-extract later: wake /
33
+ # re-plan after compaction, mid-batch re-derivation (phase-4-execution.md's DONE-arm
34
+ # DROP handling), and IN_PROGRESS predecessors built in an earlier wave or session.
35
+ #
36
+ # FALL-THROUGH RULE (load-bearing — see forge#2848 risk table): a contract that
37
+ # exists but whose Deliverables section yields ZERO paths MUST fall through to the
38
+ # INVESTIGATOR path below. A contract is an *upgrade* over the investigator source,
39
+ # never a replacement for it — blackholing to `none` here would fire Layer 4's
40
+ # conservative serialization for exactly the mid-batch issues this change exists to
41
+ # un-serialize, i.e. the bug being fixed, inverted. Note this is deliberately NOT
42
+ # the same shape as the INVESTIGATOR->body relationship in rule 2, which does NOT
43
+ # fall through on zero files (see rule 2's own note, forge#2382).
20
44
  # 1. Primary path — the issue's FORGE:INVESTIGATOR comment (if one exists), scoped to
21
45
  # ONLY its own "### Affected Files" section. Capture stops at the next markdown
22
46
  # heading of any level, so paths mentioned in "### Evidence", "### Root Cause",
23
47
  # "### Related Issues", etc. are never collected.
24
48
  # -> PROVENANCE=affected-files-section
49
+ # NOTE (forge#2382): unlike rule 0, an INVESTIGATOR comment that EXISTS but yields
50
+ # zero paths does NOT fall through to rule 2 — an investigation that scoped its own
51
+ # Affected Files section to nothing is a confirmed-empty result, not a missing one.
25
52
  # 2. Fallback path — used ONLY when no FORGE:INVESTIGATOR comment exists at all.
26
53
  # Scoped to a deliverables-shaped heading in the raw issue body:
27
54
  # "## Affected Files", "## Deliverables", or "### Files to change". Capture stops
@@ -126,6 +153,20 @@ fi
126
153
  # deeper sub-heading inside a deliverables section still closes capture
127
154
  # rather than leaking into unrelated prose.
128
155
  # --------------------------------------------------------------------------- #
156
+ extract_contract_section() {
157
+ # forge#2848 — same sentinel shape as the two helpers below, targeting the
158
+ # FORGE:CONTRACT comment's "### Deliverables" table. The `/^### Deliverables/`
159
+ # heading and the awk sentinel are the ones already proven in-tree at
160
+ # commands/work-on/build.md:218 and :258; only the path-extraction step differs
161
+ # (this script uses grep -oE via extract_paths, never build.md's grep -oP —
162
+ # forge#2436 removed the PCRE dependency on purpose, see extract_paths below).
163
+ awk '
164
+ /^### Deliverables/ { p=1; next }
165
+ /^#/ { p=0 }
166
+ p { print }
167
+ ' <<< "$1"
168
+ }
169
+
129
170
  extract_investigator_section() {
130
171
  awk '
131
172
  /^### Affected Files/ { p=1; next }
@@ -152,13 +193,49 @@ extract_paths() {
152
193
  grep -oE "$EXT_REGEX" <<< "$1" 2>/dev/null | tr -d '`' | sort -u || true
153
194
  }
154
195
 
155
- # --------------------------------------------------------------------------- #
156
- # Primary path: FORGE:INVESTIGATOR comment, scoped to its own Affected Files section
157
- # --------------------------------------------------------------------------- #
158
196
  PROVENANCE="none"
159
197
  FILES=""
160
198
  GH_CALL_FAILED=0
161
199
 
200
+ # --------------------------------------------------------------------------- #
201
+ # Highest-confidence path: FORGE:CONTRACT comment, scoped to its own Deliverables
202
+ # section (forge#2848). Runs AHEAD of the INVESTIGATOR path because a deliverables
203
+ # table states intent to change, while an Affected Files list may name files that
204
+ # are merely relevant. See rule 0 in the header for the temporal caveat (a contract
205
+ # only exists post-build, so this is inert on a cold first-pass plan).
206
+ #
207
+ # Uses the same `if ! VAR=$(cmd); then GH_CALL_FAILED=1; fi` idiom as the calls
208
+ # below — never `|| true` — so a failed contract fetch stays distinguishable from
209
+ # "no contract exists" and can still escalate to PROVENANCE=error if nothing else
210
+ # recovers real data (forge#2504).
211
+ # --------------------------------------------------------------------------- #
212
+ if ! CONTRACT_BODY=$(gh api "repos/${REPO}/issues/${NUM}/comments" \
213
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | last | .body // ""' 2>/dev/null); then
214
+ GH_CALL_FAILED=1
215
+ echo "WARNING: gh api call failed while fetching comments for issue #$NUM (repo: $REPO) — cannot confirm whether a FORGE:CONTRACT comment exists; treating as inconclusive, not empty" >&2
216
+ CONTRACT_BODY=""
217
+ fi
218
+
219
+ if [[ -n "$CONTRACT_BODY" ]]; then
220
+ SCOPED=$(extract_contract_section "$CONTRACT_BODY")
221
+ FILES=$(extract_paths "$SCOPED")
222
+ if [[ -n "$FILES" ]]; then
223
+ PROVENANCE="contract-deliverables"
224
+ fi
225
+ fi
226
+
227
+ # --------------------------------------------------------------------------- #
228
+ # Primary path: FORGE:INVESTIGATOR comment, scoped to its own Affected Files section
229
+ #
230
+ # Guarded on `-z "$FILES"` so the contract path above FALLS THROUGH when it yielded
231
+ # zero paths (no contract, no Deliverables section, or a deliverables table naming no
232
+ # recognized file extension). The guard wraps the whole INVESTIGATOR/body block from
233
+ # the outside, leaving the INVESTIGATOR->body-fallback relationship inside it
234
+ # byte-identical — that inner relationship deliberately does NOT fall through on zero
235
+ # files (forge#2382), and this change must not alter it.
236
+ # --------------------------------------------------------------------------- #
237
+ if [[ -z "$FILES" ]]; then
238
+
162
239
  # NOTE: deliberately no `| tail -1` here — `gh api --jq`'s raw-string output
163
240
  # for a multi-line `.body` field embeds literal newlines, so isolating "the
164
241
  # last comment" by taking the last output LINE would instead truncate to
@@ -215,6 +292,9 @@ else
215
292
  fi
216
293
  fi
217
294
  fi
295
+ fi # end contract fall-through guard (forge#2848) — the INVESTIGATOR/body block
296
+ # above is intentionally left un-indented so it stays byte-identical to its
297
+ # pre-forge#2848 form; only the surrounding guard is new.
218
298
 
219
299
  # forge#2504: only escalate to "error" when neither path yielded real data AND
220
300
  # at least one gh call genuinely failed along the way. A failed primary call
@@ -28,18 +28,38 @@ trap cleanup EXIT
28
28
 
29
29
  # --------------------------------------------------------------------------- #
30
30
  # Mock `gh` — intercepts:
31
- # gh api repos/<repo>/issues/<num>/comments --jq '...' -> cat $MOCK_GH_COMMENTS (if set+exists, else empty)
32
- # gh issue view <num> -R <repo> --json body --jq '...' -> cat $MOCK_GH_BODY (if set+exists, else empty)
31
+ # gh api .../comments --jq '...FORGE:CONTRACT...' -> cat $MOCK_GH_CONTRACT (if set+exists, else empty)
32
+ # gh api .../comments --jq '...FORGE:INVESTIGATOR...' -> cat $MOCK_GH_COMMENTS (if set+exists, else empty)
33
+ # gh issue view <num> -R <repo> --json body --jq '...' -> cat $MOCK_GH_BODY (if set+exists, else empty)
33
34
  # Any other invocation is a test setup error.
34
35
  #
36
+ # forge#2848: the two `gh api` call sites hit the same endpoint and differ only in
37
+ # their --jq selector, so the mock dispatches on which FORGE marker the selector
38
+ # names. This keeps the contract source and the investigator source independently
39
+ # fixturable — required to assert the contract-beats-investigator ordering rule.
40
+ #
35
41
  # forge#2504: MOCK_GH_COMMENTS_FAIL=1 / MOCK_GH_BODY_FAIL=1 force the respective
36
42
  # call site to simulate a genuine `gh` failure (non-zero exit, nothing on
37
43
  # stdout) instead of a "call succeeded, empty/no-match result" — this is the
38
44
  # distinction extract-affected-files.sh must now surface as PROVENANCE=error.
45
+ # MOCK_GH_CONTRACT_FAIL=1 does the same for the contract fetch alone; note that
46
+ # MOCK_GH_COMMENTS_FAIL=1 fails BOTH `gh api` call sites, which is the faithful
47
+ # simulation of a real network/auth/rate-limit outage (a real outage does not
48
+ # selectively spare one of two calls to the same endpoint).
39
49
  # --------------------------------------------------------------------------- #
40
50
  cat > "$TMP_BIN/gh" <<'MOCK'
41
51
  #!/usr/bin/env bash
42
52
  if [[ "$1" == "api" ]]; then
53
+ if [[ "$*" == *"FORGE:CONTRACT"* ]]; then
54
+ if [[ "${MOCK_GH_CONTRACT_FAIL:-}" == "1" || "${MOCK_GH_COMMENTS_FAIL:-}" == "1" ]]; then
55
+ echo "mock gh api: simulated failure (network error / rate limit)" >&2
56
+ exit 1
57
+ fi
58
+ if [[ -n "${MOCK_GH_CONTRACT:-}" && -f "${MOCK_GH_CONTRACT:-}" ]]; then
59
+ cat "$MOCK_GH_CONTRACT"
60
+ fi
61
+ exit 0
62
+ fi
43
63
  if [[ "${MOCK_GH_COMMENTS_FAIL:-}" == "1" ]]; then
44
64
  echo "mock gh api: simulated failure (network error / rate limit)" >&2
45
65
  exit 1
@@ -320,6 +340,138 @@ MOCK_GH_COMMENTS_FAIL=1 MOCK_GH_BODY="$BODY_SCOPED" assert_output \
320
340
  "forge#2504: primary gh api call fails but fallback succeeds with real files -> body-fallback, not error" \
321
341
  "body-fallback" "bin/engine.mjs,bin/engine/phases.mjs" 2506 -R test/repo
322
342
 
343
+ # --------------------------------------------------------------------------- #
344
+ # Scenario 12 (forge#2848): FORGE:CONTRACT comment present with a real
345
+ # "### Deliverables" table -> PROVENANCE=contract-deliverables. Paths named in
346
+ # the contract's OTHER sections ("### Proposed Approach", "### Out of Scope")
347
+ # must be ignored — same heading-scoping guarantee the investigator path has.
348
+ # --------------------------------------------------------------------------- #
349
+ CONTRACT_FULL="$TMP_FIXTURES/contract_full.txt"
350
+ cat > "$CONTRACT_FULL" <<'EOF'
351
+ <!-- FORGE:CONTRACT -->
352
+ ## Builder Contract
353
+
354
+ **Task type**: Bug Fix
355
+
356
+ ### Proposed Approach
357
+
358
+ Mirrors the approach already taken in `bin/approach-only-context.mjs`.
359
+
360
+ ### Deliverables
361
+
362
+ | File | Change | Why |
363
+ |------|--------|-----|
364
+ | `scripts/real-target.sh` | rewrite the parser | the actual defect |
365
+ | `bin/real-target.mjs` | update the caller | keeps the pair in sync |
366
+
367
+ ### Out of Scope
368
+
369
+ Anything under `bin/out-of-scope-file.mjs`.
370
+ EOF
371
+
372
+ unset MOCK_GH_COMMENTS MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
373
+ MOCK_GH_CONTRACT="$CONTRACT_FULL" assert_output \
374
+ "forge#2848: FORGE:CONTRACT ### Deliverables -> contract-deliverables, ignores Approach/Out-of-Scope paths" \
375
+ "contract-deliverables" "bin/real-target.mjs,scripts/real-target.sh" 2848 -R test/repo
376
+
377
+ # --------------------------------------------------------------------------- #
378
+ # Scenario 13 (forge#2848 ORDERING rule): both a FORGE:CONTRACT and a
379
+ # FORGE:INVESTIGATOR comment exist, and they name DIFFERENT files. The contract
380
+ # must win — a deliverables table states intent to change, while an Affected
381
+ # Files list may name files that are merely relevant. This is the assertion that
382
+ # pins the provenance precedence order; without it the two sources could silently
383
+ # swap with no test failure.
384
+ # --------------------------------------------------------------------------- #
385
+ INV_DIFFERENT="$TMP_FIXTURES/inv_different.txt"
386
+ cat > "$INV_DIFFERENT" <<'EOF'
387
+ <!-- FORGE:INVESTIGATOR -->
388
+ ## Investigation Report
389
+
390
+ ### Affected Files
391
+
392
+ 1. `scripts/investigator-guess.sh` — suspected
393
+ 2. `bin/investigator-guess.mjs` — suspected
394
+ EOF
395
+
396
+ unset MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
397
+ MOCK_GH_CONTRACT="$CONTRACT_FULL" MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
398
+ "forge#2848 ordering: contract beats investigator when both exist and disagree" \
399
+ "contract-deliverables" "bin/real-target.mjs,scripts/real-target.sh" 2849 -R test/repo
400
+
401
+ # --------------------------------------------------------------------------- #
402
+ # Scenario 14 (forge#2848 FALL-THROUGH rule — load-bearing): a FORGE:CONTRACT
403
+ # exists but yields ZERO paths. It must fall through to the INVESTIGATOR path,
404
+ # NOT blackhole to PROVENANCE=none. Blackholing here would fire Layer 4's
405
+ # conservative serialization for exactly the mid-batch issues this change exists
406
+ # to un-serialize (the bug being fixed, inverted — see the forge#2848 risk table).
407
+ # Two zero-yield shapes are covered: no "### Deliverables" heading at all, and a
408
+ # Deliverables section that names no recognized file path.
409
+ # --------------------------------------------------------------------------- #
410
+ CONTRACT_NO_DELIV="$TMP_FIXTURES/contract_no_deliv.txt"
411
+ cat > "$CONTRACT_NO_DELIV" <<'EOF'
412
+ <!-- FORGE:CONTRACT -->
413
+ ## Builder Contract
414
+
415
+ ### Proposed Approach
416
+
417
+ Prose only, mentioning `bin/should-not-be-extracted.mjs` as context.
418
+
419
+ ### Out of Scope
420
+
421
+ Everything else.
422
+ EOF
423
+
424
+ unset MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
425
+ MOCK_GH_CONTRACT="$CONTRACT_NO_DELIV" MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
426
+ "forge#2848 fall-through: contract with no ### Deliverables section -> falls through to affected-files-section" \
427
+ "affected-files-section" "bin/investigator-guess.mjs,scripts/investigator-guess.sh" 2850 -R test/repo
428
+
429
+ CONTRACT_EMPTY_DELIV="$TMP_FIXTURES/contract_empty_deliv.txt"
430
+ cat > "$CONTRACT_EMPTY_DELIV" <<'EOF'
431
+ <!-- FORGE:CONTRACT -->
432
+ ## Builder Contract
433
+
434
+ ### Deliverables
435
+
436
+ | File | Change | Why |
437
+ |------|--------|-----|
438
+ | (documentation-only, no code paths) | n/a | n/a |
439
+
440
+ ### Out of Scope
441
+
442
+ Everything else.
443
+ EOF
444
+
445
+ unset MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
446
+ MOCK_GH_CONTRACT="$CONTRACT_EMPTY_DELIV" MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
447
+ "forge#2848 fall-through: contract Deliverables naming no recognized path -> falls through to affected-files-section" \
448
+ "affected-files-section" "bin/investigator-guess.mjs,scripts/investigator-guess.sh" 2851 -R test/repo
449
+
450
+ # --------------------------------------------------------------------------- #
451
+ # Scenario 15 (forge#2848 x forge#2504): the CONTRACT fetch specifically fails
452
+ # while every other source is legitimately empty. Must yield PROVENANCE=error,
453
+ # not none — the new call site doubles the gh failure surface, so its own
454
+ # GH_CALL_FAILED wiring needs its own assertion rather than riding on the
455
+ # investigator call's coverage.
456
+ # --------------------------------------------------------------------------- #
457
+ unset MOCK_GH_CONTRACT MOCK_GH_COMMENTS MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
458
+ MOCK_GH_CONTRACT_FAIL=1 assert_output \
459
+ "forge#2848: contract gh api call fails, no other usable source -> PROVENANCE=error, not none" \
460
+ "error" "" 2852 -R test/repo
461
+
462
+ # --------------------------------------------------------------------------- #
463
+ # Scenario 16 (forge#2848 x forge#2504): the CONTRACT fetch fails but the
464
+ # investigator path succeeds with real files. A gh failure must never suppress
465
+ # or overwrite data actually recovered by a subsequent successful call — same
466
+ # invariant Scenario 11 asserts for the primary/fallback pair.
467
+ # --------------------------------------------------------------------------- #
468
+ unset MOCK_GH_CONTRACT MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
469
+ MOCK_GH_CONTRACT_FAIL=1 MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
470
+ "forge#2848: contract fetch fails but investigator succeeds with real files -> affected-files-section, not error" \
471
+ "affected-files-section" "bin/investigator-guess.mjs,scripts/investigator-guess.sh" 2853 -R test/repo
472
+
473
+ unset MOCK_GH_CONTRACT_FAIL
474
+
323
475
  # --------------------------------------------------------------------------- #
324
476
  # Summary
325
477
  # --------------------------------------------------------------------------- #