forgedock 1.30.0 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +3 -2
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +362 -33
- package/bin/engine.mjs +47 -37
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +294 -1
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +150 -15
- package/bin/lean/conformance.test.mjs +175 -3
- package/bin/lean/context.mjs +116 -43
- package/bin/lean/context.test.mjs +139 -4
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +37 -15
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +650 -124
- package/bin/lean/lean.test.mjs +1450 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +261 -38
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +138 -13
- package/bin/lean/precheck.test.mjs +113 -1
- package/bin/lean/prefilter.mjs +159 -5
- package/bin/lean/prefilter.test.mjs +152 -3
- package/bin/lean/preflight.mjs +108 -3
- package/bin/lean/preflight.test.mjs +48 -1
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +101 -13
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +296 -13
- package/bin/lean/sequence.test.mjs +263 -2
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +72 -10
- package/bin/lean/verify.test.mjs +68 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- package/commands/orchestrate.md +3 -1
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +80 -5
- package/docs/OUTCOME-CODES.md +2 -1
- package/docs/STATS.md +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/trusted-comments.test.sh +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-staging-pre.test.sh — cases for scripts/review-staging-pre.sh (forge#4358): bundle PR set, open review-finding gate,
|
|
3
|
+
# 0B.1 scope. Since #4359 the script is the only copy of these blocks (the spec calls `forgedock review-staging`).
|
|
4
|
+
# gh is a PATH stub reading per-case fixtures; git runs against a throwaway repo with a local bare "origin".
|
|
5
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
PRE="$ROOT/scripts/review-staging-pre.sh"
|
|
10
|
+
PASS=0; FAILN=0
|
|
11
|
+
ok() { PASS=$((PASS+1)); }
|
|
12
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
13
|
+
command -v jq >/dev/null 2>&1 || { echo "review-staging-pre.test.sh: jq missing, skipped"; exit 0; }
|
|
14
|
+
command -v git >/dev/null 2>&1 || { echo "review-staging-pre.test.sh: git missing, skipped"; exit 0; }
|
|
15
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
16
|
+
export FORGEDOCK_HOME="$ROOT" # resolve helpers from this checkout, not an ambient install
|
|
17
|
+
|
|
18
|
+
# (1) the blocks live only here: present in the script, absent from the spec
|
|
19
|
+
for b in bundle-pr-set bundle-pr-kind bundle-delta-credit bundle-scope-loop; do
|
|
20
|
+
grep -qx "# BEGIN $b" "$PRE" && grep -qx "# END $b" "$PRE" && ok || bad "block $b missing from review-staging-pre.sh"
|
|
21
|
+
grep -qx "# BEGIN $b" "$ROOT/commands/review-pr-staging.md" && bad "block $b still copied into commands/review-pr-staging.md" || ok
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
# (2) usage errors
|
|
25
|
+
bash "$PRE" bogus --repo o/r --pr 1 >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unknown subcommand must exit 2"
|
|
26
|
+
bash "$PRE" gate --repo o/r --pr x --prs-file /dev/null >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "non-numeric --pr must exit 2"
|
|
27
|
+
bash "$PRE" gate --repo o/r --pr 9 --prs-file "$TMP/none" >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unreadable --prs-file must exit 2 (never an empty bundle)"
|
|
28
|
+
bash "$PRE" scope --repo o/r --pr 9 --default main --staging staging --prs-file /dev/null --sd "$TMP/sd" --bogus 1 >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unknown flag must exit 2"
|
|
29
|
+
|
|
30
|
+
# gh stub: fixtures in $FIX; posted comment bodies are appended to $FIX/posted
|
|
31
|
+
mkdir -p "$TMP/bin"
|
|
32
|
+
cat > "$TMP/bin/gh" <<'STUB'
|
|
33
|
+
#!/usr/bin/env bash
|
|
34
|
+
F="$FIX"; a="$*"
|
|
35
|
+
# fail-labels / fail-issues: the degraded-label / review-finding read fails (rate limit, 5xx, expired auth)
|
|
36
|
+
case "$a" in "pr view"*"--json labels"*) [ -f "$F/fail-labels" ] && exit 1 ;; "issue list"*) [ -f "$F/fail-issues" ] && exit 1 ;; esac
|
|
37
|
+
case "$1 $2" in
|
|
38
|
+
"pr comment") while [ $# -gt 0 ]; do [ "$1" = --body ] && { printf '%s\n@@\n' "$2" >> "$F/posted"; }; shift; done; exit 0 ;;
|
|
39
|
+
"pr diff") [ -f "$F/files-$3" ] && { cat "$F/files-$3"; exit 0; }; exit 1 ;;
|
|
40
|
+
"pr view")
|
|
41
|
+
case "$a" in *"--json labels"*) [ -f "$F/degraded-$3" ] && echo true || echo false; exit 0 ;; esac
|
|
42
|
+
[ -f "$F/pr-$3.json" ] && { cat "$F/pr-$3.json"; exit 0; }; exit 1 ;;
|
|
43
|
+
"issue list")
|
|
44
|
+
n=$(printf '%s' "$a" | sed -n 's/.*--search PR #\([0-9]*\).*/\1/p')
|
|
45
|
+
[ -f "$F/findings-$n.json" ] && cat "$F/findings-$n.json" || echo '[]'; exit 0 ;;
|
|
46
|
+
esac
|
|
47
|
+
if [ "$1" = api ]; then
|
|
48
|
+
case "$a" in
|
|
49
|
+
*"/comments"*) n=$(printf '%s' "$a" | sed -n 's#.*issues/\([0-9]*\)/comments.*#\1#p'); [ -f "$F/comments-$n.json" ] && cat "$F/comments-$n.json" || echo '[]'; exit 0 ;;
|
|
50
|
+
*"repos/o/r/issues/"*) n=$(printf '%s' "$a" | sed -n 's#.*repos/o/r/issues/\([0-9]*\).*#\1#p'); [ -f "$F/issue-$n" ] && echo issue || echo pr; exit 0 ;;
|
|
51
|
+
esac
|
|
52
|
+
fi
|
|
53
|
+
echo "gh stub: unhandled: $a" >&2; exit 1
|
|
54
|
+
STUB
|
|
55
|
+
chmod +x "$TMP/bin/gh"
|
|
56
|
+
export PATH="$TMP/bin:$PATH"
|
|
57
|
+
newfix() { FIX="$TMP/fix.$1"; rm -rf "$FIX"; mkdir -p "$FIX"; export FIX; }
|
|
58
|
+
|
|
59
|
+
# throwaway repo: main, then staging = merge of PR #5 (feature commit) + a direct commit citing issue #77
|
|
60
|
+
G() { git -C "$TMP/work" -c user.email=t@t -c user.name=t -c commit.gpgsign=false "$@"; }
|
|
61
|
+
git init -q --bare "$TMP/origin.git"
|
|
62
|
+
git init -q "$TMP/work"; G checkout -q -b main
|
|
63
|
+
echo base > "$TMP/work/base.txt"; G add -A; G commit -qm base; G remote add origin "$TMP/origin.git"; G push -q origin main
|
|
64
|
+
G checkout -q -b staging; G checkout -q -b feat
|
|
65
|
+
echo a > "$TMP/work/a.js"; G add -A; G commit -qm "feat: a"; FEAT=$(G rev-parse HEAD)
|
|
66
|
+
G checkout -q staging; G merge -q --no-ff feat -m "Merge pull request #5 from o/feat"; MERGE=$(G rev-parse HEAD)
|
|
67
|
+
mkdir -p "$TMP/work/.github/workflows"; echo b > "$TMP/work/b.js"; echo x > "$TMP/work/.github/workflows/ci.yml"
|
|
68
|
+
G add -A; G commit -qm "fix: thing (#77)"; DIRECT=$(G rev-parse HEAD); G push -q origin staging
|
|
69
|
+
run() { (cd "$TMP/work" && bash "$PRE" "$@"); }
|
|
70
|
+
|
|
71
|
+
# (3) bundle: the merge-subject PR is kept, the issue cited in a subject is skipped, the list is persisted
|
|
72
|
+
newfix bundle; : > "$FIX/issue-77"
|
|
73
|
+
OUT=$(run bundle --repo o/r --default main --staging staging --pr 9 --sd "$TMP/sd" 2>&1); RC=$?
|
|
74
|
+
[ "$RC" -eq 0 ] && ok || bad "bundle must exit 0 (got $RC: $OUT)"
|
|
75
|
+
[ "$(cat "$TMP/sd/9_bundle-prs.txt" 2>/dev/null)" = 5 ] && ok || bad "bundle must persist exactly PR 5 (got '$(cat "$TMP/sd/9_bundle-prs.txt" 2>/dev/null)')"
|
|
76
|
+
case "$OUT" in *"skipped): 77"*) ok ;; *) bad "bundle must report the skipped issue 77 (got '$OUT')" ;; esac
|
|
77
|
+
|
|
78
|
+
# (4) gate
|
|
79
|
+
printf '5\n' > "$TMP/prs"
|
|
80
|
+
newfix clean; OUT=$(run gate --repo o/r --pr 9 --prs-file "$TMP/prs" 2>&1); RC=$?
|
|
81
|
+
[ "$RC" -eq 0 ] && ok || bad "clean gate must exit 0 (got $RC)"
|
|
82
|
+
case "$OUT" in *"RESULT: PASS"*) ok ;; *) bad "clean gate must print RESULT: PASS" ;; esac
|
|
83
|
+
grep -qx '<!-- FORGE:GATE_PASS:TYPE=open-review-finding -->' "$FIX/posted" && head -1 "$FIX/posted" | grep -qx '<!-- FORGE:GATE_PASS -->' && ok || bad "clean gate must post the unchanged GATE_PASS body"
|
|
84
|
+
newfix degraded; : > "$FIX/degraded-5"
|
|
85
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; RC=$?
|
|
86
|
+
[ "$RC" -eq 1 ] && grep -q 'FORGE:GATE_FAILURE:TYPE=review-panel-integrity' "$FIX/posted" && ok || bad "degraded panel must block with review-panel-integrity (rc $RC)"
|
|
87
|
+
newfix finding; jq -n '[{number:41,title:"bug (PR #5)",body:"x"}]' > "$FIX/findings-5.json"
|
|
88
|
+
OUT=$(run gate --repo o/r --pr 9 --prs-file "$TMP/prs" 2>&1); RC=$?
|
|
89
|
+
[ "$RC" -eq 1 ] && ok || bad "an open finding must block (rc $RC)"
|
|
90
|
+
case "$OUT" in *"RESULT: BLOCK DEPLOY"*) ok ;; *) bad "blocked gate must print RESULT: BLOCK DEPLOY" ;; esac
|
|
91
|
+
grep -q 'FORGE:GATE_FAILURE:TYPE=open-review-finding|FINDINGS=1' "$FIX/posted" && ok || bad "blocked gate must post the GATE_FAILURE marker with FINDINGS=1"
|
|
92
|
+
newfix unanchored; jq -n '[{number:41,title:"bug (PR #55)",body:"x"}]' > "$FIX/findings-5.json"
|
|
93
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; [ $? -eq 0 ] && ok || bad "a finding for another PR (#55) must not block #5"
|
|
94
|
+
newfix override; jq -n '[{number:41,title:"bug (PR #5)",body:"x"}]' > "$FIX/findings-5.json"
|
|
95
|
+
jq -n '[{body:"OVERRIDE: shipping with open findings — known",author_association:"OWNER",user:{type:"User",login:"o"}}]' > "$FIX/comments-9.json"
|
|
96
|
+
OUT=$(run gate --repo o/r --pr 9 --prs-file "$TMP/prs" 2>&1); RC=$?
|
|
97
|
+
[ "$RC" -eq 0 ] && case "$OUT" in *"RESULT: PASS"*) true ;; *) false ;; esac && ok || bad "a trusted override must pass (rc $RC)"
|
|
98
|
+
newfix untrusted; jq -n '[{number:41,title:"bug (PR #5)",body:"x"}]' > "$FIX/findings-5.json"
|
|
99
|
+
jq -n '[{body:"OVERRIDE: shipping with open findings — x",author_association:"NONE",user:{type:"User",login:"z"}}]' > "$FIX/comments-9.json"
|
|
100
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; [ $? -eq 1 ] && ok || bad "an untrusted override must not pass"
|
|
101
|
+
# forge#4470: the gate fails closed. A failed gh read or an empty bundle never posts GATE_PASS.
|
|
102
|
+
printf '101\n102\n' > "$TMP/prs2"
|
|
103
|
+
newfix ghfail; : > "$FIX/fail-labels"; : > "$FIX/fail-issues"
|
|
104
|
+
OUT=$(run gate --repo o/r --pr 9 --prs-file "$TMP/prs2" 2>&1); RC=$?
|
|
105
|
+
[ "$RC" -eq 1 ] && ok || bad "gh read failure blocks: gate must exit 1 when every gh read fails (rc $RC)"
|
|
106
|
+
case "$OUT" in *"RESULT: BLOCK DEPLOY"*) ok ;; *) bad "gh read failure blocks: must print RESULT: BLOCK DEPLOY (got '$OUT')" ;; esac
|
|
107
|
+
! grep -q 'FORGE:GATE_PASS' "$FIX/posted" 2>/dev/null && ok || bad "gh read failure blocks: must not post GATE_PASS"
|
|
108
|
+
grep -q 'FORGE:GATE_FAILURE:TYPE=gate-read-failure' "$FIX/posted" 2>/dev/null && ok || bad "gh read failure blocks: must post TYPE=gate-read-failure"
|
|
109
|
+
newfix issuefail; : > "$FIX/fail-issues"
|
|
110
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; RC=$?
|
|
111
|
+
[ "$RC" -eq 1 ] && ! grep -q 'FORGE:GATE_PASS' "$FIX/posted" 2>/dev/null && ok || bad "a failed review-finding read alone must block without GATE_PASS (rc $RC)"
|
|
112
|
+
newfix labelfail; : > "$FIX/fail-labels"
|
|
113
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; RC=$?
|
|
114
|
+
[ "$RC" -eq 1 ] && ! grep -q 'FORGE:GATE_PASS' "$FIX/posted" 2>/dev/null && ok || bad "a failed degraded-label read alone must block without GATE_PASS (rc $RC)"
|
|
115
|
+
newfix readfailoverride; : > "$FIX/fail-issues"
|
|
116
|
+
jq -n '[{body:"OVERRIDE: shipping with open findings — known",author_association:"OWNER",user:{type:"User",login:"o"}}]' > "$FIX/comments-9.json"
|
|
117
|
+
run gate --repo o/r --pr 9 --prs-file "$TMP/prs" >/dev/null 2>&1; [ $? -eq 1 ] && ok || bad "a trusted override must not waive a read failure"
|
|
118
|
+
newfix emptyprs; : > "$TMP/prs-empty"
|
|
119
|
+
OUT=$(run gate --repo o/r --pr 9 --prs-file "$TMP/prs-empty" 2>&1); RC=$?
|
|
120
|
+
[ "$RC" -eq 1 ] && ! grep -q 'FORGE:GATE_PASS' "$FIX/posted" 2>/dev/null && ok || bad "an empty prs-file must block without GATE_PASS (rc $RC: $OUT)"
|
|
121
|
+
newfix emptybundle
|
|
122
|
+
run bundle --repo o/r --default staging --staging staging --pr 8 --sd "$TMP/sd" >/dev/null 2>&1; RC=$?
|
|
123
|
+
[ "$RC" -ne 0 ] && ok || bad "an empty bundle PR set must exit non-zero"
|
|
124
|
+
newfix fetchfail
|
|
125
|
+
run bundle --repo o/r --default main --staging no-such-branch --pr 8 --sd "$TMP/sd" >/dev/null 2>&1; RC=$?
|
|
126
|
+
[ "$RC" -ne 0 ] && ok || bad "a failed git fetch must exit non-zero"
|
|
127
|
+
|
|
128
|
+
# (5) scope: PR 5 reviewed at its final head -> scoped; the direct commit and its files are unreviewed
|
|
129
|
+
newfix scope; H7=${FEAT:0:7}
|
|
130
|
+
jq -n --arg h "$FEAT" --arg m "$MERGE" '{headRefOid:$h,mergeCommit:{oid:$m},commits:[{oid:$h}],state:"MERGED",baseRefName:"staging",labels:[]}' > "$FIX/pr-5.json"
|
|
131
|
+
printf 'a.js\n' > "$FIX/files-5"
|
|
132
|
+
jq -n --arg r "<!-- FORGE:REVIEW_ROUTE mode=single-pr spec=review-pr.md sha=$H7 -->" \
|
|
133
|
+
--arg g "$(printf '<!-- FORGE:REVIEW-AGENT:security -->\nReviewed-SHA: %s\n\n<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->' "$FEAT")" \
|
|
134
|
+
'[{body:$r,author_association:"NONE",user:{type:"Bot",login:"b"}},{body:$g,author_association:"NONE",user:{type:"Bot",login:"b"}}]' > "$FIX/comments-5.json"
|
|
135
|
+
OUT=$(run scope --repo o/r --default main --staging staging --pr 9 --prs-file "$TMP/prs" --sd "$TMP/sd" 2>&1); RC=$?
|
|
136
|
+
[ "$RC" -eq 0 ] && ok || bad "scope must exit 0 (got $RC: $OUT)"
|
|
137
|
+
J="$TMP/sd/9_scope.json"
|
|
138
|
+
[ "$(jq -r .scope_mode "$J" 2>/dev/null)" = scoped ] && ok || bad "a reviewed bundle must be scoped (got $(jq -c . "$J" 2>/dev/null); $OUT)"
|
|
139
|
+
[ "$(jq -r '.unreviewed_commits | join(" ")' "$J" 2>/dev/null)" = "$DIRECT" ] && ok || bad "only the direct commit is unreviewed (got $(jq -c .unreviewed_commits "$J" 2>/dev/null))"
|
|
140
|
+
jq -e '.unreviewed_files | index("b.js")' "$J" >/dev/null 2>&1 && ! jq -e '.unreviewed_files | index("a.js")' "$J" >/dev/null 2>&1 && ok || bad "unreviewed files must hold b.js and not a.js"
|
|
141
|
+
jq -e '.deploy_wiring_files | index(".github/workflows/ci.yml")' "$J" >/dev/null 2>&1 && ok || bad "workflow files are deploy wiring"
|
|
142
|
+
jq -e '.reviewed_pr_count == 1' "$J" >/dev/null 2>&1 && ok || bad "reviewed_pr_count must be 1"
|
|
143
|
+
grep -q '^SCOPE_MODE=scoped$' "$TMP/sd/9_scope.env" && grep -q '^5 a.js$' "$TMP/sd/9_bundle-pr-files.txt" && ok || bad "scope.env and bundle-pr-files.txt keep the spec's format"
|
|
144
|
+
case "$OUT" in *"SCOPE_MODE=scoped reviewed_prs=1"*) ok ;; *) bad "scope must print the one-line summary" ;; esac
|
|
145
|
+
newfix scopefail
|
|
146
|
+
run scope --repo o/r --default main --staging staging --pr 9 --prs-file "$TMP/prs" --sd "$TMP/sd" >/dev/null 2>&1
|
|
147
|
+
[ "$(jq -r .scope_mode "$TMP/sd/9_scope.json" 2>/dev/null)" = full ] && ok || bad "a failed gh pr view must widen scope to full"
|
|
148
|
+
|
|
149
|
+
echo "review-staging-pre.test.sh: passed=$PASS failed=$FAILN"
|
|
150
|
+
[ "$FAILN" -eq 0 ]
|
package/scripts/symbol-index.mjs
CHANGED
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
// run prints only {"unsupported":"ts"|"js","reason":"no-parser"} and exits 0.
|
|
9
9
|
// .py targets are handed to symbol-index.py. Never emits source text or literal values.
|
|
10
10
|
//
|
|
11
|
-
// Usage: node scripts/symbol-index.mjs <file | dir>...
|
|
11
|
+
// Usage: node scripts/symbol-index.mjs [--shape] <file | dir>...
|
|
12
12
|
// Output: {"symbols":[{name,kind,start,end,lines,public[,file]}], "edges":[{from,to}],
|
|
13
13
|
// "imports":[{module,names,line[,file]}], "cycles":[[module,...],...]}
|
|
14
14
|
// kind: function|async_function|class|method|const|interface|type|enum. public = exported (methods: exported class
|
|
15
15
|
// and not private/#private). One file: bare names; otherwise `file` on entries and "<module>:<symbol>" edge ends,
|
|
16
16
|
// module = path relative to its root without extension (index files name their directory). cycles = strongly
|
|
17
17
|
// connected components of the relative-import graph between indexed modules (`import type` ignored).
|
|
18
|
+
// --shape (forge#4449, bin/lean/apishape.mjs): functions, methods/accessors/constructors and consts initialised with
|
|
19
|
+
// an arrow or function expression also carry async, generator (`function*`) and sig: the normalized parameter list
|
|
20
|
+
// (`...` rest, name, `?` optional, `: <type text>`, `=` for a default but never its value) plus " -> <return type>".
|
|
21
|
+
// Kinds are unchanged; without --shape the output is unchanged. --shape is forwarded to symbol-index.py.
|
|
18
22
|
// Exit: 0 ok or unsupported | 2 usage or missing path.
|
|
19
23
|
|
|
20
24
|
import { spawnSync } from "node:child_process";
|
|
@@ -94,7 +98,18 @@ function tarjan(graph) {
|
|
|
94
98
|
return comps;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
function
|
|
101
|
+
function fnShape(ts, fn, src) {
|
|
102
|
+
const K = ts.SyntaxKind;
|
|
103
|
+
const params = fn.parameters.map((p) => `${p.dotDotDotToken ? "..." : ""}${p.name.getText(src)}${p.questionToken ? "?" : ""}`
|
|
104
|
+
+ `${p.type ? `: ${p.type.getText(src)}` : ""}${p.initializer ? "=" : ""}`);
|
|
105
|
+
return {
|
|
106
|
+
async: (ts.getModifiers?.(fn) ?? fn.modifiers ?? []).some((m) => m.kind === K.AsyncKeyword),
|
|
107
|
+
generator: !!fn.asteriskToken,
|
|
108
|
+
sig: `(${params.join(", ")})${fn.type ? ` -> ${fn.type.getText(src)}` : ""}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function index(ts, entries, multi, shape = false) {
|
|
98
113
|
const K = ts.SyntaxKind;
|
|
99
114
|
const byFile = new Map(entries.map((e) => [path.resolve(e.file), e]));
|
|
100
115
|
const symbols = [], imports = [], edges = new Set(), graph = new Map(entries.map((e) => [e.module, new Set()]));
|
|
@@ -114,7 +129,9 @@ function index(ts, entries, multi) {
|
|
|
114
129
|
tables.set(e.module, table);
|
|
115
130
|
const add = (name, kind, node, pub, owner = null) => {
|
|
116
131
|
const start = line(src, node.getStart(src)), end = line(src, node.getEnd());
|
|
117
|
-
|
|
132
|
+
const fn = !shape ? null : ts.isFunctionLike(node) ? node
|
|
133
|
+
: ts.isVariableDeclaration(node) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) ? node.initializer : null;
|
|
134
|
+
symbols.push({ name, kind, start, end, lines: end - start + 1, public: !!pub, ...(multi ? { file: e.file } : {}), ...(fn ? fnShape(ts, fn, src) : {}) });
|
|
118
135
|
table.set(name, { node, owner });
|
|
119
136
|
};
|
|
120
137
|
const exportNames = new Set();
|
|
@@ -218,9 +235,11 @@ function index(ts, entries, multi) {
|
|
|
218
235
|
};
|
|
219
236
|
}
|
|
220
237
|
|
|
221
|
-
function main(
|
|
238
|
+
function main(args) {
|
|
239
|
+
const shape = args.includes("--shape");
|
|
240
|
+
const argv = args.filter((a) => a !== "--shape");
|
|
222
241
|
if (!argv.length || argv[0] === "-h" || argv[0] === "--help") {
|
|
223
|
-
process.stderr.write("usage: symbol-index.mjs <file | dir>...\n");
|
|
242
|
+
process.stderr.write("usage: symbol-index.mjs [--shape] <file | dir>...\n");
|
|
224
243
|
return 2;
|
|
225
244
|
}
|
|
226
245
|
for (const p of argv) if (!existsSync(p)) { process.stderr.write(`symbol-index: no such path: ${p}\n`); return 2; }
|
|
@@ -229,7 +248,7 @@ function main(argv) {
|
|
|
229
248
|
if (exts.includes(".py")) {
|
|
230
249
|
if (exts.some((x) => x !== ".py")) return emit({ unsupported: "mixed", reason: "mixed-languages" }), 0;
|
|
231
250
|
const py = path.join(path.dirname(fileURLToPath(import.meta.url)), "symbol-index.py");
|
|
232
|
-
const r = spawnSync("python3", [py, ...argv], { stdio: "inherit" });
|
|
251
|
+
const r = spawnSync("python3", [py, ...(shape ? ["--shape"] : []), ...argv], { stdio: "inherit" });
|
|
233
252
|
if (r.error) return emit({ unsupported: "python", reason: "no-python3" }), 0;
|
|
234
253
|
return r.status ?? 2;
|
|
235
254
|
}
|
|
@@ -242,7 +261,7 @@ function main(argv) {
|
|
|
242
261
|
if (!ts) return emit({ unsupported: lang, reason: "no-parser" }), 0;
|
|
243
262
|
const multi = !(argv.length === 1 && files.length === 1);
|
|
244
263
|
let out;
|
|
245
|
-
try { out = index(ts, entries, multi); } catch (err) {
|
|
264
|
+
try { out = index(ts, entries, multi, shape); } catch (err) {
|
|
246
265
|
if (err && err.reason) return emit({ unsupported: err.lang, reason: err.reason }), 0;
|
|
247
266
|
throw err;
|
|
248
267
|
}
|
package/scripts/symbol-index.py
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
# symbol-index.py: deterministic Python symbol index + module import-cycle detector for refactor:split (forge#4208).
|
|
6
6
|
# stdlib only (ast). No LLM, no network. Never emits source text, docstrings or literal values.
|
|
7
7
|
#
|
|
8
|
-
# Usage: python3 scripts/symbol-index.py <file.py | dir>...
|
|
8
|
+
# Usage: python3 scripts/symbol-index.py [--shape] <file.py | dir>...
|
|
9
9
|
# Output (stdout, one JSON object):
|
|
10
10
|
# {"symbols":[{name,kind,start,end,lines,public[,file]}], "edges":[{from,to}],
|
|
11
11
|
# "imports":[{module,names,line[,file]}], "cycles":[[module,...],...]}
|
|
12
12
|
# kind: function|async_function|class|method|const. Methods (and nested classes) are named Class.method.
|
|
13
13
|
# start includes decorators; lines = end - start + 1. public = no leading underscore (method dunders count as
|
|
14
14
|
# public), and listed in a static __all__ when the module defines one.
|
|
15
|
+
# --shape (forge#4449, bin/lean/apishape.mjs): function/async_function/method entries also carry async (bool),
|
|
16
|
+
# generator (yield in the def's own scope) and sig: the normalized parameter list (kinds, names, annotation text,
|
|
17
|
+
# "=" for a default but never its value) plus " -> <return annotation>". Without --shape the output is unchanged.
|
|
15
18
|
# One .py file: names are bare. Otherwise (a directory or several files) each entry carries `file` and edge ends
|
|
16
19
|
# are "<module>:<symbol>"; module names are dotted paths relative to each root (a root that has __init__.py is
|
|
17
20
|
# itself the top package, so root .../pkg yields pkg.a). edges = references from one indexed symbol to another.
|
|
@@ -108,9 +111,47 @@ def module_imports(body):
|
|
|
108
111
|
return out
|
|
109
112
|
|
|
110
113
|
|
|
114
|
+
def is_generator(fn):
|
|
115
|
+
"""True when yield / yield from occurs in fn's own scope (not in nested defs, lambdas or classes)."""
|
|
116
|
+
work = list(ast.iter_child_nodes(fn))
|
|
117
|
+
while work:
|
|
118
|
+
n = work.pop()
|
|
119
|
+
if isinstance(n, (ast.Yield, ast.YieldFrom)):
|
|
120
|
+
return True
|
|
121
|
+
if not isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)):
|
|
122
|
+
work.extend(ast.iter_child_nodes(n))
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def signature(fn):
|
|
127
|
+
"""Normalized parameter list of a def: names, kinds, annotation text, '=' for a default (never its value)."""
|
|
128
|
+
a = fn.args
|
|
129
|
+
|
|
130
|
+
def one(arg, default=False, prefix=""):
|
|
131
|
+
out = prefix + arg.arg
|
|
132
|
+
if arg.annotation is not None:
|
|
133
|
+
out += ": " + ast.unparse(arg.annotation)
|
|
134
|
+
return out + ("=" if default else "")
|
|
135
|
+
|
|
136
|
+
positional = a.posonlyargs + a.args
|
|
137
|
+
first_default = len(positional) - len(a.defaults)
|
|
138
|
+
parts = [one(x, i >= first_default) for i, x in enumerate(positional)]
|
|
139
|
+
if a.posonlyargs:
|
|
140
|
+
parts.insert(len(a.posonlyargs), "/")
|
|
141
|
+
if a.vararg is not None:
|
|
142
|
+
parts.append(one(a.vararg, prefix="*"))
|
|
143
|
+
elif a.kwonlyargs:
|
|
144
|
+
parts.append("*")
|
|
145
|
+
parts += [one(x, d is not None) for x, d in zip(a.kwonlyargs, a.kw_defaults)]
|
|
146
|
+
if a.kwarg is not None:
|
|
147
|
+
parts.append(one(a.kwarg, prefix="**"))
|
|
148
|
+
sig = "(" + ", ".join(parts) + ")"
|
|
149
|
+
return sig + (" -> " + ast.unparse(fn.returns) if fn.returns is not None else "")
|
|
150
|
+
|
|
151
|
+
|
|
111
152
|
class Module:
|
|
112
|
-
def __init__(self, path, name, is_pkg):
|
|
113
|
-
self.path, self.name, self.is_pkg = path, name, is_pkg
|
|
153
|
+
def __init__(self, path, name, is_pkg, shape=False):
|
|
154
|
+
self.path, self.name, self.is_pkg, self.shape = path, name, is_pkg, shape
|
|
114
155
|
self.symbols = [] # dicts without file
|
|
115
156
|
self.nodes = {} # symbol name -> (ast node, owning class name or None)
|
|
116
157
|
self.imports = [] # (ast import node)
|
|
@@ -131,8 +172,11 @@ class Module:
|
|
|
131
172
|
|
|
132
173
|
def add(name, kind, node, public, owner=None):
|
|
133
174
|
start, end = def_start(node), node.end_lineno or node.lineno
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
sym = {"name": name, "kind": kind, "start": start, "end": end, "lines": end - start + 1, "public": public}
|
|
176
|
+
if self.shape and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
177
|
+
sym.update({"async": isinstance(node, ast.AsyncFunctionDef), "generator": is_generator(node),
|
|
178
|
+
"sig": signature(node)})
|
|
179
|
+
self.symbols.append(sym)
|
|
136
180
|
self.nodes[name] = (node, owner)
|
|
137
181
|
|
|
138
182
|
def add_class(node, prefix, public):
|
|
@@ -241,12 +285,12 @@ def tarjan(graph):
|
|
|
241
285
|
return comps
|
|
242
286
|
|
|
243
287
|
|
|
244
|
-
def build(paths):
|
|
288
|
+
def build(paths, shape=False):
|
|
245
289
|
entries = walk_roots(paths)
|
|
246
290
|
multi = not (len(entries) == 1 and os.path.isfile(paths[0]) and len(paths) == 1)
|
|
247
291
|
modules, by_name = [], {}
|
|
248
292
|
for path, name, is_pkg in entries:
|
|
249
|
-
m = Module(path, name, is_pkg)
|
|
293
|
+
m = Module(path, name, is_pkg, shape)
|
|
250
294
|
m.parse()
|
|
251
295
|
modules.append(m)
|
|
252
296
|
by_name.setdefault(name, m)
|
|
@@ -335,15 +379,17 @@ def build(paths):
|
|
|
335
379
|
|
|
336
380
|
|
|
337
381
|
def main(argv):
|
|
382
|
+
shape = "--shape" in argv
|
|
383
|
+
argv = [a for a in argv if a != "--shape"]
|
|
338
384
|
if not argv or argv[0] in ("-h", "--help"):
|
|
339
|
-
sys.stderr.write("usage: symbol-index.py <file.py | dir>...\n")
|
|
385
|
+
sys.stderr.write("usage: symbol-index.py [--shape] <file.py | dir>...\n")
|
|
340
386
|
return 2
|
|
341
387
|
for p in argv:
|
|
342
388
|
if not os.path.exists(p):
|
|
343
389
|
sys.stderr.write("symbol-index: no such path: %s\n" % p)
|
|
344
390
|
return 2
|
|
345
391
|
try:
|
|
346
|
-
out = build(argv)
|
|
392
|
+
out = build(argv, shape)
|
|
347
393
|
except Unsupported as u:
|
|
348
394
|
out = {"unsupported": u.lang, "reason": u.reason}
|
|
349
395
|
sys.stdout.write(json.dumps(out, sort_keys=True, separators=(",", ":")) + "\n")
|
|
@@ -67,7 +67,7 @@ eq "trusted synthesis accepted" "$(printf '[%s]' "$(c NONE Bot b "$FAKE_SYN")" |
|
|
|
67
67
|
eq "synthesis marker mid-body rejected" "$(printf '[%s]' "$(c NONE Bot b "intro${NL}${FAKE_SYN}")" | bash "$TC" count "$SYNTH_RE")" 0
|
|
68
68
|
eq "synthesis for a different head rejected" "$(printf '[%s]' "$(c NONE Bot b "<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->${NL}Reviewed-SHA: ${OTHER}")" | bash "$TC" count "$SYNTH_RE")" 0
|
|
69
69
|
|
|
70
|
-
# Staging review
|
|
70
|
+
# Staging review (scripts/review-staging-post.sh delivery, formerly the commands/review-pr-staging.md fences): same predicate, domains may carry digits/hyphens (bug-hunter-api)
|
|
71
71
|
STG_RE="^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
|
|
72
72
|
STG_DOM_RE="^<!-- FORGE:REVIEW-AGENT:bug-hunter-api -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
|
|
73
73
|
STG_TRUE="<!-- FORGE:REVIEW-AGENT:bug-hunter-api -->${NL}Reviewed-SHA: ${SHA}${NL}<!-- FINDING:real-2 -->"
|