forgedock 1.12.0 → 1.13.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/phases-args.test.mjs +2 -1
- package/bin/engine/phases.mjs +349 -90
- package/bin/engine/runlog.mjs +25 -2
- package/bin/engine-cli.mjs +4 -1
- package/bin/engine.mjs +72 -19
- package/bin/hooks/interactive-engine.mjs +135 -209
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +10 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +258 -23
- package/bin/watch-utils.mjs +1 -0
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +8 -5
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +80 -53
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +15 -6
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +12 -12
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +394 -53
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +106 -7
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +137 -25
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +15 -15
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +20 -5
- package/package.json +2 -1
- package/packages/protocol/src/phases.js +21 -0
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +103 -13
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- package/scripts/select-fix-targets.sh +17 -5
- package/scripts/transition-label.sh +3 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Fixtures for scripts/check-blast-radius.sh. Run: bash scripts/check-blast-radius.test.sh (bash 3.2 compatible)
|
|
6
|
+
|
|
7
|
+
set -u
|
|
8
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
9
|
+
S="$DIR/check-blast-radius.sh"
|
|
10
|
+
T=$(mktemp -d "${TMPDIR:-/tmp}/check-blast-radius-test.XXXXXX")
|
|
11
|
+
trap 'rm -rf "$T"' EXIT
|
|
12
|
+
pass=0; fail=0
|
|
13
|
+
OUT=""; RC=0
|
|
14
|
+
|
|
15
|
+
g() { git -C "$T/r" -c user.name=t -c user.email=t@example.com -c commit.gpgsign=false "$@" >/dev/null 2>&1; }
|
|
16
|
+
run() { OUT=$(bash "$S" --manifest "$T/m" --base staging --repo "$T/r" 2>&1); RC=$?; }
|
|
17
|
+
manifest() { { echo '<!-- FORGE:BLAST_RADIUS:BEGIN -->'; cat; echo '<!-- FORGE:BLAST_RADIUS:END -->'; } > "$T/m"; }
|
|
18
|
+
expect() { # expect <want_rc> <desc> [want_substring]
|
|
19
|
+
if [ "$RC" = "$1" ] && { [ -z "${3:-}" ] || case "$OUT" in *"$3"*) true ;; *) false ;; esac; }; then
|
|
20
|
+
pass=$((pass+1))
|
|
21
|
+
else
|
|
22
|
+
fail=$((fail+1)); echo "FAIL: $2 (rc=$RC, want $1)"; printf '%s\n' "$OUT" | sed 's/^/ /'
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
expect_absent() { # expect_absent <desc> <substring>
|
|
26
|
+
case "$OUT" in *"$2"*) fail=$((fail+1)); echo "FAIL: $1 (unexpected '$2')"; printf '%s\n' "$OUT" | sed 's/^/ /' ;; *) pass=$((pass+1)) ;; esac
|
|
27
|
+
}
|
|
28
|
+
reset_branch() { g checkout -q -f staging; g branch -q -D feat; g checkout -q -b feat; }
|
|
29
|
+
|
|
30
|
+
mkdir "$T/r"; git -C "$T/r" init -q -b staging >/dev/null 2>&1 || { git -C "$T/r" init -q; g checkout -b staging; }
|
|
31
|
+
mkdir -p "$T/r/routers" "$T/r/docs"
|
|
32
|
+
for f in a b c; do printf 'def handler():\n use(billing_principal)\n' > "$T/r/routers/$f.py"; done
|
|
33
|
+
echo 'notes about nothing' > "$T/r/docs/readme.md"
|
|
34
|
+
g add -A; g commit -m base
|
|
35
|
+
g checkout -q -b feat
|
|
36
|
+
|
|
37
|
+
SYM='SYMBOL: id=s1 kind=field name=billing_principal query="billing_principal"'
|
|
38
|
+
|
|
39
|
+
# (a) 3 sibling callers, 1 changed, 2 unlisted -> FAIL naming both unlisted files
|
|
40
|
+
echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; g add -A; g commit -m "change a"
|
|
41
|
+
manifest <<M
|
|
42
|
+
$SYM
|
|
43
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
44
|
+
M
|
|
45
|
+
run; expect 1 "3 siblings, 1 changed, 2 unlisted fails" "UNLISTED: s1 routers/b.py"
|
|
46
|
+
case "$OUT" in *"UNLISTED: s1 routers/c.py"*) pass=$((pass+1)) ;; *) fail=$((fail+1)); echo "FAIL: second unlisted sibling not named"; echo "$OUT" ;; esac
|
|
47
|
+
expect_absent "changed file is not unlisted" "UNLISTED: s1 routers/a.py"
|
|
48
|
+
|
|
49
|
+
# (b) all 3 changed -> PASS
|
|
50
|
+
echo 'use(billing_principal, new)' >> "$T/r/routers/b.py"; echo 'use(billing_principal, new)' >> "$T/r/routers/c.py"
|
|
51
|
+
g add -A; g commit -m "change b c"
|
|
52
|
+
manifest <<M
|
|
53
|
+
$SYM
|
|
54
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
55
|
+
M
|
|
56
|
+
run; expect 0 "all siblings changed passes (changed files need no row)" "OK:"
|
|
57
|
+
|
|
58
|
+
# (c) 1 changed + 2 verified-unaffected -> PASS
|
|
59
|
+
reset_branch; echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; g add -A; g commit -m "change a"
|
|
60
|
+
manifest <<M
|
|
61
|
+
$SYM
|
|
62
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
63
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=verified-unaffected reason=reads-only-never-writes
|
|
64
|
+
HIT: symbol=s1 file=routers/c.py role=sibling disposition=verified-unaffected reason=dead-code-path
|
|
65
|
+
M
|
|
66
|
+
run; expect 0 "verified-unaffected siblings pass" "OK:"
|
|
67
|
+
|
|
68
|
+
# (c2) staged but uncommitted and untracked changes count (validate runs before the commit)
|
|
69
|
+
reset_branch; echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; echo 'use(billing_principal, new)' >> "$T/r/routers/b.py"
|
|
70
|
+
g add routers/a.py; echo 'billing_principal = 1' > "$T/r/routers/d.py"
|
|
71
|
+
manifest <<M
|
|
72
|
+
$SYM
|
|
73
|
+
M
|
|
74
|
+
run; expect 1 "uncommitted: c.py still unlisted" "UNLISTED: s1 routers/c.py"
|
|
75
|
+
expect_absent "staged change counts" "UNLISTED: s1 routers/a.py"
|
|
76
|
+
expect_absent "unstaged tracked change counts" "UNLISTED: s1 routers/b.py"
|
|
77
|
+
expect_absent "untracked new file counts" "UNLISTED: s1 routers/d.py"
|
|
78
|
+
g checkout -q -f staging; rm -f "$T/r/routers/d.py"; g checkout -q -f feat; rm -f "$T/r/routers/d.py"; g reset -q --hard
|
|
79
|
+
|
|
80
|
+
# (d) change row whose file is not in the diff -> FAIL (planned but not done)
|
|
81
|
+
reset_branch; echo 'use(billing_principal, new)' >> "$T/r/routers/a.py"; g add -A; g commit -m "change a"
|
|
82
|
+
manifest <<M
|
|
83
|
+
$SYM
|
|
84
|
+
HIT: symbol=s1 file=routers/a.py role=consumer disposition=change
|
|
85
|
+
HIT: symbol=s1 file=docs/readme.md role=consumer disposition=change
|
|
86
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=verified-unaffected reason=x-reason
|
|
87
|
+
HIT: symbol=s1 file=routers/c.py role=sibling disposition=verified-unaffected reason=x-reason
|
|
88
|
+
M
|
|
89
|
+
run; expect 1 "planned-but-not-done change row fails" "NOT_DONE: s1 docs/readme.md"
|
|
90
|
+
|
|
91
|
+
# (e) rc 2 cases
|
|
92
|
+
manifest <<M
|
|
93
|
+
SYMBOL: id=s1 kind=field name=x query="-rf"
|
|
94
|
+
M
|
|
95
|
+
run; expect 2 "leading dash query rejected"
|
|
96
|
+
manifest <<M
|
|
97
|
+
SYMBOL: id=s1 kind=field name=x query=""
|
|
98
|
+
M
|
|
99
|
+
run; expect 2 "empty query rejected"
|
|
100
|
+
manifest <<M
|
|
101
|
+
SYMBOL: id=s1 kind=field name=x query="ab"
|
|
102
|
+
M
|
|
103
|
+
run; expect 2 "too-short query rejected"
|
|
104
|
+
manifest <<M
|
|
105
|
+
SYMBOL: id=s1 kind=field name=x query="foo bar baz"
|
|
106
|
+
M
|
|
107
|
+
run; expect 2 "query with spaces rejected"
|
|
108
|
+
manifest <<M
|
|
109
|
+
SYMBOL: id=s1 kind=field name=x query="a;rm -rf"
|
|
110
|
+
M
|
|
111
|
+
run; expect 2 "query with shell metacharacters rejected"
|
|
112
|
+
manifest <<M
|
|
113
|
+
$SYM
|
|
114
|
+
this is not a record
|
|
115
|
+
M
|
|
116
|
+
run; expect 2 "malformed line rejected"
|
|
117
|
+
manifest <<M
|
|
118
|
+
$SYM
|
|
119
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=verified-unaffected
|
|
120
|
+
M
|
|
121
|
+
run; expect 2 "verified-unaffected without reason rejected"
|
|
122
|
+
manifest <<M
|
|
123
|
+
$SYM
|
|
124
|
+
HIT: symbol=s9 file=routers/b.py role=sibling disposition=change
|
|
125
|
+
M
|
|
126
|
+
run; expect 2 "HIT with unknown symbol rejected"
|
|
127
|
+
manifest <<M
|
|
128
|
+
$SYM
|
|
129
|
+
HIT: symbol=s1 file=routers/b.py role=sibling disposition=maybe
|
|
130
|
+
M
|
|
131
|
+
run; expect 2 "invalid disposition rejected"
|
|
132
|
+
manifest <<M
|
|
133
|
+
$SYM
|
|
134
|
+
$SYM
|
|
135
|
+
M
|
|
136
|
+
run; expect 2 "duplicate symbol id rejected"
|
|
137
|
+
printf '<!-- FORGE:BLAST_RADIUS:BEGIN -->\n%s\n' "$SYM" > "$T/m"
|
|
138
|
+
run; expect 2 "unterminated BEGIN rejected"
|
|
139
|
+
manifest <<M
|
|
140
|
+
$SYM
|
|
141
|
+
M
|
|
142
|
+
OUT=$(bash "$S" --manifest "$T/m" --base no-such-ref --repo "$T/r" 2>&1); RC=$?; expect 2 "bad base ref is rc 2, not a pass"
|
|
143
|
+
mkdir "$T/notgit"
|
|
144
|
+
OUT=$(bash "$S" --manifest "$T/m" --base staging --repo "$T/notgit" 2>&1); RC=$?; expect 2 "non-git repo is rc 2, not a pass"
|
|
145
|
+
OUT=$(bash "$S" --manifest "$T/missing" --base staging --repo "$T/r" 2>&1); RC=$?; expect 2 "missing manifest file is rc 2"
|
|
146
|
+
OUT=$(bash "$S" --base staging 2>&1); RC=$?; expect 2 "missing --manifest is rc 2"
|
|
147
|
+
|
|
148
|
+
# (f) absent / empty manifest -> SKIP rc 0
|
|
149
|
+
: > "$T/m"; run; expect 0 "empty manifest skips" "SKIP: no manifest"
|
|
150
|
+
printf '## Implementation Plan\nno block here\n' > "$T/m"; run; expect 0 "no BEGIN/END block skips" "SKIP: no manifest"
|
|
151
|
+
printf '## Plan\nprose\n<!-- FORGE:BLAST_RADIUS:BEGIN -->\n%s\nHIT: symbol=s1 file=routers/a.py role=consumer disposition=change\n<!-- FORGE:BLAST_RADIUS:END -->\ntrailing prose\n<!-- FORGE:ARCHITECT:COMPLETE -->\n' "$SYM" > "$T/m"
|
|
152
|
+
run; expect 1 "whole comment body is accepted, prose outside the block ignored" "UNLISTED: s1 routers/b.py"
|
|
153
|
+
|
|
154
|
+
# fixed-string, not regex: a query with metacharacter-like text must match literally
|
|
155
|
+
g checkout -q -f staging; echo 'a.b(c)' > "$T/r/docs/regex.txt"; echo 'aXb(c)' > "$T/r/docs/other.txt"; g add -A; g commit -m regex
|
|
156
|
+
g branch -q -D feat; g checkout -q -b feat
|
|
157
|
+
manifest <<M
|
|
158
|
+
SYMBOL: id=s1 kind=function name=dot query="a.b"
|
|
159
|
+
M
|
|
160
|
+
run; expect 1 "fixed-string match lists the literal hit" "UNLISTED"
|
|
161
|
+
expect_absent "regex-style wildcard does not match aXb" "other.txt"
|
|
162
|
+
HIT_OK='HIT: symbol=s1 file=docs/regex.txt role=producer disposition=verified-unaffected reason=literal-only'
|
|
163
|
+
manifest <<M
|
|
164
|
+
SYMBOL: id=s1 kind=function name=dot query="a.b"
|
|
165
|
+
$HIT_OK
|
|
166
|
+
M
|
|
167
|
+
run; expect 0 "literal hit covered by verified-unaffected" "OK:"
|
|
168
|
+
|
|
169
|
+
# zero-hit symbol passes
|
|
170
|
+
manifest <<M
|
|
171
|
+
SYMBOL: id=s1 kind=flag name=nothing query="zz_never_present_zz"
|
|
172
|
+
M
|
|
173
|
+
run; expect 0 "zero-hit symbol passes" "OK:"
|
|
174
|
+
|
|
175
|
+
# query too broad -> rc 2
|
|
176
|
+
reset_branch; mkdir -p "$T/r/many"; i=0; while [ "$i" -lt 205 ]; do echo broadtoken > "$T/r/many/f$i.txt"; i=$((i+1)); done; g add -A; g commit -m many
|
|
177
|
+
manifest <<M
|
|
178
|
+
SYMBOL: id=s1 kind=flag name=broad query="broadtoken"
|
|
179
|
+
M
|
|
180
|
+
run; expect 2 "over-broad query is rc 2" "too broad"
|
|
181
|
+
|
|
182
|
+
echo "check-blast-radius.test.sh: $pass passed, $fail failed"
|
|
183
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# check-contract-scope.sh — Parser/validator for the `### Out of Scope` section of a
|
|
6
|
+
# FORGE:CONTRACT comment. One grammar shared by work-on:build
|
|
7
|
+
# (validate before posting) and review-pr (list, to demote findings
|
|
8
|
+
# that match a contract-declared item).
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# check-contract-scope.sh validate [<file>] # exit 0 valid, 1 invalid (one line per bad item on stderr)
|
|
12
|
+
# check-contract-scope.sh list [<file>] # TSV: disposition<TAB>path<TAB>issue, one row per path
|
|
13
|
+
# Input is the contract comment body, from <file> or stdin.
|
|
14
|
+
#
|
|
15
|
+
# Grammar. The section is either the single line `None.` or a list of bullets, each naming at
|
|
16
|
+
# least one backticked path or symbol followed by exactly one disposition:
|
|
17
|
+
# - `path` — deferred → #N: <why> (a follow-up issue exists; `->` is accepted for `→`)
|
|
18
|
+
# - `path` — not-affected: <evidence>
|
|
19
|
+
# - `path` — accepted-risk: <reason>
|
|
20
|
+
# `deferred` without an issue number, an item without a backticked path, an item with no
|
|
21
|
+
# disposition, and a missing or empty section are all invalid.
|
|
22
|
+
#
|
|
23
|
+
# `list` prints disposition as one of: deferred, not-affected, accepted-risk. The issue column is
|
|
24
|
+
# the number for deferred rows and empty otherwise. A trailing `/` is stripped from paths. `*`
|
|
25
|
+
# is not a glob: consumers match literally (equal, or under the path as a directory prefix).
|
|
26
|
+
# `list` on an invalid section prints nothing and exits 1 (fail toward filing in review).
|
|
27
|
+
#
|
|
28
|
+
# Exit: 0 ok; 1 invalid section; 2 usage error. Portable: bash 3.2, no network.
|
|
29
|
+
|
|
30
|
+
set -u
|
|
31
|
+
|
|
32
|
+
usage() { echo "ERROR: Usage: check-contract-scope.sh validate|list [<file>]" >&2; exit 2; }
|
|
33
|
+
|
|
34
|
+
MODE="${1:-}"
|
|
35
|
+
case "$MODE" in validate|list) ;; *) usage ;; esac
|
|
36
|
+
[ "$#" -le 2 ] || usage
|
|
37
|
+
if [ "$#" -eq 2 ]; then
|
|
38
|
+
[ -r "$2" ] || { echo "ERROR: not readable: $2" >&2; exit 2; }
|
|
39
|
+
BODY=$(cat "$2")
|
|
40
|
+
else
|
|
41
|
+
BODY=$(cat)
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
SECTION=$(printf '%s\n' "$BODY" | tr -d '\r' | awk '
|
|
45
|
+
/^### Out of Scope[[:space:]]*$/ { p = 1; found = 1; next }
|
|
46
|
+
p && /^### / { p = 0 }
|
|
47
|
+
p && /^> Pipeline powered by/ { p = 0 }
|
|
48
|
+
p && /^<!--/ { p = 0 }
|
|
49
|
+
p { print }
|
|
50
|
+
END { if (!found) exit 3 }
|
|
51
|
+
')
|
|
52
|
+
RC=$?
|
|
53
|
+
if [ "$RC" -ne 0 ]; then
|
|
54
|
+
echo "invalid: no '### Out of Scope' section" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
ROWS=""
|
|
59
|
+
ERRS=""
|
|
60
|
+
COUNT=0
|
|
61
|
+
NONE=0
|
|
62
|
+
ITEM=""
|
|
63
|
+
|
|
64
|
+
flush_item() {
|
|
65
|
+
[ -n "$ITEM" ] || return 0
|
|
66
|
+
COUNT=$((COUNT + 1))
|
|
67
|
+
_text=$(printf '%s' "$ITEM" | sed -E 's/deferred[[:space:]]*(→|->)[[:space:]]*/deferred → /g')
|
|
68
|
+
# Earliest disposition keyword wins; the prefix before it holds the paths.
|
|
69
|
+
_pre_n="${_text%%not-affected:*}"
|
|
70
|
+
_pre_a="${_text%%accepted-risk:*}"
|
|
71
|
+
_pre_d="${_text%%deferred → *}"
|
|
72
|
+
_disp=""; _pre="$_text"
|
|
73
|
+
if [ "$_pre_n" != "$_text" ]; then _disp="not-affected"; _pre="$_pre_n"; fi
|
|
74
|
+
if [ "$_pre_a" != "$_text" ] && { [ -z "$_disp" ] || [ "${#_pre_a}" -lt "${#_pre}" ]; }; then _disp="accepted-risk"; _pre="$_pre_a"; fi
|
|
75
|
+
if [ "$_pre_d" != "$_text" ] && { [ -z "$_disp" ] || [ "${#_pre_d}" -lt "${#_pre}" ]; }; then _disp="deferred"; _pre="$_pre_d"; fi
|
|
76
|
+
_issue=""
|
|
77
|
+
if [ -z "$_disp" ]; then
|
|
78
|
+
ERRS="${ERRS}untyped item (needs deferred → #N, not-affected:, or accepted-risk:): ${ITEM}
|
|
79
|
+
"
|
|
80
|
+
ITEM=""; return 0
|
|
81
|
+
fi
|
|
82
|
+
if [ "$_disp" = "deferred" ]; then
|
|
83
|
+
_rest="${_text#*deferred → }"
|
|
84
|
+
if [[ "$_rest" =~ ^#([0-9]+) ]]; then
|
|
85
|
+
_issue="${BASH_REMATCH[1]}"
|
|
86
|
+
else
|
|
87
|
+
ERRS="${ERRS}deferred item has no issue number (needs deferred → #N): ${ITEM}
|
|
88
|
+
"
|
|
89
|
+
ITEM=""; return 0
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
_paths=$(printf '%s' "$_pre" | grep -o '`[^`][^`]*`' | tr -d '`')
|
|
93
|
+
if [ -z "$_paths" ]; then
|
|
94
|
+
ERRS="${ERRS}item names no backticked path or symbol: ${ITEM}
|
|
95
|
+
"
|
|
96
|
+
ITEM=""; return 0
|
|
97
|
+
fi
|
|
98
|
+
while IFS= read -r _p; do
|
|
99
|
+
[ -n "$_p" ] || continue
|
|
100
|
+
_p="${_p%/}"
|
|
101
|
+
ROWS="${ROWS}${_disp} ${_p} ${_issue}
|
|
102
|
+
"
|
|
103
|
+
done <<EOP
|
|
104
|
+
$_paths
|
|
105
|
+
EOP
|
|
106
|
+
ITEM=""
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
while IFS= read -r line; do
|
|
110
|
+
trimmed=$(printf '%s' "$line" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
|
|
111
|
+
[ -n "$trimmed" ] || continue
|
|
112
|
+
case "$trimmed" in
|
|
113
|
+
"None."|"- None."|"* None.") NONE=1; continue ;;
|
|
114
|
+
"- "*|"* "*)
|
|
115
|
+
flush_item
|
|
116
|
+
ITEM=$(printf '%s' "$trimmed" | sed -E 's/^[-*][[:space:]]+//') ;;
|
|
117
|
+
*)
|
|
118
|
+
if [ -n "$ITEM" ]; then ITEM="${ITEM} ${trimmed}"
|
|
119
|
+
else ERRS="${ERRS}unexpected text outside a bullet: ${trimmed}
|
|
120
|
+
"; fi ;;
|
|
121
|
+
esac
|
|
122
|
+
done <<EOS
|
|
123
|
+
$SECTION
|
|
124
|
+
EOS
|
|
125
|
+
flush_item
|
|
126
|
+
|
|
127
|
+
if [ "$COUNT" -eq 0 ] && [ "$NONE" -eq 0 ] && [ -z "$ERRS" ]; then
|
|
128
|
+
ERRS="empty '### Out of Scope' section (write None. or typed items)
|
|
129
|
+
"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if [ -n "$ERRS" ]; then
|
|
133
|
+
printf 'invalid: %s' "$ERRS" >&2
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
[ "$MODE" = "list" ] && printf '%s' "$ROWS"
|
|
137
|
+
exit 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Fixtures for scripts/check-contract-scope.sh (typed `### Out of Scope` grammar).
|
|
6
|
+
# Run: bash scripts/check-contract-scope.test.sh (bash 3.2 compatible)
|
|
7
|
+
|
|
8
|
+
set -u
|
|
9
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
10
|
+
S="$DIR/check-contract-scope.sh"
|
|
11
|
+
pass=0; fail=0
|
|
12
|
+
TAB=$(printf '\t')
|
|
13
|
+
|
|
14
|
+
mk() { printf '## Builder Contract\n\n### Deliverables\n\n| a | b |\n\n### Out of Scope\n\n%s\n\n> Pipeline powered by [ForgeDock](https://example.invalid)\n' "$1"; }
|
|
15
|
+
|
|
16
|
+
# check <valid|invalid> <description> <section-text>
|
|
17
|
+
check() {
|
|
18
|
+
want="$1"; desc="$2"; body=$(mk "$3")
|
|
19
|
+
printf '%s\n' "$body" | bash "$S" validate >/dev/null 2>&1; got=$?
|
|
20
|
+
if { [ "$want" = valid ] && [ "$got" = 0 ]; } || { [ "$want" = invalid ] && [ "$got" = 1 ]; }; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $desc (want $want, exit $got)"; fi
|
|
21
|
+
}
|
|
22
|
+
# list_is <description> <section-text> <expected TSV (printf-style, \t and \n)>
|
|
23
|
+
list_is() {
|
|
24
|
+
desc="$1"; body=$(mk "$2"); want=$(printf "$3")
|
|
25
|
+
got=$(printf '%s\n' "$body" | bash "$S" list 2>/dev/null)
|
|
26
|
+
if [ "$got" = "$want" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: $desc"; echo " want: $want"; echo " got: $got"; fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
check valid "None. accepted" 'None.'
|
|
30
|
+
check valid "deferred with issue" '- `scripts/a.sh` — deferred → #12: tracked there'
|
|
31
|
+
check valid "not-affected with evidence" '- `bin/engine/` — not-affected: no reader found by grep'
|
|
32
|
+
check valid "accepted-risk with reason" '- `pkg/types.js` — accepted-risk: body-only format'
|
|
33
|
+
check valid "ASCII arrow accepted" '- `a.sh` — deferred -> #7: later'
|
|
34
|
+
check valid "no-space arrow accepted" '- `a.sh` — deferred→#7: later'
|
|
35
|
+
check valid "three mixed items" '- `a/` — not-affected: x
|
|
36
|
+
- `b.js` — accepted-risk: y
|
|
37
|
+
- `c.md` — deferred → #3446: z'
|
|
38
|
+
check valid "continuation line joins item" '- `a.sh` — deferred → #5:
|
|
39
|
+
because of reasons'
|
|
40
|
+
check invalid "untyped bullet rejected" '- `a.sh` — out of scope for now'
|
|
41
|
+
check invalid "deferred without number rejected" '- `a.sh` — deferred: later'
|
|
42
|
+
check invalid "deferred arrow without number" '- `a.sh` — deferred → TBD: later'
|
|
43
|
+
check invalid "no backticked path rejected" '- the sync script — deferred → #9: later'
|
|
44
|
+
check invalid "bare prose rejected" 'Nothing relevant.'
|
|
45
|
+
check invalid "one bad item among good ones" '- `a` — not-affected: ok
|
|
46
|
+
- `b` — nope'
|
|
47
|
+
check invalid "empty section rejected" ''
|
|
48
|
+
|
|
49
|
+
# Missing section
|
|
50
|
+
printf '## Builder Contract\n\n### Deliverables\n\nx\n' | bash "$S" validate >/dev/null 2>&1; got=$?
|
|
51
|
+
if [ "$got" = 1 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: missing section (want exit 1, got $got)"; fi
|
|
52
|
+
|
|
53
|
+
list_is "list rows for three forms" '- `a/` — not-affected: x
|
|
54
|
+
- `b.js` — accepted-risk: y
|
|
55
|
+
- `c.md` — deferred → #3446: z' "not-affected${TAB}a${TAB}\naccepted-risk${TAB}b.js${TAB}\ndeferred${TAB}c.md${TAB}3446"
|
|
56
|
+
list_is "multi-path item emits one row per path" '- `x.sh`, `y.sh` — deferred → #8: both' "deferred${TAB}x.sh${TAB}8\ndeferred${TAB}y.sh${TAB}8"
|
|
57
|
+
list_is "why-text with other keywords does not change disposition" '- `a` — accepted-risk: not-affected elsewhere, deferred → #1 is separate' "accepted-risk${TAB}a${TAB}"
|
|
58
|
+
list_is "None. lists nothing" 'None.' ""
|
|
59
|
+
list_is "invalid section lists nothing" '- `a` — untyped' ""
|
|
60
|
+
|
|
61
|
+
# Section ends at the next ### heading
|
|
62
|
+
printf '### Out of Scope\n\n- `a` — not-affected: x\n\n### Quality\n\n- plain bullet\n' | bash "$S" validate >/dev/null 2>&1; got=$?
|
|
63
|
+
if [ "$got" = 0 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: section must end at next ### (exit $got)"; fi
|
|
64
|
+
|
|
65
|
+
# Live-shaped contract (mixed items, trailing attribution line)
|
|
66
|
+
LIVE=$(mk '- `bin/engine/` — not-affected: no engine module reads the contract Out of Scope section (grep in investigation).
|
|
67
|
+
- `packages/protocol/src/types.js` — accepted-risk: CONTRACT requiredFields stays [Task type].
|
|
68
|
+
- `commands/work-on/build/architect.md` — deferred → #3446: blast-radius manifest is tracked there.')
|
|
69
|
+
got=$(printf '%s\n' "$LIVE" | bash "$S" list 2>/dev/null | wc -l | tr -d ' ')
|
|
70
|
+
if [ "$got" = 3 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: live-shaped contract rows (want 3, got $got)"; fi
|
|
71
|
+
|
|
72
|
+
# File argument and usage errors
|
|
73
|
+
TMPF=$(mktemp "${TMPDIR:-/tmp}/ccs.XXXXXX"); printf '%s\n' "$LIVE" > "$TMPF"
|
|
74
|
+
bash "$S" validate "$TMPF" >/dev/null 2>&1; got=$?
|
|
75
|
+
if [ "$got" = 0 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: file argument (exit $got)"; fi
|
|
76
|
+
rm -f "$TMPF"
|
|
77
|
+
bash "$S" validate /nonexistent/x >/dev/null 2>&1; got=$?
|
|
78
|
+
if [ "$got" = 2 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: unreadable file (want 2, got $got)"; fi
|
|
79
|
+
bash "$S" bogus </dev/null >/dev/null 2>&1; got=$?
|
|
80
|
+
if [ "$got" = 2 ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: bad mode (want 2, got $got)"; fi
|
|
81
|
+
|
|
82
|
+
echo "check-contract-scope.test.sh: passed=$pass failed=$fail"
|
|
83
|
+
[ "$fail" -eq 0 ]
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# Usage:
|
|
14
14
|
# check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>...
|
|
15
15
|
# check-spec-bash.sh --positional [<file.md>...]
|
|
16
|
+
# check-spec-bash.sh --fence-state <VAR>... -- <file.md>...
|
|
16
17
|
# check-spec-bash.sh (no args: same as --positional over all Skill-loaded commands/**/*.md,
|
|
17
18
|
# excluding orchestrate/**, pipeline-health/**, review-pr-agents/**)
|
|
18
19
|
#
|
|
@@ -25,6 +26,13 @@
|
|
|
25
26
|
# Scope the file list to Skill-loaded specs (Read-loaded orchestrate/**,
|
|
26
27
|
# pipeline-health/** and the review-pr-agents catalog are not substituted).
|
|
27
28
|
# Output: `FAIL <file>:<line> positional arg ...` then `SPEC-POSITIONAL: ...`.
|
|
29
|
+
# --fence-state Different check, opt-in: every Bash tool call is a fresh shell, so a variable bound
|
|
30
|
+
# in one fence is empty in the next. Fail any ```bash/sh fence that reads `$VAR` or
|
|
31
|
+
# `${VAR...}` for a listed VAR without assigning it in the same fence (`VAR=`,
|
|
32
|
+
# `export VAR=`, `read VAR`, `for VAR in`) at or before its first read. Comment-only lines are ignored; `$REPO`
|
|
33
|
+
# does not match `$REPO_FLAG`. Suppress a deliberate read by putting
|
|
34
|
+
# `allowlist:fence-state` on the same line. Pseudo-tool-call fences are scanned too.
|
|
35
|
+
# Output: `FAIL <file>:<line> fence-state ...` then `SPEC-FENCE-STATE: ...`.
|
|
28
36
|
# --base <ref> Only check blocks that contain a line changed relative to <ref>
|
|
29
37
|
# (`git diff -U0 <ref> -- <file>`), so pre-existing blocks never fail
|
|
30
38
|
# a new change. Without --base every block is checked.
|
|
@@ -45,14 +53,22 @@
|
|
|
45
53
|
|
|
46
54
|
set -u
|
|
47
55
|
|
|
48
|
-
BASE=""; SHELLCHECK=0; POSITIONAL=0; FILES=""
|
|
49
|
-
usage() { echo "ERROR: Usage: check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>... | --positional [<file.md>...]" >&2; exit 2; }
|
|
56
|
+
BASE=""; SHELLCHECK=0; POSITIONAL=0; FILES=""; FENCESTATE=0; FSVARS=""
|
|
57
|
+
usage() { echo "ERROR: Usage: check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>... | --positional [<file.md>...] | --fence-state <VAR>... -- <file.md>..." >&2; exit 2; }
|
|
50
58
|
|
|
51
59
|
while [ "$#" -gt 0 ]; do
|
|
52
60
|
case "$1" in
|
|
53
61
|
--base) [ "$#" -ge 2 ] || usage; BASE="$2"; shift 2 ;;
|
|
54
62
|
--shellcheck) SHELLCHECK=1; shift ;;
|
|
55
63
|
--positional) POSITIONAL=1; shift ;;
|
|
64
|
+
--fence-state)
|
|
65
|
+
FENCESTATE=1; shift
|
|
66
|
+
while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do
|
|
67
|
+
case "$1" in -*|*[!A-Za-z0-9_]*) usage ;; esac
|
|
68
|
+
FSVARS="$FSVARS $1"; shift
|
|
69
|
+
done
|
|
70
|
+
[ "$#" -gt 0 ] && [ -n "$FSVARS" ] || usage
|
|
71
|
+
shift ;;
|
|
56
72
|
--*) usage ;;
|
|
57
73
|
*) FILES="$FILES
|
|
58
74
|
$1"; shift ;;
|
|
@@ -62,7 +78,7 @@ done
|
|
|
62
78
|
# No file arguments: default to the --positional check over every Skill-loaded spec under commands/
|
|
63
79
|
# (orchestrate/**, pipeline-health/** and the review-pr-agents catalog are Read-loaded, not substituted).
|
|
64
80
|
# Resolved relative to this script, so it works from any cwd and from linked worktrees.
|
|
65
|
-
if [ -z "$FILES" ] && { [ "$#" -eq 0 ] && [ "$POSITIONAL" = 0 ] && [ -z "$BASE" ] && [ "$SHELLCHECK" = 0 ] || [ "$POSITIONAL" = 1 ]; }; then
|
|
81
|
+
if [ -z "$FILES" ] && [ "$FENCESTATE" = 0 ] && { [ "$#" -eq 0 ] && [ "$POSITIONAL" = 0 ] && [ -z "$BASE" ] && [ "$SHELLCHECK" = 0 ] || [ "$POSITIONAL" = 1 ]; }; then
|
|
66
82
|
POSITIONAL=1
|
|
67
83
|
cd "$(dirname "$0")/.." || exit 2
|
|
68
84
|
FILES=$(find commands -name '*.md' -not -path 'commands/orchestrate/*' \
|
|
@@ -70,6 +86,47 @@ if [ -z "$FILES" ] && { [ "$#" -eq 0 ] && [ "$POSITIONAL" = 0 ] && [ -z "$BASE"
|
|
|
70
86
|
fi
|
|
71
87
|
[ -n "$FILES" ] || usage
|
|
72
88
|
|
|
89
|
+
if [ "$FENCESTATE" = 1 ]; then
|
|
90
|
+
sfail=0; sfiles=0
|
|
91
|
+
while IFS= read -r f; do
|
|
92
|
+
[ -n "$f" ] || continue
|
|
93
|
+
case "$f" in *.md) ;; *) continue ;; esac
|
|
94
|
+
[ -f "$f" ] || { echo "SKIP $f missing"; continue; }
|
|
95
|
+
sfiles=$((sfiles + 1))
|
|
96
|
+
hits=$(awk -v f="$f" -v vars="$FSVARS" '
|
|
97
|
+
function flush( i, v, ln) {
|
|
98
|
+
for (i = 1; i <= nv; i++) {
|
|
99
|
+
v = vn[i]
|
|
100
|
+
if (first[v] != "" && !asg[v]) print "FAIL " f ":" first[v] " fence-state: $" v " read in a fence before it is assigned (fresh shell per Bash call): " firsttxt[v]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function reset( i) { for (i = 1; i <= nv; i++) { asg[vn[i]] = 0; first[vn[i]] = ""; firsttxt[vn[i]] = "" } }
|
|
104
|
+
BEGIN { nv = split(vars, vn, " ") }
|
|
105
|
+
!inb && /^[ \t]*```(bash|sh)[ \t]*$/ { inb = 1; reset(); next }
|
|
106
|
+
!inb && /^[ \t]*```/ { other = !other; next }
|
|
107
|
+
other { next }
|
|
108
|
+
inb && /^[ \t]*```[ \t]*$/ { flush(); inb = 0; next }
|
|
109
|
+
inb {
|
|
110
|
+
if ($0 ~ /^[ \t]*#/) next
|
|
111
|
+
for (i = 1; i <= nv; i++) {
|
|
112
|
+
v = vn[i]
|
|
113
|
+
if (first[v] == "" && ($0 ~ ("(^|[;&|( \t])(export[ \t]+|local[ \t]+|declare[ \t]+(-[A-Za-z]+[ \t]+)?|readonly[ \t]+)?" v "=") ||
|
|
114
|
+
$0 ~ ("(^|[;&|( \t])read[ \t]+(-[A-Za-z]+[ \t]+)*([A-Za-z0-9_]+[ \t]+)*" v "([ \t;]|$)") ||
|
|
115
|
+
$0 ~ ("(^|[;&|( \t])for[ \t]+" v "[ \t]+in[ \t]"))) asg[v] = 1
|
|
116
|
+
if (first[v] == "" && $0 !~ /allowlist:fence-state/ &&
|
|
117
|
+
($0 ~ ("\\$" v "([^A-Za-z0-9_]|$)") || $0 ~ ("\\$\\{" v "([^A-Za-z0-9_]|$)"))) { first[v] = NR; firsttxt[v] = $0 }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
' "$f")
|
|
121
|
+
if [ -n "$hits" ]; then printf '%s\n' "$hits"; sfail=$((sfail + $(printf '%s\n' "$hits" | wc -l))); fi
|
|
122
|
+
done <<EOS
|
|
123
|
+
$FILES
|
|
124
|
+
EOS
|
|
125
|
+
echo "SPEC-FENCE-STATE: files=$sfiles violations=$sfail"
|
|
126
|
+
[ "$sfail" -eq 0 ]
|
|
127
|
+
exit $?
|
|
128
|
+
fi
|
|
129
|
+
|
|
73
130
|
if [ "$POSITIONAL" = 1 ]; then
|
|
74
131
|
pfail=0; pfiles=0
|
|
75
132
|
while IFS= read -r f; do
|
|
@@ -109,5 +109,61 @@ check 1 "FAIL pos-bad.md:3" "--positional flags shell \$1"
|
|
|
109
109
|
check 1 "violations=2" "--positional counts violations" -- --positional pos-bad.md
|
|
110
110
|
check 0 "violations=0" "--positional passes safe forms, prose, allowlist" -- --positional pos-good.md
|
|
111
111
|
|
|
112
|
+
# --fence-state: a listed var read in a fence without a prior assignment in that same fence is flagged.
|
|
113
|
+
cat > "$T/fs-bad.md" <<EOF
|
|
114
|
+
${FENCE}bash
|
|
115
|
+
gh pr view "\$PR_NUMBER" -R "\$REPO" --json state
|
|
116
|
+
${FENCE}
|
|
117
|
+
${FENCE}bash
|
|
118
|
+
X=\$(gh pr diff \${PR_NUMBER} --name-only)
|
|
119
|
+
${FENCE}
|
|
120
|
+
${FENCE}bash
|
|
121
|
+
echo "\${REPO:-none}"
|
|
122
|
+
${FENCE}
|
|
123
|
+
${FENCE}bash
|
|
124
|
+
gh pr view "\$PR_NUMBER"
|
|
125
|
+
PR_NUMBER="{PR_NUMBER}"
|
|
126
|
+
${FENCE}
|
|
127
|
+
EOF
|
|
128
|
+
cat > "$T/fs-good.md" <<EOF
|
|
129
|
+
Prose mentions \$PR_NUMBER and \$REPO freely.
|
|
130
|
+
${FENCE}bash
|
|
131
|
+
# comment reads \$PR_NUMBER and \$REPO
|
|
132
|
+
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"
|
|
133
|
+
gh pr view "\$PR_NUMBER" -R "\$REPO"
|
|
134
|
+
${FENCE}
|
|
135
|
+
${FENCE}bash
|
|
136
|
+
export REPO="{GH_REPO}"
|
|
137
|
+
for PR_NUMBER in 1 2; do echo "\${PR_NUMBER}"; done
|
|
138
|
+
echo "\$REPO"
|
|
139
|
+
${FENCE}
|
|
140
|
+
${FENCE}bash
|
|
141
|
+
REPO="\${REPO:-\$(gh repo view)}"
|
|
142
|
+
${FENCE}
|
|
143
|
+
${FENCE}bash
|
|
144
|
+
gh pr view \${REPO_FLAG} "\$OTHER"
|
|
145
|
+
${FENCE}
|
|
146
|
+
${FENCE}bash
|
|
147
|
+
echo "\$PR_NUMBER" # allowlist:fence-state
|
|
148
|
+
${FENCE}
|
|
149
|
+
${FENCE}text
|
|
150
|
+
echo \$PR_NUMBER \$REPO
|
|
151
|
+
${FENCE}
|
|
152
|
+
${FENCE}bash
|
|
153
|
+
PR_NUMBER="{PR_NUMBER}"
|
|
154
|
+
OUT=\$(Skill(skill="x", args="\$PR_NUMBER"))
|
|
155
|
+
${FENCE}
|
|
156
|
+
EOF
|
|
157
|
+
check 1 "FAIL fs-bad.md:2 fence-state: \$PR_NUMBER" "--fence-state flags \$VAR read" -- --fence-state PR_NUMBER REPO -- fs-bad.md
|
|
158
|
+
check 1 "FAIL fs-bad.md:2 fence-state: \$REPO" "--fence-state flags each listed var" -- --fence-state PR_NUMBER REPO -- fs-bad.md
|
|
159
|
+
check 1 "FAIL fs-bad.md:5 fence-state: \$PR_NUMBER" "--fence-state flags \${VAR} form" -- --fence-state PR_NUMBER REPO -- fs-bad.md
|
|
160
|
+
check 1 "FAIL fs-bad.md:8 fence-state: \$REPO" "--fence-state flags \${VAR:-default}" -- --fence-state PR_NUMBER REPO -- fs-bad.md
|
|
161
|
+
check 1 "FAIL fs-bad.md:11 fence-state: \$PR_NUMBER" "--fence-state flags read before assignment" -- --fence-state PR_NUMBER REPO -- fs-bad.md
|
|
162
|
+
check 1 "violations=5" "--fence-state counts violations" -- --fence-state PR_NUMBER REPO -- fs-bad.md
|
|
163
|
+
check 0 "violations=0" "--fence-state ignores unlisted vars" -- --fence-state UNLISTED -- fs-bad.md
|
|
164
|
+
check 0 "violations=0" "--fence-state passes declared, loop, export, REPO_FLAG, allowlist, comments, prose, non-bash fences" -- --fence-state PR_NUMBER REPO -- fs-good.md
|
|
165
|
+
check 2 "Usage" "--fence-state without -- is a usage error" -- --fence-state PR_NUMBER fs-good.md
|
|
166
|
+
check 2 "Usage" "--fence-state without vars is a usage error" -- --fence-state -- fs-good.md
|
|
167
|
+
|
|
112
168
|
echo "check-spec-bash.test.sh: passed=$pass failed=$fail"
|
|
113
169
|
[ "$fail" -eq 0 ]
|
|
@@ -88,6 +88,7 @@ MARKER_REGISTRY="
|
|
|
88
88
|
BASESYNC_REMEDIATION
|
|
89
89
|
BATCH_MEMBERS
|
|
90
90
|
BENCH_SCORECARD
|
|
91
|
+
BLAST_RADIUS
|
|
91
92
|
BLOCKED_ON_HUMAN_MERGE
|
|
92
93
|
BODY
|
|
93
94
|
BUILDER
|
|
@@ -106,6 +107,7 @@ MARKER_REGISTRY="
|
|
|
106
107
|
DECISION_RECORD
|
|
107
108
|
DECOMPOSED
|
|
108
109
|
DECOMPOSE_BLOCKED
|
|
110
|
+
DEFERRED_FROM
|
|
109
111
|
DESIGN
|
|
110
112
|
DIFF_SIZE
|
|
111
113
|
DISPATCH
|
|
@@ -416,6 +418,33 @@ if [ -f "$WORKON_SPEC" ]; then
|
|
|
416
418
|
fi
|
|
417
419
|
fi
|
|
418
420
|
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
# Re-review claim id must be printed by both producers (forge#3472)
|
|
423
|
+
# ---------------------------------------------------------------------------
|
|
424
|
+
# A claim id generated in one bash block and compared in a later one is lost unless the producer prints it.
|
|
425
|
+
# Orchestrator: the Step 2 claim block's final echo must carry CLAIM_ID=. Router: Claim pre-check step 3 must
|
|
426
|
+
# require printing CLAIM_ID=. Scoped to the Step 2 / Claim pre-check text only; full-line HTML comments skipped.
|
|
427
|
+
# WIRE:PROVEN: manual mutation in a temp copy: removing CLAIM_ID= from the Step 2 echo fired the orchestrator check, removing it from router step 3 fired the router check, clean tree passes
|
|
428
|
+
P4_SPEC="$COMMANDS_DIR/orchestrate/phase-4-execution.md"
|
|
429
|
+
if [ -f "$P4_SPEC" ]; then
|
|
430
|
+
STEP2_ECHO=$(awk '
|
|
431
|
+
/Step 2 — dispatch/ { on = 1 }
|
|
432
|
+
/Step 3 — outcome/ { on = 0 }
|
|
433
|
+
on && !/^[[:space:]]*<!--.*-->[[:space:]]*$/ && /^[[:space:]]*echo "REREVIEW_MARKER=/ { print }
|
|
434
|
+
' "$P4_SPEC" || true)
|
|
435
|
+
if ! printf '%s\n' "$STEP2_ECHO" | grep -qF 'CLAIM_ID='; then
|
|
436
|
+
echo "HIGH | $P4_SPEC | Step 2 claim block's final echo does not print CLAIM_ID= (own claim id is lost across bash blocks)" >&2
|
|
437
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
438
|
+
fi
|
|
439
|
+
fi
|
|
440
|
+
if [ -f "$WORKON_SPEC" ]; then
|
|
441
|
+
ROUTER_STEP3=$(printf '%s\n' "${PRECHECK_STEPS:-}" | grep '^STEP3:' || true)
|
|
442
|
+
if [ -z "$ROUTER_STEP3" ] || [[ "$ROUTER_STEP3" != *CLAIM_ID=* ]]; then
|
|
443
|
+
echo "HIGH | $WORKON_SPEC | claim pre-check step 3 does not require printing CLAIM_ID= (router own claim id is lost across bash blocks)" >&2
|
|
444
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
445
|
+
fi
|
|
446
|
+
fi
|
|
447
|
+
|
|
419
448
|
# ---------------------------------------------------------------------------
|
|
420
449
|
# Summary
|
|
421
450
|
# ---------------------------------------------------------------------------
|