forgedock 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +54 -4
- package/bin/engine/runlog.mjs +10 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +32 -2
- package/bin/engine.mjs +187 -10
- package/bin/runner.mjs +89 -13
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +136 -28
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +201 -40
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +31 -20
- package/commands/work-on/build/architect.md +54 -1
- package/commands/work-on/build/implement.md +28 -1
- package/commands/work-on/build/validate.md +321 -29
- package/commands/work-on/build.md +184 -17
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +43 -17
- package/commands/work-on/review.md +43 -22
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +93 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +29 -3
- package/package.json +2 -2
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/cap-findings.test.sh +2 -0
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-markers.sh +4 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +379 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +451 -0
- package/scripts/ci-local/scoping.mjs +531 -0
- package/scripts/ci-local.sh +98 -0
- package/scripts/ci-local.test.sh +1108 -0
- package/scripts/classify-finding.sh +33 -7
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +2 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/lib/telemetry.sh +3 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +294 -53
- package/scripts/review-finalize.test.sh +347 -9
- package/scripts/review-pr-delta-wiring.test.sh +13 -0
- package/scripts/review-prepare.sh +5 -3
- package/scripts/review-prepare.test.sh +18 -2
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +10 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +14 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +41 -3
- package/scripts/with-run-tmpdir.sh +20 -0
|
@@ -39,13 +39,15 @@ case "$a" in
|
|
|
39
39
|
"api --paginate repos/o/r/pulls/"*"/files"*) cat "$FIX/pr-files.txt" 2>/dev/null || exit 1 ;;
|
|
40
40
|
"api repos/o/r/pulls/"*"--jq .changed_files") cat "$FIX/changed-n" 2>/dev/null || exit 1 ;;
|
|
41
41
|
"api repos/o/r/pulls/"*"--jq .head.sha") cat "$FIX/head" ;;
|
|
42
|
+
"api repos/o/r/pulls/"*"--jq .base.sha") cat "$FIX/base-sha" 2>/dev/null || exit 1 ;;
|
|
42
43
|
"api repos/o/r/branches/"*"/protection"*) cat "$FIX/protection" 2>/dev/null || exit 1 ;;
|
|
43
44
|
"api repos/o/r/issues/"*"/comments --paginate --jq"*) cat "$FIX/route-bodies" 2>/dev/null; exit 0 ;;
|
|
44
45
|
"api repos/o/r/issues/"*"/comments --jq"*) cat "$FIX/investigator" 2>/dev/null; exit 0 ;;
|
|
45
46
|
"api repos/o/r/commits/"*) cat "$FIX/commit-sha" 2>/dev/null || exit 1 ;;
|
|
46
47
|
"api repos/o/r/compare/"*) cat "$FIX/compare-files" 2>/dev/null || exit 1 ;;
|
|
47
48
|
"pr comment"*) { printf '%s\n' '=====COMMENT====='; for x in "$@"; do last="$x"; done; printf '%s\n' "$last"; } >> "$FIX/posted.log"; exit 0 ;;
|
|
48
|
-
"issue
|
|
49
|
+
"issue view"*"--json state"*) [ -f "$FIX/issue-closed" ] && echo CLOSED || echo OPEN; exit 0 ;;
|
|
50
|
+
"issue comment"*) [ -f "$FIX/issue-comment-fail" ] && exit 1; printf 'ISSUE_COMMENT %s\n' "$3" >> "$FIX/issue-comments.log"; { for x in "$@"; do last="$x"; done; printf '%s\n' "$last"; } >> "$FIX/issue-bodies.log"; exit 0 ;;
|
|
49
51
|
"label create"*) exit 0 ;;
|
|
50
52
|
"pr view"*"headRefOid"*) cat "$FIX/head" ;;
|
|
51
53
|
"pr view"*"baseRefName"*) echo main ;;
|
|
@@ -184,6 +186,52 @@ run "$GOOD" extract; rc_is "no trusted bodies" "$RC" 3; has "no trusted bodies"
|
|
|
184
186
|
reset; run "$NOTRUST" extract; rc_is "trust script missing" "$RC" 3; has "trust script missing" "scripts/trusted-comments.sh unresolvable or PR comments unreadable; refusing to read clean (fail closed)"
|
|
185
187
|
reset; run "$GOOD" extract; rc_is "comments unreadable" "$RC" 3; has "comments unreadable" "refusing to read clean"
|
|
186
188
|
|
|
189
|
+
# a findings summary list above the per-finding sections must not hide the scenario (summary bullets never start the section)
|
|
190
|
+
reset
|
|
191
|
+
cm MEMBER User "$(agent_body bug-hunter "$SHA" 'Findings summary:
|
|
192
|
+
- BUG-1: first problem
|
|
193
|
+
- BUG-2: second problem
|
|
194
|
+
- BUG-3: third problem
|
|
195
|
+
|
|
196
|
+
### BUG-1 first problem
|
|
197
|
+
**Failure scenario**: one breaks
|
|
198
|
+
### BUG-2 second problem
|
|
199
|
+
**Failure scenario**: two breaks
|
|
200
|
+
### BUG-3 third problem
|
|
201
|
+
**Fix efficacy**: n/a
|
|
202
|
+
' '<!-- FINDING:BUG-1|CONFIRMED|MEDIUM|a.py:2|First problem alpha -->
|
|
203
|
+
<!-- FINDING:BUG-2|CONFIRMED|MEDIUM|b.py:10|Second problem bravo -->
|
|
204
|
+
<!-- FINDING:BUG-3|CONFIRMED|MEDIUM|c.py:18|Third problem charlie -->')"
|
|
205
|
+
flush; run "$GOOD" extract; rc_is "summary list" "$RC" 0
|
|
206
|
+
want "summary: first id keeps its scenario" '[.findings[] | select(.id == "BUG-1") | .scenario] | join(",")' findings.json "present"
|
|
207
|
+
want "summary: middle id keeps its scenario" '[.findings[] | select(.id == "BUG-2") | .scenario] | join(",")' findings.json "present"
|
|
208
|
+
want "summary: a section without a scenario stays absent" '[.findings[] | select(.id == "BUG-3") | .scenario] | join(",")' findings.json "absent"
|
|
209
|
+
|
|
210
|
+
# the same id repeated across two agents binds each finding to its own body: agent and scenario
|
|
211
|
+
reset
|
|
212
|
+
cm MEMBER User "$(agent_body security "$SHA" '### BUG-1 alpha
|
|
213
|
+
**Failure scenario**: alpha scenario
|
|
214
|
+
' '<!-- FINDING:BUG-1|CONFIRMED|MEDIUM|a.py:2|Alpha problem in security -->')"
|
|
215
|
+
cm MEMBER User "$(agent_body billing "$SHA" '### BUG-1 omega
|
|
216
|
+
no scenario written here
|
|
217
|
+
' '<!-- FINDING:BUG-1|CONFIRMED|MEDIUM|d.py:15|Omega problem in billing -->')"
|
|
218
|
+
flush; run "$GOOD" extract; rc_is "repeated id" "$RC" 0
|
|
219
|
+
want "repeated id: first binds its own agent" '[.findings[] | select(.title | test("Alpha")) | .agent] | join(",")' findings.json "security"
|
|
220
|
+
want "repeated id: first keeps its scenario" '[.findings[] | select(.title | test("Alpha")) | .scenario] | join(",")' findings.json "present"
|
|
221
|
+
want "repeated id: second binds its own agent" '[.findings[] | select(.title | test("Omega")) | .agent] | join(",")' findings.json "billing"
|
|
222
|
+
want "repeated id: second does not borrow the first scenario" '[.findings[] | select(.title | test("Omega")) | .scenario] | join(",")' findings.json "absent"
|
|
223
|
+
|
|
224
|
+
# the identical marker line repeated across two heads (DELTA) binds each occurrence to its own head's body
|
|
225
|
+
reset; mkctx 1 DELTA "$OLD_SHA"
|
|
226
|
+
cm MEMBER User "$(agent_body security "$SHA" '### BUG-1 same
|
|
227
|
+
no scenario at the current head
|
|
228
|
+
' '<!-- FINDING:BUG-1|CONFIRMED|MEDIUM|a.py:2|Same marker both heads -->')"
|
|
229
|
+
cm MEMBER User "$(agent_body billing "$OLD_SHA" '### BUG-1 same
|
|
230
|
+
**Failure scenario**: earlier head scenario
|
|
231
|
+
' '<!-- FINDING:BUG-1|CONFIRMED|MEDIUM|a.py:2|Same marker both heads -->')"
|
|
232
|
+
flush; run "$GOOD" extract
|
|
233
|
+
want "repeated across heads: both occurrences extracted" '.counts.findings_extracted' findings.json "2"
|
|
234
|
+
|
|
187
235
|
# ======================= extract: 6B dedup =======================
|
|
188
236
|
reset
|
|
189
237
|
cm MEMBER User "$(agent_body security "$SHA" '### A-1 x
|
|
@@ -231,12 +279,20 @@ want "gate3: HIGH exempt" '.findings[] | select(.id == "N-2") | .route' findings
|
|
|
231
279
|
want "gate3: unreadable prose keeps the finding" '.findings[] | select(.id == "U-1") | .route' findings.json "file"
|
|
232
280
|
want "gate: introduced finding stays" '.findings[] | select(.id == "N-3") | .route' findings.json "file"
|
|
233
281
|
want "gate: counts" '[.counts.findings_preexisting, .counts.notes_demoted_no_scenario] | join(",")' findings.json "1,1"
|
|
282
|
+
# scope (#3975): recorded for every finding, any severity, computed from the added-line map
|
|
283
|
+
want "scope: MEDIUM far from added lines" '.findings[] | select(.id == "P-1") | .scope' findings.json "pre-existing"
|
|
284
|
+
want "scope: HIGH is recorded although never routed pre-existing" '.findings[] | select(.id == "P-2") | .scope' findings.json "pre-existing"
|
|
285
|
+
want "scope: HIGH near an added line" '.findings[] | select(.id == "N-2") | .scope' findings.json "introduced"
|
|
286
|
+
want "scope: MEDIUM near an added line" '.findings[] | select(.id == "N-3") | .scope' findings.json "introduced"
|
|
287
|
+
want "scope: every finding carries a valid scope" '[.findings[].scope | select(. == "introduced" or . == "pre-existing" or . == "unknown")] | length' findings.json "$(jq '.findings | length' "$WORK/out/findings.json")"
|
|
288
|
+
want "scope: introduced count" '.counts.findings_introduced' findings.json "$(jq '[.findings[] | select(.scope == "introduced")] | length' "$WORK/out/findings.json")"
|
|
234
289
|
# an unreadable files API classifies nothing as pre-existing
|
|
235
290
|
reset; echo '[]' > "$FIX/pr-files.json"
|
|
236
291
|
cm MEMBER User "$(agent_body security "$SHA" '### P-1 pre
|
|
237
292
|
**Failure scenario**: s
|
|
238
293
|
' '<!-- FINDING:P-1|LIKELY|MEDIUM|a.py:19|Pre-existing defect far from added lines -->')"; flush
|
|
239
294
|
run "$GOOD" extract; want "unreadable diff keeps" '.findings[0].route' findings.json "file"
|
|
295
|
+
want "unreadable diff: scope unknown" '.findings[0].scope' findings.json "unknown"
|
|
240
296
|
|
|
241
297
|
# gate 2 route table (ported from the retired spec sketch test): severity normalisation, deleted/renamed/patch-less files, files absent from the diff
|
|
242
298
|
reset
|
|
@@ -258,8 +314,60 @@ run "$GOOD" extract; rc_is "gate2 table" "$RC" 0
|
|
|
258
314
|
for pair in G-1:file G-2:pre-existing G-3:file G-4:file G-5:file G-6:file G-7:file G-8:file G-9:pre-existing G-10:pre-existing G-11:file G-12:pre-existing G-13:file; do
|
|
259
315
|
want "gate2 table: ${pair%%:*}" ".findings[] | select(.id == \"${pair%%:*}\") | .route" findings.json "${pair#*:}"
|
|
260
316
|
done
|
|
317
|
+
for pair in G-1:introduced G-2:pre-existing G-3:pre-existing G-4:unknown G-7:pre-existing G-13:introduced G-12:pre-existing; do
|
|
318
|
+
want "scope table: ${pair%%:*}" ".findings[] | select(.id == \"${pair%%:*}\") | .scope" findings.json "${pair#*:}"
|
|
319
|
+
done
|
|
261
320
|
want "gate2 table: a file absent from the PR and from the head is stale (gate 1), not pre-existing" '[.findings[].id] | contains(["G-14"]) | not' findings.json "true"
|
|
262
321
|
|
|
322
|
+
# symbol fallback (#4061): a stale or call-site cited line still scopes introduced when a named identifier is in the added lines
|
|
323
|
+
reset
|
|
324
|
+
printf '%s\n' '[{"filename":"s.sh","status":"modified","patch":"@@ -9,2 +9,3 @@\n ctx baseOnlyName\n+stepKey=1 stepKeyed=2 a*b $(x) self return result computeLaneBudget\n ctx"}]' > "$FIX/pr-files.json"
|
|
325
|
+
# The base version of s.sh (a dangling commit, HEAD untouched): base-present words never count as new symbols.
|
|
326
|
+
_bb=$(printf 'ctx baseOnlyName\nctx\nself return result\n' | git -C "$REPO_DIR" hash-object -w --stdin)
|
|
327
|
+
_bt=$(printf '100644 blob %s\ts.sh\n' "$_bb" | git -C "$REPO_DIR" mktree)
|
|
328
|
+
git -C "$REPO_DIR" commit-tree "$_bt" -m base-s > "$FIX/base-sha"
|
|
329
|
+
SY_PROSE=""; SY_MARKS=""; SY_N=0
|
|
330
|
+
sy() { # sy <id> <title> <extra prose line>
|
|
331
|
+
SY_PROSE="${SY_PROSE}### $1 finding
|
|
332
|
+
**Failure scenario**: s
|
|
333
|
+
$3
|
|
334
|
+
"
|
|
335
|
+
SY_N=$((SY_N + 1))
|
|
336
|
+
SY_MARKS="${SY_MARKS}<!-- FINDING:$1|LIKELY|MEDIUM|s.sh:$((300 + SY_N * 20))|$2 -->
|
|
337
|
+
"
|
|
338
|
+
}
|
|
339
|
+
sy Y-1 "alpha stale" 'The dedup key `stepKey` is wrong'
|
|
340
|
+
sy Y-2 "bravo base" 'Calls `baseOnlyName` badly'
|
|
341
|
+
sy Y-3 "charlie generic" 'Uses `this` and `file` and `abc` here'
|
|
342
|
+
sy Y-4 "delta substring" 'Wrong `stepKe` or `step` usage'
|
|
343
|
+
sy Y-5 "echo plain" 'No backticks and no cause line at all'
|
|
344
|
+
sy Y-6 "foxtrot meta" 'Odd `a*b` and `$(x)` and `[` tokens'
|
|
345
|
+
sy Y-7 "golf rootcause" 'Root cause: stepKey is built too late'
|
|
346
|
+
sy Y-8 "hotel self" 'The `self` handle leaks'
|
|
347
|
+
sy Y-9 "india return" 'Missing `return` on the error path'
|
|
348
|
+
sy Y-10 "juliet result" 'The `result` is discarded'
|
|
349
|
+
sy Y-11 "kilo newfn" 'The new `computeLaneBudget` overflows'
|
|
350
|
+
cm MEMBER User "$(agent_body security "$SHA" "$SY_PROSE" "$SY_MARKS")"; flush
|
|
351
|
+
run "$GOOD" extract; rc_is "symbol fallback" "$RC" 0
|
|
352
|
+
# stale cited line (outside every added hunk) naming an added identifier: Y-1, Y-7 introduced
|
|
353
|
+
# generic words (this, file) and identifiers under 4 characters (abc): Y-3 never triggers the fallback
|
|
354
|
+
for pair in Y-1:introduced Y-2:pre-existing Y-3:pre-existing Y-4:pre-existing Y-5:pre-existing Y-6:pre-existing Y-7:introduced Y-8:pre-existing Y-9:pre-existing Y-10:pre-existing Y-11:introduced; do
|
|
355
|
+
want "symbol fallback: ${pair%%:*}" ".findings[] | select(.id == \"${pair%%:*}\") | .scope" findings.json "${pair#*:}"
|
|
356
|
+
done
|
|
357
|
+
want "symbol fallback: stale cited line naming an added identifier is introduced" '[.findings[] | select(.id == "Y-1" or .id == "Y-7") | .scope] | join(",")' findings.json "introduced,introduced"
|
|
358
|
+
want "symbol fallback: generic words and short identifiers never trigger the fallback" '.findings[] | select(.id == "Y-3") | .scope' findings.json "pre-existing"
|
|
359
|
+
want "symbol fallback: introduced routes file" '.findings[] | select(.id == "Y-1") | .route' findings.json "file"
|
|
360
|
+
want "symbol fallback: base-only stays pre-existing route" '.findings[] | select(.id == "Y-2") | .route' findings.json "pre-existing"
|
|
361
|
+
want "symbol fallback: self/return/result present in base stay pre-existing (DSEC-1/DBH-1)" '[.findings[] | select(.id == "Y-8" or .id == "Y-9" or .id == "Y-10") | .scope] | join(",")' findings.json "pre-existing,pre-existing,pre-existing"
|
|
362
|
+
want "symbol fallback: a brand-new function name is introduced" '.findings[] | select(.id == "Y-11") | .scope' findings.json "introduced"
|
|
363
|
+
# unreadable added text (extraction failure) keeps the line-window result
|
|
364
|
+
reset; echo '[]' > "$FIX/pr-files.json"
|
|
365
|
+
cm MEMBER User "$(agent_body security "$SHA" '### Y-1 f
|
|
366
|
+
**Failure scenario**: s
|
|
367
|
+
`stepKey`
|
|
368
|
+
' '<!-- FINDING:Y-1|LIKELY|MEDIUM|s.sh:300|alpha stale -->')"; flush
|
|
369
|
+
run "$GOOD" extract; want "symbol fallback: no diff keeps" '.findings[0].scope' findings.json "unknown"
|
|
370
|
+
|
|
263
371
|
# DELTA: the earlier reviewed head's findings are read as well
|
|
264
372
|
reset; mkctx 1 DELTA "$OLD_SHA"
|
|
265
373
|
cm MEMBER User "$(agent_body security "$SHA" '### D-1 new
|
|
@@ -277,14 +385,53 @@ run "$GOOD" extract; rc_is "delta: earlier head unreadable" "$RC" 3; has "delta:
|
|
|
277
385
|
# clean review
|
|
278
386
|
reset; cm MEMBER User "$(agent_body security "$SHA" 'all good' 'no findings')"; flush
|
|
279
387
|
run "$GOOD" extract; has "clean" "STRUCTURED_FINDINGS=0"; has "clean" "CLEAN_STRUCTURED=true"
|
|
388
|
+
has "clean: no undelivered agent" "AGENTS_WITHOUT_BLOCK=
|
|
389
|
+
"
|
|
390
|
+
lacks() { case "$OUT" in *"$2"*) bad "$1: output has '$2'" ;; *) ok ;; esac; }
|
|
391
|
+
nob() { printf '<!-- FORGE:REVIEW-AGENT:%s -->\n**Verdict: APPROVE - no findings.**\nReviewed-SHA: %s\n' "$1" "$2"; }
|
|
392
|
+
|
|
393
|
+
# verdict-only agent comment (no findings block): undelivered, never a finding, never clean
|
|
394
|
+
reset; cm MEMBER User "$(nob security "$SHA")"; flush
|
|
395
|
+
run "$GOOD" extract; rc_is "no-block agent" "$RC" 0
|
|
396
|
+
has "no-block agent: reported undelivered" "AGENTS_WITHOUT_BLOCK=security"
|
|
397
|
+
has "no-block agent: no finding created" "STRUCTURED_FINDINGS=0"
|
|
398
|
+
lacks "no-block agent: not clean" "CLEAN_STRUCTURED=true"
|
|
399
|
+
# mix: one clean (empty block) agent and one block-less agent
|
|
400
|
+
reset; cm MEMBER User "$(agent_body auth "$SHA" 'all good' '')"; cm MEMBER User "$(nob security "$SHA")"; flush
|
|
401
|
+
run "$GOOD" extract
|
|
402
|
+
has "mixed: block-less agent listed" "AGENTS_WITHOUT_BLOCK=security"
|
|
403
|
+
lacks "mixed: block-delivering agent not listed" "AGENTS_WITHOUT_BLOCK=auth"
|
|
404
|
+
lacks "mixed: block-delivering agent not listed (joined)" "security auth"
|
|
405
|
+
lacks "mixed: not clean" "CLEAN_STRUCTURED=true"
|
|
406
|
+
# SEC-1: a re-dispatched agent's later complete block clears its earlier block-less comment
|
|
407
|
+
reset; cm MEMBER User "$(nob security "$SHA")"; cm MEMBER User "$(agent_body security "$SHA" 'all good' '')"; flush
|
|
408
|
+
run "$GOOD" extract
|
|
409
|
+
lacks "redispatch: delivered domain not listed" "AGENTS_WITHOUT_BLOCK=security"
|
|
410
|
+
has "redispatch: clean" "CLEAN_STRUCTURED=true"
|
|
411
|
+
# SEC-2: START without END (truncated) is not delivered
|
|
412
|
+
reset; cm MEMBER User "$(printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n<!-- REVIEW-FINDINGS-START -->\n' "$SHA")"; flush
|
|
413
|
+
run "$GOOD" extract
|
|
414
|
+
has "START without END: undelivered" "AGENTS_WITHOUT_BLOCK=security"
|
|
415
|
+
lacks "START without END: not clean" "CLEAN_STRUCTURED=true"
|
|
416
|
+
# a synthesized block must not mask a block-less raw agent comment
|
|
417
|
+
reset; cm MEMBER User "$(nob security "$SHA")"
|
|
418
|
+
cm MEMBER User "$(printf '<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->\nclean\nReviewed-SHA: %s\n' "$SHA")"; flush
|
|
419
|
+
run "$GOOD" extract
|
|
420
|
+
has "synth: block-less agent still reported" "AGENTS_WITHOUT_BLOCK=security"
|
|
421
|
+
lacks "synth: not clean" "CLEAN_STRUCTURED=true"
|
|
422
|
+
# an untrusted block-less comment is ignored (cannot park a review)
|
|
423
|
+
reset; cm MEMBER User "$(agent_body auth "$SHA" 'ok' '')"; cm NONE User "$(nob security "$SHA")"; flush
|
|
424
|
+
run "$GOOD" extract
|
|
425
|
+
lacks "untrusted block-less ignored" "AGENTS_WITHOUT_BLOCK=security"
|
|
426
|
+
has "untrusted block-less ignored: clean" "CLEAN_STRUCTURED=true"
|
|
280
427
|
|
|
281
428
|
# ======================= classify =======================
|
|
282
|
-
# mkfind <id> <conf> <sev> <file> <line> <title> [route] [agent] [scenario-text]
|
|
429
|
+
# mkfind <id> <conf> <sev> <file> <line> <title> [route] [agent] [scenario-text] [scope]
|
|
283
430
|
FJ=""
|
|
284
431
|
mkfind() {
|
|
285
|
-
FJ="${FJ:+$FJ,}$(jq -n -c --arg id "$1" --arg c "$2" --arg s "$3" --arg f "$4" --arg l "$5" --arg t "$6" --arg r "${7:-file}" --arg a "${8:-}" \
|
|
432
|
+
FJ="${FJ:+$FJ,}$(jq -n -c --arg id "$1" --arg c "$2" --arg s "$3" --arg f "$4" --arg l "$5" --arg t "$6" --arg r "${7:-file}" --arg a "${8:-}" --arg sc "${10:-}" \
|
|
286
433
|
'{id: $id, confidence: $c, severity: $s, loc: ($f + ":" + $l), file: $f, line_start: ($l | tonumber), line_end: ($l | tonumber), title: $t, agent: $a,
|
|
287
|
-
scenario: "present", fix_efficacy: "", text: ($t + "\n" + $f), route: $r}')"
|
|
434
|
+
scenario: "present", fix_efficacy: "", text: ($t + "\n" + $f), route: $r} + (if $sc == "" then {} else {scope: $sc} end)')"
|
|
288
435
|
}
|
|
289
436
|
putfind() { printf '{"schema_version":1,"review_sha":"%s","review_mode":"FULL","counts":{"findings_extracted":%s,"findings_dropped_stale":0,"findings_preexisting":%s,"notes_demoted_no_scenario":%s},"findings":[%s]}\n' \
|
|
290
437
|
"$SHA" "${1:-1}" "${2:-0}" "${3:-0}" "$FJ" > "$WORK/findings-in.json"; FJ=""; mkdir -p "$WORK/out"; cp "$WORK/findings-in.json" "$WORK/out/findings.json"; }
|
|
@@ -298,7 +445,7 @@ run "$GOOD" classify; rc_is "classify" "$RC" 0
|
|
|
298
445
|
has "classify: filed" "FINDINGS_FILED=1"
|
|
299
446
|
want "classify: note" '.findings[] | select(.id == "X-1") | .disposition | startswith("NOTE")' classified.json "true"
|
|
300
447
|
want "classify: issue" '.findings[] | select(.id == "X-2") | .class' classified.json "ISSUE"
|
|
301
|
-
posted_has "marker first line" "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=0 notes_listed=1 notes_dropped=0 findings_filed=1 inpr_fix=0 contract_demoted=0 findings_dropped_stale=0 findings_preexisting=0 notes_demoted_no_scenario=0 lineage=none classifier=script notes_reflag=0 notes_capped=0 reviewed_sha=${SHA} -->"
|
|
448
|
+
posted_has "marker first line" "<!-- FORGE:NOTE_DISPOSITION: notes_fixed=0 notes_listed=1 notes_dropped=0 findings_filed=1 inpr_fix=0 contract_demoted=0 findings_dropped_stale=0 findings_preexisting=0 introduced=0 notes_demoted_no_scenario=0 lineage=none classifier=script notes_reflag=0 notes_capped=0 reviewed_sha=${SHA} -->"
|
|
302
449
|
posted_has "note list line" "- X-1 a.py:5 [POSSIBLE] Rename a local variable for clarity:"
|
|
303
450
|
# the later-head reader (prior_findings_tsv) must be able to parse the list line this script wrote
|
|
304
451
|
ROUND=$(grep '^- X-1' "$FIX/posted.log" | sed -n 's/^- [^ ]* \([^ ]*\) \[[A-Za-z?]*\] \(.*\): [^:]*$/\1\t\2/p')
|
|
@@ -343,6 +490,19 @@ mkfind I-1 CONFIRMED MEDIUM a.py 5 "Confirmed defect inside the changed file"; p
|
|
|
343
490
|
run "$GOOD" classify --auto-merge true; has "inpr round 2" "INPR_FIX=0"
|
|
344
491
|
posted_lacks "no second in-pr fix request" "FORGE:INPR_FIX"
|
|
345
492
|
want "inpr round 2 files it" '.findings[0].class' classified.json "ISSUE"
|
|
493
|
+
# scope (#3975): an introduced MEDIUM LIKELY finding in the diff is an in-PR fix; other scopes keep filing it
|
|
494
|
+
reset; mkctx 1 FULL "" 12; echo "false" > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"; echo '[]' > "$FIX/comments.json"
|
|
495
|
+
mkfind S-1 LIKELY MEDIUM a.py 5 "Introduced likely defect in new code" file "" "" introduced
|
|
496
|
+
mkfind S-2 LIKELY MEDIUM a.py 6 "Pre-existing likely defect in old code" file "" "" pre-existing
|
|
497
|
+
mkfind S-3 LIKELY MEDIUM a.py 7 "Likely defect with unknown provenance" file "" "" unknown
|
|
498
|
+
mkfind S-4 LIKELY MEDIUM a.py 8 "Likely defect with no scope recorded"; putfind 4
|
|
499
|
+
run "$GOOD" classify --auto-merge true; has "scope inpr" "INPR_FIX=1"
|
|
500
|
+
want "scope: introduced LIKELY is INPR_FIX" '.findings[] | select(.id == "S-1") | .class' classified.json "INPR_FIX"
|
|
501
|
+
want "scope: pre-existing LIKELY is ISSUE" '.findings[] | select(.id == "S-2") | .class' classified.json "ISSUE"
|
|
502
|
+
want "scope: unknown LIKELY is ISSUE" '.findings[] | select(.id == "S-3") | .class' classified.json "ISSUE"
|
|
503
|
+
want "scope: absent LIKELY is never INPR_FIX" '.findings[] | select(.id == "S-4") | .class != "INPR_FIX"' classified.json "true"
|
|
504
|
+
want "scope: carried into classified.json" '.findings[] | select(.id == "S-1") | .scope' classified.json "introduced"
|
|
505
|
+
posted_has "NOTE_DISPOSITION records introduced count" " introduced=1 "
|
|
346
506
|
# an untrusted FORGE:INPR_FIX marker does not use up the round
|
|
347
507
|
reset; mkctx 1 FULL "" 12; printf 'a.py\n' > "$FIX/files"
|
|
348
508
|
cm NONE User "<!-- FORGE:INPR_FIX: round=1 head=${OLD_SHA} max_severity=MEDIUM findings=I-0 -->"; flush
|
|
@@ -353,16 +513,49 @@ run "$GOOD" classify --auto-merge true; has "untrusted marker" "INPR_FIX=1"
|
|
|
353
513
|
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
354
514
|
CONTRACT_BODY=$(printf '<!-- FORGE:CONTRACT -->\n### Out of Scope\n\n- `docs/` — deferred → #30: tracked there\n')
|
|
355
515
|
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
356
|
-
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12
|
|
516
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
357
517
|
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
358
518
|
run "$GOOD" classify; has "contract" "CONTRACT_DEMOTED=1"
|
|
359
519
|
want "contract-deferred" '.findings[0].disposition' classified.json "NOTE contract-deferred #30"
|
|
360
520
|
[ "$(cat "$FIX/issue-comments.log" 2>/dev/null)" = "ISSUE_COMMENT 30" ] && ok || bad "deferred issue comment not posted"
|
|
361
521
|
posted_has "contract list line" "tracked on #30"
|
|
522
|
+
# substring-only / longer-path / legacy / empty bodies must not verify (e1, e3, e4); [x] and regex-metachar paths do (e2, e3)
|
|
523
|
+
for B in 'x <!-- FORGE:DEFERRED_FROM: #12 --> docs/' 'x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/guide.md.bak` — why' 'x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/x`' 'x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/`x' ''; do
|
|
524
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
525
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
526
|
+
echo "{\"state\":\"open\",\"body\":\"$B\",\"author_association\":\"MEMBER\",\"user\":{\"type\":\"User\"}}" > "$FIX/issue-30.json"
|
|
527
|
+
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
528
|
+
run "$GOOD" classify; has "unverified-body '$B'" "CONTRACT_DEMOTED=0"
|
|
529
|
+
done
|
|
530
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
531
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
532
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [x] `docs/`","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
533
|
+
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
534
|
+
run "$GOOD" classify; has "checked-item-verifies" "CONTRACT_DEMOTED=1"
|
|
535
|
+
# a failing comment on the deferred issue, or a no-longer-open one, files the finding instead of demoting it
|
|
536
|
+
for _mode in issue-comment-fail issue-closed; do
|
|
537
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
538
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
539
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
540
|
+
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
541
|
+
: > "$FIX/$_mode"
|
|
542
|
+
run "$GOOD" classify; has "$_mode not counted" "CONTRACT_DEMOTED=0"
|
|
543
|
+
want "$_mode falls back to the no-contract disposition" '.findings[0].disposition' classified.json "ISSUE MEDIUM-LIKELY"
|
|
544
|
+
rm -f "$FIX/$_mode"
|
|
545
|
+
done
|
|
546
|
+
# a failing comment on a LOW/POSSIBLE finding falls back to its no-contract disposition (a NOTE), not a blanket ISSUE (DB-1)
|
|
547
|
+
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
548
|
+
cm MEMBER User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
549
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
550
|
+
mkfind K-1 POSSIBLE LOW docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
551
|
+
: > "$FIX/issue-comment-fail"
|
|
552
|
+
run "$GOOD" classify; has "low-fallback not counted" "CONTRACT_DEMOTED=0"
|
|
553
|
+
want "low finding falls back to its NOTE" '.findings[0].class' classified.json "NOTE"
|
|
554
|
+
rm -f "$FIX/issue-comment-fail"
|
|
362
555
|
# an untrusted contract demotes nothing
|
|
363
556
|
reset; mkctx 1 FULL "" 12; echo false > "$FIX/labels"; printf 'a.py\n' > "$FIX/files"
|
|
364
557
|
cm NONE User "$CONTRACT_BODY"; flush "$FIX/comments-12.json"; echo '[]' > "$FIX/comments.json"
|
|
365
|
-
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12
|
|
558
|
+
echo '{"state":"open","body":"x <!-- FORGE:DEFERRED_FROM: #12 -->\n- [ ] `docs/` — why","author_association":"MEMBER","user":{"type":"User"}}' > "$FIX/issue-30.json"
|
|
366
559
|
mkfind K-1 LIKELY MEDIUM docs/guide.md 3 "Docs example is misleading"; putfind 1
|
|
367
560
|
run "$GOOD" classify; has "untrusted contract" "CONTRACT_DEMOTED=0"
|
|
368
561
|
|
|
@@ -458,6 +651,7 @@ mkfind F-3 CONFIRMED LOW a.py 2 "Low defect to file"
|
|
|
458
651
|
mkfind F-4 CONFIRMED LOW billing/pay.py 2 "Low billing defect"
|
|
459
652
|
mkfind F-5 CONFIRMED MEDIUM a.py 19 "Pre-existing medium defect" file-pre-existing
|
|
460
653
|
mkfind F-6 LIKELY LOW a.py 3 "A note, never filed" file
|
|
654
|
+
FJ=$(printf '%s' "[$FJ]" | jq -c 'map(if .id == "F-2" then . + {scope: "introduced"} elif .id == "F-5" then . + {scope: "pre-existing"} else . end) | .[]' | paste -sd, -)
|
|
461
655
|
putfind 6
|
|
462
656
|
cp "$WORK/out/findings.json" "$WORK/out/classified.json"
|
|
463
657
|
jq '.findings |= map(. + {class: (if .id == "F-6" then "NOTE" else "ISSUE" end), disposition: (if .id == "F-6" then "NOTE x" else "ISSUE x" end)})' "$WORK/out/findings.json" > "$WORK/out/classified.json"
|
|
@@ -466,6 +660,9 @@ want "worklist: notes are not filed" '[.items[].id] | join(",")' worklist.json "
|
|
|
466
660
|
want "priority from severity (HIGH)" '.items[] | select(.id == "F-1") | .priority' worklist.json "priority:P1"
|
|
467
661
|
want "priority from severity (MEDIUM)" '.items[] | select(.id == "F-2") | .priority' worklist.json "priority:P2"
|
|
468
662
|
want "labels" '.items[] | select(.id == "F-2") | .labels | join(",")' worklist.json "review-finding,needs-validation,priority:P2"
|
|
663
|
+
want "worklist item carries scope" '.items[] | select(.id == "F-2") | .scope' worklist.json "introduced"
|
|
664
|
+
want "worklist item scope pre-existing" '.items[] | select(.id == "F-5") | .scope' worklist.json "pre-existing"
|
|
665
|
+
want "worklist item without scope is unknown" '.items[] | select(.id == "F-1") | .scope' worklist.json "unknown"
|
|
469
666
|
want "P3 batchable" '.items[] | select(.id == "F-3") | .batchable' worklist.json "true"
|
|
470
667
|
want "P3 on a billing path is not batchable" '.items[] | select(.id == "F-4") | .batchable' worklist.json "false"
|
|
471
668
|
want "pre-existing label only" '.items[] | select(.id == "F-5") | .labels | join(",")' worklist.json "pre-existing,priority:P2"
|
|
@@ -602,6 +799,54 @@ rc_is "inpr: untrusted request ignored" "$RC" 0
|
|
|
602
799
|
gate_reset; cm MEMBER User "<!-- FORGE:INPR_FIX: head=$SHA -->"; cm NONE User "<!-- FORGE:INPR_FIX_WAIVED: head=$SHA -->"; flush; mg "$GATE"
|
|
603
800
|
rc_is "inpr: untrusted waiver ignored" "$RC" 3
|
|
604
801
|
|
|
802
|
+
# --- degraded-gate gate (forge#3628): issue 42's newest trusted QUALITY_GATE / ACCEPTANCE_GATE name the degraded checks; PR 7's comments must cover each ---
|
|
803
|
+
QG_DEG=$'<!-- FORGE:QUALITY_GATE -->\n## Quality Gate Result\n\n**Result**: PASS\n**Degraded**: python.test — test run exercised nothing; blast-radius — check did not complete'
|
|
804
|
+
QG_OK=$'<!-- FORGE:QUALITY_GATE -->\n## Quality Gate Result\n\n**Result**: PASS\n**Degraded**: none'
|
|
805
|
+
AG_DEG=$'<!-- FORGE:ACCEPTANCE_GATE -->\n## Acceptance Gate — DEGRADED\n\n**Degraded checks (verified nothing)**: python.test — test run exercised nothing\n\n<!-- FORGE:ACCEPTANCE_GATE:DEGRADED -->'
|
|
806
|
+
AGM='<!-- FORGE:REVIEW-AGENT:security -->'
|
|
807
|
+
SEC_FULL="$AGM"$'\nReviewed-SHA: '"$SHA"$'\n\n### Degraded Paths Reviewed\n- python.test: read the skipped DB tests by hand\n- blast-radius: callers checked manually\n\n### Other\nx'
|
|
808
|
+
SEC_PART="$AGM"$'\nReviewed-SHA: '"$SHA"$'\n\n### Degraded Paths Reviewed\n- python.test: read the skipped DB tests by hand\n\n### Other\nblast-radius is mentioned only outside the section'
|
|
809
|
+
SEC_GENERIC="$AGM"$'\nReviewed-SHA: '"$SHA"$'\n\n### Degraded Paths Reviewed\nAll degraded paths were reviewed.\n'
|
|
810
|
+
SEC_STALE="$AGM"$'\nReviewed-SHA: '"$OLD_SHA"$'\n\n### Degraded Paths Reviewed\n- python.test: read the skipped DB tests by hand\n- blast-radius: callers checked manually\n'
|
|
811
|
+
SEC_NEG="$AGM"$'\nReviewed-SHA: '"$SHA"$'\n\n### Degraded Paths Reviewed\n- python.test: NOT reviewed\n- blast-radius: callers checked manually\n'
|
|
812
|
+
SEC_PLAIN=$'### Degraded Paths Reviewed\n- python.test: read the skipped DB tests by hand\n- blast-radius: callers checked manually\n'
|
|
813
|
+
SEC_AG="$AGM"$'\nReviewed-SHA: '"$SHA"$'\n\n### Degraded Paths Reviewed\n- python.test: covered by hand\n'
|
|
814
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; mg "$GATE"
|
|
815
|
+
rc_is "degraded: no section" "$RC" 3; has "degraded: no section" "REVIEW_RESULT: status: BLOCKED, blocker: degraded-gate-unreviewed"
|
|
816
|
+
logged "degraded: no section needs-human" edits.log "needs-human"; logged "degraded: no section comment" issue-bodies.log "FORGE:DEGRADED_GATE_UNREVIEWED"; not_merged "degraded: no section"
|
|
817
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; cm MEMBER User "$SEC_PART"; flush; mg "$GATE"
|
|
818
|
+
rc_is "degraded: partial coverage" "$RC" 3; has "degraded: partial coverage" "blocker: degraded-gate-unreviewed"; logged "degraded: partial names the gap" issue-bodies.log "not covered: blast-radius"; not_merged "degraded: partial"
|
|
819
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; cm MEMBER User "$SEC_GENERIC"; flush; mg "$GATE"
|
|
820
|
+
rc_is "degraded: generic section" "$RC" 3; has "degraded: generic section" "blocker: degraded-gate-unreviewed"; not_merged "degraded: generic"
|
|
821
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; cm NONE User "$SEC_FULL"; flush; mg "$GATE"
|
|
822
|
+
rc_is "degraded: untrusted section ignored" "$RC" 3; has "degraded: untrusted section ignored" "blocker: degraded-gate-unreviewed"
|
|
823
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; cm MEMBER User "$SEC_STALE"; flush; mg "$GATE"
|
|
824
|
+
rc_is "degraded: stale-head section ignored" "$RC" 3; has "degraded: stale-head section ignored" "blocker: degraded-gate-unreviewed"
|
|
825
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; cm MEMBER User "$SEC_NEG"; flush; mg "$GATE"
|
|
826
|
+
rc_is "degraded: negated item is not coverage" "$RC" 3; logged "degraded: negated item names the gap" issue-bodies.log "not covered: python.test"
|
|
827
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; cm MEMBER User "$SEC_PLAIN"; flush; mg "$GATE"
|
|
828
|
+
rc_is "degraded: plain comment without review marker ignored" "$RC" 3
|
|
829
|
+
gate_reset; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; cm MEMBER User "$SEC_FULL"; flush; mg "$GATE"
|
|
830
|
+
rc_is "degraded: full per-check section" "$RC" 0; has "degraded: full per-check section" "MERGE_OUTCOME=merged"; merged "degraded: full"
|
|
831
|
+
gate_reset; cm Bot Bot "$QG_DEG"; flush "$FIX/comments-42.json"; cm NONE Bot "$SEC_FULL"; flush; mg "$GATE"
|
|
832
|
+
rc_is "degraded: bot-authored gate and section" "$RC" 0
|
|
833
|
+
gate_reset; cm MEMBER User "$AG_DEG"; flush "$FIX/comments-42.json"; mg "$GATE"
|
|
834
|
+
rc_is "degraded: ACCEPTANCE_GATE:DEGRADED unreviewed" "$RC" 3; has "degraded: ACCEPTANCE_GATE:DEGRADED unreviewed" "blocker: degraded-gate-unreviewed"
|
|
835
|
+
gate_reset; cm MEMBER User "$AG_DEG"; flush "$FIX/comments-42.json"; cm MEMBER User "$SEC_AG"; flush; mg "$GATE"
|
|
836
|
+
rc_is "degraded: ACCEPTANCE_GATE:DEGRADED reviewed" "$RC" 0
|
|
837
|
+
gate_reset; cm MEMBER User "$QG_OK"; flush "$FIX/comments-42.json"; mg "$GATE"
|
|
838
|
+
rc_is "degraded: Degraded none is unaffected" "$RC" 0; has "degraded: Degraded none" "MERGE_OUTCOME=merged"
|
|
839
|
+
gate_reset; cm MEMBER User "$QG_DEG"; cm MEMBER User "$QG_OK"; flush "$FIX/comments-42.json"; mg "$GATE"
|
|
840
|
+
rc_is "degraded: a newer clean gate clears it" "$RC" 0
|
|
841
|
+
gate_reset; cm MEMBER User "$QG_OK"; cm MEMBER User "$QG_DEG"; flush "$FIX/comments-42.json"; mg "$GATE"
|
|
842
|
+
rc_is "degraded: a newer degraded gate wins" "$RC" 3
|
|
843
|
+
gate_reset; cm NONE User "$QG_DEG"; flush "$FIX/comments-42.json"; mg "$GATE"
|
|
844
|
+
rc_is "degraded: untrusted DEGRADED marker cannot block" "$RC" 0
|
|
845
|
+
gate_reset; mg "$GATE"
|
|
846
|
+
rc_is "degraded: no gate comments is unaffected" "$RC" 0
|
|
847
|
+
gate_reset; rm -f "$FIX/comments.json"; mg "$GATE"
|
|
848
|
+
rc_is "degraded: unreadable comments fail closed" "$RC" 3; not_merged "degraded: unreadable"
|
|
849
|
+
|
|
605
850
|
# --- --issue requirement ---
|
|
606
851
|
gate_reset; mkctx 1 FULL "" ""; mg "$GATE"
|
|
607
852
|
rc_is "issue: missing" "$RC" 3; has "issue: missing" "blocker: auto-merge requires --issue"; posted_has "issue: missing" "no --issue was given"
|
|
@@ -648,15 +893,38 @@ rc_is "guard: non-boolean" "$RC" 3; has "guard: non-boolean" "merge guard state
|
|
|
648
893
|
# --- verdict / calibration / trust / base-conflict routing ---
|
|
649
894
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
650
895
|
rc_is "route: only base conflict" "$RC" 3; has "route: only base conflict" "blocker: base-conflict"; has "route: only base conflict" "MERGE_OUTCOME=base-conflict"
|
|
651
|
-
logged "route: base-sync comment" issue-bodies.log "the pipeline
|
|
896
|
+
logged "route: base-sync comment" issue-bodies.log "the work-on pipeline performs the base sync"; logged "route: standalone remediate action" issue-bodies.log "--remediate"; not_logged "route: base conflict is not needs-human" edits.log "needs-human"; not_merged "route: base conflict"
|
|
652
897
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --other-blocking true
|
|
653
898
|
rc_is "route: base conflict plus blocker" "$RC" 3; has "route: base conflict plus blocker" "MERGE_OUTCOME=needs-human"; logged "route: needs-human" edits.log "needs-human"
|
|
899
|
+
# other-blocking-derived: a persisted true (written by verdict) is ORed in; a passed false never lowers it
|
|
900
|
+
ob() { mkdir -p "$WORK/out"; printf '%s\n' "$1" > "$WORK/out/other-blocking.txt"; }
|
|
901
|
+
gate_reset; ob true; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --other-blocking false
|
|
902
|
+
rc_is "other-blocking-derived: persisted true beats passed false" "$RC" 3; has "other-blocking-derived: persisted true beats passed false" "MERGE_OUTCOME=needs-human"
|
|
903
|
+
not_has() { case "$OUT" in *"$2"*) bad "$1: output has '$2'" ;; *) ok ;; esac; }
|
|
904
|
+
not_has "other-blocking-derived: not base-conflict" "MERGE_OUTCOME=base-conflict"; logged "other-blocking-derived: needs-human" edits.log "needs-human"
|
|
905
|
+
gate_reset; ob false; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
906
|
+
rc_is "other-blocking-derived: persisted false keeps base-conflict route" "$RC" 3; has "other-blocking-derived: persisted false" "MERGE_OUTCOME=base-conflict"
|
|
907
|
+
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
908
|
+
has "other-blocking-derived: missing file uses passed flag" "MERGE_OUTCOME=base-conflict"
|
|
909
|
+
for junk in "" "maybe" "TRUE" "yes"; do
|
|
910
|
+
gate_reset; ob "$junk"; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true
|
|
911
|
+
has "other-blocking-derived: garbage file '$junk' ignored" "MERGE_OUTCOME=base-conflict"
|
|
912
|
+
done
|
|
913
|
+
gate_reset; ob true; mg "$GATE" --other-blocking ""
|
|
914
|
+
rc_is "other-blocking-derived: unbound passed flag still fails closed" "$RC" 3; has "other-blocking-derived: unbound still fails closed" "merge guard state unbound"
|
|
654
915
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --trust-needs-human true --table-cell-key "feature x HIGH"
|
|
655
916
|
rc_is "route: base conflict plus trust" "$RC" 3; logged "route: trust reason" issue-bodies.log "provenance trust: NOVEL_NEEDS_HUMAN tier for cell \`feature x HIGH\`"; logged "route: trust needs-human" edits.log "needs-human"
|
|
656
917
|
gate_reset; mg "$GATE" --verdict "CHANGES REQUESTED" --base-conflict true --calibration-needs-human true --calibration-note "overconfident cell"
|
|
657
918
|
rc_is "route: base conflict plus calibration" "$RC" 3; logged "route: calibration reason" issue-bodies.log "calibration threshold: overconfident cell"; logged "route: calibration needs-human" edits.log "needs-human"
|
|
658
919
|
gate_reset; mg "$GATE" --has-purpose-regression true
|
|
659
920
|
rc_is "route: purpose regression with APPROVED" "$RC" 3; logged "route: purpose reason" issue-bodies.log "purpose regression detected by Phase 7A"; logged "route: purpose needs-human" edits.log "needs-human"; not_merged "route: purpose"
|
|
921
|
+
for payload in "it's cal" "\$(touch $FIX/SENTINEL)" "\`touch $FIX/SENTINEL\`" "'; touch $FIX/SENTINEL; '"; do
|
|
922
|
+
gate_reset; rm -f "$FIX/SENTINEL"; printf '%s' "$payload" > "$FIX/cn.txt"
|
|
923
|
+
mg "$GATE" --calibration-needs-human true --calibration-note-file "$FIX/cn.txt"
|
|
924
|
+
rc_is "calibration note file: gate blocks" "$RC" 3
|
|
925
|
+
if [ -e "$FIX/SENTINEL" ]; then bad "calibration note file: payload executed ($payload)"; else ok; fi
|
|
926
|
+
logged "calibration note file: rendered as data" issue-bodies.log "calibration threshold: $payload"
|
|
927
|
+
done
|
|
660
928
|
gate_reset; mg "$GATE" --calibration-needs-human true --calibration-note "note"
|
|
661
929
|
rc_is "route: calibration alone blocks an APPROVED verdict" "$RC" 3; not_merged "route: calibration alone"
|
|
662
930
|
gate_reset; mg "$GATE" --trust-needs-human true
|
|
@@ -666,7 +934,7 @@ rc_is "route: CHANGES REQUESTED" "$RC" 3; logged "route: verdict reason" issue-b
|
|
|
666
934
|
|
|
667
935
|
# --- pre-merge mergeability guard ---
|
|
668
936
|
gate_reset; echo "CONFLICTING|DIRTY" > "$FIX/premerge"; mg "$GATE"
|
|
669
|
-
rc_is "premerge: conflicting" "$RC" 3; has "premerge: conflicting" "blocker: base-conflict"; not_logged "premerge: conflict is not needs-human" edits.log "needs-human"; not_merged "premerge: conflicting"
|
|
937
|
+
rc_is "premerge: conflicting" "$RC" 3; has "premerge: conflicting" "blocker: base-conflict"; not_logged "premerge: conflict is not needs-human" edits.log "needs-human"; logged "premerge: standalone base-sync wording" issue-bodies.log "/work-on 42 --remediate"; not_merged "premerge: conflicting"
|
|
670
938
|
gate_reset; echo "MERGEABLE|BEHIND" > "$FIX/premerge"; echo true > "$FIX/protection"; mg "$GATE"
|
|
671
939
|
rc_is "premerge: behind under strict protection" "$RC" 3; has "premerge: behind under strict protection" "blocker: base-conflict"
|
|
672
940
|
gate_reset; echo "MERGEABLE|BEHIND" > "$FIX/premerge"; echo false > "$FIX/protection"; mg "$GATE"
|
|
@@ -728,6 +996,22 @@ reset; MH=MERGEABLE; MHS=CLEAN; vd --result clean --intensity-tier FAST --table-
|
|
|
728
996
|
logged "verdict: trust annotation" reviews.log '**Intensity tier**: FAST | **Cell**: `bug HIGH` | **Shadow mode**: false'
|
|
729
997
|
reset; MH=CONFLICTING; MHS=DIRTY; vd --result blocking --blocking-count 1
|
|
730
998
|
has "verdict: blocking" "VERDICT=CHANGES REQUESTED"; logged "verdict: blocking body" reviews.log "CHANGES REQUESTED: commit"; logged "verdict: base conflict line" reviews.log "🔴 Base Conflict:"
|
|
999
|
+
# other-blocking-derived: verdict resolves and persists OTHER_BLOCKING
|
|
1000
|
+
OBF="$WORK/out/other-blocking.txt"
|
|
1001
|
+
reset; MH=MERGEABLE; MHS=CLEAN; vd --result blocking --blocking-count 1 --other-blocking false
|
|
1002
|
+
has "other-blocking-derived: unexplained blocking forces true" "OTHER_BLOCKING=true"; [ "$(cat "$OBF" 2>/dev/null)" = "true" ] && ok || bad "other-blocking-derived: file lacks true"
|
|
1003
|
+
reset; MH=MERGEABLE; MHS=CLEAN; vd --result blocking --blocking-count 1
|
|
1004
|
+
has "other-blocking-derived: no flag still derives true" "OTHER_BLOCKING=true"
|
|
1005
|
+
reset; MH=CONFLICTING; MHS=DIRTY; vd --result blocking --blocking-count 1 --other-blocking false
|
|
1006
|
+
has "other-blocking-derived: base conflict explains the block" "OTHER_BLOCKING=false"; [ "$(cat "$OBF" 2>/dev/null)" = "false" ] && ok || bad "other-blocking-derived: file lacks false"
|
|
1007
|
+
reset; MH=CONFLICTING; MHS=DIRTY; vd --result blocking --blocking-count 1 --other-blocking true
|
|
1008
|
+
has "other-blocking-derived: reviewer true is kept" "OTHER_BLOCKING=true"
|
|
1009
|
+
reset; MH=MERGEABLE; MHS=CLEAN; mkdir -p "$WORK/out"; echo true > "$OBF"; vd --result clean --other-blocking true
|
|
1010
|
+
has "other-blocking-derived: clean result resets to false" "OTHER_BLOCKING=false"; [ "$(cat "$OBF" 2>/dev/null)" = "false" ] && ok || bad "other-blocking-derived: stale true survived a clean re-review"
|
|
1011
|
+
reset; MH=MERGEABLE; MHS=CLEAN; mkdir -p "$WORK/out"; echo true > "$OBF"; vd --result stale
|
|
1012
|
+
[ -e "$OBF" ] && bad "other-blocking-derived: stale result must remove the file" || ok
|
|
1013
|
+
reset; MH=MERGEABLE; MHS=CLEAN; vd --result blocking --other-blocking maybe --blocking-count 1
|
|
1014
|
+
has "other-blocking-derived: garbage flag, unexplained block" "OTHER_BLOCKING=true"
|
|
731
1015
|
reset; MH=CONFLICTING; MHS=DIRTY; vd --result clean
|
|
732
1016
|
has "verdict: clean coerced over a base conflict" "VERDICT_COERCED=true"; has "verdict: coerced verdict" "VERDICT=CHANGES REQUESTED"; logged "verdict: coerced body" reviews.log "CHANGES REQUESTED: commit"
|
|
733
1017
|
reset; MH=MERGEABLE; MHS=CLEAN; vd --result clean --has-purpose-regression true --purpose-findings "F-1"
|
|
@@ -742,6 +1026,30 @@ has "7A: blocked log" "PURPOSE REGRESSION GATE: BLOCKED"; has "7A: escalation" "
|
|
|
742
1026
|
reset; MH=MERGEABLE; MHS=CLEAN; vd --is-milestone true --has-purpose-regression false; has "7A: passed log" "PURPOSE REGRESSION GATE: PASSED"
|
|
743
1027
|
reset; MH=MERGEABLE; MHS=CLEAN; vd --has-purpose-regression true
|
|
744
1028
|
case "$OUT" in *"PURPOSE REGRESSION GATE"*) bad "7A: non-milestone PRs print no gate log" ;; *) ok ;; esac
|
|
1029
|
+
# free text arrives through *-file flags and is data, never code (injection payload classes: apostrophe, command substitution, backtick, quote breakout, newline)
|
|
1030
|
+
SENT="$FIX/SENTINEL"
|
|
1031
|
+
for payload in "it's a F-1" "\$(touch $SENT)" "\`touch $SENT\`" "'; touch $SENT; '" "\"; touch $SENT; \"" "F-1
|
|
1032
|
+
touch $SENT"; do
|
|
1033
|
+
reset; rm -f "$SENT"; MH=MERGEABLE; MHS=CLEAN
|
|
1034
|
+
printf '%s' "$payload" > "$FIX/pf.txt"; printf '%s' "$payload" > "$FIX/ag.txt"
|
|
1035
|
+
vd --result clean --has-purpose-regression true --purpose-findings-file "$FIX/pf.txt" --agents-file "$FIX/ag.txt"
|
|
1036
|
+
rc_is "payload file: verdict runs" "$RC" 0
|
|
1037
|
+
if [ -e "$SENT" ]; then bad "payload file: payload executed ($payload)"; else ok; fi
|
|
1038
|
+
logged "payload file: purpose text kept as data" reviews.log "See: $(printf '%s' "$payload" | tr '\n' ' ')"
|
|
1039
|
+
done
|
|
1040
|
+
reset; MH=MERGEABLE; MHS=CLEAN; printf 'a\001b\033c\nd' > "$FIX/pf.txt"
|
|
1041
|
+
vd --result clean --has-purpose-regression true --purpose-findings-file "$FIX/pf.txt"
|
|
1042
|
+
logged "payload file: control characters stripped, newline folded" reviews.log "See: abc d"
|
|
1043
|
+
reset; MH=MERGEABLE; MHS=CLEAN; head -c 5000 /dev/zero | tr '\0' 'x' > "$FIX/pf.txt"
|
|
1044
|
+
vd --result clean --has-purpose-regression true --purpose-findings-file "$FIX/pf.txt"
|
|
1045
|
+
if grep -qF "$(head -c 2001 /dev/zero | tr '\0' 'x')" "$FIX/reviews.log" 2>/dev/null; then bad "payload file: text must be capped at 2000 bytes"; else ok; fi
|
|
1046
|
+
logged "payload file: capped text still rendered" reviews.log "See: $(head -c 2000 /dev/zero | tr '\0' 'x')"
|
|
1047
|
+
reset; MH=MERGEABLE; MHS=CLEAN
|
|
1048
|
+
vd --result clean --has-purpose-regression true --purpose-findings-file "$FIX/does-not-exist.txt"
|
|
1049
|
+
rc_is "payload file: missing file tolerated" "$RC" 0
|
|
1050
|
+
reset; MH=MERGEABLE; MHS=CLEAN; printf 'from-file' > "$FIX/pf.txt"
|
|
1051
|
+
vd --result clean --has-purpose-regression true --purpose-findings "from-flag" --purpose-findings-file "$FIX/pf.txt"
|
|
1052
|
+
logged "payload file: file wins over inline flag" reviews.log "See: from-file"; not_logged "payload file: inline flag overridden" reviews.log "from-flag"
|
|
745
1053
|
reset; OUT=$(cd "$REPO_DIR" && FORGEDOCK_HOME="$GOOD" bash "$ROOT/scripts/review-finalize.sh" verdict --pr 7 --repo o/r --context "$CTXF" --merge-health 2>/dev/null); RC=$?
|
|
746
1054
|
rc_is "verdict: flag without a value" "$RC" 2
|
|
747
1055
|
reset; vd --result bogus; rc_is "verdict: invalid result" "$RC" 2
|
|
@@ -780,6 +1088,7 @@ has "calibrate: untrusted investigator ignored" "could not resolve task type/con
|
|
|
780
1088
|
reset; mkctx 1 FULL "" 42; inv_comments MEMBER User "$(printf 'see <!-- FORGE:INVESTIGATOR -->\n**Task Type**: Refactor\n**Confidence**: HIGH\n')"; run "$GOOD" calibrate
|
|
781
1089
|
has "calibrate: unanchored marker ignored" "could not resolve task type/confidence"
|
|
782
1090
|
reset; mkctx 1 FULL "" 42; inv_comments MEMBER User "$(printf '<!-- FORGE:INVESTIGATOR -->\n**Task Type**: Re\x27fa$(id)ctor\n**Confidence**: HIGH\n')"; run "$GOOD" calibrate
|
|
1091
|
+
if [ -s "$(dirname "$CTXF")/review-finalize-7/calibration-note.txt" ] || find "$FIX" "$(dirname "$CTXF")" -name calibration-note.txt 2>/dev/null | grep -q .; then ok; else bad "calibrate: must write calibration-note.txt under the out dir"; fi
|
|
783
1092
|
note=$(printf '%s\n' "$OUT" | sed -n 's/^CALIBRATION_NOTE=//p'); case "$note" in *"'"*|*'$'*|*'`'*) bad "calibrate: note carries a shell metacharacter: $note" ;; *) ok ;; esac
|
|
784
1093
|
setcal "$(CAL_ROW Refactor HIGH true overconfidence)"
|
|
785
1094
|
reset; mkctx 1 FULL "" 42; run "$GOOD" calibrate; has "calibrate: no investigator comment fails safe" "CALIBRATION_NEEDS_HUMAN=false"; has "calibrate: unresolved note" "task type or confidence not resolved"
|
|
@@ -814,6 +1123,21 @@ reset; rm -f "$FIX/comments.json"; ig; has "integrity: unreadable comments count
|
|
|
814
1123
|
reset; mkctx 1 BASE_SYNC_ONLY "$OLD_SHA" 42; cm MEMBER User "$(agent_body security "$SHA" 'a' '')"; flush; ig
|
|
815
1124
|
has "integrity: unverified carry is not reported as carried" "CARRIED_FROM: "; case "$OUT" in *"carried from"*) bad "integrity: carried requires a verified record" ;; *) ok ;; esac
|
|
816
1125
|
|
|
1126
|
+
# forge#3971: a build-time FORGE:SELFREVIEW-AGENT comment (even a trusted one pinned to the current head) is never a panel member
|
|
1127
|
+
sr_body() { printf '<!-- FORGE:SELFREVIEW-AGENT:%s -->\n%s\n<!-- REVIEW-FINDINGS-START -->\n%s\n<!-- REVIEW-FINDINGS-END -->\nReviewed-SHA: %s\nTree: abc123\n' "$1" "$3" "$4" "$2"; }
|
|
1128
|
+
reset
|
|
1129
|
+
cm MEMBER User "$(sr_body security "$SHA" 'self' '<!-- FINDING:SEC-9|CONFIRMED|HIGH|a.py:9|Self-review finding -->')"; cm MEMBER Bot "<!-- FORGE:SELFREVIEW -->
|
|
1130
|
+
Tree: abc123"; flush
|
|
1131
|
+
ig; has "integrity: self-review comments never count as panel agents" "ACTUAL_AGENT_COUNT=0"
|
|
1132
|
+
case "$OUT" in *security*) bad "integrity: SELFREVIEW-AGENT domain leaked into the panel" ;; *) ok ;; esac
|
|
1133
|
+
reset
|
|
1134
|
+
cm MEMBER User "$(sr_body security "$SHA" 'self' '<!-- FINDING:SEC-9|CONFIRMED|HIGH|a.py:9|Self-review finding -->')"; flush
|
|
1135
|
+
run "$GOOD" extract
|
|
1136
|
+
has "extract: a self-review comment alone is no review (fail closed)" "no trusted reviewer comments"
|
|
1137
|
+
reset
|
|
1138
|
+
cm MEMBER User "$(sr_body security "$SHA" 'self' '')"; cm MEMBER User "$(agent_body api "$SHA" 'real' '')"; flush
|
|
1139
|
+
ig; has "integrity: only the REVIEW-AGENT counts beside a self-review" "ACTUAL_AGENT_COUNT=1"; has "integrity: only the real domain" "ACTUAL_AGENT_DOMAINS=api"
|
|
1140
|
+
|
|
817
1141
|
# --- Phase 7-9 subcommands run without a review-prepare context (flags only) ---
|
|
818
1142
|
nctx() { local sub="$1"; shift; OUT=$(cd "$REPO_DIR" && FORGEDOCK_HOME="$GATE" bash "$ROOT/scripts/review-finalize.sh" "$sub" --pr 7 --repo o/r "$@" 2>"$WORK/stderr"); RC=$?; }
|
|
819
1143
|
gate_reset; nctx merge-gate --issue 42 --base main --verdict APPROVED --has-purpose-regression false --calibration-needs-human false --trust-needs-human false --base-conflict false --other-blocking false
|
|
@@ -829,5 +1153,19 @@ OUT=$(bash "$ROOT/scripts/review-finalize.sh" bogus 2>/dev/null); RC=$?; rc_is "
|
|
|
829
1153
|
OUT=$(bash "$ROOT/scripts/review-finalize.sh" extract --pr x --repo o/r --context "$CTXF" 2>/dev/null); RC=$?; rc_is "non-numeric pr" "$RC" 2
|
|
830
1154
|
OUT=$(bash "$ROOT/scripts/review-finalize.sh" extract --pr 7 --repo o/r 2>/dev/null); RC=$?; rc_is "missing context" "$RC" 2
|
|
831
1155
|
|
|
1156
|
+
# ======================= forge#4037: build.md red-green DEGRADED entry survives the real name splitter =======================
|
|
1157
|
+
RG_LINE=$(grep -F 'RG_DEGRADED_LIST="${RG_DEGRADED_LIST}' "$ROOT/commands/work-on/build.md" | sed -n '1p' | sed 's/^[[:space:]]*\*) //; s/ ;;.*$//')
|
|
1158
|
+
SPLIT_LINE=$(grep -F 'DEG_NAME_LIST=$(printf' "$ROOT/scripts/review-finalize.sh" | sed -n '1p' | sed 's/^[[:space:]]*//')
|
|
1159
|
+
if [ -n "$RG_LINE" ] && [ -n "$SPLIT_LINE" ]; then
|
|
1160
|
+
RG_DEGRADED_LIST=""; ID="AC2"; RG_BASE=UNRUNNABLE; RG_HEAD=RAN
|
|
1161
|
+
eval "$RG_LINE"
|
|
1162
|
+
RG_DEGRADED_LIST="${RG_DEGRADED_LIST}; AC3 — base run cannot execute (base=UNRUNNABLE head=RAN), so the test is unproven"
|
|
1163
|
+
DEG_NAMES="$RG_DEGRADED_LIST"
|
|
1164
|
+
eval "$SPLIT_LINE"
|
|
1165
|
+
[ "$DEG_NAME_LIST" = "$(printf 'AC2\nAC3')" ] && ok || bad "red-green DEGRADED entries split into exactly their check names (got: $DEG_NAME_LIST)"
|
|
1166
|
+
else
|
|
1167
|
+
bad "could not extract the build.md DEGRADED entry or the review-finalize splitter"
|
|
1168
|
+
fi
|
|
1169
|
+
|
|
832
1170
|
echo "review-finalize tests: $pass passed, $fail failed"
|
|
833
1171
|
[ "$fail" -eq 0 ]
|
|
@@ -232,6 +232,19 @@ case "$(p4 DELTA 'security auth billing')" in "PANEL_STATUS: SHORTFALL"*) ok ;;
|
|
|
232
232
|
{ cm "$(body_agent security "$HEAD")" User NONE; cm "$(body_agent auth "$HEAD")" User NONE; } | jq -s '.' > "$FAKE_COMMENTS"
|
|
233
233
|
case "$(p4 FULL 'security auth')" in "PANEL_STATUS: SHORTFALL"*) ok ;; *) bad "untrusted reviewer markers must not satisfy the panel: $(p4 FULL 'security auth')" ;; esac
|
|
234
234
|
|
|
235
|
+
# a verdict without a findings block is undelivered (#3976): not complete, never counted
|
|
236
|
+
{ cm "$(printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n\nVerdict: APPROVE, no findings.' "$HEAD")" User MEMBER; cm "$(body_agent auth "$HEAD")" User MEMBER; } | jq -s '.' > "$FAKE_COMMENTS"
|
|
237
|
+
case "$(p4 FULL 'security auth')" in "PANEL_STATUS: SHORTFALL (ACTUAL_AGENT_COUNT=1)"*) ok ;; *) bad "a block-less reviewer comment must not satisfy Phase 4: $(p4 FULL 'security auth')" ;; esac
|
|
238
|
+
# the 3C pending-agent count and the staging 6.75 delivery count carry the same block lookahead as Phase 4
|
|
239
|
+
STAGING="$ROOT/commands/review-pr-staging.md"
|
|
240
|
+
for spec in "$SPEC" "$STAGING"; do
|
|
241
|
+
n=$(grep -F 'count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->' "$spec" | wc -l)
|
|
242
|
+
want=2; [ "$spec" = "$STAGING" ] && want=1
|
|
243
|
+
[ "$n" -eq "$want" ] && ok || bad "$(basename "$spec"): $n delivery counts carry the findings-block lookahead, want $want"
|
|
244
|
+
if grep -E 'count "\^<!-- FORGE:REVIEW-AGENT:\$\{AGENT_DOMAIN\}' "$spec" >/dev/null; then bad "$(basename "$spec"): header-only delivery regex remains"; else ok; fi
|
|
245
|
+
if grep -qE 'scan the trusted (agent comments|reviewer bodies) for unstructured|no unstructured finding' "$spec"; then bad "$(basename "$spec"): free-text finding scan remains"; else ok; fi
|
|
246
|
+
done
|
|
247
|
+
|
|
235
248
|
# ---- 6. The 6A read of the carried / earlier head's findings is scripts/review-finalize.sh extract (covered by scripts/review-finalize.test.sh).
|
|
236
249
|
|
|
237
250
|
echo "review-pr-delta-wiring tests: pass=$PASS fail=$FAILN"
|
|
@@ -13,7 +13,7 @@
|
|
|
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)
|
|
16
|
-
# --out-dir directory for per-domain diff slices (default: ${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}/review-prepare-<pr>)
|
|
16
|
+
# --out-dir directory for per-domain diff slices, named slice-<sha7>-<domain>.diff (default: ${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}/review-prepare-<pr>)
|
|
17
17
|
# --checkout-pr `git checkout -f --detach <review sha>` in the CURRENT repo (must be a throwaway review checkout, never the
|
|
18
18
|
# main checkout); the recorded original ref is restored by restore_ref on every exit path
|
|
19
19
|
# --build-gate block (default): an empty trusted typescript.build / python.format on a full-build lane BLOCKs.
|
|
@@ -482,7 +482,7 @@ if [ "$REVIEW_MODE" != "BASE_SYNC_ONLY" ]; then
|
|
|
482
482
|
SLICE_FILES_SRC="$FILES"; [ "$REVIEW_MODE" = "DELTA" ] && SLICE_FILES_SRC="$DELTA_FILES"
|
|
483
483
|
# slice <domain> <diff-header regex|ALL> <file-path regex|ALL>
|
|
484
484
|
slice() {
|
|
485
|
-
local dom="$1" dre="$2" fre="$3" body paths df="$OUT_DIR/slice-$1.diff"
|
|
485
|
+
local dom="$1" dre="$2" fre="$3" body paths tmp df="$OUT_DIR/slice-${REVIEW_SHA_SHORT:-unresolved}-$1.diff"
|
|
486
486
|
if [ "$dre" = "ALL" ]; then body="$FULL_DIFF"
|
|
487
487
|
else
|
|
488
488
|
body=$(printf '%s\n' "$FULL_DIFF" | awk -v re="$dre" '/^diff --git/ { in_block=0 } $0 ~ re { in_block=1 } in_block { print }')
|
|
@@ -492,7 +492,9 @@ if [ "$REVIEW_MODE" != "BASE_SYNC_ONLY" ]; then
|
|
|
492
492
|
else paths=$(printf '%s\n' "$SLICE_FILES_SRC" | grep -iE "$fre" || echo ""); [ -n "$paths" ] || paths="$SLICE_FILES_SRC"; fi
|
|
493
493
|
# 100K-char cap, same as the runner's tool-result cap
|
|
494
494
|
if [ "${#body}" -gt 102400 ]; then body="$(printf '%s' "$body" | head -c 102400)"$'\n'"... [TRUNCATED — diff exceeded 100K chars; focus on the files listed]"; fi
|
|
495
|
-
|
|
495
|
+
# Publish atomically: temp file in $OUT_DIR (same filesystem) then rename, so a concurrent reader sees nothing or the whole file.
|
|
496
|
+
tmp=$(mktemp "$OUT_DIR/.slice-XXXXXX") || { echo "review-prepare: cannot create temp slice in $OUT_DIR" >&2; exit 2; }
|
|
497
|
+
if printf '%s\n' "$body" > "$tmp" && chmod 644 "$tmp"; then mv -f "$tmp" "$df"; else rm -f "$tmp"; echo "review-prepare: cannot write slice $df" >&2; exit 2; fi
|
|
496
498
|
SLICES_JSON=$(printf '%s' "$SLICES_JSON" | jq -c --arg d "$dom" --arg df "$df" --arg p "$paths" '. + {($d): {paths: ($p | split("\n") | map(select(length > 0))), diff_file: $df}}')
|
|
497
499
|
}
|
|
498
500
|
slice security ALL ALL
|