forgedock 1.16.2 → 1.17.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 +92 -0
- package/bin/engine/phases.mjs +86 -26
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +13 -5
- package/bin/engine/telemetry-seam.mjs +62 -0
- package/bin/engine/telemetry-seam.test.mjs +60 -0
- package/bin/engine/telemetry.mjs +148 -0
- package/bin/engine/telemetry.test.mjs +120 -0
- package/bin/engine-cli.mjs +12 -1
- package/bin/engine.mjs +145 -40
- package/bin/forgedock.mjs +7 -1
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +187 -0
- package/bin/runner.mjs +7 -0
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +140 -0
- package/bin/stats/index.mjs +154 -0
- package/bin/stats/local-source.mjs +79 -0
- package/bin/stats/package.json +6 -0
- package/bin/stats/render.mjs +82 -0
- package/bin/stats/stats.test.mjs +224 -0
- package/commands/cleanup.md +83 -2
- package/commands/deploy-info.md +8 -4
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +17 -2
- package/commands/orchestrate/phase-5-cleanup.md +18 -2
- package/commands/pipeline-health/phase-1-context.md +2 -0
- package/commands/pipeline-health/phase-2-metrics.md +12 -5
- package/commands/pipeline-status.md +21 -1
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents.md +10 -2
- package/commands/review-pr-staging.md +102 -25
- package/commands/review-pr.md +119 -103
- package/commands/work-on/build/implement.md +26 -5
- package/commands/work-on/build/validate.md +3 -2
- package/commands/work-on/build.md +95 -64
- package/commands/work-on/close.md +17 -6
- package/commands/work-on/decompose.md +45 -4
- package/commands/work-on/investigate.md +6 -2
- package/commands/work-on/remediate.md +22 -6
- package/commands/work-on/review.md +2 -2
- package/docs/CONFIG.md +33 -0
- package/docs/OUTCOME-CODES.md +48 -0
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +62 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +13 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/scripts/builder-resume-match.test.sh +58 -0
- package/scripts/cap-findings.sh +110 -0
- package/scripts/cap-findings.test.sh +130 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +65 -16
- package/scripts/classify-finding.test.sh +43 -1
- package/scripts/classify-grep-exit.sh +20 -0
- package/scripts/classify-grep-exit.test.sh +46 -0
- package/scripts/close-parent-c3.test.sh +54 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +5 -0
- package/scripts/doctor-pipeline-state.sh +43 -0
- package/scripts/file-deferred-followups.sh +159 -0
- package/scripts/file-deferred-followups.test.sh +106 -0
- package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
- package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
- package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
- package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
- package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
- package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
- package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
- package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
- package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
- package/scripts/forge-root.test.sh +8 -3
- package/scripts/lib/fence-bootstrap.sh +4 -3
- package/scripts/lib/pr-issue-number.sh +18 -0
- package/scripts/lib/review-template-resolve.sh +41 -0
- package/scripts/lib/telemetry.sh +168 -0
- package/scripts/remediate-liveness.sh +42 -6
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-pr-domain-map.test.sh +8 -3
- package/scripts/review-pr-gate2.test.sh +15 -0
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +65 -5
- package/scripts/review-prepare.sh +517 -0
- package/scripts/review-prepare.test.sh +270 -0
- package/scripts/review-roster.sh +172 -0
- package/scripts/review-roster.test.sh +163 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/wait-ci-green.sh +8 -1
- package/templates/forge.yaml.minimal +9 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Tests for scripts/review-prepare.sh (review-pr Phases 1-3B + delta/slices) and its shared libs. A stubbed `gh` and throwaway git repos
|
|
6
|
+
# stand in for GitHub and the PR checkout. Run: FORGEDOCK_HOME=$PWD bash scripts/review-prepare.test.sh (bash 3.2 compatible)
|
|
7
|
+
|
|
8
|
+
set -u
|
|
9
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
10
|
+
ROOT=$(cd "$DIR/.." && pwd)
|
|
11
|
+
pass=0; fail=0
|
|
12
|
+
ok() { pass=$((pass+1)); }
|
|
13
|
+
bad() { fail=$((fail+1)); echo "FAIL: $1"; }
|
|
14
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/review-prepare-test.XXXXXX")
|
|
15
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
16
|
+
command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not installed"; exit 0; }
|
|
17
|
+
command -v yq >/dev/null 2>&1 || { echo "SKIP: yq not installed"; exit 0; }
|
|
18
|
+
|
|
19
|
+
export HOME="$WORK/home"; mkdir -p "$HOME"
|
|
20
|
+
unset FORGE_ROOT FORGE_HOME CLAUDE_PLUGIN_ROOT FORGE_CONFIG FORGE_SCRATCHPAD
|
|
21
|
+
export REVIEW_PREPARE_SLEEP=0 GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
|
22
|
+
|
|
23
|
+
# ---- stubbed gh: every answer comes from files under $FIX ----
|
|
24
|
+
FIX="$WORK/fix"; mkdir -p "$FIX" "$WORK/bin"
|
|
25
|
+
cat > "$WORK/bin/gh" <<'STUB'
|
|
26
|
+
#!/usr/bin/env bash
|
|
27
|
+
a="$*"
|
|
28
|
+
case "$a" in
|
|
29
|
+
"pr view"*"mergeable,mergeStateStatus"*)
|
|
30
|
+
f="$FIX/merge.seq"; l=$(head -n 1 "$f"); [ "$(wc -l < "$f")" -gt 1 ] && sed -i.bak 1d "$f"; echo "$l" ;;
|
|
31
|
+
"pr view"*"--json body"*) cat "$FIX/body.txt" ;;
|
|
32
|
+
"pr view"*) cat "$FIX/prinfo.json" ;;
|
|
33
|
+
"pr diff"*"--name-only"*) [ -f "$FIX/difffail" ] && exit 1; cat "$FIX/files" ;;
|
|
34
|
+
"pr diff"*) cat "$FIX/diff" ;;
|
|
35
|
+
"pr list"*) cat "$FIX/prior" 2>/dev/null || echo 1 ;;
|
|
36
|
+
"api --paginate"*) [ -f "$FIX/comments.fail" ] && exit 1; cat "$FIX/comments.json" 2>/dev/null || echo '[]' ;;
|
|
37
|
+
"issue view"*"labels"*) cat "$FIX/labels" 2>/dev/null || echo false ;;
|
|
38
|
+
"issue view"*"body"*) cat "$FIX/issuebody" 2>/dev/null ;;
|
|
39
|
+
*) exit 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
STUB
|
|
42
|
+
chmod +x "$WORK/bin/gh"; export FIX PATH="$WORK/bin:$PATH"
|
|
43
|
+
|
|
44
|
+
# ---- fake trusted install roots (the real one, or a trimmed copy with a stub delta resolver) ----
|
|
45
|
+
mkroot() { # mkroot <dir> [no-roster] [delta-stub-file] [templates]
|
|
46
|
+
local r="$1"; mkdir -p "$r/scripts/lib"
|
|
47
|
+
cp "$ROOT/scripts/lib/forge-resolve.sh" "$ROOT/scripts/lib/review-delta-mode.sh" "$ROOT/scripts/lib/pr-issue-number.sh" "$ROOT/scripts/lib/review-template-resolve.sh" "$r/scripts/lib/"
|
|
48
|
+
cp "$ROOT/scripts/trusted-comments.sh" "$r/scripts/"
|
|
49
|
+
[ "${2:-}" = "no-roster" ] || cp "$ROOT/scripts/review-roster.sh" "$r/scripts/"
|
|
50
|
+
[ -n "${3:-}" ] && cp "$3" "$r/scripts/review-delta.sh"
|
|
51
|
+
if [ -n "${4:-}" ]; then mkdir -p "$r/commands/review-pr-agents"; echo "# protocols" > "$r/commands/review-pr-agents/protocols.md"; fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ---- throwaway PR checkout: main (A) + one more commit (B = PR head) ----
|
|
55
|
+
REPO_DIR="$WORK/repo"; mkdir -p "$REPO_DIR/services/api"
|
|
56
|
+
git -C "$REPO_DIR" init -q -b main
|
|
57
|
+
echo a > "$REPO_DIR/a.txt"; git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm A
|
|
58
|
+
SHA_A=$(git -C "$REPO_DIR" rev-parse HEAD)
|
|
59
|
+
git -C "$REPO_DIR" checkout -q -b pr-branch; echo b > "$REPO_DIR/services/api/auth.py"
|
|
60
|
+
git -C "$REPO_DIR" add -A; git -C "$REPO_DIR" commit -qm B
|
|
61
|
+
SHA_B=$(git -C "$REPO_DIR" rev-parse HEAD)
|
|
62
|
+
git -C "$REPO_DIR" checkout -q main
|
|
63
|
+
git -C "$REPO_DIR" remote add origin "$REPO_DIR"
|
|
64
|
+
|
|
65
|
+
CFG="$WORK/forge.yaml"
|
|
66
|
+
printf 'project:\n name: Demo\nverification:\n commands:\n typescript:\n build: "npm run build"\n python:\n format: "ruff format --check"\n' > "$CFG"
|
|
67
|
+
|
|
68
|
+
# setup <head> <base> <files> <diff> [sha]
|
|
69
|
+
setup() {
|
|
70
|
+
rm -f "${FIX:?}"/*; printf 'MERGEABLE|CLEAN\n' > "$FIX/merge.seq"; : > "$FIX/body.txt"
|
|
71
|
+
printf '{"number":7,"title":"t","author":{"login":"dev"},"baseRefName":"%s","headRefName":"%s","headRefOid":"%s","additions":5,"deletions":2,"changedFiles":1}\n' "$2" "$1" "${5:-$SHA_B}" > "$FIX/prinfo.json"
|
|
72
|
+
printf '%s\n' "$3" > "$FIX/files"; printf '%s\n' "$4" > "$FIX/diff"
|
|
73
|
+
}
|
|
74
|
+
CTX=""; RC=0
|
|
75
|
+
# prep <install root> [extra args...]
|
|
76
|
+
prep() {
|
|
77
|
+
local r="$1"; shift
|
|
78
|
+
CTX=$(cd "$REPO_DIR" && FORGEDOCK_HOME="$r" FORGE_CONFIG="${CFG_OVERRIDE:-$CFG}" bash "$ROOT/scripts/review-prepare.sh" --pr 7 --repo o/r --out-dir "$WORK/out" "$@" 2>"$WORK/stderr"); RC=$?
|
|
79
|
+
}
|
|
80
|
+
jqv() { printf '%s' "$CTX" | jq -r "$1" 2>/dev/null; }
|
|
81
|
+
eq() { local got; got=$(jqv "$2"); if [ "$got" = "$3" ]; then ok; else bad "$1: $2 = '$got', want '$3'"; fi; }
|
|
82
|
+
rc_is() { if [ "$RC" = "$2" ]; then ok; else bad "$1: exit $RC, want $2 ($(head -c 300 "$WORK/stderr"))"; fi; }
|
|
83
|
+
|
|
84
|
+
DIFF_PLAIN='diff --git a/README.md b/README.md
|
|
85
|
+
+hello'
|
|
86
|
+
DIFF_AUTH_BILL='diff --git a/services/api/auth.py b/services/api/auth.py
|
|
87
|
+
+def f(): jwt = get_current_user(); stripe.charge()
|
|
88
|
+
diff --git a/services/api/billing.py b/services/api/billing.py
|
|
89
|
+
+credit = 1'
|
|
90
|
+
|
|
91
|
+
# --- 1. happy path, schema, small tier ---
|
|
92
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
93
|
+
prep "$ROOT"
|
|
94
|
+
rc_is "happy path" 0
|
|
95
|
+
eq "schema" .schema_version 1; eq "status" .status OK; eq "pr number" .pr.number 7; eq "review sha" .pr.review_sha "$SHA_B"
|
|
96
|
+
eq "sha7" .pr.review_sha_short "$(printf '%s' "$SHA_B" | cut -c1-7)"
|
|
97
|
+
eq "small tier" .roster.tier small; eq "small roster" '.roster.agents | join(" ")' security
|
|
98
|
+
eq "lines" '.pr.additions + .pr.deletions' 7
|
|
99
|
+
eq "first-time false" .pr.first_time_contributor false
|
|
100
|
+
eq "template source resolved" '.templates.source != "none"' true
|
|
101
|
+
eq "delta FULL" .delta.mode FULL
|
|
102
|
+
|
|
103
|
+
# --- 2. merge health ---
|
|
104
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'UNKNOWN|UNKNOWN\nUNKNOWN|UNKNOWN\nMERGEABLE|CLEAN\n' > "$FIX/merge.seq"
|
|
105
|
+
prep "$ROOT"; eq "retry count" .merge_health.retries 2; eq "retry result" .merge_health.mergeable MERGEABLE; eq "retry state" .merge_health.state CLEAN
|
|
106
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'CONFLICTING|DIRTY\n' > "$FIX/merge.seq"
|
|
107
|
+
prep "$ROOT"; eq "conflicting" .merge_health.mergeable CONFLICTING; eq "conflicting state" .merge_health.state DIRTY; eq "no retry on CONFLICTING" .merge_health.retries 0
|
|
108
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'UNKNOWN|UNKNOWN\n' > "$FIX/merge.seq"
|
|
109
|
+
prep "$ROOT"; eq "unknown stays unknown after 3 retries" .merge_health.retries 3; eq "still UNKNOWN" .merge_health.mergeable UNKNOWN
|
|
110
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'garbage|$(rm -rf)\n' > "$FIX/merge.seq"
|
|
111
|
+
prep "$ROOT"; eq "garbage state normalized" .merge_health.state UNKNOWN
|
|
112
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; echo 0 > "$FIX/prior"
|
|
113
|
+
prep "$ROOT"; eq "first-time contributor" .pr.first_time_contributor true
|
|
114
|
+
|
|
115
|
+
# --- 3. classification + scores ---
|
|
116
|
+
setup feat/x staging "services/api/auth.py
|
|
117
|
+
services/api/billing.py" "$DIFF_AUTH_BILL"
|
|
118
|
+
prep "$ROOT"
|
|
119
|
+
eq "services has API" '.classification.services | index("API") != null' true
|
|
120
|
+
eq "domains has AUTH" '.classification.domains | index("AUTH") != null' true
|
|
121
|
+
eq "domains has BILLING" '.classification.domains | index("BILLING") != null' true
|
|
122
|
+
eq "auth score" '.scores.AUTH > 0' true; eq "billing score" '.scores.BILLING > 0' true
|
|
123
|
+
eq "security baseline" .scores.SECURITY 1
|
|
124
|
+
eq "risk FINANCIAL" '.risk_signals | index("FINANCIAL") != null' true
|
|
125
|
+
eq "slice auth file exists" '.slices.auth.diff_file | length > 0' true
|
|
126
|
+
if grep -q "auth.py" "$WORK/out/slice-auth.diff" 2>/dev/null; then ok; else bad "auth slice should hold the auth.py diff"; fi
|
|
127
|
+
if grep -q "billing.py" "$WORK/out/slice-auth.diff" 2>/dev/null; then bad "auth slice leaked the billing diff"; else ok; fi
|
|
128
|
+
eq "auth slice paths" '.slices.auth.paths | join(" ")' "services/api/auth.py"
|
|
129
|
+
eq "security slice is the full diff" '.slices.security.paths | length' 2
|
|
130
|
+
|
|
131
|
+
# --- 4. roster: thorough, scraping config ordering, unresolved roster script ---
|
|
132
|
+
setup feat/x staging "services/api/auth.py
|
|
133
|
+
services/api/billing.py" "$DIFF_AUTH_BILL"
|
|
134
|
+
prep "$ROOT" --thorough true
|
|
135
|
+
eq "thorough tier" .roster.tier full
|
|
136
|
+
eq "thorough roster has auth+billing+concurrency" '.roster.agents | (index("auth") != null) and (index("billing") != null) and (index("concurrency") != null)' true
|
|
137
|
+
SCRAPE_DIFF='diff --git a/x/scraper.py b/x/scraper.py
|
|
138
|
+
+stealth scrape'
|
|
139
|
+
setup feat/x staging "x/scraper.py" "$SCRAPE_DIFF"
|
|
140
|
+
prep "$ROOT" --thorough true; eq "scraping off without config" '.roster.agents | index("scraper") != null' false
|
|
141
|
+
printf 'review:\n domains:\n scraping: "tiers"\n' > "$WORK/scrape.yaml"
|
|
142
|
+
CFG_OVERRIDE="$WORK/scrape.yaml" prep "$ROOT" --thorough true
|
|
143
|
+
eq "scraping scored on first pass once configured" '.roster.agents | index("scraper") != null' true
|
|
144
|
+
eq "scraping configured flag" .config.scraping_configured true
|
|
145
|
+
eq "absent yq key is empty, not null" '.config.domains.auth' ""
|
|
146
|
+
mkroot "$WORK/root-noroster" no-roster
|
|
147
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
148
|
+
prep "$WORK/root-noroster"
|
|
149
|
+
eq "unresolved roster script falls back to full" .roster.tier full
|
|
150
|
+
eq "unresolved roster still has security" '.roster.agents | index("security") != null' true
|
|
151
|
+
|
|
152
|
+
# --- 5. delta mode ---
|
|
153
|
+
cat > "$WORK/delta-stub.sh" <<STUB
|
|
154
|
+
#!/usr/bin/env bash
|
|
155
|
+
case "\${DELTA_KIND:-DELTA}" in
|
|
156
|
+
DELTA) printf 'DELTA %s..%s\nFULL_ROUNDS=%s\n' "$SHA_A" "$SHA_B" "\${DELTA_ROUNDS:-1}" ;;
|
|
157
|
+
BASE_SYNC_ONLY) printf 'BASE_SYNC_ONLY %s\nFULL_ROUNDS=1\n' "$SHA_A" ;;
|
|
158
|
+
JUNK) echo nonsense ;;
|
|
159
|
+
esac
|
|
160
|
+
STUB
|
|
161
|
+
mkroot "$WORK/root-delta" "" "$WORK/delta-stub.sh" templates
|
|
162
|
+
FIXFILES="services/api/auth.py
|
|
163
|
+
services/api/billing.py"
|
|
164
|
+
setup feat/x staging "$FIXFILES" "$DIFF_AUTH_BILL"
|
|
165
|
+
DELTA_KIND=DELTA DELTA_ROUNDS=1 prep "$WORK/root-delta"
|
|
166
|
+
eq "DELTA mode" .delta.mode DELTA; eq "DELTA from" .delta.from "$SHA_A"; eq "DELTA files" '.delta.files | join(" ")' "services/api/auth.py"
|
|
167
|
+
eq "early rounds keep the 3B roster" '.roster.agents | index("security") != null' true
|
|
168
|
+
eq "DELTA slice paths narrow to the delta" '.slices.security.paths | join(" ")' "services/api/auth.py"
|
|
169
|
+
eq "full_files stays whole" '.full_files | length' 2
|
|
170
|
+
DELTA_KIND=DELTA DELTA_ROUNDS=2 prep "$WORK/root-delta"
|
|
171
|
+
eq "round 3 roster is security + touched domains" '.roster.agents | join(" ")' "security auth api"
|
|
172
|
+
DELTA_KIND=BASE_SYNC_ONLY prep "$WORK/root-delta"
|
|
173
|
+
eq "BASE_SYNC_ONLY mode" .delta.mode BASE_SYNC_ONLY; eq "carried from" .delta.carried_from "$SHA_A"
|
|
174
|
+
eq "BASE_SYNC_ONLY roster empty" '.roster.agents | length' 0; eq "BASE_SYNC_ONLY no slices" '.slices | length' 0
|
|
175
|
+
DELTA_KIND=JUNK prep "$WORK/root-delta"; eq "junk resolver output is FULL" .delta.mode FULL
|
|
176
|
+
mkroot "$WORK/root-nodelta" "" "" templates
|
|
177
|
+
prep "$WORK/root-nodelta"; eq "unresolved delta script is FULL" .delta.mode FULL
|
|
178
|
+
DELTA_KIND=DELTA prep "$WORK/root-delta" --thorough true; eq "thorough never goes DELTA" .delta.mode FULL
|
|
179
|
+
setup feat/x staging "$FIXFILES" "$DIFF_AUTH_BILL" "notasha"
|
|
180
|
+
DELTA_KIND=DELTA prep "$WORK/root-delta"; eq "unvalidated sha is FULL" .delta.mode FULL
|
|
181
|
+
|
|
182
|
+
# --- 6. full-build lane: empty trusted config is BLOCKED, never a silent skip ---
|
|
183
|
+
TS_DIFF='diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
|
|
184
|
+
+x'
|
|
185
|
+
printf 'project:\n name: Demo\nverification:\n commands:\n typescript:\n typecheck: "tsc"\n' > "$WORK/nobuild.yaml"
|
|
186
|
+
setup milestone/m1 staging "web/src/app/page.tsx" "$TS_DIFF"
|
|
187
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT"
|
|
188
|
+
rc_is "empty ts build on a milestone lane" 3; eq "BLOCKED status" .status BLOCKED
|
|
189
|
+
eq "blocker names typescript.build" '.blockers | join(" ") | test("typescript.build")' true
|
|
190
|
+
eq "config status blocked" .phase2.build_config.status blocked; eq "lane flag" .lane.requires_full_build true
|
|
191
|
+
prep "$ROOT"; rc_is "configured ts build passes" 0; eq "status OK" .status OK
|
|
192
|
+
PY_DIFF='diff --git a/app/x.py b/app/x.py
|
|
193
|
+
+x'
|
|
194
|
+
printf 'project:\n name: Demo\n' > "$WORK/empty.yaml"
|
|
195
|
+
setup milestone/m1 staging "app/x.py" "$PY_DIFF"
|
|
196
|
+
CFG_OVERRIDE="$WORK/empty.yaml" prep "$ROOT"; rc_is "empty python.format on full-build lane" 3
|
|
197
|
+
eq "blocker names python.format" '.blockers | join(" ") | test("python.format")' true
|
|
198
|
+
setup milestone/m1 staging "README.md" "$DIFF_PLAIN"
|
|
199
|
+
CFG_OVERRIDE="$WORK/empty.yaml" prep "$ROOT"; rc_is "docs-only milestone needs no build config" 0
|
|
200
|
+
CFG_OVERRIDE="$WORK/missing.yaml" prep "$ROOT"; rc_is "no forge.yaml at all on a full-build lane" 3
|
|
201
|
+
setup feat/x staging "web/src/app/page.tsx" "$TS_DIFF"
|
|
202
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT"; rc_is "empty ts build on a normal lane is fine" 0
|
|
203
|
+
setup staging main "web/src/app/page.tsx" "$TS_DIFF"
|
|
204
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT"; rc_is "staging to main requires the build" 3
|
|
205
|
+
|
|
206
|
+
# --- 7. templates: none is a fatal setup error ---
|
|
207
|
+
mkroot "$WORK/root-notpl"
|
|
208
|
+
mkdir -p "$WORK/emptyrepo"; printf 'paths:\n root: %s\n' "$WORK/emptyrepo" > "$WORK/emptyroot.yaml"
|
|
209
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
210
|
+
CFG_OVERRIDE="$WORK/emptyroot.yaml" prep "$WORK/root-notpl"
|
|
211
|
+
rc_is "no template source" 3; eq "templates none" .templates.source none
|
|
212
|
+
|
|
213
|
+
# --- 8. issue number extraction and trusted contract ---
|
|
214
|
+
. "$ROOT/scripts/lib/pr-issue-number.sh"
|
|
215
|
+
chk() { local got; got=$(pr_issue_number_of "$2"); if [ "$got" = "$3" ]; then ok; else bad "$1: got '$got', want '$3'"; fi; }
|
|
216
|
+
chk "Closes" "Closes #12" 12
|
|
217
|
+
chk "closes lowercase" "intro
|
|
218
|
+
closes #7 and more" 7
|
|
219
|
+
chk "Fixes" "Fixes #345" 345
|
|
220
|
+
chk "Resolved" "Resolved #9" 9
|
|
221
|
+
chk "no reference yields empty, not the body" "Just a description with no issue reference" ""
|
|
222
|
+
chk "bare #N is not a closing keyword" "see #44" ""
|
|
223
|
+
chk "non numeric" "Closes #abc" ""
|
|
224
|
+
chk "first wins" "Closes #1, Fixes #2" 1
|
|
225
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'Just text, no issue\n' > "$FIX/body.txt"
|
|
226
|
+
prep "$ROOT"; eq "no linked issue" .contract.issue null; eq "no flags" .contract.flags ""
|
|
227
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'Fixes #55\n' > "$FIX/body.txt"
|
|
228
|
+
printf '[{"body":"<!-- FORGE:CONTRACT -->\\n**Risk**: HIGH\\nbilling change","author_association":"OWNER","user":{"login":"o","type":"User"}}]' > "$FIX/comments.json"
|
|
229
|
+
prep "$ROOT"; eq "trusted contract issue" .contract.issue 55
|
|
230
|
+
eq "trusted contract HIGH_RISK" '.contract.flags | test("HIGH_RISK")' true; eq "trusted contract billing flag" '.contract.flags | test("CONTRACT_BILLING")' true
|
|
231
|
+
eq "HIGH_RISK lifts the roster above small" '.roster.tier != "small"' true
|
|
232
|
+
printf '[{"body":"<!-- FORGE:CONTRACT -->\\n**Risk**: HIGH","author_association":"NONE","user":{"login":"evil","type":"User"}}]' > "$FIX/comments.json"
|
|
233
|
+
prep "$ROOT"; eq "planted contract from an untrusted author is ignored" .contract.flags ""; eq "roster stays small" .roster.tier small
|
|
234
|
+
touch "$FIX/comments.fail"
|
|
235
|
+
prep "$ROOT"; eq "a failed contract read widens, never narrows" '.contract.flags | test("HIGH_RISK")' true
|
|
236
|
+
rm -f "$FIX/comments.fail"
|
|
237
|
+
mkroot "$WORK/root-notrust"; rm "$WORK/root-notrust/scripts/trusted-comments.sh"
|
|
238
|
+
prep "$WORK/root-notrust"; eq "unresolved trust script widens, never narrows" '.contract.flags | test("HIGH_RISK")' true
|
|
239
|
+
|
|
240
|
+
# --- 9. restore_ref: git checkout -f of the recorded ref on every exit path ---
|
|
241
|
+
setup feat/x staging "services/api/auth.py" "$DIFF_AUTH_BILL"
|
|
242
|
+
echo dirty > "$REPO_DIR/a.txt"
|
|
243
|
+
prep "$ROOT" --checkout-pr
|
|
244
|
+
rc_is "checkout-pr run" 0
|
|
245
|
+
if [ "$(git -C "$REPO_DIR" symbolic-ref --short HEAD 2>/dev/null)" = "main" ]; then ok; else bad "restore_ref left HEAD detached, want main"; fi
|
|
246
|
+
git -C "$REPO_DIR" checkout -f -q main
|
|
247
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN" "0000000000000000000000000000000000000000"
|
|
248
|
+
prep "$ROOT" --checkout-pr
|
|
249
|
+
rc_is "unknown sha cannot be checked out" 3
|
|
250
|
+
if [ "$(git -C "$REPO_DIR" symbolic-ref --short HEAD 2>/dev/null)" = "main" ]; then ok; else bad "failed checkout moved HEAD"; fi
|
|
251
|
+
if grep -q 'checkout -f "\$ORIG_REF"' "$ROOT/scripts/review-prepare.sh"; then ok; else bad "restore_ref must use git checkout -f"; fi
|
|
252
|
+
|
|
253
|
+
# --- 10. fail closed on gh and bad arguments ---
|
|
254
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
255
|
+
prep "$ROOT"; rc_is "gh pr diff failure" 3; eq "blocked on gh failure" .status BLOCKED
|
|
256
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
257
|
+
for bad_args in "--pr abc --repo o/r" "--pr 7 --repo noslash" "--pr 7 --repo o/r;x/y" "--pr 7 --repo o/r --bogus" "--repo o/r --pr"; do
|
|
258
|
+
# shellcheck disable=SC2086
|
|
259
|
+
(cd "$REPO_DIR" && FORGEDOCK_HOME="$ROOT" bash "$ROOT/scripts/review-prepare.sh" $bad_args >/dev/null 2>&1); rcx=$?
|
|
260
|
+
if [ "$rcx" = 2 ]; then ok; else bad "args '$bad_args' exit $rcx, want 2"; fi
|
|
261
|
+
done
|
|
262
|
+
# every helper resolves through the trusted resolver, never the cwd
|
|
263
|
+
if grep -q 'forge_resolve' "$ROOT/scripts/review-prepare.sh" && grep -q 'schema_version' "$ROOT/scripts/review-prepare.sh"; then ok; else bad "script must use forge_resolve and document schema_version"; fi
|
|
264
|
+
mkdir -p "$WORK/hostile/scripts"; printf '#!/usr/bin/env bash\necho SELECTED_AGENTS=evil\n' > "$WORK/hostile/scripts/review-roster.sh"
|
|
265
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
266
|
+
CTX=$(cd "$WORK/hostile" && FORGEDOCK_HOME="$ROOT" FORGE_CONFIG="$CFG" bash "$ROOT/scripts/review-prepare.sh" --pr 7 --repo o/r --out-dir "$WORK/out" 2>/dev/null)
|
|
267
|
+
eq "a hostile cwd roster script is never used" '.roster.agents | index("evil") != null' false
|
|
268
|
+
|
|
269
|
+
echo "review-prepare tests: $pass passed, $fail failed"
|
|
270
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-roster.sh — risk-tiered reviewer roster for /review-pr Phase 3B and /review-pr-staging (forge#3599).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Pure function: flags in, key=value lines out. No network, no git, no eval; every flag value is data.
|
|
7
|
+
# Resolve it through forge_resolve (trusted FORGE_ROOT / plugin cache), never from the repo under review.
|
|
8
|
+
#
|
|
9
|
+
# Usage: review-roster.sh [--scores "AUTH=n BILLING=n CONCURRENCY=n DATABASE=n INFRA=n SECURITY=n SCRAPING=n FRONTEND=n API=n"]
|
|
10
|
+
# [--changed-lines N] [--changed-files N] [--files-file PATH|-] [--fix-pr true|false]
|
|
11
|
+
# [--source-severity SEV] [--source-domain DOMAIN] [--contract-flags "HIGH_RISK CONTRACT_AUTH ..."]
|
|
12
|
+
# [--churn true|false] [--danger-zone true|false] [--thorough true|false] [--milestone true|false]
|
|
13
|
+
# [--scraping-configured true|false] [--database-container true|false] [--spec-cli true|false]
|
|
14
|
+
# Output (stdout): SELECTED_AGENTS=<canonical marker domains, space separated> TIER=<tier> REASON=<text>
|
|
15
|
+
# Exit: 0 always for a parseable call; 2 on an unknown flag (callers fall back to the full union, never a narrower roster).
|
|
16
|
+
#
|
|
17
|
+
# Tiers, evaluated in this order:
|
|
18
|
+
# full THOROUGH or milestone->staging: Security plus every domain with a score > 0.
|
|
19
|
+
# fix-pr the PR closes a `review-finding` issue: Security, plus the source finding's domain when it was HIGH+.
|
|
20
|
+
# small <= 60 changed lines, or one file of <= 150 lines: Security only.
|
|
21
|
+
# medium 61-150 lines: Security plus the score-escalation table (no top-2 baseline).
|
|
22
|
+
# standard > 150 lines, or an unknown size (fail toward more agents): top-2 domains by score plus the table.
|
|
23
|
+
# Path triggers escalate in EVERY tier except full (which already runs the union): CONTRACT HIGH_RISK, cross-critical
|
|
24
|
+
# pairs, churn hot-spot, danger zone, bin/engine* (Concurrency + spec-cli), bin/ (spec-cli), auth/session/trust-filter
|
|
25
|
+
# files (Auth), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
|
|
26
|
+
# Auth and Infra are path-gated: a diff-text score alone never selects them. Security is a hard floor in every tier.
|
|
27
|
+
# spec-cli is added only with --spec-cli true (the persona file exists in the repo under review); in the standard tier it
|
|
28
|
+
# also covers commands/**. Bash 3.2 portable (no mapfile, associative arrays, grep -P).
|
|
29
|
+
set -u
|
|
30
|
+
|
|
31
|
+
S_AUTH=0; S_BILLING=0; S_CONCURRENCY=0; S_DATABASE=0; S_INFRA=0; S_SCRAPING=0; S_FRONTEND=0; S_API=0
|
|
32
|
+
LINES=""; NFILES=""; FILES_FILE=""; FIX_PR=false; SRC_SEV=""; SRC_DOM=""; CFLAGS=""
|
|
33
|
+
CHURN=false; DANGER=false; THOROUGH=false; MILESTONE=false; SCRAPING_ON=false; DB_CONTAINER=false; SPEC_CLI=false
|
|
34
|
+
|
|
35
|
+
num() { case "${1:-}" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$1" ;; esac; }
|
|
36
|
+
agent_domain() { local l; l=$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]'); case "$l" in infrastructure) l=infra ;; scraping) l=scraper ;; web) l=frontend ;; esac; printf '%s' "$l"; }
|
|
37
|
+
add_agent() {
|
|
38
|
+
local AGENT CUR HAVE=""
|
|
39
|
+
AGENT=$(agent_domain "${1}")
|
|
40
|
+
for CUR in $SELECTED_AGENTS; do
|
|
41
|
+
[ "$(agent_domain "$CUR")" = "$AGENT" ] && HAVE=1
|
|
42
|
+
done
|
|
43
|
+
[ -n "$HAVE" ] || SELECTED_AGENTS="$SELECTED_AGENTS $AGENT"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
while [ $# -gt 0 ]; do
|
|
47
|
+
case "$1" in
|
|
48
|
+
--scores)
|
|
49
|
+
for kv in ${2:-}; do
|
|
50
|
+
v=$(num "${kv#*=}")
|
|
51
|
+
case "${kv%%=*}" in
|
|
52
|
+
AUTH) S_AUTH=$v ;; BILLING) S_BILLING=$v ;; CONCURRENCY) S_CONCURRENCY=$v ;; DATABASE) S_DATABASE=$v ;;
|
|
53
|
+
INFRA) S_INFRA=$v ;; SCRAPING) S_SCRAPING=$v ;; FRONTEND) S_FRONTEND=$v ;; API) S_API=$v ;;
|
|
54
|
+
esac
|
|
55
|
+
done; shift 2 ;;
|
|
56
|
+
--changed-lines) LINES="${2:-}"; shift 2 ;;
|
|
57
|
+
--changed-files) NFILES="${2:-}"; shift 2 ;;
|
|
58
|
+
--files-file) FILES_FILE="${2:-}"; shift 2 ;;
|
|
59
|
+
--fix-pr) FIX_PR="${2:-false}"; shift 2 ;;
|
|
60
|
+
--source-severity) SRC_SEV="${2:-}"; shift 2 ;;
|
|
61
|
+
--source-domain) SRC_DOM="${2:-}"; shift 2 ;;
|
|
62
|
+
--contract-flags) CFLAGS="${2:-}"; shift 2 ;;
|
|
63
|
+
--churn) CHURN="${2:-false}"; shift 2 ;;
|
|
64
|
+
--danger-zone) DANGER="${2:-false}"; shift 2 ;;
|
|
65
|
+
--thorough) THOROUGH="${2:-false}"; shift 2 ;;
|
|
66
|
+
--milestone) MILESTONE="${2:-false}"; shift 2 ;;
|
|
67
|
+
--scraping-configured) SCRAPING_ON="${2:-false}"; shift 2 ;;
|
|
68
|
+
--database-container) DB_CONTAINER="${2:-false}"; shift 2 ;;
|
|
69
|
+
--spec-cli) SPEC_CLI="${2:-false}"; shift 2 ;;
|
|
70
|
+
*) echo "review-roster: unknown argument: $1" >&2; exit 2 ;;
|
|
71
|
+
esac
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
FILES=""
|
|
75
|
+
if [ "$FILES_FILE" = "-" ]; then FILES=$(cat)
|
|
76
|
+
elif [ -n "$FILES_FILE" ] && [ -f "$FILES_FILE" ]; then FILES=$(cat "$FILES_FILE"); fi
|
|
77
|
+
[ -n "$NFILES" ] || NFILES=$(printf '%s\n' "$FILES" | grep -c . || true)
|
|
78
|
+
case "$LINES" in ''|*[!0-9]*) LINES_KNOWN=false; LINES=0 ;; *) LINES_KNOWN=true ;; esac
|
|
79
|
+
case "$NFILES" in ''|*[!0-9]*) NFILES=0 ;; esac
|
|
80
|
+
|
|
81
|
+
SELECTED_AGENTS="security"
|
|
82
|
+
TIER=""; REASON=""
|
|
83
|
+
|
|
84
|
+
# Domains that may be picked by score alone. Auth and Infra are path-gated, Scraping is opt-in.
|
|
85
|
+
top_domains() { # $1 = how many
|
|
86
|
+
local d out="" list=""
|
|
87
|
+
list="$S_BILLING:BILLING $S_CONCURRENCY:CONCURRENCY $S_DATABASE:DATABASE $S_FRONTEND:FRONTEND $S_API:API"
|
|
88
|
+
[ "$SCRAPING_ON" = "true" ] && list="$list $S_SCRAPING:SCRAPING"
|
|
89
|
+
for d in $(printf '%s\n' "$list" | tr ' ' '\n' | sort -t: -k1,1 -rn | head -n "$1"); do
|
|
90
|
+
[ "${d%%:*}" -gt 0 ] && out="$out ${d#*:}"
|
|
91
|
+
done
|
|
92
|
+
printf '%s' "$out"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if [ "$THOROUGH" = "true" ] || [ "$MILESTONE" = "true" ]; then
|
|
96
|
+
TIER=full; REASON="thorough or milestone-to-staging: full union"
|
|
97
|
+
[ "$S_AUTH" -gt 0 ] && add_agent Auth
|
|
98
|
+
[ "$S_BILLING" -gt 0 ] && { add_agent Billing; add_agent Concurrency; }
|
|
99
|
+
[ "$S_DATABASE" -gt 0 ] && add_agent Database
|
|
100
|
+
[ "$S_INFRA" -gt 0 ] && add_agent Infra
|
|
101
|
+
[ "$S_SCRAPING" -gt 0 ] && [ "$SCRAPING_ON" = "true" ] && add_agent Scraping
|
|
102
|
+
[ "$S_FRONTEND" -gt 0 ] && add_agent Frontend
|
|
103
|
+
[ "$S_API" -gt 0 ] && add_agent API
|
|
104
|
+
printf 'SELECTED_AGENTS=%s\nTIER=%s\nREASON=%s\n' "${SELECTED_AGENTS# }" "$TIER" "$REASON"
|
|
105
|
+
exit 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
HIGH_RISK=false
|
|
109
|
+
case " $CFLAGS " in *" HIGH_RISK "*) HIGH_RISK=true ;; esac
|
|
110
|
+
|
|
111
|
+
if [ "$FIX_PR" = "true" ]; then
|
|
112
|
+
TIER=fix-pr; REASON="fix PR for a review-finding issue"
|
|
113
|
+
elif [ "$LINES_KNOWN" = "true" ] && { [ "$LINES" -le 60 ] || { [ "$NFILES" -eq 1 ] && [ "$LINES" -le 150 ]; }; }; then
|
|
114
|
+
TIER=small; REASON="lines=$LINES files=$NFILES"
|
|
115
|
+
elif [ "$LINES_KNOWN" = "true" ] && [ "$LINES" -le 150 ]; then
|
|
116
|
+
TIER=medium; REASON="lines=$LINES"
|
|
117
|
+
else
|
|
118
|
+
TIER=standard
|
|
119
|
+
if [ "$LINES_KNOWN" = "true" ]; then REASON="lines=$LINES"; else REASON="size unknown"; fi
|
|
120
|
+
fi
|
|
121
|
+
# CONTRACT HIGH_RISK is never narrowed: a small, medium or fix-PR roster is lifted to the standard roster.
|
|
122
|
+
BASE_TIER="$TIER"
|
|
123
|
+
if [ "$HIGH_RISK" = "true" ] && [ "$TIER" != "standard" ]; then TIER=standard; REASON="$REASON; CONTRACT HIGH_RISK lifts $BASE_TIER to standard"; fi
|
|
124
|
+
|
|
125
|
+
TRIG=""
|
|
126
|
+
trig() { case " $TRIG " in *" $1 "*) ;; *) TRIG="$TRIG $1" ;; esac; }
|
|
127
|
+
|
|
128
|
+
# Tier baseline
|
|
129
|
+
case "$TIER" in
|
|
130
|
+
fix-pr)
|
|
131
|
+
case "$(printf '%s' "$SRC_SEV" | tr '[:lower:]' '[:upper:]')" in
|
|
132
|
+
HIGH|CRITICAL|BLOCKER)
|
|
133
|
+
if [ -n "$SRC_DOM" ]; then add_agent "$SRC_DOM"; else for d in $(top_domains 1); do add_agent "$d"; done; fi
|
|
134
|
+
REASON="$REASON; source finding ${SRC_SEV} adds its domain" ;;
|
|
135
|
+
esac ;;
|
|
136
|
+
standard) for d in $(top_domains 2); do add_agent "$d"; done ;;
|
|
137
|
+
esac
|
|
138
|
+
|
|
139
|
+
# Score escalation table (medium and standard only; diff-text signals, so never in the small or fix-pr tiers)
|
|
140
|
+
if [ "$TIER" = "medium" ] || [ "$TIER" = "standard" ]; then
|
|
141
|
+
[ "$S_BILLING" -ge 3 ] && { add_agent Billing; add_agent Concurrency; trig billing-score; }
|
|
142
|
+
[ "$S_DATABASE" -ge 3 ] && { add_agent Database; trig database-score; }
|
|
143
|
+
[ "$S_SCRAPING" -ge 3 ] && [ "$SCRAPING_ON" = "true" ] && { add_agent Scraping; trig scraping; }
|
|
144
|
+
[ "$S_INFRA" -ge 3 ] && [ "$DB_CONTAINER" = "true" ] && { add_agent Infra; trig db-container; }
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Path and contract triggers (every tier)
|
|
148
|
+
if [ "$HIGH_RISK" = "true" ]; then
|
|
149
|
+
trig contract-high-risk
|
|
150
|
+
case " $CFLAGS " in *" CONTRACT_AUTH "*) add_agent Auth ;; esac
|
|
151
|
+
case " $CFLAGS " in *" CONTRACT_BILLING "*) add_agent Billing; add_agent Concurrency ;; esac
|
|
152
|
+
case " $CFLAGS " in *" CONTRACT_DATABASE "*) add_agent Database ;; esac
|
|
153
|
+
fi
|
|
154
|
+
{ [ "$S_AUTH" -ge 2 ] && [ "$S_BILLING" -ge 2 ]; } && { add_agent Auth; add_agent Billing; add_agent Concurrency; trig cross-critical; }
|
|
155
|
+
{ [ "$S_AUTH" -ge 2 ] && [ "$S_DATABASE" -ge 3 ]; } && { add_agent Auth; add_agent Database; trig cross-critical; }
|
|
156
|
+
{ [ "$S_BILLING" -ge 2 ] && [ "$S_CONCURRENCY" -ge 2 ]; } && { add_agent Billing; add_agent Concurrency; add_agent Database; trig cross-critical; }
|
|
157
|
+
if [ "$CHURN" = "true" ] || [ "$DANGER" = "true" ]; then
|
|
158
|
+
for d in $(top_domains 1); do add_agent "$d"; trig churn-or-danger-zone; done
|
|
159
|
+
fi
|
|
160
|
+
hit() { printf '%s\n' "$FILES" | grep -qiE "$1"; }
|
|
161
|
+
hit '(^|[/_.-])(auth|authn|authz|session|sessions|jwt|oauth|login|logout|password|trust[-_]?filter|trusted[-_a-z]*)([/_.-]|$)' && { add_agent Auth; trig auth-path; }
|
|
162
|
+
hit '^\.github/|(^|/)Dockerfile|docker-compose|(^|/)(deploy|infra|traefik|nginx|k8s|helm|terraform)/|\.tf$' && { add_agent Infra; trig infra-path; }
|
|
163
|
+
hit 'migration|\.sql$' && { add_agent Database; trig migration-path; }
|
|
164
|
+
hit '^sdk/|openapi.*\.json$|openapi-versions/' && { add_agent API; trig sdk-path; }
|
|
165
|
+
hit '^bin/engine' && { add_agent Concurrency; trig bin-engine; }
|
|
166
|
+
if [ "$SPEC_CLI" = "true" ]; then
|
|
167
|
+
hit '^bin/' && { add_agent spec-cli; trig bin; }
|
|
168
|
+
[ "$TIER" = "standard" ] && hit '^commands/' && { add_agent spec-cli; trig commands; }
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
[ -n "$TRIG" ] && REASON="$REASON; triggers:$TRIG"
|
|
172
|
+
printf 'SELECTED_AGENTS=%s\nTIER=%s\nREASON=%s\n' "${SELECTED_AGENTS# }" "$TIER" "$REASON"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Tests for scripts/review-roster.sh (risk-tiered reviewer roster, forge#3599): the tier cases, the path triggers that
|
|
6
|
+
# escalate in every tier, and the regression table of the 13 review-driven holds. Run: bash scripts/review-roster.test.sh
|
|
7
|
+
# (bash 3.2 compatible)
|
|
8
|
+
#
|
|
9
|
+
# Hold table fixture: the changed-line count, file count and file list of each PR, the PR's linked-issue lineage, and
|
|
10
|
+
# the reviewers whose posted findings were CRITICAL or BLOCKER on it (from the FORGE:REVIEW-AGENT comments). The
|
|
11
|
+
# regression gate: at least one catching reviewer stays selected on every hold, and the sole catcher always does.
|
|
12
|
+
|
|
13
|
+
set -u
|
|
14
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
15
|
+
SCRIPT="$DIR/review-roster.sh"
|
|
16
|
+
pass=0; fail=0
|
|
17
|
+
ok() { pass=$((pass+1)); }
|
|
18
|
+
bad() { fail=$((fail+1)); echo "FAIL: $1"; }
|
|
19
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/review-roster.XXXXXX")
|
|
20
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
21
|
+
|
|
22
|
+
SEL=""; TIER=""; REASON=""
|
|
23
|
+
# run <files, newline separated> <review-roster flags...>
|
|
24
|
+
run() {
|
|
25
|
+
printf '%s\n' "$1" > "$WORK/files"; shift
|
|
26
|
+
out=$(bash "$SCRIPT" --files-file "$WORK/files" "$@") || { bad "script exited non-zero for: $*"; return 1; }
|
|
27
|
+
SEL=$(printf '%s\n' "$out" | sed -n 's/^SELECTED_AGENTS=//p')
|
|
28
|
+
TIER=$(printf '%s\n' "$out" | sed -n 's/^TIER=//p')
|
|
29
|
+
REASON=$(printf '%s\n' "$out" | sed -n 's/^REASON=//p')
|
|
30
|
+
}
|
|
31
|
+
has() { case " $SEL " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
|
32
|
+
expect_sel() { if [ "$SEL" = "$2" ]; then ok; else bad "$1: roster '$SEL', want '$2' (tier $TIER; $REASON)"; fi; }
|
|
33
|
+
expect_has() { if has "$2"; then ok; else bad "$1: '$2' missing from '$SEL' (tier $TIER; $REASON)"; fi; }
|
|
34
|
+
expect_not() { if has "$2"; then bad "$1: '$2' should not be in '$SEL' (tier $TIER)"; else ok; fi; }
|
|
35
|
+
expect_tier() { if [ "$TIER" = "$2" ]; then ok; else bad "$1: tier '$TIER', want '$2'"; fi; }
|
|
36
|
+
|
|
37
|
+
SC_QUIET="AUTH=0 BILLING=0 CONCURRENCY=0 DATABASE=0 INFRA=0 SECURITY=1 SCRAPING=0 FRONTEND=0 API=0"
|
|
38
|
+
SC_NOISY="AUTH=3 BILLING=1 CONCURRENCY=0 DATABASE=1 INFRA=3 SECURITY=4 SCRAPING=0 FRONTEND=2 API=2"
|
|
39
|
+
SC_BUSY="AUTH=3 BILLING=3 CONCURRENCY=3 DATABASE=3 INFRA=3 SECURITY=1 SCRAPING=0 FRONTEND=2 API=2"
|
|
40
|
+
|
|
41
|
+
# Case 1: a small diff gives Security only, even when diff-text scores are loud
|
|
42
|
+
run "commands/foo.md" --scores "$SC_NOISY" --changed-lines 40 --changed-files 1
|
|
43
|
+
expect_sel "small diff" "security"; expect_tier "small diff" small
|
|
44
|
+
run "scripts/a.sh
|
|
45
|
+
scripts/b.sh" --scores "$SC_NOISY" --changed-lines 60 --changed-files 2
|
|
46
|
+
expect_sel "60-line boundary" "security"
|
|
47
|
+
# a single file is small up to 150 lines, not beyond
|
|
48
|
+
run "scripts/a.sh" --scores "$SC_QUIET" --changed-lines 120 --changed-files 1
|
|
49
|
+
expect_tier "single file" small
|
|
50
|
+
run "scripts/a.sh" --scores "$SC_QUIET" --changed-lines 400 --changed-files 1
|
|
51
|
+
expect_tier "single large file" standard
|
|
52
|
+
|
|
53
|
+
# Case 2: a small diff touching bin/engine/** adds concurrency and spec-cli
|
|
54
|
+
run "bin/engine/phases.mjs" --scores "$SC_QUIET" --changed-lines 30 --changed-files 1 --spec-cli true
|
|
55
|
+
expect_has "small bin/engine" concurrency; expect_has "small bin/engine" spec-cli; expect_has "small bin/engine" security; expect_tier "small bin/engine" small
|
|
56
|
+
run "bin/engine/phases.mjs" --scores "$SC_QUIET" --changed-lines 30 --changed-files 1 --spec-cli false
|
|
57
|
+
expect_has "consumer repo bin/engine" concurrency; expect_not "consumer repo bin/engine" spec-cli
|
|
58
|
+
|
|
59
|
+
# Case 3: a fix PR gives Security only
|
|
60
|
+
run "commands/orchestrate/phase-4-execution.md
|
|
61
|
+
commands/work-on.md" --scores "$SC_NOISY" --changed-lines 200 --changed-files 2 --fix-pr true --source-severity MEDIUM
|
|
62
|
+
expect_sel "fix PR" "security"; expect_tier "fix PR" fix-pr
|
|
63
|
+
|
|
64
|
+
# Case 4: a fix PR whose source finding was HIGH adds the domain agent
|
|
65
|
+
run "commands/work-on.md" --scores "AUTH=0 BILLING=0 CONCURRENCY=2 DATABASE=0 INFRA=0 FRONTEND=0 API=0" --changed-lines 40 --fix-pr true --source-severity HIGH --source-domain Concurrency
|
|
66
|
+
expect_sel "fix PR, HIGH source finding" "security concurrency"
|
|
67
|
+
run "commands/work-on.md" --scores "AUTH=0 BILLING=0 CONCURRENCY=2 DATABASE=0 INFRA=0 FRONTEND=0 API=0" --changed-lines 40 --fix-pr true --source-severity critical
|
|
68
|
+
expect_sel "fix PR, CRITICAL source, top domain" "security concurrency"
|
|
69
|
+
|
|
70
|
+
# Case 5: CONTRACT HIGH_RISK escalates in every tier
|
|
71
|
+
for tier_args in "--changed-lines 30 --changed-files 1" "--changed-lines 100" "--changed-lines 400" "--changed-lines 30 --fix-pr true"; do
|
|
72
|
+
# shellcheck disable=SC2086
|
|
73
|
+
run "svc/x.py" --scores "$SC_QUIET" $tier_args --contract-flags "HIGH_RISK CONTRACT_AUTH CONTRACT_BILLING"
|
|
74
|
+
expect_has "HIGH_RISK ($tier_args)" auth; expect_has "HIGH_RISK ($tier_args)" billing; expect_has "HIGH_RISK ($tier_args)" concurrency
|
|
75
|
+
done
|
|
76
|
+
run "svc/x.py" --scores "$SC_QUIET" --changed-lines 30 --contract-flags "CONTRACT_AUTH"
|
|
77
|
+
expect_not "CONTRACT_AUTH without HIGH_RISK" auth
|
|
78
|
+
|
|
79
|
+
# Case 6: --thorough and milestone give the full union
|
|
80
|
+
run "svc/x.py" --scores "$SC_BUSY" --changed-lines 10 --thorough true
|
|
81
|
+
expect_tier "thorough" full
|
|
82
|
+
for d in security auth billing concurrency database infra frontend api; do expect_has "thorough" "$d"; done
|
|
83
|
+
expect_not "thorough, scraping not configured" scraper
|
|
84
|
+
run "svc/x.py" --scores "$SC_BUSY SCRAPING=3" --changed-lines 10 --milestone true --scraping-configured true
|
|
85
|
+
expect_tier "milestone" full; expect_has "milestone" scraper
|
|
86
|
+
|
|
87
|
+
# Path gates: auth and infra only by path; the other path triggers fire in the small tier
|
|
88
|
+
run "svc/app/routers/session.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
89
|
+
expect_has "auth path" auth
|
|
90
|
+
run "scripts/trusted-comments.sh" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
91
|
+
expect_has "trust-filter path" auth
|
|
92
|
+
run "commands/review-pr.md" --scores "AUTH=3 INFRA=3 DATABASE=0 BILLING=0 CONCURRENCY=0" --changed-lines 120
|
|
93
|
+
expect_not "auth from prose score alone" auth; expect_not "infra from prose score alone" infra
|
|
94
|
+
run ".github/workflows/ci.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
95
|
+
expect_has "infra path" infra
|
|
96
|
+
run "docker-compose.prod.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
97
|
+
expect_has "docker path" infra
|
|
98
|
+
run "db/migrations/0042_add.sql" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
99
|
+
expect_has "migration path" database
|
|
100
|
+
run "sdk/node/src/index.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
101
|
+
expect_has "sdk path" api
|
|
102
|
+
run "svc/x.py" --scores "AUTH=2 BILLING=2" --changed-lines 20 --changed-files 1
|
|
103
|
+
expect_has "cross-critical" auth; expect_has "cross-critical" billing
|
|
104
|
+
run "svc/x.py" --scores "BILLING=3 CONCURRENCY=3" --changed-lines 20 --changed-files 1 --churn true
|
|
105
|
+
expect_has "churn hot-spot top domain" billing
|
|
106
|
+
run "svc/x.py" --scores "BILLING=3" --changed-lines 20 --changed-files 2
|
|
107
|
+
expect_not "billing score in small tier" billing
|
|
108
|
+
run "svc/x.py
|
|
109
|
+
svc/y.py" --scores "BILLING=3" --changed-lines 100
|
|
110
|
+
expect_has "billing score in medium tier" billing; expect_has "billing score in medium tier" concurrency
|
|
111
|
+
|
|
112
|
+
# Tier shape: standard keeps the top-2 baseline, medium does not
|
|
113
|
+
run "web/src/app/a.tsx
|
|
114
|
+
web/src/b.tsx" --scores "FRONTEND=3 API=2 BILLING=1 DATABASE=1" --changed-lines 300
|
|
115
|
+
expect_tier "standard" standard; expect_has "standard" frontend; expect_has "standard" api; expect_not "standard top-2 only" billing
|
|
116
|
+
run "web/src/app/a.tsx
|
|
117
|
+
web/src/b.tsx" --scores "FRONTEND=3 API=2" --changed-lines 100
|
|
118
|
+
expect_sel "medium has no baseline" "security"
|
|
119
|
+
run "svc/x.py" --scores "FRONTEND=3" --changed-lines ""
|
|
120
|
+
expect_tier "unknown size" standard; expect_has "unknown size" frontend
|
|
121
|
+
run "commands/x.md
|
|
122
|
+
commands/y.md" --scores "$SC_QUIET" --changed-lines 300 --spec-cli true
|
|
123
|
+
expect_has "standard commands/** adds spec-cli" spec-cli
|
|
124
|
+
run "commands/x.md
|
|
125
|
+
commands/y.md" --scores "$SC_QUIET" --changed-lines 100 --spec-cli true
|
|
126
|
+
expect_not "medium commands/** has no spec-cli" spec-cli
|
|
127
|
+
|
|
128
|
+
# Roster output is canonical and duplicate free
|
|
129
|
+
run "bin/engine/a.mjs
|
|
130
|
+
.github/workflows/a.yml" --scores "$SC_BUSY" --changed-lines 300 --spec-cli true
|
|
131
|
+
n=$(printf '%s' "$SEL" | tr ' ' '\n' | grep -c .); u=$(printf '%s' "$SEL" | tr ' ' '\n' | sort -u | grep -c .)
|
|
132
|
+
if [ "$n" -eq "$u" ]; then ok; else bad "duplicate roster entries in '$SEL'"; fi
|
|
133
|
+
case "$SEL" in *[A-Z]*) bad "non-canonical roster '$SEL'" ;; *) ok ;; esac
|
|
134
|
+
|
|
135
|
+
# Unknown flag fails (callers fall back to the full union)
|
|
136
|
+
if bash "$SCRIPT" --bogus 1 >/dev/null 2>&1; then bad "unknown flag accepted"; else ok; fi
|
|
137
|
+
|
|
138
|
+
# Regression table: the 13 review-driven holds. Columns: PR | lines | files | fix-PR | catchers (CRITICAL/BLOCKER) | sole catcher | file list
|
|
139
|
+
hold() {
|
|
140
|
+
pr="$1"; lines="$2"; nf="$3"; fixpr="$4"; catchers="$5"; sole="$6"; files="$7"
|
|
141
|
+
run "$(printf '%s' "$files" | tr ' ' '\n')" --scores "$SC_QUIET" --changed-lines "$lines" --changed-files "$nf" --fix-pr "$fixpr" --spec-cli true
|
|
142
|
+
found=""
|
|
143
|
+
for c in $catchers; do has "$c" && found=1; done
|
|
144
|
+
if [ -n "$found" ]; then ok; else bad "hold #$pr: none of [$catchers] selected: '$SEL' (tier $TIER)"; fi
|
|
145
|
+
if [ -n "$sole" ]; then if has "$sole"; then ok; else bad "hold #$pr: sole catcher '$sole' not selected: '$SEL'"; fi; fi
|
|
146
|
+
if has security; then ok; else bad "hold #$pr: security missing"; fi
|
|
147
|
+
}
|
|
148
|
+
hold 3426 126 3 true "security concurrency" "" "commands/orchestrate/phase-4-execution.md commands/work-on.md commands/work-on/remediate.md"
|
|
149
|
+
hold 3438 227 8 false "spec-cli" "spec-cli" "commands/review-pr-agents/protocols.md commands/review-pr.md commands/work-on.md commands/work-on/remediate.md commands/work-on/review.md docs/WORK-ON-RUNTIME.md docs/spec/review-protocol.md scripts/forge-root.test.sh"
|
|
150
|
+
hold 3461 454 12 false "security spec-cli" "" "commands/work-on.md commands/work-on/build.md commands/work-on/build/implement.md commands/work-on/close.md commands/work-on/decompose.md docs/CONFIG.md docs/spec/forge-protocol-v1.md forge.yaml.example packages/protocol/src/types.js scripts/check-spec-markers.sh scripts/diff-size.sh scripts/diff-size.test.sh"
|
|
151
|
+
hold 3464 53 2 true "security concurrency" "" "commands/orchestrate/phase-4-execution.md commands/work-on.md"
|
|
152
|
+
hold 3480 56 3 true "security" "" "commands/orchestrate/phase-4-execution.md commands/work-on.md scripts/check-spec-markers.sh"
|
|
153
|
+
hold 3490 220 3 false "security" "" "commands/review-pr-staging.md commands/review-pr.md scripts/review-pr-arguments.test.sh"
|
|
154
|
+
hold 3500 263 5 false "security" "" ".github/workflows/check-commands.yml commands/work-on/build/validate.md commands/work-on/review.md scripts/check-branch-ancestry.sh scripts/check-branch-ancestry.test.sh"
|
|
155
|
+
hold 3502 813 11 false "concurrency" "concurrency" "bin/engine-cli.mjs bin/engine.mjs bin/engine/phases-args.test.mjs bin/engine/phases.mjs bin/engine/runlog.mjs bin/tests/engine-crash.test.mjs bin/tests/engine-phases.test.mjs bin/tests/engine-runlog.test.mjs bin/tests/engine.test.mjs docs/WORK-ON-RUNTIME.md packages/protocol/src/phases.js"
|
|
156
|
+
hold 3509 274 6 false "security spec-cli" "" "bin/engine.mjs bin/engine/phases.mjs bin/tests/engine-crash.test.mjs bin/tests/engine-phases.test.mjs bin/tests/engine-runlog.test.mjs bin/tests/engine.test.mjs"
|
|
157
|
+
hold 3513 205 7 false "security concurrency infra spec-cli" "" "commands/orchestrate/phase-4-execution.md commands/review-pr.md commands/work-on.md commands/work-on/remediate.md commands/work-on/review.md docs/WORK-ON-RUNTIME.md scripts/check-spec-markers.sh"
|
|
158
|
+
hold 3520 4977 9 false "concurrency" "concurrency" "bin/engine-cli.mjs bin/engine.mjs bin/engine/runlog.mjs bin/forgedock.mjs bin/runner.mjs bin/tests/engine.test.mjs bin/tests/runner.test.mjs commands/orchestrate/phase-4-execution.md devdocs/modules/runner.md"
|
|
159
|
+
hold 3524 156 5 false "spec-cli" "spec-cli" "bin/engine.mjs bin/engine/phases.mjs bin/runner.mjs bin/tests/engine-phases.test.mjs bin/tests/engine.test.mjs"
|
|
160
|
+
hold 3555 137 8 false "security" "" "bin/engine/phases.mjs bin/tests/engine-phases.test.mjs commands/work-on/build.md commands/work-on/build/implement.md commands/work-on/decompose.md commands/work-on/investigate.md packages/protocol/CHANGELOG.md packages/protocol/src/phases.js"
|
|
161
|
+
|
|
162
|
+
echo "review-roster: $pass passed, $fail failed"
|
|
163
|
+
[ "$fail" -eq 0 ]
|
|
@@ -19,6 +19,9 @@ set -u
|
|
|
19
19
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
20
20
|
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
|
21
21
|
cd "$REPO_ROOT" || exit 1
|
|
22
|
+
# Gate scripts emit best-effort telemetry (forge#3834); suites run with it off so stubbed gh calls never buffer into ~/.forge.
|
|
23
|
+
# scripts/telemetry.test.sh sets its own environment.
|
|
24
|
+
export FORGE_TELEMETRY=off
|
|
22
25
|
|
|
23
26
|
TIMEOUT_SECS="${SHELL_TEST_TIMEOUT:-300}"
|
|
24
27
|
# WIRE:PROVEN — manual: SHELL_TEST_TIMEOUT=abc and SHELL_TEST_TIMEOUT=0 both print the error and exit 2
|