diff --git a/plugins/uberdev/lib/goal-state.sh b/plugins/uberdev/lib/goal-state.sh index 8b7eaf1b..a96d9c1e 100644 --- a/plugins/uberdev/lib/goal-state.sh +++ b/plugins/uberdev/lib/goal-state.sh @@ -3859,14 +3859,22 @@ _uberdev_goal_rebase_collision_chain() { # comm -12 cares only about set contents, not iteration count, so this # saves one `sort -u` per remaining-PR iteration. local merged_sorted - merged_sorted="$(printf '%s\n' "$merged_diff" | sort -u)" + # LC_ALL=C ON EVERY MEMBER OF THE sort/comm TRIO, INCLUDING `comm` ITSELF. + # This compares PATHS AS BYTES. `sort` orders by the ambient collation and + # `comm`'s merge walk assumes the order its own collation would produce; when + # the two disagree the walk desynchronises. Here that fails OPEN -- a `comm + # -12` intersection that misses a shared path reports NO collision between two + # PRs that do collide, and the rebase this function exists to trigger never + # fires. The sorts happen to share a shell today, so they agree by accident; + # pinning makes it a property of the code. + merged_sorted="$(printf '%s\n' "$merged_diff" | LC_ALL=C sort -u)" local pr while IFS= read -r pr; do [ -n "$pr" ] || continue [ "$pr" = "$merged_pr" ] && continue local pr_diff intersection pr_diff="$(gh pr diff "$pr" --name-only 2>/dev/null || true)" - intersection="$(comm -12 <(printf '%s\n' "$merged_sorted") <(printf '%s\n' "$pr_diff" | sort -u))" + intersection="$(LC_ALL=C comm -12 <(printf '%s\n' "$merged_sorted") <(printf '%s\n' "$pr_diff" | LC_ALL=C sort -u))" if [ -n "$intersection" ]; then # Collision detected — refresh main and audit the transition. git fetch origin main 2>/dev/null || true diff --git a/plugins/uberdev/lib/premerge-findings.py b/plugins/uberdev/lib/premerge-findings.py index 47a73031..1cad8c85 100644 --- a/plugins/uberdev/lib/premerge-findings.py +++ b/plugins/uberdev/lib/premerge-findings.py @@ -252,6 +252,14 @@ MAX_LINE_DIGITS = len(str(MAX_LINE)) MAX_FINDINGS = 64 +# How many paths ONE attempt may add to the next attempt's wave plan from its +# fixers' `blocked_on_file` returns. Equal to the default `--max-per-wave`, so +# the widening can never exceed one extra wave's worth of files -- "optional, +# repeatable" with no cap is unbounded allow-list growth, and by +# CONVERGE_REPAIR_CEILING attempts a set that grows every round constrains +# nothing. Overflow is DROPPED, not refused: see `_read_blocked_on`. +MAX_BLOCKED_ON = 8 + _CONTROL = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]") @@ -334,9 +342,18 @@ def _bounded_text(value: object, limit: int, token: str, what: str) -> str: def _repo_path(value: object) -> str: """Normalise a repo-relative POSIX path, or refuse. - Same predicate as `code_fixer_contract.py`'s scope validator, restated here - only because this file must reject BEFORE the aggregate is written -- a path - that escapes the repo has to die at parse time, not at publish time. + LEXICAL ONLY, and that is the whole predicate: `posixpath.normpath` plus the + `..`/`.git` first-segment check. It never touches the filesystem, so it is + NOT the same predicate as `code_fixer_contract.py`'s scope validator -- that + one pairs its lexical half with `beneath()`, a realpath+commonpath + containment check. This docstring used to claim the equivalence outright, + and the claim was false; a caller reading it as containment would have + shipped a symlink escape. Any caller turning a path into a FILESYSTEM + AUTHORIZATION decision must run `_beneath_repo_root` as well. + + Restated here rather than imported because this file must reject BEFORE the + aggregate is written -- a path that escapes the repo has to die at parse + time, not at publish time. """ if not isinstance(value, str) or not value: fail("finding_schema_invalid", "file must be a non-empty string") @@ -354,6 +371,27 @@ def _repo_path(value: object) -> str: return value +def _beneath_repo_root(root: str, relative: str) -> bool: + """The filesystem half `_repo_path` deliberately does not do. + + Same predicate as `code_fixer_contract.beneath`: realpath BOTH sides, then + `commonpath`. A tracked symlink whose path is lexically repo-relative and + whose target resolves outside the repo passes `_repo_path` and fails here. + No symlink is tracked in this repo today, which is exactly why the check + belongs in the code rather than in a sentence about the tree. + + Returns False on any error rather than raising: the caller drops a row it + cannot vouch for, and a validator that throws where the caller expected a + verdict is a validator that fails open at the call site. + """ + try: + root_abs = os.path.realpath(os.path.abspath(root)) + path_abs = os.path.realpath(os.path.abspath(os.path.join(root, relative))) + return os.path.commonpath((root_abs, path_abs)) == root_abs + except (TypeError, ValueError, OSError): + return False + + _RUN_ID_RE = re.compile(r"\A[0-9]{8}-[0-9]{6}-[0-9a-f]+\Z") @@ -1029,6 +1067,114 @@ def _carry_prior_suggestions(out_dir: str, attempt: int) -> "list[dict]": return carried +def _read_blocked_on( + out_dir: str, attempt: int, repo_root: str, already: "set[str]" +) -> "tuple[list[dict], int]": + """Attempt N-1's `blocked_on_file` rows: validated, deduped, capped. + + EXACTLY ONE PREDECESSOR, NEVER A RANGE. `_carry_prior_suggestions` walks + `range(1, attempt)` because a suggestion is never fixed and must not be + lost. A blocked-on path is the opposite -- it widens the set of files a + fixer may edit -- so a union accumulated over every earlier attempt grows + monotonically and, by CONVERGE_REPAIR_CEILING, constrains nothing. + + A ROW THAT FAILS VALIDATION IS DROPPED AND COUNTED, NEVER FATAL. Refusing + would cost the whole attempt over one malformed row an agent emitted -- + the trade `_drop_unfilable` already declines. Dropping is also the + fail-CLOSED direction: a dropped path is a path no fixer may touch. + + Returns (rows, dropped). + """ + prior = attempt - 1 + if prior < 1: + return [], 0 + path = os.path.join(out_dir, "blocked-on-%02d.json" % prior) + if not os.path.exists(path): + return [], 0 + try: + document = _load(path) + if not isinstance(document, dict): + fail("blocked_on_schema_invalid", "the blocked-on artifact must be a JSON object") + if document.get("schema_version") != SCHEMA_VERSION: + fail("blocked_on_schema_invalid", f"schema_version must be {SCHEMA_VERSION}") + rows = document.get("blocked_on") + if not isinstance(rows, list): + fail("blocked_on_schema_invalid", "blocked_on must be a JSON array") + except Refusal as refusal: + # Announced, never silent, and never fatal -- same shape and same + # reasoning as `_carry_prior_suggestions`' tolerant arm. + print( + "premerge-findings: blocked_on_unreadable_skipped: " + f"attempt {prior} ({refusal.token}); no path is widened from it", + file=sys.stderr, + ) + return [], 0 + + kept: "list[dict]" = [] + seen: "set[str]" = set(already) + dropped = 0 + for record in rows: + if not isinstance(record, dict): + dropped += 1 + continue + try: + target = _repo_path(record.get("file")) + origin = _repo_path(record.get("from_file")) + reason = _bounded_text( + record.get("reason"), + MAX_SUMMARY_BYTES, + "blocked_on_schema_invalid", + "blocked_on reason", + ) + except Refusal: + dropped += 1 + continue + # The wave prompt pastes finding objects VERBATIM into an + # `` envelope with no encoder between them, + # so a row carrying the tag's own name could close the envelope early + # and read as the controller's words. `_canonical_json` solves this for + # the aggregate by escaping the markup characters; here the row is + # dropped, because nothing downstream re-encodes it. + # + # ALL THREE reachable strings are checked, not the reason alone. + # `target` is the value the AGENT chose and it lands verbatim in the + # synthesised finding's `file`; `origin` lands inside the + # controller-authored `summary`. Checking one of the three would leave + # the most agent-controlled of them unchecked, which is the direction + # this drop exists to close. + if any( + "external-untrusted-input" in value.casefold() + for value in (reason, origin, target) + ): + dropped += 1 + continue + if not _beneath_repo_root(repo_root, target): + dropped += 1 + continue + # It must already exist. A widened path that does not is a path no + # commit could carry anyway -- the fix commit fence refuses on + # untracked files -- so admitting one only grows the allowed set. + # + # It also closes the separator injection: the allowed set is derived + # NEWLINE-separated by `jq -r`, and a `file` carrying an embedded + # newline would split into two entries. No such file exists on disk, so + # the row dies here. + if not os.path.isfile(os.path.join(repo_root, target)): + dropped += 1 + continue + if target in seen: + dropped += 1 + continue + if len(kept) >= MAX_BLOCKED_ON: + dropped += 1 + continue + seen.add(target) + kept.append( + {"file": target, "from_file": origin, "reason": reason, "attempt": prior} + ) + return kept, dropped + + # The tag that fronts an unhashable field's stand-in. A one-off object, so the # stand-in is a TUPLE and can therefore never compare equal to the string or the # integer a well-formed row puts in the same slot. @@ -1436,6 +1582,60 @@ def cmd_plan(args: argparse.Namespace) -> int: # beats being saved by a ranking accident at the end of the run. _encode_aggregate(_drop_unfilable(union)[0], pr_number, run_id, allow_blockers=True) + # THE WIDENING GOES THROUGH `_fix_waves`, NOT AROUND THE SCOPE GUARD. + # + # The fix-commit fence derives the committable set solely from + # `jq -r '.waves[][].file' fix-waves-.json`. Widening THAT from + # agent-returned text would make a path committable while no agent owns it, + # and two agents in the next wave could then edit it concurrently against + # one un-isolated worktree with the disjointness guard silent. Feeding the + # paths in here instead makes each one a first-class wave member with + # exactly one owner, and leaves the guard's derivation untouched. + if args.carry_blocked and not args.repo_root: + fail( + "blocked_on_root_missing", + "--carry-blocked requires --repo-root: containment is a filesystem check", + ) + blocked, blocked_dropped = ( + _read_blocked_on(out_dir, attempt, args.repo_root, {f["file"] for f in blockers}) + if args.carry_blocked + else ([], 0) + ) + synthesised: "list[dict]" = [] + for offset, row in enumerate(blocked, start=1): + # CONTROLLER-AUTHORED AND DETERMINISTIC. `summary` feeds `_fingerprint`, + # which is the cross-attempt identity `_blocker_fingerprints` counts on; + # letting an agent author it would make that identity agent-authored + # too. The agent's own words go in `failure_scenario`, where nothing + # keys on them. + summary = ( + "cross-file consequence: attempt %d's fixer for %s reported this file " + "as one it needed in order to finish the fix" % (row["attempt"], row["from_file"]) + ) + synthesised.append( + { + # After every reviewer finding, so `_fix_waves`' first-appearance + # ordering puts the reviewer's own blockers in the earlier waves. + "rank": len(findings) + offset, + "file": row["file"], + # NEVER A LINE. The consequence was observed at coordinates other + # wave members have already moved; the reason survives a rewrite + # and a line number does not. + "line": None, + "summary": summary, + "failure_scenario": row["reason"], + "category": None, + "verdict": None, + "fingerprint": _fingerprint(row["file"], summary), + "severity": "blocker", + "severity_source": "controller", + } + ) + # Both lists, so `counts.total`, `counts.blocker` and `counts.category_backed` + # keep describing the same population the artifact actually holds. + findings = findings + synthesised + blockers = blockers + synthesised + classified = { "schema_version": SCHEMA_VERSION, "attempt": attempt, @@ -1461,6 +1661,13 @@ def cmd_plan(args: argparse.Namespace) -> int: # "we filed 9 suggestions" can be told apart from "this review found # 9 suggestions" -- under a loop those are different numbers. "suggestion_carried": carried_count, + # How many paths this attempt's wave plan gained from the PREVIOUS + # attempt's `blocked_on_file` returns, and how many rows were + # refused or displaced by MAX_BLOCKED_ON on the way in. Reported so + # "the reviewer found 3 blockers" can be told apart from "the + # reviewer found 1 and the loop carried 2 cross-file consequences". + "blocked_on_admitted": len(synthesised), + "blocked_on_dropped": blocked_dropped, }, "blockers": blockers, "suggestions": suggestions, @@ -1532,7 +1739,7 @@ def cmd_plan(args: argparse.Namespace) -> int: # matches on it): append, never reorder or insert. print( "PREMERGE_TRIAGE TOTAL=%d BLOCKER=%d SUGGESTION=%d WAVES=%d CATEGORY_BACKED=%d" - " ATTEMPT=%d CARRIED=%d" + " ATTEMPT=%d CARRIED=%d BLOCKED=%d" % ( len(findings), len(blockers), @@ -1541,6 +1748,7 @@ def cmd_plan(args: argparse.Namespace) -> int: classified["counts"]["category_backed"], attempt, carried_count, + len(synthesised), ) ) return 0 @@ -2120,6 +2328,11 @@ def main(argv: "list[str]") -> int: plan.add_argument("--attempt", type=int, default=1) plan.add_argument("--head-sha", default=None) plan.add_argument("--carry-prior", action="store_true") + plan.add_argument("--carry-blocked", action="store_true") + # Only required WITH --carry-blocked, and `cmd_plan` refuses the pair rather + # than defaulting it: containment is a filesystem check and there is no safe + # root to guess. Left optional so every existing call site is unchanged. + plan.add_argument("--repo-root", default=None) plan.set_defaults(handler=cmd_plan) gate = sub.add_parser("assert-green", help="the pre-simplify clean gate") diff --git a/plugins/uberdev/lib/review-consolidate.sh b/plugins/uberdev/lib/review-consolidate.sh index f8133f6f..668e4d29 100644 --- a/plugins/uberdev/lib/review-consolidate.sh +++ b/plugins/uberdev/lib/review-consolidate.sh @@ -655,11 +655,22 @@ review_consolidate_continue() { # The staged set may not have grown past what the merge itself staged plus the # conflicted paths. local allowed staged extra + # LC_ALL=C ON EVERY MEMBER OF THE sort/comm TRIO, INCLUDING `comm` ITSELF. + # This guard compares PATHS AS BYTES. `sort` orders by the ambient collation + # and `comm`'s merge walk assumes the order its own collation would produce; + # when the two disagree the walk desynchronises and reports a path present in + # BOTH lists as extra -- refusing a merge whose staged set was correct. That + # consequence is not theoretical: feed `comm -13` two lists holding the SAME + # three paths in two different orders and it names one of them as extra. + # The two sorts share a shell today and so agree by accident; pinning makes + # the guard's correctness a property of the code rather than of whatever + # LC_ALL / LC_COLLATE / LANG the operator's shell happened to inherit, which + # varies by platform -- and this guard runs on every platform UberDev does. allowed="$( - { cat "$scan_dir/staged-at-conflict-$number.txt" 2>/dev/null || :; tr '\0' '\n' <"$conflicts"; } | sort -u + { cat "$scan_dir/staged-at-conflict-$number.txt" 2>/dev/null || :; tr '\0' '\n' <"$conflicts"; } | LC_ALL=C sort -u )" - staged="$(git -C "$worktree" diff --cached --name-only 2>/dev/null | sort -u)" - extra="$(comm -13 <(printf '%s\n' "$allowed") <(printf '%s\n' "$staged"))" + staged="$(git -C "$worktree" diff --cached --name-only 2>/dev/null | LC_ALL=C sort -u)" + extra="$(LC_ALL=C comm -13 <(printf '%s\n' "$allowed") <(printf '%s\n' "$staged"))" if [ -n "$extra" ]; then _uberdev_consolidate_error "#$number: the staged set grew beyond the merge's own set plus the conflicted paths: $(tr '\n' ' ' <<<"$extra")" return 2 diff --git a/plugins/uberdev/skills/premerge-pipeline/SKILL.md b/plugins/uberdev/skills/premerge-pipeline/SKILL.md index b69866de..cfe094db 100644 --- a/plugins/uberdev/skills/premerge-pipeline/SKILL.md +++ b/plugins/uberdev/skills/premerge-pipeline/SKILL.md @@ -117,8 +117,18 @@ PREMERGE_CONVERGE_DEFAULT= 3 # REPAIR rounds, --converge dials it PREMERGE_REPAIR_CEILING = 6 # the runaway backstop --converge cannot pass PREMERGE_WAIT_CI_CEILING = 8 # WAIT_CI re-probes before the loop calls CI dead PREMERGE_RERUN_FLAKY_CAP = 1 # `gh run rerun` attempts per RUN +PREMERGE_MAX_BLOCKED_ON = 8 # blocked_on_file paths one attempt may add — see `### 2a — The fixer-wave mechanism` ``` +`PREMERGE_MAX_BLOCKED_ON` is **declared in `lib/premerge-findings.py`** +(`MAX_BLOCKED_ON`) and restated here for the reader. It is equal to +`PREMERGE_MAX_FIX_WAVE` on purpose: one attempt's `blocked_on_file` returns may +add at most one extra wave's worth of files to the next attempt's plan, so the +widening is bounded by something a reader can check rather than by a number +someone picked. Rows past the cap are **dropped and counted**, never refused — +a dropped path is simply a path no fixer may touch, and the finding survives to +the next attempt exactly as it does today. + `PREMERGE_REPAIR_CEILING` and `PREMERGE_WAIT_CI_CEILING` are **declared in `lib/premerge-findings.py`** (`CONVERGE_REPAIR_CEILING`, `CONVERGE_WAIT_CI_CEILING`) and restated here for the reader. The library is the enforcer — it refuses an @@ -738,15 +748,26 @@ python3 -I -B "$UBERDEV_PREMERGE_PLUGIN_ROOT/lib/premerge-findings.py" plan \ --attempt "$PREMERGE_ATTEMPT" \ --head-sha "$PREMERGE_HEAD_SHA" \ --carry-prior \ + --carry-blocked \ + --repo-root "$PREMERGE_ROOT" \ --max-per-wave 8 || exit 74 ``` The fence prints one line: ``` -PREMERGE_TRIAGE TOTAL= BLOCKER= SUGGESTION= WAVES= CATEGORY_BACKED= ATTEMPT= CARRIED= +PREMERGE_TRIAGE TOTAL= BLOCKER= SUGGESTION= WAVES= CATEGORY_BACKED= ATTEMPT= CARRIED= BLOCKED= ``` +`--carry-blocked` is why `BLOCKED` exists: it is how many paths the PREVIOUS +attempt's `blocked_on_file` returns added to this attempt's wave plan. It counts +admitted paths only; rows refused by validation or displaced by +`PREMERGE_MAX_BLOCKED_ON` are counted in `classified.json` under +`counts.blocked_on_dropped`. A `BLOCKED=` above zero on a run whose reviewer +found nothing new is the loop finishing a cross-file fix the previous attempt +could only report — the round that would otherwise have been spent +rediscovering the block. + `--carry-prior` is why `CARRIED` exists. Blockers are things the loop is actively removing, so only the latest review's blocker set means anything. **Suggestions are never fixed by the loop** — so a suggestion the reviewer raised on attempt 1 @@ -792,7 +813,17 @@ Fix the code-review findings listed below in the single file . Rules: * Edit ONLY . If the honest fix requires touching another file, do not - make it — return `status: REFUSED` naming the file you would have needed. + make it — return `status: REFUSED` and name that file under + `blocked_on_file`. That is the FIRST emission case. + * The SECOND emission case is `status: APPLIED`. If your fix is correct and + complete in but leaves a CONSEQUENCE in another file — a comment, + a doc line or a sibling half of a two-way contract that your edit has just + made false — name that file under `blocked_on_file` too. Nobody else is + scoped to it, so a consequence you do not report is a consequence that + survives the round untouched. + * Give the REASON, never a line range. Other agents are editing their own + files in this same wave, so the coordinates you read may already have + moved by the time anyone acts on them. * Do NOT run git. No add, no commit, no stash, no checkout, no push. * Do NOT create new files. * If a finding is wrong, already handled, or would change intended behaviour @@ -814,8 +845,45 @@ outcomes: - rank: outcome: fixed | skipped | no_change_needed reason: +blocked_on_file: # OPTIONAL, repeatable. Omit the key entirely when empty. + - file: + reason: ``` +**Before that commit, write what the wave reported.** Collect every +`blocked_on_file` entry from every agent in every wave of this attempt into +`$PREMERGE_RUN_DIR/blocked-on-.json`, `` zero-padded to this attempt's +index, in exactly this shape: + +```json +{ + "schema_version": 1, + "attempt": 3, + "blocked_on": [ + { "file": "tests/agent-description-budget.test.sh", + "from_file": ".github/workflows/test.yml", + "reason": "the windows-skip-list is a two-way runtime contract; the marker block and the fixture's own guard must move together" } + ] +} +``` + +Write `"blocked_on": []` when the wave reported nothing, and **do not skip the +artifact** — an absent file and an empty array must never be the same +observable state, for the same reason `review-input.json` may not be skipped. + +**This file does not widen anything by itself.** The next attempt's `plan` +reads it, validates every value, caps the count at `PREMERGE_MAX_BLOCKED_ON`, +and turns each survivor into a blocker finding on that file — which +`_fix_waves` then groups into a wave like any other, with exactly one owner. +The commit fence below keeps deriving its allowed set from +`fix-waves-.json` and from nothing else, which is what keeps "committable" +and "owned by exactly one agent" the same set. Never add a path to the fence's +allowed list directly: that is the one edit this whole design exists to avoid. + +**Only the immediately preceding attempt is read.** `plan` opens +`blocked-on-.json` by name and never a range, so the set cannot accumulate +across the repair budget into an allow-list that constrains nothing. + After the last wave returns, **the controller makes exactly one commit** — agents never touch git, and the fence checks what they actually changed against the wave plan before it stages anything: diff --git a/plugins/uberdev/skills/premerge-pipeline/references/common-mistakes.md b/plugins/uberdev/skills/premerge-pipeline/references/common-mistakes.md index 42475511..a8a54d74 100644 --- a/plugins/uberdev/skills/premerge-pipeline/references/common-mistakes.md +++ b/plugins/uberdev/skills/premerge-pipeline/references/common-mistakes.md @@ -79,6 +79,30 @@ puts agent-authored commits on the stack PR unexamined. **Two agents in one wave sharing a file.** `_fix_waves` groups by path so this cannot happen — do not "optimise" it into one-agent-per-finding. +**Adding a `blocked_on_file` path straight to the commit fence's allowed set.** +The smallest possible diff, and wrong twice over. The fence derives the +committable set from `fix-waves-.json` and nothing else, which is what keeps +"committable" and "owned by exactly one agent" the same set. A path in the +allowed list with no wave entry is committable while nobody owns it, and +`_fix_waves` never grouped it, so the disjointness above says nothing about the +two next-wave agents that may now both edit it against the one worktree those +waves share with no isolation. Worse, the wave plan is also what DISPATCHES the +fixers, so a `blocked_on_file` path that reached only the allowed list is edited +by nobody: the cross-file consequence survives the round untouched — the very +failure the widening was reaching for. The paths go through `_fix_waves` as +synthesised blocker findings, never around it. + +**Unioning `blocked_on_file` across every earlier attempt.** The sibling in the +same file invites it — `_carry_prior_suggestions` walks `range(1, attempt)` — +and copying that shape reads as consistency. It is correct there because a +suggestion is never fixed and must not be lost. A blocked-on path is the +opposite: it WIDENS what a fixer may edit, so a union over every earlier attempt +only ever grows, and the repair budget runs to `PREMERGE_REPAIR_CEILING` rounds +— a set that grows every round is no longer a guard by the last of them. Exactly +one predecessor: `plan --carry-blocked` opens `blocked-on-.json` by name, +never a range, and what survives validation is capped at +`PREMERGE_MAX_BLOCKED_ON`. + **Trusting "Edit ONLY ``" because the prompt says it.** The prompt is the instruction; the commit fence's scope check is the enforcement. `git add -u` stages every tracked modification in the tree, so without that comparison diff --git a/tests/goal.test.sh b/tests/goal.test.sh index 4b60465b..92034770 100755 --- a/tests/goal.test.sh +++ b/tests/goal.test.sh @@ -4811,6 +4811,92 @@ else fi echo +echo "== G56: the PR file-collision detector compares paths as BYTES (#755) ==" +# Scoped to the FUNCTION BODY, never the whole file: goal-state.sh is 3800+ +# lines and a whole-file grep for LC_ALL=C is satisfied by any other call site +# in it. The predicate lives in ONE function so the anti-vacuity rows below +# drive the SAME code the live row does -- a probe that re-implements the rule +# it checks is permanently green no matter what the shipped rule does. +g56_body() { # g56_body FILE OPENER -> the function body, opener to its closing brace + awk -v opener="$2" ' + index($0, opener) == 1 { inf = 1; next } + inf && /^}/ { exit } + inf { print } + ' "$1" +} +g56_unpinned() { # g56_unpinned FILE OPENER -> offending lines, empty when clean + # Comment lines are stripped first: the shipped guard carries its own + # rationale, and that paragraph says the words "sort" and "comm", so a probe + # over the raw body is satisfied by the code's own prose. + # + # THEN NEUTRALISE PINNED PAIRS PER TOKEN -- NEVER PER LINE. A trailing + # `grep -v 'LC_ALL=C'` was the first shape tried and it CANNOT SEE the defect + # it exists for: the `intersection=` line runs TWO commands, so pinning only + # the `comm` leaves the string `LC_ALL=C` on the line and `grep -v` then + # discards the whole line, unpinned `sort` and all. Measured on the + # half-pinned body: zero hits. Rewriting each PINNED pair into a single token + # instead leaves every still-UNPINNED `sort`/`comm` on that same line visible + # to the offender grep -- which is what the G56.mutation-half row proves. + # + # `sed -E` rather than a BRE `\(sort\|comm\)`: `\|` is a GNU extension that + # BSD sed matches as a LITERAL pipe, so the BRE form silently neutralises + # nothing on macOS and reds the live row there. Measured, not assumed. + g56_body "$1" "$2" \ + | grep -vE '^[[:space:]]*#' \ + | sed -E 's/LC_ALL=C[[:space:]]+(sort|comm)/PINNED_\1/g' \ + | grep -nE '(^|[^=[:alnum:]_./-])(sort|comm)[[:space:]]' || : +} +G56_OPENER='_uberdev_goal_rebase_collision_chain() {' +G56_LINES="$(g56_body "$GOAL_LIB" "$G56_OPENER" | grep -c . || :)" +if [ "${G56_LINES:-0}" -ge 10 ]; then + echo " PASS G56.extract: the collision-chain body extracted $G56_LINES lines"; PASS=$((PASS + 1)) +else + echo " FAIL G56.extract: extracted only ${G56_LINES:-0} lines — every row below is vacuous"; FAIL=$((FAIL + 1)) +fi +G56_HITS="$(g56_unpinned "$GOAL_LIB" "$G56_OPENER")" +if [ -z "$G56_HITS" ]; then + echo " PASS G56.live: every sort/comm in the collision detector is LC_ALL=C-pinned"; PASS=$((PASS + 1)) +else + echo " FAIL G56.live: unpinned sort/comm in the collision detector: $G56_HITS"; FAIL=$((FAIL + 1)) +fi +# ANTI-VACUITY: TWO MUTANTS, THE SAME PREDICATE FUNCTION. Without these a typo +# in the grep would make G56.live pass forever. +G56_MUTANT="$(mktemp)" +cat >"$G56_MUTANT" <<'EOF_G56_FULL' +_uberdev_goal_rebase_collision_chain() { + local merged_sorted + merged_sorted="$(printf '%s\n' "$merged_diff" | sort -u)" + intersection="$(comm -12 <(printf '%s\n' "$merged_sorted") <(printf '%s\n' "$pr_diff" | sort -u))" +} +EOF_G56_FULL +G56_MUTANT_HITS="$(g56_unpinned "$G56_MUTANT" "$G56_OPENER")" +rm -f "$G56_MUTANT" +if [ -n "$G56_MUTANT_HITS" ]; then + echo " PASS G56.mutation: a fully unpinned body reds through the same predicate"; PASS=$((PASS + 1)) +else + echo " FAIL G56.mutation: the predicate cannot see an unpinned sort/comm — G56.live is vacuous"; FAIL=$((FAIL + 1)) +fi +# THE HALF-PINNED MUTANT IS THE ONE THAT MATTERS. It is the shipped +# `intersection=` line with the `comm` pinned and the inner `sort` left bare -- +# the exact half-fix a reviewer waves through, and the exact body the rejected +# line-level predicate scored as CLEAN. A predicate that greens here is a +# predicate that would have shipped the very defect this section is named for. +G56_MUTANT_HALF="$(mktemp)" +cat >"$G56_MUTANT_HALF" <<'EOF_G56_HALF' +_uberdev_goal_rebase_collision_chain() { + local merged_sorted + merged_sorted="$(printf '%s\n' "$merged_diff" | LC_ALL=C sort -u)" + intersection="$(LC_ALL=C comm -12 <(printf '%s\n' "$merged_sorted") <(printf '%s\n' "$pr_diff" | sort -u))" +} +EOF_G56_HALF +G56_HALF_HITS="$(g56_unpinned "$G56_MUTANT_HALF" "$G56_OPENER")" +rm -f "$G56_MUTANT_HALF" +if [ -n "$G56_HALF_HITS" ]; then + echo " PASS G56.mutation-half: a half-pinned line still reds — the predicate is per-token, not per-line"; PASS=$((PASS + 1)) +else + echo " FAIL G56.mutation-half: a pinned comm hides an unpinned sort on the same line — G56.live is vacuous"; FAIL=$((FAIL + 1)) +fi + echo "== Summary ==" printf '%d passed, %d failed\n' "$PASS" "$FAIL" diff --git a/tests/premerge-findings.test.sh b/tests/premerge-findings.test.sh index e5d98040..c5b155e9 100644 --- a/tests/premerge-findings.test.sh +++ b/tests/premerge-findings.test.sh @@ -25,6 +25,17 @@ LIB="$REPO_ROOT/plugins/uberdev/lib/premerge-findings.py" [ -r "$LIB" ] || { echo "FATAL: required file missing or unreadable: $LIB" >&2; exit 2; } command -v python3 >/dev/null 2>&1 || { echo "FATAL: python3 is required by ${0##*/}" >&2; exit 2; } +# B32 derives the fix-commit fence's allowed set by EXECUTING the fence's own +# jq expression rather than a transcription of it, so jq is a hard requirement +# of this file now. A skipped row would report success over an unrun check. +command -v jq >/dev/null 2>&1 || { echo "FATAL: jq is required by ${0##*/}" >&2; exit 2; } + +# The markdown the B32 rows pull that expression OUT of. FATAL here rather than +# tolerated downstream: if this file moved, every B32 row would fail carrying +# the extractor's sentinel and the one real cause would be buried under forty +# derived ones. +SKILL_MD="$REPO_ROOT/plugins/uberdev/skills/premerge-pipeline/SKILL.md" +[ -r "$SKILL_MD" ] || { echo "FATAL: required file missing or unreadable: $SKILL_MD" >&2; exit 2; } WORK="$(mktemp -d)" @@ -55,6 +66,105 @@ plan() { new_case() { local d; d="$(mktemp -d "$WORK/case.XXXXXX")"; printf '%s' "$d"; } +# ---- acceptance criterion 2's machinery (the B32 rows) -------------------- + +# plan_blocked DIR ATTEMPT ROOT -> plan at ATTEMPT with the widening turned on +plan_blocked() { + local dir="$1" attempt="$2" root="$3" + python3 -I -B "$LIB" plan --input "$dir/in.json" --out-dir "$dir" \ + --attempt "$attempt" --carry-blocked --repo-root "$root" \ + >"$dir/out.txt" 2>"$dir/err.txt" +} + +# write_blocked DIR NN JSON_ROWS -> DIR/blocked-on-NN.json +write_blocked() { + local dir="$1" pad="$2" rows="$3" + python3 -I -B -c ' +import json, sys +path, attempt, rows = sys.argv[1], int(sys.argv[2]), sys.argv[3] +with open(path, "w", encoding="utf-8") as fh: + json.dump({"schema_version": 1, "attempt": attempt, + "blocked_on": json.loads(rows)}, fh) +' "$dir/blocked-on-$pad.json" "$((10#$pad))" "$rows" +} + +# ONE copy of the extractor, never two. `fence_jq_line` drives it over the +# SHIPPED SKILL.md for every live row, and B32h drives THE SAME variable over +# deliberately broken SKILL-shaped bodies. A freshly typed second copy in the +# mutation rows would leave a typo in the live one green forever -- which is +# how an anti-vacuity row ends up being the vacuous one. +FENCE_JQ_AWK=' +$0 == "```bash uberdev-executable origin=premerge-fix-commit" { inf = 1; next } +inf && index($0, "```") == 1 { exit } +inf && index($0, "jq -r") == 1 { print; exit } +' + +# fence_jq_line FILE -> the fix-commit fence's own column-0 `jq -r` line, or +# nothing. Column 0 AND inside THAT fence, because two other jq calls would +# otherwise answer for the scope guard: the ordering guard's `jq -r '.verdict'` +# lives in a command substitution in this same fence (so it is not at column 0), +# and other fences in the file carry jq of their own (so the walk must stop at +# this fence's closing backticks). B32h drives both decoys. +fence_jq_line() { awk "$FENCE_JQ_AWK" "$1"; } + +# allowed_set DIR NN [SKILL_FILE] -> the allowed set the fix-commit fence would +# derive, computed with the fence's OWN expression, extracted from SKILL.md +# rather than transcribed. A transcribed copy is a test of the copy. +allowed_set() { + local dir="$1" pad="$2" skill="${3:-$SKILL_MD}" expr + expr="$(fence_jq_line "$skill")" + [ -n "$expr" ] || { printf 'NO-JQ-LINE-IN-FENCE'; return 0; } + # A SUBSHELL, because the fence line ends in `|| exit 2` and that is the + # point -- it is the shipped bytes. `eval` runs in the CURRENT shell, so an + # unguarded eval of that line would exit this whole fixture on a jq failure: + # nothing after it prints, and a suite that dies early looks exactly like a + # silent pass. The subshell absorbs the exit and the `||` arm sees it. + ( + PREMERGE_WAVES_FILE="$dir/fix-waves-$pad.json" + PREMERGE_SCOPE_RAW="$dir/scope-$pad.raw" + eval "$expr" + ) || { printf 'JQ-FAILED'; return 0; } + LC_ALL=C sort -u <"$dir/scope-$pad.raw" +} + +# admits SET PATH -> true when the space-joined SET holds PATH as a whole entry. +# ONE membership predicate for every positive AND negative row in the section, +# so a broken one cannot leave the negative rows quietly green: the same call is +# expected to succeed somewhere and to fail somewhere else on every run. +admits() { case " $1 " in *" $2 "*) return 0 ;; *) return 1 ;; esac; } + +# dup_lines FILE -> the lines FILE holds MORE THAN ONCE. +# +# Pointed at the RAW `jq -r` output and NOT at `allowed_set`'s stdout, which +# ends in `sort -u`: a duplicate detector reading THAT is green whatever the +# wave plan holds -- a guard that cannot fail. The raw file is `.waves[][].file` +# verbatim, which is the surface a second owner for one path shows up on. +dup_lines() { LC_ALL=C sort <"$1" | uniq -d; } + +# triage_blocked DIR -> the BLOCKED= field of the triage line, or a loud +# sentinel. BLOCKED is the line's last field, so the suffix strip is exact -- +# and one reader serves both the widened row (1) and the opt-out row (0), which +# is what keeps either from passing over a broken extraction. +triage_blocked() { + local line + line="$(grep '^PREMERGE_TRIAGE ' "$1/out.txt")" + case "$line" in + *" BLOCKED="*) printf '%s' "${line##* BLOCKED=}" ;; + *) printf 'NO-TRIAGE-LINE' ;; + esac +} + +# counts_field DIR NN NAME -> classified-NN.json's counts.NAME. A renamed key +# answers with a sentinel instead of a traceback, so the row that reads it fails +# saying which key went missing. +counts_field() { + python3 -I -B -c ' +import json, sys +counts = json.load(open(sys.argv[1] + "/classified-" + sys.argv[2] + ".json"))["counts"] +print(counts.get(sys.argv[3], "NO-SUCH-COUNT")) +' "$1" "$2" "$3" +} + # -------------------------------------------------------------------------- echo "== B1: plan happy path — the three severity provenances ==" D="$(new_case)" @@ -2123,6 +2233,392 @@ else bad "B29: one corrupt artifact still costs the whole run: $(cat "$D/b29.err")" fi +# -------------------------------------------------------------------------- +echo "== B32: attempt N+1's wave plan widens from attempt N's blocked_on_file ==" +# The whole acceptance criterion, executed. The allowed set is derived by the +# FENCE'S OWN jq expression, pulled out of SKILL.md — so a change to that line +# in the shipped fence changes what this row measures, which is the only way a +# fixture can prove anything about a guard that lives in a markdown fence. +D="$(new_case)" +B32_ROOT="$(mktemp -d "$WORK/root.XXXXXX")" +mkdir -p "$B32_ROOT/lib" "$B32_ROOT/tests" +: >"$B32_ROOT/lib/a.sh"; : >"$B32_ROOT/lib/b.sh"; : >"$B32_ROOT/tests/fixture.sh" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"tests/fixture.sh","from_file":"lib/a.sh","reason":"two-way contract: both halves move together"} +]' +if plan_blocked "$D" 2 "$B32_ROOT"; then + ok "B32: plan accepted --carry-blocked --repo-root" +else + bad "B32: plan refused: $(cat "$D/err.txt")" +fi +# Kept for B32e, which greps THIS stderr — the run where nothing was skipped — +# with the same pattern it uses on the run where something was. +B32_CLEAN_ERR="$D/err.txt" +B32_GOT="$(allowed_set "$D" 02 | tr '\n' ' ')" +if [ "$B32_GOT" = "lib/a.sh tests/fixture.sh " ]; then + ok "B32: the allowed set is exactly the reviewer's file plus the blocked-on file" +else + bad "B32: allowed set was '$B32_GOT', wanted 'lib/a.sh tests/fixture.sh '" +fi +[ "$(triage_blocked "$D")" = "1" ] \ + && ok "B32: the triage line reports BLOCKED=1" \ + || bad "B32: BLOCKED was $(triage_blocked "$D"), wanted 1: $(cat "$D/out.txt")" +# AND NO OTHERS. lib/b.sh exists in the root and is named by nothing, so it +# must not be admitted — the row that tells "widened correctly" apart from +# "widened to whatever was lying around". All three rows drive the SAME +# `admits`, two expecting a hit and one expecting a miss, so a predicate that +# answered the same way every time would red here rather than pass everywhere. +admits "$B32_GOT" "lib/a.sh" \ + && ok "B32: the reviewer's own file is admitted" \ + || bad "B32: the reviewer's file is missing from '$B32_GOT'" +admits "$B32_GOT" "tests/fixture.sh" \ + && ok "B32: the blocked-on file is admitted" \ + || bad "B32: the blocked-on file is missing from '$B32_GOT'" +admits "$B32_GOT" "lib/b.sh" \ + && bad "B32: an unnamed file reached the allowed set: '$B32_GOT'" \ + || ok "B32: a file named by nothing is not admitted" +# Exactly one owner. Two entries for one path would put two agents on one file. +B32_DUP="$(dup_lines "$D/scope-02.raw")" +[ -z "$B32_DUP" ] && ok "B32: every assigned path appears exactly once" \ + || bad "B32: duplicated wave entry: $B32_DUP" +# ANTI-VACUITY for the row above, through the SAME `dup_lines`: a raw list that +# genuinely repeats a path. Without it `dup_lines` could be broken outright — +# or pointed at a `sort -u`ed stream — and the live row would stay green. +printf 'lib/a.sh\nlib/a.sh\ntests/fixture.sh\n' >"$D/dupe.raw" +[ -n "$(dup_lines "$D/dupe.raw")" ] \ + && ok "B32: and dup_lines can actually see a repeat" \ + || bad "B32: dup_lines saw nothing in a list that repeats lib/a.sh" + +echo "== B32a: a blocked-on path the reviewer already named adds no second owner ==" +# `_read_blocked_on` seeds its dedupe with THIS attempt's blocker files, so the +# row is dropped rather than made a second wave member. The check that can see +# it is the raw list: a second entry for lib/a.sh vanishes into `sort -u`. +D="$(new_case)" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"lib/a.sh","from_file":"lib/a.sh","reason":"the fixer named its own file"} +]' +plan_blocked "$D" 2 "$B32_ROOT" || bad "B32a: plan failed: $(cat "$D/err.txt")" +B32A_GOT="$(allowed_set "$D" 02 | tr '\n' ' ')" +[ "$B32A_GOT" = "lib/a.sh " ] && ok "B32a: the already-named row widens nothing" \ + || bad "B32a: allowed set was '$B32A_GOT', wanted 'lib/a.sh '" +B32A_DUP="$(dup_lines "$D/scope-02.raw")" +[ -z "$B32A_DUP" ] && ok "B32a: and lib/a.sh still has exactly one owner" \ + || bad "B32a: lib/a.sh gained a second owner: $B32A_DUP" +[ "$(counts_field "$D" 02 blocked_on_dropped)" = "1" ] \ + && ok "B32a: the dropped duplicate is counted, not silent" \ + || bad "B32a: blocked_on_dropped was $(counts_field "$D" 02 blocked_on_dropped), wanted 1" + +echo "== B32b: the union does not accumulate across attempts ==" +# Attempt 3 reads blocked-on-02.json and NOTHING else. A monotonically growing +# allowed set admits most of the tree by CONVERGE_REPAIR_CEILING, at which +# point it is no longer a guard. +D="$(new_case)" +: >"$B32_ROOT/tests/older.sh"; : >"$B32_ROOT/tests/newer.sh" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"tests/older.sh","from_file":"lib/a.sh","reason":"attempt 1 said so"} +]' +write_blocked "$D" 02 '[ + {"file":"tests/newer.sh","from_file":"lib/a.sh","reason":"attempt 2 said so"} +]' +# THE CONTROL FIRST, same case dir and same predicate: at attempt 2 the +# attempt-1 artifact IS read and tests/older.sh IS admitted. Without it the row +# below would be just as green if tests/older.sh were unadmittable for some +# unrelated reason, and it would then be proving nothing about accumulation. +plan_blocked "$D" 2 "$B32_ROOT" || bad "B32b: the control plan failed: $(cat "$D/err.txt")" +B32B_CTL="$(allowed_set "$D" 02 | tr '\n' ' ')" +admits "$B32B_CTL" "tests/older.sh" \ + && ok "B32b: attempt 2 does admit attempt 1's path" \ + || bad "B32b: the control never admitted tests/older.sh: '$B32B_CTL'" +plan_blocked "$D" 3 "$B32_ROOT" || bad "B32b: plan failed: $(cat "$D/err.txt")" +B32B_GOT="$(allowed_set "$D" 03 | tr '\n' ' ')" +if [ "$B32B_GOT" = "lib/a.sh tests/newer.sh " ]; then + ok "B32b: attempt 3 admits attempt 2's paths and not attempt 1's" +else + bad "B32b: allowed set was '$B32B_GOT', wanted 'lib/a.sh tests/newer.sh '" +fi + +echo "== B32c: every value is validated, and a bad one is dropped not fatal ==" +# Five rejects and one survivor in ONE document. Refusing would cost the whole +# attempt over a row an agent emitted; dropping is the fail-CLOSED direction, +# because a dropped path is a path no fixer may touch. +# +# The envelope rows name the TOKEN, not a whole closing tag, because that is +# exactly what the library's predicate matches on. ONE copy of it, spliced into +# all three of `reason`, `file` and `from_file` below, so the three cases cannot +# disagree about what string they are testing. +B32_ENVELOPE_TOKEN='external-untrusted-input' +D="$(new_case)" +: >"$B32_ROOT/tests/good.sh" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"/etc/passwd","from_file":"lib/a.sh","reason":"absolute"}, + {"file":"../outside.sh","from_file":"lib/a.sh","reason":"traversal"}, + {"file":".git/config","from_file":"lib/a.sh","reason":"dotgit"}, + {"file":"tests/does-not-exist.sh","from_file":"lib/a.sh","reason":"absent"}, + {"file":"tests/good.sh","from_file":"lib/a.sh","reason":"'"$B32_ENVELOPE_TOKEN"' escape attempt"}, + {"file":"tests/good.sh","from_file":"lib/a.sh","reason":"the honest one"} +]' +plan_blocked "$D" 2 "$B32_ROOT" || bad "B32c: plan refused a document it should tolerate: $(cat "$D/err.txt")" +B32C_GOT="$(allowed_set "$D" 02 | tr '\n' ' ')" +if [ "$B32C_GOT" = "lib/a.sh tests/good.sh " ]; then + ok "B32c: only the valid row is admitted, and the attempt survives" +else + bad "B32c: allowed set was '$B32C_GOT', wanted 'lib/a.sh tests/good.sh '" +fi +[ "$(counts_field "$D" 02 blocked_on_dropped)" = "5" ] \ + && ok "B32c: all five rejected rows are counted, not silent" \ + || bad "B32c: blocked_on_dropped was $(counts_field "$D" 02 blocked_on_dropped), wanted 5" +# THE IN-DOCUMENT CONTROL. One row of the six survives, so "five dropped" is not +# the same sentence as "every row is dropped" — a validator that refused +# everything would show 6 here and 0 admitted, and both rows would red. +[ "$(counts_field "$D" 02 blocked_on_admitted)" = "1" ] \ + && ok "B32c: and the honest row in the same document still lands" \ + || bad "B32c: blocked_on_admitted was $(counts_field "$D" 02 blocked_on_admitted), wanted 1" + +echo "== B32c2: the envelope token is refused in the AGENT-CHOSEN file value too ==" +# `_read_blocked_on` checks THREE strings, not two: `reason`, `from_file` and +# `file`. `file` is the value the agent chose and it lands verbatim in the +# synthesised finding's `file` key, which the wave prompt pastes into the +# untrusted-input envelope with no encoder between them — the most +# agent-controlled of the three, and the one a two-string check leaves open. +D="$(new_case)" +: >"$B32_ROOT/tests/$B32_ENVELOPE_TOKEN-probe.sh" +: >"$B32_ROOT/tests/envelope-probe.sh" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"tests/'"$B32_ENVELOPE_TOKEN"'-probe.sh","from_file":"lib/a.sh","reason":"an honest reason"} +]' +plan_blocked "$D" 2 "$B32_ROOT" || bad "B32c2: plan refused a document it should tolerate: $(cat "$D/err.txt")" +B32C2_GOT="$(allowed_set "$D" 02 | tr '\n' ' ')" +admits "$B32C2_GOT" "tests/$B32_ENVELOPE_TOKEN-probe.sh" \ + && bad "B32c2: a path carrying the envelope token reached the allowed set: '$B32C2_GOT'" \ + || ok "B32c2: a path carrying the envelope token is dropped" +[ "$(counts_field "$D" 02 blocked_on_dropped)" = "1" ] \ + && ok "B32c2: and that drop is counted" \ + || bad "B32c2: blocked_on_dropped was $(counts_field "$D" 02 blocked_on_dropped), wanted 1" +# THE CONTROL — the same row shape, the same directory, the same everything but +# the token in the path. It IS admitted, so the row above is measuring the token +# and not "a path under tests/ ending -probe.sh is unadmittable". +D="$(new_case)" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"tests/envelope-probe.sh","from_file":"lib/a.sh","reason":"an honest reason"} +]' +plan_blocked "$D" 2 "$B32_ROOT" || bad "B32c2: the control plan failed: $(cat "$D/err.txt")" +B32C2_CTL="$(allowed_set "$D" 02 | tr '\n' ' ')" +admits "$B32C2_CTL" "tests/envelope-probe.sh" \ + && ok "B32c2: the same row without the token in its path is admitted" \ + || bad "B32c2: the control was refused too, so the row above proves nothing: '$B32C2_CTL'" +# And the third string. `from_file` lands inside the controller-authored +# `summary`, so it is checked as well — same target path as the control above, +# so the only difference between admitted and dropped is where the token sits. +D="$(new_case)" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"tests/envelope-probe.sh","from_file":"lib/'"$B32_ENVELOPE_TOKEN"'.sh","reason":"an honest reason"} +]' +plan_blocked "$D" 2 "$B32_ROOT" || bad "B32c2: the from_file plan failed: $(cat "$D/err.txt")" +B32C2_FROM="$(allowed_set "$D" 02 | tr '\n' ' ')" +admits "$B32C2_FROM" "tests/envelope-probe.sh" \ + && bad "B32c2: a row whose from_file carries the token was admitted: '$B32C2_FROM'" \ + || ok "B32c2: the token in from_file drops the row too" + +echo "== B32d: the widening is capped, and --repo-root is required ==" +D="$(new_case)" +B32D_ROWS='[' +B32D_SEP='' +for B32D_N in 1 2 3 4 5 6 7 8 9 10; do + : >"$B32_ROOT/tests/cap$B32D_N.sh" + B32D_ROWS="$B32D_ROWS$B32D_SEP{\"file\":\"tests/cap$B32D_N.sh\",\"from_file\":\"lib/a.sh\",\"reason\":\"r$B32D_N\"}" + B32D_SEP=',' +done +B32D_ROWS="$B32D_ROWS]" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 "$B32D_ROWS" +plan_blocked "$D" 2 "$B32_ROOT" || bad "B32d: plan failed: $(cat "$D/err.txt")" +B32D_SET="$(allowed_set "$D" 02 | tr '\n' ' ')" +B32D_COUNT="$(allowed_set "$D" 02 | grep -c '^tests/cap')" +[ "$B32D_COUNT" = "8" ] && ok "B32d: 10 blocked-on paths admit exactly MAX_BLOCKED_ON of them" \ + || bad "B32d: admitted $B32D_COUNT capped paths, wanted 8" +[ "$(counts_field "$D" 02 blocked_on_dropped)" = "2" ] \ + && ok "B32d: the two displaced rows are counted, not silent" \ + || bad "B32d: blocked_on_dropped was $(counts_field "$D" 02 blocked_on_dropped), wanted 2" +# WHICH eight, on the same `admits` predicate. The cap keeps the rows it reached +# first and displaces the tail, so "eight of them" cannot be green over the +# wrong eight — and these two rows are each other's mutation. +admits "$B32D_SET" "tests/cap1.sh" \ + && ok "B32d: the first row survives the cap" \ + || bad "B32d: the first row was displaced: '$B32D_SET'" +admits "$B32D_SET" "tests/cap9.sh" \ + && bad "B32d: an overflow row reached the allowed set: '$B32D_SET'" \ + || ok "B32d: the overflow rows are the ones displaced" +B32D_ROOT_TOKEN='blocked_on_root_missing' +if python3 -I -B "$LIB" plan --input "$D/in.json" --out-dir "$D" --attempt 2 \ + --carry-blocked >/dev/null 2>"$D/noroot.err"; then + bad "B32d: --carry-blocked was accepted without --repo-root" +else + grep -q "$B32D_ROOT_TOKEN" "$D/noroot.err" \ + && ok "B32d: --carry-blocked without --repo-root refuses with its own token" \ + || bad "B32d: wrong token: $(cat "$D/noroot.err")" +fi +# ANTI-VACUITY, same variable and same grep, over the stderr of the run in this +# very section that DID pass --repo-root: a pattern loose enough to match +# anything would be green there too. +grep -q "$B32D_ROOT_TOKEN" "$D/err.txt" \ + && bad "B32d: the refusal token appears on a run that supplied --repo-root" \ + || ok "B32d: and that token is absent when --repo-root is supplied" + +echo "== B32e: a corrupt blocked-on artifact widens nothing and kills nothing ==" +B32E_SKIP_TOKEN='blocked_on_unreadable_skipped' +D="$(new_case)" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +printf 'not json at all' >"$D/blocked-on-01.json" +if plan_blocked "$D" 2 "$B32_ROOT"; then + ok "B32e: an unreadable artifact does not cost the attempt" +else + bad "B32e: plan died on a corrupt blocked-on artifact: $(cat "$D/err.txt")" +fi +B32E_GOT="$(allowed_set "$D" 02 | tr '\n' ' ')" +[ "$B32E_GOT" = "lib/a.sh " ] && ok "B32e: and it widens nothing" \ + || bad "B32e: allowed set was '$B32E_GOT', wanted 'lib/a.sh '" +grep -q "$B32E_SKIP_TOKEN" "$D/err.txt" \ + && ok "B32e: the skip is announced, not silent" \ + || bad "B32e: nothing announced the skip: $(cat "$D/err.txt")" +# ANTI-VACUITY, same variable and same grep, over B32's stderr — an attempt +# whose artifact read cleanly. A token that appeared unconditionally, or a grep +# that matched anything, would be green there and the row above would be air. +grep -q "$B32E_SKIP_TOKEN" "$B32_CLEAN_ERR" \ + && bad "B32e: the skip token appears on an attempt that read its artifact fine" \ + || ok "B32e: and it is absent when the artifact reads cleanly" + +echo "== B32f: containment is a filesystem check, not a lexical one ==" +# A symlink whose path is lexically repo-relative and whose target resolves +# outside. `_repo_path` alone admits it; `_beneath_repo_root` is what refuses. +D="$(new_case)" +B32F_OUT="$(mktemp -d "$WORK/outside.XXXXXX")" +: >"$B32F_OUT/secret.sh" +ln -s "$B32F_OUT" "$B32_ROOT/escape" 2>/dev/null || : +# The in-root twin: same two-segment shape, same depth, real directory. It is +# what makes the row below mean "resolved outside the root" rather than "a path +# one directory down is unadmittable". +mkdir -p "$B32_ROOT/inside" +: >"$B32_ROOT/inside/secret.sh" +if [ -L "$B32_ROOT/escape" ]; then + write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} + ]' + write_blocked "$D" 01 '[ + {"file":"escape/secret.sh","from_file":"lib/a.sh","reason":"lexically fine"}, + {"file":"inside/secret.sh","from_file":"lib/a.sh","reason":"genuinely inside"} + ]' + plan_blocked "$D" 2 "$B32_ROOT" || bad "B32f: plan failed: $(cat "$D/err.txt")" + B32F_GOT="$(allowed_set "$D" 02 | tr '\n' ' ')" + admits "$B32F_GOT" "escape/secret.sh" \ + && bad "B32f: a symlink resolving outside the root was admitted: '$B32F_GOT'" \ + || ok "B32f: a symlink resolving outside the root is refused" + admits "$B32F_GOT" "inside/secret.sh" \ + && ok "B32f: and the same shape resolving INSIDE the root is admitted" \ + || bad "B32f: the control was refused too, so the row above proves nothing: '$B32F_GOT'" + [ "$(counts_field "$D" 02 blocked_on_dropped)" = "1" ] \ + && ok "B32f: exactly the escaping row is dropped" \ + || bad "B32f: blocked_on_dropped was $(counts_field "$D" 02 blocked_on_dropped), wanted 1" +else + bad "B32f: could not create the symlink fixture — this row proves nothing" +fi + +echo "== B32g: --carry-blocked is opt-in ==" +D="$(new_case)" +write_input "$D" '[ + {"file":"lib/a.sh","line":1,"summary":"one","failure_scenario":"y","severity":"blocker"} +]' +write_blocked "$D" 01 '[ + {"file":"tests/good.sh","from_file":"lib/a.sh","reason":"present but unrequested"} +]' +python3 -I -B "$LIB" plan --input "$D/in.json" --out-dir "$D" --attempt 2 \ + >"$D/out.txt" 2>"$D/err.txt" || bad "B32g: plan failed: $(cat "$D/err.txt")" +B32G_GOT="$(allowed_set "$D" 02 | tr '\n' ' ')" +[ "$B32G_GOT" = "lib/a.sh " ] && ok "B32g: without the flag the artifact is not read" \ + || bad "B32g: allowed set was '$B32G_GOT', wanted 'lib/a.sh '" +[ "$(triage_blocked "$D")" = "0" ] \ + && ok "B32g: and the line reports BLOCKED=0" \ + || bad "B32g: BLOCKED was $(triage_blocked "$D"), wanted 0: $(cat "$D/out.txt")" + +echo "== B32h: the extractor reads the SHIPPED line, and says so when it cannot ==" +# Every row above is only as honest as `fence_jq_line`. These drive THE SAME +# variable over SKILL-shaped bodies a transcription would sail through, so a +# broken extractor cannot leave the whole section quietly green. +# +# Shape only, never the byte string: re-pinning those bytes here would be a +# second copy of the lock tests/premerge.test.sh already holds, free to drift +# from it. What this row owes is proof that something real came out. +B32H_LIVE="$(fence_jq_line "$SKILL_MD")" +case "$B32H_LIVE" in + "jq -r "*PREMERGE_WAVES_FILE*PREMERGE_SCOPE_RAW*) + ok "B32h: the live extraction is the fence's own scope-guard jq line" ;; + *) bad "B32h: extracted '$B32H_LIVE' from the shipped fence" ;; +esac +B32H_DIR="$(new_case)" +# The fence is present but its column-0 jq line is gone. Two decoys: the gate's +# own `jq -r` inside a command substitution (not at column 0) and a real +# column-0 one in a LATER fence. Both must be invisible. +{ + echo 'prose above the fence' + echo '```bash uberdev-executable origin=premerge-fix-commit' + echo 'set -u' + echo 'PREMERGE_GATE_SAID="$(jq -r '"'"'.verdict'"'"' <"$F")" || exit 2' + echo '```' + echo '```bash uberdev-executable origin=premerge-some-other-fence' + echo 'jq -r '"'"'.waves[][].file'"'"' <"$PREMERGE_WAVES_FILE" >"$PREMERGE_SCOPE_RAW" || exit 2' + echo '```' +} >"$B32H_DIR/no-jq.md" +[ -z "$(fence_jq_line "$B32H_DIR/no-jq.md")" ] \ + && ok "B32h: a fix-commit fence with no column-0 jq line extracts nothing" \ + || bad "B32h: extracted '$(fence_jq_line "$B32H_DIR/no-jq.md")' from a fence that has no such line" +printf 'no fence here at all\n' >"$B32H_DIR/no-fence.md" +[ -z "$(fence_jq_line "$B32H_DIR/no-fence.md")" ] \ + && ok "B32h: a file with no fix-commit fence extracts nothing" \ + || bad "B32h: extracted '$(fence_jq_line "$B32H_DIR/no-fence.md")' from a file with no fence" +# The mutation for the two rows above: a synthetic fence that DOES carry the +# line. Without it, an extractor hardwired to print nothing would pass both. +{ + echo '```bash uberdev-executable origin=premerge-fix-commit' + echo 'set -u' + echo 'jq -r '"'"'.waves[][].file'"'"' <"$PREMERGE_WAVES_FILE" >"$PREMERGE_SCOPE_RAW" || exit 2' + echo '```' +} >"$B32H_DIR/has-jq.md" +case "$(fence_jq_line "$B32H_DIR/has-jq.md")" in + "jq -r "*) ok "B32h: and it does extract one when the fence carries it" ;; + *) bad "B32h: the extractor is blind even to a fence that has the line" ;; +esac +# End to end: `allowed_set` hands the sentinel back as the VALUE, so every +# comparing row above would fail loudly printing it rather than silently +# comparing an empty string against an empty string. +B32H_SENTINEL="$(allowed_set "$D" 02 "$B32H_DIR/no-jq.md")" +[ "$B32H_SENTINEL" = "NO-JQ-LINE-IN-FENCE" ] \ + && ok "B32h: allowed_set returns the sentinel as the value when extraction fails" \ + || bad "B32h: allowed_set answered '$B32H_SENTINEL' over a fence with no jq line" + # -------------------------------------------------------------------------- rm -rf "$WORK" diff --git a/tests/premerge.test.sh b/tests/premerge.test.sh index 2c91f42b..b7d2545d 100644 --- a/tests/premerge.test.sh +++ b/tests/premerge.test.sh @@ -57,6 +57,15 @@ # declared. The fence re-assigns the number, so editing the Constants # block -- the only copy the prose points at -- changed nothing at # runtime and no test noticed (#724) +# P21 — the fixer-wave return contract names `blocked_on_file` and BOTH of +# its emission cases, scoped to the §2a prompt block rather than to the +# whole file +# P21b — the security row: the commit fence's allowed set STILL derives only +# from `fix-waves-.json`, so a path is committable only while +# exactly one agent owns it. Exactly one producer, and no fence names +# the blocked-on artifact +# P21c — the widening is declared on both surfaces, capped, and reads exactly +# one predecessor; both bypass entries are in common-mistakes.md # P20 — every backticked heading reference in the Constants block resolves to # a heading that exists in this file (#724) # P16 — repo-agnosticism: Phase 5's bump probes the TARGET repo (not the @@ -1194,6 +1203,105 @@ fi assert_fixed "$SKILL" '`PREMERGE_CI_SETTLE_SECS` is consumed by the **controller**' \ "P20: the settle constant's consumer is stated, not left to be re-discovered" +echo "== P21: the fixer-wave return contract names blocked_on_file (#755) ==" +# AC 1: the field and BOTH emission cases. Scoped to the §2a prompt block, not +# to the whole file — SKILL.md holds the contract AND the paragraphs about it, +# and a whole-file grep for the field name is satisfied by the paragraph that +# explains it after the contract itself has gone. +P21_PROMPT="$(awk ' + $0 == "### 2a — The fixer-wave mechanism" { in2a = 1 } + in2a && $0 == "Return exactly this YAML and nothing else:" { infield = 1 } + infield && index($0, "```") == 1 { exit } + infield { print } +' "$SKILL")" +P21_PROMPT_LINES="$(grep -c . <<<"$P21_PROMPT")" +if [ "$P21_PROMPT_LINES" -ge 6 ]; then + echo " PASS P21: the §2a return block extracted $P21_PROMPT_LINES lines"; PASS=$((PASS + 1)) +else + echo " FAIL P21: the §2a return block extracted $P21_PROMPT_LINES lines — every row below is vacuous"; FAIL=$((FAIL + 1)) +fi +assert_in "$P21_PROMPT" 'blocked_on_file:' "P21: the return contract declares the field" +assert_in "$P21_PROMPT" '- file:' "P21: and it carries a path" +assert_in "$P21_PROMPT" 'reason:' "P21: and a reason" +# BOTH emission cases, against the RULES block that the agent actually obeys. +P21_RULES="$(awk ' + $0 == "### 2a — The fixer-wave mechanism" { in2a = 1 } + in2a && $0 == "Rules:" { inrules = 1; next } + inrules && $0 == "Return exactly this YAML and nothing else:" { exit } + inrules { print } +' "$SKILL")" +P21_RULE_LINES="$(grep -c . <<<"$P21_RULES")" +if [ "$P21_RULE_LINES" -ge 8 ]; then + echo " PASS P21: the §2a rules block extracted $P21_RULE_LINES lines"; PASS=$((PASS + 1)) +else + echo " FAIL P21: the §2a rules block extracted $P21_RULE_LINES lines — the rows below are vacuous"; FAIL=$((FAIL + 1)) +fi +assert_in "$P21_RULES" 'status: REFUSED' "P21: the REFUSED emission case is stated in the rules" +assert_in "$P21_RULES" 'status: APPLIED' "P21: the APPLIED emission case is stated in the rules" +assert_in "$P21_RULES" 'blocked_on_file' "P21: both cases name the field the agent must emit" +assert_in "$P21_RULES" 'never a line range' "P21: the reason is carried, the coordinates are not" + +echo "== P21b: the allowed set still derives ONLY from the wave plan (#755) ==" +# THE SECURITY ROW. `blocked_on_file` must reach the next attempt through +# `_fix_waves`, so a path is committable only while exactly one agent owns it. +# A fence that reads the blocked-on artifact directly has bypassed that, and +# the symptom is two agents editing one file with the disjointness guard silent. +# +# The two bypass patterns are HOISTED into variables because P21c drives the +# SAME strings over a body that does bypass. A second, freshly-typed copy of a +# pattern down there would make that anti-vacuity row constant-true: a typo in +# either pattern here leaves the row it guards matching nothing, and the row +# that exists to catch exactly that stays green because it reads its own copy. +# One string, two readers — that is the whole mechanism. +P21B_BYPASS_RE='blocked.on' +P21B_APPEND_RE='PREMERGE_ASSIGNED_LIST"$' +assert_in "$FIX_FENCE" 'jq -r '"'"'.waves[][].file'"'"' <"$PREMERGE_WAVES_FILE" >"$PREMERGE_SCOPE_RAW" || exit 2' \ + "P21b: the allowed set is still derived from the wave plan, unchanged" +assert_not_in "$FIX_FENCE" "$P21B_BYPASS_RE" \ + "P21b: and the fix-commit fence names no blocked-on artifact" +assert_not_in "$FIX_FENCE" "$P21B_APPEND_RE" \ + "P21b: nothing appends to the assigned list after it is built" +# Exactly ONE producer for the assigned list. A second `>>` or a second `jq` +# writing it is the widening this design refuses, and a row asserting only that +# the first producer survives would pass with both present. +P21B_PRODUCERS="$(grep -cE '>+[[:space:]]*"\$PREMERGE_ASSIGNED_LIST"' <<<"$FIX_FENCE")" +if [ "$P21B_PRODUCERS" = "1" ]; then + echo " PASS P21b: the assigned list has exactly one producer"; PASS=$((PASS + 1)) +else + echo " FAIL P21b: the assigned list has $P21B_PRODUCERS producers, wanted 1"; FAIL=$((FAIL + 1)) +fi + +echo "== P21c: the widening is declared, bounded and non-accumulating (#755) ==" +assert_fixed "$FENCES" '--carry-blocked' "P21c: a fence turns the widening on" +assert_fixed "$FENCES" '--repo-root "$PREMERGE_ROOT"' "P21c: and hands it a root to contain against" +assert_fixed "$SKILL" 'PREMERGE_MAX_BLOCKED_ON' "P21c: the SKILL declares the cap" +assert_fixed "$LIB" 'MAX_BLOCKED_ON' "P21c: the library declares the cap it enforces" +assert_fixed "$LIB" 'blocked-on-%02d.json' "P21c: the library reads ONE predecessor by name, not a range" +assert_fixed "$LIB" '_beneath_repo_root' "P21c: containment is a filesystem check the library actually has" +assert_fixed "$SKILL" 'blocked-on-.json' "P21c: the SKILL names the artifact the controller writes" +assert_fixed "$SKILL" 'BLOCKED=' "P21c: the triage line documents the new field" +assert_grep "$SKILL_MISTAKES" 'Adding a `blocked_on_file` path straight to the commit fence' \ + "P21c: the bypass is written down as a mistake, not left to be rediscovered" +assert_grep "$SKILL_MISTAKES" 'Unioning `blocked_on_file` across every earlier attempt' \ + "P21c: and so is the accumulating union" +# ANTI-VACUITY for the two assert_not_in rows above: the SAME two patterns — +# read out of $P21B_BYPASS_RE and $P21B_APPEND_RE, never re-typed — over a body +# that DOES bypass. Both must match here, or the row they guard is asserting +# nothing about the real fence. Herestrings, not pipes: an early-exiting reader +# behind a pipe is the EPIPE class tests/epipe-guard.test.sh bans. +P21C_MUTANT='PREMERGE_ASSIGNED_LIST="$PREMERGE_RUN_DIR/x.assigned" +jq -r ".blocked_on[].file" <"$PREMERGE_RUN_DIR/blocked-on-01.json" >>"$PREMERGE_ASSIGNED_LIST"' +if grep -qE -e "$P21B_BYPASS_RE" <<<"$P21C_MUTANT"; then + echo " PASS P21c: the bypass detector sees a bypass when there is one"; PASS=$((PASS + 1)) +else + echo " FAIL P21c: the bypass detector cannot see a bypass — P21b is vacuous"; FAIL=$((FAIL + 1)) +fi +if grep -qE -e "$P21B_APPEND_RE" <<<"$P21C_MUTANT"; then + echo " PASS P21c: the append detector sees an append when there is one"; PASS=$((PASS + 1)) +else + echo " FAIL P21c: the append detector cannot see an append — P21b is vacuous"; FAIL=$((FAIL + 1)) +fi + echo "" echo "== Summary ==" echo " passed: $PASS" diff --git a/tests/review-pr-consolidate.test.sh b/tests/review-pr-consolidate.test.sh index f05b1966..d5387830 100644 --- a/tests/review-pr-consolidate.test.sh +++ b/tests/review-pr-consolidate.test.sh @@ -887,6 +887,84 @@ RCXZ_EOF fi fi +# --------------------------------------------------------------------------- +echo "== RCX15: the staged-set scope guard compares paths as BYTES (#755) ==" +# Scoped to the FUNCTION BODY. review-consolidate.sh is long and a whole-file +# grep for LC_ALL=C is satisfied by any other call site in it. ONE predicate +# function, driven over the live body AND over two deliberately-broken ones, so +# no row below can be green merely because the grep is wrong — a transcribed +# copy of the rule would be permanently green no matter what the guard does. +rcx15_body() { # rcx15_body FILE OPENER -> the function body, opener to its closing brace + awk -v opener="$2" ' + index($0, opener) == 1 { inf = 1; next } + inf && /^}/ { exit } + inf { print } + ' "$1" +} +rcx15_unpinned() { # rcx15_unpinned FILE OPENER -> offending lines, empty when clean + # NEUTRALISE PER TOKEN, NEVER PER LINE. Filtering the offender lines through a + # line-level `grep -v LC_ALL=C` is blind to a line carrying TWO commands with + # only one of them pinned: `LC_ALL=C comm -13 <(…) <(… | sort -u)` still holds + # the string LC_ALL=C, so the whole line is discarded and the unpinned `sort` + # never reaches the verdict. Rewriting each PINNED pair to `PINNED_` + # first leaves every UNPINNED occurrence exactly where it stood, so a + # half-pinned line reds on its unpinned half. RCX15.mutation-half is the row + # that proves it, and a fully-unpinned mutant cannot: it carries no pinned + # token for the line-level filter to be fooled by. + # + # Full-line comments are stripped first — the guard carries its own rationale + # and that paragraph says the words "sort" and "comm" in prose. + rcx15_body "$1" "$2" \ + | grep -vE '^[[:space:]]*#' \ + | sed -E 's/LC_ALL=C[[:space:]]+(sort|comm)/PINNED_\1/g' \ + | grep -nE '(^|[^=[:alnum:]_./-])(sort|comm)[[:space:]]' || : +} +RCX15_OPENER='review_consolidate_continue() {' +RCX15_LINES="$(rcx15_body "$CONSOLIDATE_LIB" "$RCX15_OPENER" | grep -c . || :)" +if [ "${RCX15_LINES:-0}" -ge 20 ]; then + pass "RCX15.extract: review_consolidate_continue extracted $RCX15_LINES lines" +else + fail "RCX15.extract: extracted only ${RCX15_LINES:-0} lines — every row below is vacuous" +fi +RCX15_HITS="$(rcx15_unpinned "$CONSOLIDATE_LIB" "$RCX15_OPENER")" +if [ -z "$RCX15_HITS" ]; then + pass "RCX15.live: every sort/comm in the staged-set guard is LC_ALL=C-pinned" +else + fail "RCX15.live: unpinned sort/comm in the staged-set guard" \ + "(line numbers index the comment-stripped body; pinned pairs read as PINNED_*)" \ + "$RCX15_HITS" +fi +# ANTI-VACUITY. Both rows run through the SAME rcx15_unpinned defined above. +RCX15_MUTANT="$TMP/rcx15-mutant-none.sh" +cat >"$RCX15_MUTANT" <<'EOF_RCX15' +review_consolidate_continue() { + local allowed staged extra + allowed="$(cat "$scan_dir/x.txt" | sort -u)" + extra="$(comm -13 <(printf '%s\n' "$allowed") <(printf '%s\n' "$staged"))" +} +EOF_RCX15 +RCX15_MUTANT_HITS="$(rcx15_unpinned "$RCX15_MUTANT" "$RCX15_OPENER")" +if [ -n "$RCX15_MUTANT_HITS" ]; then + pass "RCX15.mutation-none: a fully unpinned body reds through the same predicate" +else + fail "RCX15.mutation-none: the predicate cannot see an unpinned sort/comm — RCX15.live is vacuous" +fi +# THE ROW A FULLY-UNPINNED MUTANT CANNOT PRODUCE: one line, two commands, only +# the `comm` pinned. That is the shape the next edit to that line will have. +RCX15_HALF="$TMP/rcx15-mutant-half.sh" +cat >"$RCX15_HALF" <<'EOF_RCX15H' +review_consolidate_continue() { + local allowed staged extra + extra="$(LC_ALL=C comm -13 <(printf '%s\n' "$allowed") <(printf '%s\n' "$staged" | sort -u))" +} +EOF_RCX15H +RCX15_HALF_HITS="$(rcx15_unpinned "$RCX15_HALF" "$RCX15_OPENER")" +if [ -n "$RCX15_HALF_HITS" ]; then + pass "RCX15.mutation-half: a pinned comm and an unpinned sort on ONE line still reds" +else + fail "RCX15.mutation-half: a half-pinned line reads as clean — the predicate is line-level, so RCX15.live is blind to the next edit to that line" +fi + echo echo "== Summary ==" echo " passed: $PASS" diff --git a/tests/skill-size.test.sh b/tests/skill-size.test.sh index 7baaee35..648e6382 100755 --- a/tests/skill-size.test.sh +++ b/tests/skill-size.test.sh @@ -72,7 +72,7 @@ LIMIT=500 # extraction is right for material a reader consults on demand, and wrong for # material the runtime obeys. WAIVERS="$(cat <<'EOF' -premerge-pipeline 2323 ~54% fenced; rose 2274->2323 re-inlining the severity rule the controller reads at runtime +premerge-pipeline 2391 ~54% fenced; rose 2323->2391 adding the blocked_on_file wave-return contract the controller obeys at runtime merge-pipeline 1778 ~27% fenced, no references/ yet — prose extraction is open work subagent-driven-dev 1019 ~64% fenced — one ~450-line bash block plus dispatch examples orchestrator 1012 ~53% fenced, 26 executable blocks; 2 reference files already split out