forgedock 1.16.2 → 1.18.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 +114 -0
- package/bin/engine/phases.mjs +211 -100
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +20 -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 +17 -1
- package/bin/engine.mjs +227 -48
- package/bin/forgedock.mjs +8 -2
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +220 -0
- package/bin/runner.mjs +70 -4
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +211 -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 +312 -0
- package/commands/cleanup.md +90 -2
- package/commands/deploy-info.md +8 -4
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +28 -6
- 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 +196 -496
- package/commands/review-pr.md +512 -3134
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +13 -1
- package/commands/work-on/build/implement.md +28 -6
- package/commands/work-on/build/validate.md +5 -2
- package/commands/work-on/build.md +197 -145
- package/commands/work-on/close.md +66 -20
- package/commands/work-on/decompose.md +113 -34
- package/commands/work-on/investigate.md +15 -14
- package/commands/work-on/remediate.md +49 -19
- package/commands/work-on/review.md +7 -5
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +76 -24
- package/docs/OUTCOME-CODES.md +53 -0
- package/docs/RELEASE-NOTES.md +24 -0
- package/docs/STATS.md +63 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +19 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/packages/protocol/src/phases.js +5 -5
- 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-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -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 +123 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +97 -3
- package/scripts/diff-size.test.sh +59 -2
- 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 +18 -11
- 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/publish-staging-sync.test.sh +48 -0
- package/scripts/remediate-liveness.sh +42 -6
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-finalize.sh +1427 -0
- package/scripts/review-finalize.test.sh +833 -0
- package/scripts/review-pr-delta-wiring.test.sh +19 -72
- package/scripts/review-pr-domain-map.test.sh +23 -13
- package/scripts/review-pr-gate2.test.sh +24 -65
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +56 -80
- package/scripts/review-prepare.sh +553 -0
- package/scripts/review-prepare.test.sh +288 -0
- package/scripts/review-roster.sh +186 -0
- package/scripts/review-roster.test.sh +185 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/trusted-comments.test.sh +9 -2
- package/scripts/wait-ci-green.sh +8 -1
- package/scripts/wait-ci-green.test.sh +3 -3
- package/templates/forge.yaml.minimal +9 -0
|
@@ -0,0 +1,288 @@
|
|
|
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","isCrossRepository":%s,"additions":5,"deletions":2,"changedFiles":1}\n' "$2" "$1" "${5:-$SHA_B}" "${CROSS_REPO:-false}" > "$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 "review depth defaults to STANDARD" .roster.review_depth STANDARD
|
|
98
|
+
eq "review weight key present in context" '.roster | has("review_weight")' true
|
|
99
|
+
eq "small tier" .roster.tier small; eq "small roster" '.roster.agents | join(" ")' security
|
|
100
|
+
eq "lines" '.pr.additions + .pr.deletions' 7
|
|
101
|
+
eq "first-time false" .pr.first_time_contributor false
|
|
102
|
+
eq "template source resolved" '.templates.source != "none"' true
|
|
103
|
+
eq "delta FULL" .delta.mode FULL
|
|
104
|
+
|
|
105
|
+
# --- 2. merge health ---
|
|
106
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'UNKNOWN|UNKNOWN\nUNKNOWN|UNKNOWN\nMERGEABLE|CLEAN\n' > "$FIX/merge.seq"
|
|
107
|
+
prep "$ROOT"; eq "retry count" .merge_health.retries 2; eq "retry result" .merge_health.mergeable MERGEABLE; eq "retry state" .merge_health.state CLEAN
|
|
108
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'CONFLICTING|DIRTY\n' > "$FIX/merge.seq"
|
|
109
|
+
prep "$ROOT"; eq "conflicting" .merge_health.mergeable CONFLICTING; eq "conflicting state" .merge_health.state DIRTY; eq "no retry on CONFLICTING" .merge_health.retries 0
|
|
110
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'UNKNOWN|UNKNOWN\n' > "$FIX/merge.seq"
|
|
111
|
+
prep "$ROOT"; eq "unknown stays unknown after 3 retries" .merge_health.retries 3; eq "still UNKNOWN" .merge_health.mergeable UNKNOWN
|
|
112
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'garbage|$(rm -rf)\n' > "$FIX/merge.seq"
|
|
113
|
+
prep "$ROOT"; eq "garbage state normalized" .merge_health.state UNKNOWN
|
|
114
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; echo 0 > "$FIX/prior"
|
|
115
|
+
prep "$ROOT"; eq "first-time contributor" .pr.first_time_contributor true
|
|
116
|
+
|
|
117
|
+
# --- 3. classification + scores ---
|
|
118
|
+
setup feat/x staging "services/api/auth.py
|
|
119
|
+
services/api/billing.py" "$DIFF_AUTH_BILL"
|
|
120
|
+
prep "$ROOT"
|
|
121
|
+
eq "services has API" '.classification.services | index("API") != null' true
|
|
122
|
+
eq "domains has AUTH" '.classification.domains | index("AUTH") != null' true
|
|
123
|
+
eq "domains has BILLING" '.classification.domains | index("BILLING") != null' true
|
|
124
|
+
eq "auth score" '.scores.AUTH > 0' true; eq "billing score" '.scores.BILLING > 0' true
|
|
125
|
+
eq "security baseline" .scores.SECURITY 1
|
|
126
|
+
eq "risk FINANCIAL" '.risk_signals | index("FINANCIAL") != null' true
|
|
127
|
+
eq "slice auth file exists" '.slices.auth.diff_file | length > 0' true
|
|
128
|
+
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
|
|
129
|
+
if grep -q "billing.py" "$WORK/out/slice-auth.diff" 2>/dev/null; then bad "auth slice leaked the billing diff"; else ok; fi
|
|
130
|
+
eq "auth slice paths" '.slices.auth.paths | join(" ")' "services/api/auth.py"
|
|
131
|
+
eq "security slice is the full diff" '.slices.security.paths | length' 2
|
|
132
|
+
|
|
133
|
+
# --- 4. roster: thorough, scraping config ordering, unresolved roster script ---
|
|
134
|
+
setup feat/x staging "services/api/auth.py
|
|
135
|
+
services/api/billing.py" "$DIFF_AUTH_BILL"
|
|
136
|
+
prep "$ROOT" --thorough true
|
|
137
|
+
eq "thorough tier" .roster.tier full
|
|
138
|
+
eq "thorough roster has auth+billing+concurrency" '.roster.agents | (index("auth") != null) and (index("billing") != null) and (index("concurrency") != null)' true
|
|
139
|
+
SCRAPE_DIFF='diff --git a/x/scraper.py b/x/scraper.py
|
|
140
|
+
+stealth scrape'
|
|
141
|
+
setup feat/x staging "x/scraper.py" "$SCRAPE_DIFF"
|
|
142
|
+
prep "$ROOT" --thorough true; eq "scraping off without config" '.roster.agents | index("scraper") != null' false
|
|
143
|
+
printf 'review:\n domains:\n scraping: "tiers"\n' > "$WORK/scrape.yaml"
|
|
144
|
+
CFG_OVERRIDE="$WORK/scrape.yaml" prep "$ROOT" --thorough true
|
|
145
|
+
eq "scraping scored on first pass once configured" '.roster.agents | index("scraper") != null' true
|
|
146
|
+
eq "scraping configured flag" .config.scraping_configured true
|
|
147
|
+
eq "absent yq key is empty, not null" '.config.domains.auth' ""
|
|
148
|
+
mkroot "$WORK/root-noroster" no-roster
|
|
149
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
150
|
+
prep "$WORK/root-noroster"
|
|
151
|
+
eq "unresolved roster script falls back to full" .roster.tier full
|
|
152
|
+
eq "unresolved roster still has security" '.roster.agents | index("security") != null' true
|
|
153
|
+
|
|
154
|
+
# --- 5. delta mode ---
|
|
155
|
+
cat > "$WORK/delta-stub.sh" <<STUB
|
|
156
|
+
#!/usr/bin/env bash
|
|
157
|
+
case "\${DELTA_KIND:-DELTA}" in
|
|
158
|
+
DELTA) printf 'DELTA %s..%s\nFULL_ROUNDS=%s\n' "$SHA_A" "$SHA_B" "\${DELTA_ROUNDS:-1}" ;;
|
|
159
|
+
BASE_SYNC_ONLY) printf 'BASE_SYNC_ONLY %s\nFULL_ROUNDS=1\n' "$SHA_A" ;;
|
|
160
|
+
JUNK) echo nonsense ;;
|
|
161
|
+
esac
|
|
162
|
+
STUB
|
|
163
|
+
mkroot "$WORK/root-delta" "" "$WORK/delta-stub.sh" templates
|
|
164
|
+
FIXFILES="services/api/auth.py
|
|
165
|
+
services/api/billing.py"
|
|
166
|
+
setup feat/x staging "$FIXFILES" "$DIFF_AUTH_BILL"
|
|
167
|
+
DELTA_KIND=DELTA DELTA_ROUNDS=1 prep "$WORK/root-delta"
|
|
168
|
+
eq "DELTA mode" .delta.mode DELTA; eq "DELTA from" .delta.from "$SHA_A"; eq "DELTA files" '.delta.files | join(" ")' "services/api/auth.py"
|
|
169
|
+
eq "early rounds keep the 3B roster" '.roster.agents | index("security") != null' true
|
|
170
|
+
eq "DELTA slice paths narrow to the delta" '.slices.security.paths | join(" ")' "services/api/auth.py"
|
|
171
|
+
eq "full_files stays whole" '.full_files | length' 2
|
|
172
|
+
DELTA_KIND=DELTA DELTA_ROUNDS=2 prep "$WORK/root-delta"
|
|
173
|
+
eq "round 3 roster is security + touched domains" '.roster.agents | join(" ")' "security auth api"
|
|
174
|
+
DELTA_KIND=BASE_SYNC_ONLY prep "$WORK/root-delta"
|
|
175
|
+
eq "BASE_SYNC_ONLY mode" .delta.mode BASE_SYNC_ONLY; eq "carried from" .delta.carried_from "$SHA_A"
|
|
176
|
+
eq "BASE_SYNC_ONLY roster empty" '.roster.agents | length' 0; eq "BASE_SYNC_ONLY no slices" '.slices | length' 0
|
|
177
|
+
DELTA_KIND=JUNK prep "$WORK/root-delta"; eq "junk resolver output is FULL" .delta.mode FULL
|
|
178
|
+
mkroot "$WORK/root-nodelta" "" "" templates
|
|
179
|
+
prep "$WORK/root-nodelta"; eq "unresolved delta script is FULL" .delta.mode FULL
|
|
180
|
+
DELTA_KIND=DELTA prep "$WORK/root-delta" --thorough true; eq "thorough never goes DELTA" .delta.mode FULL
|
|
181
|
+
setup feat/x staging "$FIXFILES" "$DIFF_AUTH_BILL" "notasha"
|
|
182
|
+
DELTA_KIND=DELTA prep "$WORK/root-delta"; eq "unvalidated sha is FULL" .delta.mode FULL
|
|
183
|
+
|
|
184
|
+
# --- 6. full-build lane: empty trusted config is BLOCKED, never a silent skip ---
|
|
185
|
+
TS_DIFF='diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
|
|
186
|
+
+x'
|
|
187
|
+
printf 'project:\n name: Demo\nverification:\n commands:\n typescript:\n typecheck: "tsc"\n' > "$WORK/nobuild.yaml"
|
|
188
|
+
setup milestone/m1 staging "web/src/app/page.tsx" "$TS_DIFF"
|
|
189
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT"
|
|
190
|
+
rc_is "empty ts build on a milestone lane" 3; eq "BLOCKED status" .status BLOCKED
|
|
191
|
+
eq "blocker names typescript.build" '.blockers | join(" ") | test("typescript.build")' true
|
|
192
|
+
eq "config status blocked" .phase2.build_config.status blocked; eq "lane flag" .lane.requires_full_build true
|
|
193
|
+
prep "$ROOT"; rc_is "configured ts build passes" 0; eq "status OK" .status OK
|
|
194
|
+
# --build-gate advisory (review-pr-staging): an unconfigured build is a SKIPPED Phase 2 finding, never a block (#3952 QA-1)
|
|
195
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT" --build-gate advisory
|
|
196
|
+
rc_is "advisory build gate does not block" 0; eq "advisory status OK" .status OK
|
|
197
|
+
eq "advisory config status skipped" .phase2.build_config.status skipped
|
|
198
|
+
eq "advisory records SKIPPED finding" '.phase2.findings | join(" ") | test("SKIPPED — typescript.build")' true
|
|
199
|
+
PY_DIFF='diff --git a/app/x.py b/app/x.py
|
|
200
|
+
+x'
|
|
201
|
+
printf 'project:\n name: Demo\n' > "$WORK/empty.yaml"
|
|
202
|
+
setup milestone/m1 staging "app/x.py" "$PY_DIFF"
|
|
203
|
+
CFG_OVERRIDE="$WORK/empty.yaml" prep "$ROOT"; rc_is "empty python.format on full-build lane" 3
|
|
204
|
+
eq "blocker names python.format" '.blockers | join(" ") | test("python.format")' true
|
|
205
|
+
setup milestone/m1 staging "README.md" "$DIFF_PLAIN"
|
|
206
|
+
CFG_OVERRIDE="$WORK/empty.yaml" prep "$ROOT"; rc_is "docs-only milestone needs no build config" 0
|
|
207
|
+
CFG_OVERRIDE="$WORK/missing.yaml" prep "$ROOT"; rc_is "no forge.yaml at all on a full-build lane" 3
|
|
208
|
+
setup feat/x staging "web/src/app/page.tsx" "$TS_DIFF"
|
|
209
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT"; rc_is "empty ts build on a normal lane is fine" 0
|
|
210
|
+
setup staging main "web/src/app/page.tsx" "$TS_DIFF"
|
|
211
|
+
CFG_OVERRIDE="$WORK/nobuild.yaml" prep "$ROOT"; rc_is "staging to main requires the build" 3
|
|
212
|
+
|
|
213
|
+
# --- 7. templates: none is a fatal setup error ---
|
|
214
|
+
mkroot "$WORK/root-notpl"
|
|
215
|
+
mkdir -p "$WORK/emptyrepo"; printf 'paths:\n root: %s\n' "$WORK/emptyrepo" > "$WORK/emptyroot.yaml"
|
|
216
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
217
|
+
CFG_OVERRIDE="$WORK/emptyroot.yaml" prep "$WORK/root-notpl"
|
|
218
|
+
rc_is "no template source" 3; eq "templates none" .templates.source none
|
|
219
|
+
|
|
220
|
+
# --- 8. issue number extraction and trusted contract ---
|
|
221
|
+
. "$ROOT/scripts/lib/pr-issue-number.sh"
|
|
222
|
+
chk() { local got; got=$(pr_issue_number_of "$2"); if [ "$got" = "$3" ]; then ok; else bad "$1: got '$got', want '$3'"; fi; }
|
|
223
|
+
chk "Closes" "Closes #12" 12
|
|
224
|
+
chk "closes lowercase" "intro
|
|
225
|
+
closes #7 and more" 7
|
|
226
|
+
chk "Fixes" "Fixes #345" 345
|
|
227
|
+
chk "Resolved" "Resolved #9" 9
|
|
228
|
+
chk "no reference yields empty, not the body" "Just a description with no issue reference" ""
|
|
229
|
+
chk "bare #N is not a closing keyword" "see #44" ""
|
|
230
|
+
chk "non numeric" "Closes #abc" ""
|
|
231
|
+
chk "first wins" "Closes #1, Fixes #2" 1
|
|
232
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'Just text, no issue\n' > "$FIX/body.txt"
|
|
233
|
+
prep "$ROOT"; eq "no linked issue" .contract.issue null; eq "no flags" .contract.flags ""
|
|
234
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'Fixes #55\n' > "$FIX/body.txt"
|
|
235
|
+
printf '[{"body":"<!-- FORGE:CONTRACT -->\\n**Risk**: HIGH\\nbilling change","author_association":"OWNER","user":{"login":"o","type":"User"}}]' > "$FIX/comments.json"
|
|
236
|
+
prep "$ROOT"; eq "trusted contract issue" .contract.issue 55
|
|
237
|
+
eq "trusted contract HIGH_RISK" '.contract.flags | test("HIGH_RISK")' true; eq "trusted contract billing flag" '.contract.flags | test("CONTRACT_BILLING")' true
|
|
238
|
+
eq "HIGH_RISK lifts the roster above small" '.roster.tier != "small"' true
|
|
239
|
+
printf '[{"body":"<!-- FORGE:CONTRACT -->\\n**Risk**: HIGH","author_association":"NONE","user":{"login":"evil","type":"User"}}]' > "$FIX/comments.json"
|
|
240
|
+
prep "$ROOT"; eq "planted contract from an untrusted author is ignored" .contract.flags ""; eq "roster stays small" .roster.tier small
|
|
241
|
+
touch "$FIX/comments.fail"
|
|
242
|
+
prep "$ROOT"; eq "a failed contract read widens, never narrows" '.contract.flags | test("HIGH_RISK")' true
|
|
243
|
+
rm -f "$FIX/comments.fail"
|
|
244
|
+
mkroot "$WORK/root-notrust"; rm "$WORK/root-notrust/scripts/trusted-comments.sh"
|
|
245
|
+
prep "$WORK/root-notrust"; eq "unresolved trust script widens, never narrows" '.contract.flags | test("HIGH_RISK")' true
|
|
246
|
+
|
|
247
|
+
# --- 9. restore_ref: git checkout -f of the recorded ref on every exit path ---
|
|
248
|
+
setup feat/x staging "services/api/auth.py" "$DIFF_AUTH_BILL"
|
|
249
|
+
echo dirty > "$REPO_DIR/a.txt"
|
|
250
|
+
prep "$ROOT" --checkout-pr
|
|
251
|
+
rc_is "checkout-pr run" 0
|
|
252
|
+
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
|
|
253
|
+
git -C "$REPO_DIR" checkout -f -q main
|
|
254
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN" "0000000000000000000000000000000000000000"
|
|
255
|
+
prep "$ROOT" --checkout-pr
|
|
256
|
+
rc_is "unknown sha cannot be checked out" 3
|
|
257
|
+
if [ "$(git -C "$REPO_DIR" symbolic-ref --short HEAD 2>/dev/null)" = "main" ]; then ok; else bad "failed checkout moved HEAD"; fi
|
|
258
|
+
if grep -q 'checkout -f "\$ORIG_REF"' "$ROOT/scripts/review-prepare.sh"; then ok; else bad "restore_ref must use git checkout -f"; fi
|
|
259
|
+
|
|
260
|
+
# --- 10. fail closed on gh and bad arguments ---
|
|
261
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
262
|
+
prep "$ROOT"; rc_is "gh pr diff failure" 3; eq "blocked on gh failure" .status BLOCKED
|
|
263
|
+
# a staging bundle review (--no-delta) reads an API-oversized diff from the long-lived branches; a per-PR review still blocks
|
|
264
|
+
setup pr-branch main "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
265
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta falls back to git when the API diff fails" 0; eq "fallback status" .status OK
|
|
266
|
+
eq "fallback file list comes from git" '.full_files | index("services/api/auth.py") != null' true; eq "--no-delta mode is FULL" .delta.mode FULL
|
|
267
|
+
# forge#3935: the fallback is pinned to the review SHA and refuses fork PRs
|
|
268
|
+
setup pr-branch main "README.md" "$DIFF_PLAIN" "0000000000000000000000000000000000000000"; : > "$FIX/difffail"
|
|
269
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta fallback refuses a branch tip that is not the review SHA" 3
|
|
270
|
+
CROSS_REPO=true setup pr-branch main "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
271
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta fallback refuses a cross-repository PR" 3
|
|
272
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"; : > "$FIX/difffail"
|
|
273
|
+
prep "$ROOT" --no-delta; rc_is "--no-delta with unfetchable branches still fails closed" 3; eq "unfetchable blocked" .status BLOCKED
|
|
274
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
275
|
+
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
|
|
276
|
+
# shellcheck disable=SC2086
|
|
277
|
+
(cd "$REPO_DIR" && FORGEDOCK_HOME="$ROOT" bash "$ROOT/scripts/review-prepare.sh" $bad_args >/dev/null 2>&1); rcx=$?
|
|
278
|
+
if [ "$rcx" = 2 ]; then ok; else bad "args '$bad_args' exit $rcx, want 2"; fi
|
|
279
|
+
done
|
|
280
|
+
# every helper resolves through the trusted resolver, never the cwd
|
|
281
|
+
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
|
|
282
|
+
mkdir -p "$WORK/hostile/scripts"; printf '#!/usr/bin/env bash\necho SELECTED_AGENTS=evil\n' > "$WORK/hostile/scripts/review-roster.sh"
|
|
283
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
284
|
+
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)
|
|
285
|
+
eq "a hostile cwd roster script is never used" '.roster.agents | index("evil") != null' false
|
|
286
|
+
|
|
287
|
+
echo "review-prepare tests: $pass passed, $fail failed"
|
|
288
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
# [--review-depth STANDARD|EXTENDED]
|
|
15
|
+
# Output (stdout): SELECTED_AGENTS=<canonical marker domains, space separated> TIER=<tier> REASON=<text>
|
|
16
|
+
# Exit: 0 always for a parseable call; 2 on an unknown flag (callers fall back to the full union, never a narrower roster).
|
|
17
|
+
#
|
|
18
|
+
# Tiers, evaluated in this order:
|
|
19
|
+
# full THOROUGH or milestone->staging: Security plus every domain with a score > 0.
|
|
20
|
+
# fix-pr the PR closes a `review-finding` issue: Security, plus the source finding's domain when it was HIGH+.
|
|
21
|
+
# small <= 60 changed lines, or one file of <= 150 lines: Security only.
|
|
22
|
+
# medium 61-150 lines: Security plus the score-escalation table (no top-2 baseline).
|
|
23
|
+
# standard > 150 lines, or an unknown size (fail toward more agents): top-2 domains by score plus the table.
|
|
24
|
+
# --review-depth EXTENDED (forge#3862: the build's FORGE:DIFF_SIZE review_weight or out-of-plan scope flagged extended
|
|
25
|
+
# review) only ever ADDS reviewers: a small, medium or fix-pr roster is lifted to standard and the standard baseline takes
|
|
26
|
+
# the top-3 domains instead of top-2. It never narrows a roster and never touches the Security floor. Anything but the
|
|
27
|
+
# exact value EXTENDED is STANDARD. An extended roster prints an extra DEPTH=extended line.
|
|
28
|
+
# Path triggers escalate in EVERY tier except full (which already runs the union): CONTRACT HIGH_RISK, cross-critical
|
|
29
|
+
# pairs, churn hot-spot, danger zone, bin/engine* (Concurrency + spec-cli), bin/ (spec-cli), auth/session/trust-filter
|
|
30
|
+
# files (Auth), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
|
|
31
|
+
# Auth and Infra are path-gated: a diff-text score alone never selects them. Security is a hard floor in every tier.
|
|
32
|
+
# spec-cli is added only with --spec-cli true (the persona file exists in the repo under review); in the standard tier it
|
|
33
|
+
# also covers commands/**. Bash 3.2 portable (no mapfile, associative arrays, grep -P).
|
|
34
|
+
set -u
|
|
35
|
+
|
|
36
|
+
S_AUTH=0; S_BILLING=0; S_CONCURRENCY=0; S_DATABASE=0; S_INFRA=0; S_SCRAPING=0; S_FRONTEND=0; S_API=0
|
|
37
|
+
LINES=""; NFILES=""; FILES_FILE=""; FIX_PR=false; SRC_SEV=""; SRC_DOM=""; CFLAGS=""
|
|
38
|
+
DEPTH=STANDARD; CHURN=false; DANGER=false; THOROUGH=false; MILESTONE=false; SCRAPING_ON=false; DB_CONTAINER=false; SPEC_CLI=false
|
|
39
|
+
|
|
40
|
+
num() { case "${1:-}" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$1" ;; esac; }
|
|
41
|
+
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"; }
|
|
42
|
+
add_agent() {
|
|
43
|
+
local AGENT CUR HAVE=""
|
|
44
|
+
AGENT=$(agent_domain "${1}")
|
|
45
|
+
for CUR in $SELECTED_AGENTS; do
|
|
46
|
+
[ "$(agent_domain "$CUR")" = "$AGENT" ] && HAVE=1
|
|
47
|
+
done
|
|
48
|
+
[ -n "$HAVE" ] || SELECTED_AGENTS="$SELECTED_AGENTS $AGENT"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
while [ $# -gt 0 ]; do
|
|
52
|
+
case "$1" in
|
|
53
|
+
--scores)
|
|
54
|
+
for kv in ${2:-}; do
|
|
55
|
+
v=$(num "${kv#*=}")
|
|
56
|
+
case "${kv%%=*}" in
|
|
57
|
+
AUTH) S_AUTH=$v ;; BILLING) S_BILLING=$v ;; CONCURRENCY) S_CONCURRENCY=$v ;; DATABASE) S_DATABASE=$v ;;
|
|
58
|
+
INFRA) S_INFRA=$v ;; SCRAPING) S_SCRAPING=$v ;; FRONTEND) S_FRONTEND=$v ;; API) S_API=$v ;;
|
|
59
|
+
esac
|
|
60
|
+
done; shift 2 ;;
|
|
61
|
+
--changed-lines) LINES="${2:-}"; shift 2 ;;
|
|
62
|
+
--changed-files) NFILES="${2:-}"; shift 2 ;;
|
|
63
|
+
--files-file) FILES_FILE="${2:-}"; shift 2 ;;
|
|
64
|
+
--fix-pr) FIX_PR="${2:-false}"; shift 2 ;;
|
|
65
|
+
--source-severity) SRC_SEV="${2:-}"; shift 2 ;;
|
|
66
|
+
--source-domain) SRC_DOM="${2:-}"; shift 2 ;;
|
|
67
|
+
--contract-flags) CFLAGS="${2:-}"; shift 2 ;;
|
|
68
|
+
--churn) CHURN="${2:-false}"; shift 2 ;;
|
|
69
|
+
--danger-zone) DANGER="${2:-false}"; shift 2 ;;
|
|
70
|
+
--thorough) THOROUGH="${2:-false}"; shift 2 ;;
|
|
71
|
+
--milestone) MILESTONE="${2:-false}"; shift 2 ;;
|
|
72
|
+
--scraping-configured) SCRAPING_ON="${2:-false}"; shift 2 ;;
|
|
73
|
+
--database-container) DB_CONTAINER="${2:-false}"; shift 2 ;;
|
|
74
|
+
--spec-cli) SPEC_CLI="${2:-false}"; shift 2 ;;
|
|
75
|
+
--review-depth) [ "${2:-}" = "EXTENDED" ] && DEPTH=EXTENDED; shift 2 ;;
|
|
76
|
+
*) echo "review-roster: unknown argument: $1" >&2; exit 2 ;;
|
|
77
|
+
esac
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
FILES=""
|
|
81
|
+
if [ "$FILES_FILE" = "-" ]; then FILES=$(cat)
|
|
82
|
+
elif [ -n "$FILES_FILE" ] && [ -f "$FILES_FILE" ]; then FILES=$(cat "$FILES_FILE"); fi
|
|
83
|
+
[ -n "$NFILES" ] || NFILES=$(printf '%s\n' "$FILES" | grep -c . || true)
|
|
84
|
+
case "$LINES" in ''|*[!0-9]*) LINES_KNOWN=false; LINES=0 ;; *) LINES_KNOWN=true ;; esac
|
|
85
|
+
case "$NFILES" in ''|*[!0-9]*) NFILES=0 ;; esac
|
|
86
|
+
|
|
87
|
+
SELECTED_AGENTS="security"
|
|
88
|
+
TIER=""; REASON=""
|
|
89
|
+
|
|
90
|
+
# Domains that may be picked by score alone. Auth and Infra are path-gated, Scraping is opt-in.
|
|
91
|
+
top_domains() { # $1 = how many
|
|
92
|
+
local d out="" list=""
|
|
93
|
+
list="$S_BILLING:BILLING $S_CONCURRENCY:CONCURRENCY $S_DATABASE:DATABASE $S_FRONTEND:FRONTEND $S_API:API"
|
|
94
|
+
[ "$SCRAPING_ON" = "true" ] && list="$list $S_SCRAPING:SCRAPING"
|
|
95
|
+
for d in $(printf '%s\n' "$list" | tr ' ' '\n' | sort -t: -k1,1 -rn | head -n "$1"); do
|
|
96
|
+
[ "${d%%:*}" -gt 0 ] && out="$out ${d#*:}"
|
|
97
|
+
done
|
|
98
|
+
printf '%s' "$out"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if [ "$THOROUGH" = "true" ] || [ "$MILESTONE" = "true" ]; then
|
|
102
|
+
TIER=full; REASON="thorough or milestone-to-staging: full union"
|
|
103
|
+
[ "$S_AUTH" -gt 0 ] && add_agent Auth
|
|
104
|
+
[ "$S_BILLING" -gt 0 ] && { add_agent Billing; add_agent Concurrency; }
|
|
105
|
+
[ "$S_DATABASE" -gt 0 ] && add_agent Database
|
|
106
|
+
[ "$S_INFRA" -gt 0 ] && add_agent Infra
|
|
107
|
+
[ "$S_SCRAPING" -gt 0 ] && [ "$SCRAPING_ON" = "true" ] && add_agent Scraping
|
|
108
|
+
[ "$S_FRONTEND" -gt 0 ] && add_agent Frontend
|
|
109
|
+
[ "$S_API" -gt 0 ] && add_agent API
|
|
110
|
+
printf 'SELECTED_AGENTS=%s\nTIER=%s\nREASON=%s\n' "${SELECTED_AGENTS# }" "$TIER" "$REASON"
|
|
111
|
+
exit 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
HIGH_RISK=false
|
|
115
|
+
case " $CFLAGS " in *" HIGH_RISK "*) HIGH_RISK=true ;; esac
|
|
116
|
+
|
|
117
|
+
if [ "$FIX_PR" = "true" ]; then
|
|
118
|
+
TIER=fix-pr; REASON="fix PR for a review-finding issue"
|
|
119
|
+
elif [ "$LINES_KNOWN" = "true" ] && { [ "$LINES" -le 60 ] || { [ "$NFILES" -eq 1 ] && [ "$LINES" -le 150 ]; }; }; then
|
|
120
|
+
TIER=small; REASON="lines=$LINES files=$NFILES"
|
|
121
|
+
elif [ "$LINES_KNOWN" = "true" ] && [ "$LINES" -le 150 ]; then
|
|
122
|
+
TIER=medium; REASON="lines=$LINES"
|
|
123
|
+
else
|
|
124
|
+
TIER=standard
|
|
125
|
+
if [ "$LINES_KNOWN" = "true" ]; then REASON="lines=$LINES"; else REASON="size unknown"; fi
|
|
126
|
+
fi
|
|
127
|
+
# CONTRACT HIGH_RISK is never narrowed: a small, medium or fix-PR roster is lifted to the standard roster.
|
|
128
|
+
BASE_TIER="$TIER"
|
|
129
|
+
if [ "$HIGH_RISK" = "true" ] && [ "$TIER" != "standard" ]; then TIER=standard; REASON="$REASON; CONTRACT HIGH_RISK lifts $BASE_TIER to standard"; fi
|
|
130
|
+
# Extended review depth (forge#3862) is only ever additive: lift to standard, widen the standard baseline to top-3.
|
|
131
|
+
TOP_N=2
|
|
132
|
+
if [ "$DEPTH" = "EXTENDED" ]; then
|
|
133
|
+
TOP_N=3
|
|
134
|
+
if [ "$TIER" != "standard" ]; then REASON="$REASON; extended review depth lifts $TIER to standard"; TIER=standard; else REASON="$REASON; extended review depth"; fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
TRIG=""
|
|
138
|
+
trig() { case " $TRIG " in *" $1 "*) ;; *) TRIG="$TRIG $1" ;; esac; }
|
|
139
|
+
|
|
140
|
+
# Tier baseline
|
|
141
|
+
case "$TIER" in
|
|
142
|
+
fix-pr)
|
|
143
|
+
case "$(printf '%s' "$SRC_SEV" | tr '[:lower:]' '[:upper:]')" in
|
|
144
|
+
HIGH|CRITICAL|BLOCKER)
|
|
145
|
+
if [ -n "$SRC_DOM" ]; then add_agent "$SRC_DOM"; else for d in $(top_domains 1); do add_agent "$d"; done; fi
|
|
146
|
+
REASON="$REASON; source finding ${SRC_SEV} adds its domain" ;;
|
|
147
|
+
esac ;;
|
|
148
|
+
standard) for d in $(top_domains "$TOP_N"); do add_agent "$d"; done ;;
|
|
149
|
+
esac
|
|
150
|
+
|
|
151
|
+
# Score escalation table (medium and standard only; diff-text signals, so never in the small or fix-pr tiers)
|
|
152
|
+
if [ "$TIER" = "medium" ] || [ "$TIER" = "standard" ]; then
|
|
153
|
+
[ "$S_BILLING" -ge 3 ] && { add_agent Billing; add_agent Concurrency; trig billing-score; }
|
|
154
|
+
[ "$S_DATABASE" -ge 3 ] && { add_agent Database; trig database-score; }
|
|
155
|
+
[ "$S_SCRAPING" -ge 3 ] && [ "$SCRAPING_ON" = "true" ] && { add_agent Scraping; trig scraping; }
|
|
156
|
+
[ "$S_INFRA" -ge 3 ] && [ "$DB_CONTAINER" = "true" ] && { add_agent Infra; trig db-container; }
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# Path and contract triggers (every tier)
|
|
160
|
+
if [ "$HIGH_RISK" = "true" ]; then
|
|
161
|
+
trig contract-high-risk
|
|
162
|
+
case " $CFLAGS " in *" CONTRACT_AUTH "*) add_agent Auth ;; esac
|
|
163
|
+
case " $CFLAGS " in *" CONTRACT_BILLING "*) add_agent Billing; add_agent Concurrency ;; esac
|
|
164
|
+
case " $CFLAGS " in *" CONTRACT_DATABASE "*) add_agent Database ;; esac
|
|
165
|
+
fi
|
|
166
|
+
{ [ "$S_AUTH" -ge 2 ] && [ "$S_BILLING" -ge 2 ]; } && { add_agent Auth; add_agent Billing; add_agent Concurrency; trig cross-critical; }
|
|
167
|
+
{ [ "$S_AUTH" -ge 2 ] && [ "$S_DATABASE" -ge 3 ]; } && { add_agent Auth; add_agent Database; trig cross-critical; }
|
|
168
|
+
{ [ "$S_BILLING" -ge 2 ] && [ "$S_CONCURRENCY" -ge 2 ]; } && { add_agent Billing; add_agent Concurrency; add_agent Database; trig cross-critical; }
|
|
169
|
+
if [ "$CHURN" = "true" ] || [ "$DANGER" = "true" ]; then
|
|
170
|
+
for d in $(top_domains 1); do add_agent "$d"; trig churn-or-danger-zone; done
|
|
171
|
+
fi
|
|
172
|
+
hit() { printf '%s\n' "$FILES" | grep -qiE "$1"; }
|
|
173
|
+
hit '(^|[/_.-])(auth|authn|authz|session|sessions|jwt|oauth|login|logout|password|trust[-_]?filter|trusted[-_a-z]*)([/_.-]|$)' && { add_agent Auth; trig auth-path; }
|
|
174
|
+
hit '^\.github/|(^|/)Dockerfile|docker-compose|(^|/)(deploy|infra|traefik|nginx|k8s|helm|terraform)/|\.tf$' && { add_agent Infra; trig infra-path; }
|
|
175
|
+
hit 'migration|\.sql$' && { add_agent Database; trig migration-path; }
|
|
176
|
+
hit '^sdk/|openapi.*\.json$|openapi-versions/' && { add_agent API; trig sdk-path; }
|
|
177
|
+
hit '^bin/engine' && { add_agent Concurrency; trig bin-engine; }
|
|
178
|
+
if [ "$SPEC_CLI" = "true" ]; then
|
|
179
|
+
hit '^bin/' && { add_agent spec-cli; trig bin; }
|
|
180
|
+
[ "$TIER" = "standard" ] && hit '^commands/' && { add_agent spec-cli; trig commands; }
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
[ -n "$TRIG" ] && REASON="$REASON; triggers:$TRIG"
|
|
184
|
+
printf 'SELECTED_AGENTS=%s\nTIER=%s\nREASON=%s\n' "${SELECTED_AGENTS# }" "$TIER" "$REASON"
|
|
185
|
+
[ "$DEPTH" = "EXTENDED" ] && printf 'DEPTH=extended\n'
|
|
186
|
+
exit 0
|