Skip to content

Commit 78330d6

Browse files
PhysShellclaude
andcommitted
fix(S2): owen-rewrite defends its own contract — the five HOLD blockers
The rewriter is the load-bearing step, so it may not assume the Python gate ran. Every guarantee it makes it now proves for itself, over the exact bytes it parses. 1. SELF-CONTAINED input / hash / root validation. The CLI took raw plan + candidates on faith (and CI called it with no validate_apply_inputs at all): a malformed shape could surface as a GetProperty exception, `manual_review` needed no candidate, and a rooted or `..` path in `postimage/<rel>` could have thrown the write clean out of the out-dir — the "never writes the source tree" promise rested on the JSON's good manners. It now RE-DERIVES the canonical plan projection from the bundle and requires the plan to BE it: bundle_sha256 binding (a canonical-JSON writer matching Python's json.dumps producer byte for byte), exact envelope key sets, fixed constraints, a total ordered decision<->candidate bijection, per-candidate action permissions, canonical root-relative source path, symlink-resolved root confinement, an out-dir that is neither the root nor inside it nor already populated, and a postimage destination confined to the out-dir. Every shape failure is exit 2, never a traceback. validated_plan_sha256 now hashes the SAME planBytes that were parsed (it re-read the file at report time). 2. STRICT UTF-8. `new StreamReader(..., Encoding.UTF8, ...)` silently replaces invalid bytes with U+FFFD and still reports CodePage 65001, so the encoding check passed and the post-image could re-encode bytes OUTSIDE the declared edits — invisibly to EditsAreExact, which compares the already-corrupted decoded text. Now a throwing decoder (UTF8Encoding(false, throwOnInvalidBytes: true)); DecoderFallbackException and a UTF-16 BOM are controlled refusals. 3. TARGET API GRAMMAR, before the replacement exists. `target_api.subscribe` was an arbitrary plan string interpolated straight into C#. It is now parsed with Roslyn and must be a canonical identifier / member-access chain and nothing else — no invocation, arguments, generics, `?.`, operators, object creation or trailing junk — and the replacement is built from the VALIDATED syntax node via SyntaxFactory, not string interpolation. 4. EXTRACTOR-COMPATIBLE normalization. Handler/receiver were compared as whitespace- stripped raw text, and SyntacticTypeFqn dropped type parameters — so a candidate for `N.ViewModel<T>` was compared against `N.ViewModel` and could never match. Both sides are now re-derived with the PRODUCERS' own derivations (extractor `a.Right.ToString()` for handler, the collector's last-'.' split for source, `FixNormWs`+`NormalizeHandler` for the computed identities, and a type FQN carrying its type parameter list the way SymbolDisplay prints it), so the comparison is exact over SHA-pinned bytes rather than a lossy re-normalization. The weak wrapper now receives the NORMALIZED handler: `new PropertyChangedEventHandler(OnChanged)` reaches it as `OnChanged`. 5. TRANSACTIONAL publication. postimage-then-report meant a failed report write left the postimage behind, breaking "nothing written". The whole bundle is now staged in a sibling dir and published with ONE atomic rename; any failure discards the staging dir entirely, and a pre-existing out-dir is a hard refusal so runs cannot mix. Regressions (tests/rewriter_regressions.sh, new CI step) drive the real chain end to end and cover all five groups: 12 input/hash/path refusals — including path confinement against an attacker who owns BOTH files (bundle re-hashed so only the rewriter's own check can refuse) — 8 target-grammar refusals, out-dir confinement + a failed publication leaving no out-dir and no staging dir, the normalization forms (new samples/ FixRewriteSample.cs: explicit delegate construction, `this.`-qualified handler, generic containing type, and the nested type S0 itself refuses), and encoding (BOM preserved, all CRLF endings preserved with no lone LF, invalid UTF-8 refused). Plus the happy path, byte-identical re-run, source tree untouched, and the stale-SHA refusal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JxKjqdGEFzq4UzZupw379G
1 parent 7f3c142 commit 78330d6

4 files changed

Lines changed: 1023 additions & 252 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,45 +1701,29 @@ jobs:
17011701
17021702
# S2 (deterministic apply) — the Owen.CSharp.Rewriter core over the full chain:
17031703
# extractor --fix-candidates -> own-fix candidates -> validate_plan -> owen-rewrite.
1704-
# Asserts the postimage carries the converted wrappers, the SOURCE TREE is never
1705-
# touched, and a stale preimage SHA is a hard refusal that writes nothing.
1706-
- name: S2 owen-rewrite core (postimage + guards)
1707-
run: |
1708-
cls=Own.Samples.FixCandidates.TwoOnOneLine
1709-
sample=frontend/roslyn/samples/FixCandidatesSample.cs
1710-
printf '[weak-subscription]\nsubscribe = ["WeakEvents.AddPropertyChanged"]\n' > "$RUNNER_TEMP/s2.toml"
1711-
dotnet run --project frontend/roslyn/OwnSharp.Extractor -- "$sample" --fix-candidates \
1712-
-o "$RUNNER_TEMP/s2_facts.json"
1713-
python -m ownlang own-fix subscriptions candidates "$RUNNER_TEMP/s2_facts.json" \
1714-
--config "$RUNNER_TEMP/s2.toml" --class "$cls" \
1715-
--output "$RUNNER_TEMP/s2_candidates.json" --root .
1716-
python -c "
1717-
import json, sys
1718-
sys.path.insert(0, '.')
1719-
from ownlang.fix_plan import validate_plan
1720-
c = json.load(open(sys.argv[1]))
1721-
d = [{'finding_id': x['finding_id'], 'action': 'convert_acquire'} for x in c['candidates']]
1722-
json.dump(validate_plan(c, {'version': 1, 'decisions': d}), open(sys.argv[2], 'w'))
1723-
" "$RUNNER_TEMP/s2_candidates.json" "$RUNNER_TEMP/s2_plan.json"
1724-
dotnet run --project frontend/roslyn/Owen.CSharp.Rewriter -- \
1725-
--plan "$RUNNER_TEMP/s2_plan.json" --candidates "$RUNNER_TEMP/s2_candidates.json" \
1726-
--root . --out "$RUNNER_TEMP/s2_out"
1727-
post="$RUNNER_TEMP/s2_out/postimage/$sample"
1728-
grep -q 'WeakEvents.AddPropertyChanged(a, OnA)' "$post" \
1729-
&& grep -q 'WeakEvents.AddPropertyChanged(b, OnB)' "$post" \
1730-
|| { echo 'FAIL: postimage missing the converted wrappers'; exit 1; }
1731-
git diff --quiet -- "$sample" \
1732-
|| { echo 'FAIL: owen-rewrite modified the source tree'; exit 1; }
1733-
# stale preimage SHA -> hard refusal, nothing written
1704+
# The rewriter is the LOAD-BEARING step, so its regressions assert the guarantees it
1705+
# must hold on its own, without assuming the Python gate ever ran: hash/root/input
1706+
# validation, the strict UTF-8 decoder, the target-API grammar, extractor-compatible
1707+
# identity normalization, transactional publication, and — throughout — that the
1708+
# source tree is never written to and a refusal writes nothing at all.
1709+
- name: S2 owen-rewrite core (guards + regressions)
1710+
run: bash tests/rewriter_regressions.sh "$RUNNER_TEMP/s2"
1711+
1712+
# A stale preimage SHA is the one refusal worth pinning in the workflow itself: it is
1713+
# the invariant the whole hash-bound chain rests on.
1714+
- name: S2 owen-rewrite — a stale preimage SHA is a hard refusal
1715+
run: |
17341716
zero=$(printf '0%.0s' $(seq 1 64))
1735-
sed "s/sha256:[0-9a-f]\{64\}/sha256:$zero/g" "$RUNNER_TEMP/s2_plan.json" > "$RUNNER_TEMP/s2_stale.json"
1736-
if dotnet run --project frontend/roslyn/Owen.CSharp.Rewriter -- \
1737-
--plan "$RUNNER_TEMP/s2_stale.json" --candidates "$RUNNER_TEMP/s2_candidates.json" \
1738-
--root . --out "$RUNNER_TEMP/s2_stale_out" 2>/dev/null; then
1717+
sed "s/sha256:[0-9a-f]\{64\}/sha256:$zero/g" "$RUNNER_TEMP/s2/fc-plan.json" \
1718+
> "$RUNNER_TEMP/s2/stale.json"
1719+
if dotnet run --project frontend/roslyn/Owen.CSharp.Rewriter --no-build -- \
1720+
--plan "$RUNNER_TEMP/s2/stale.json" --candidates "$RUNNER_TEMP/s2/fc-candidates.json" \
1721+
--root . --out "$RUNNER_TEMP/s2/stale_out" 2>/dev/null; then
17391722
echo 'FAIL: a stale preimage SHA must be refused'; exit 1
17401723
fi
1741-
[ ! -d "$RUNNER_TEMP/s2_stale_out" ] || { echo 'FAIL: a refused run left an output dir'; exit 1; }
1742-
echo 'OK: owen-rewrite converts the acquires, leaves the source tree untouched, refuses a stale SHA'
1724+
[ ! -d "$RUNNER_TEMP/s2/stale_out" ] || { echo 'FAIL: a refused run left an output dir'; exit 1; }
1725+
git diff --quiet -- frontend/roslyn/samples/ || { echo 'FAIL: the source tree was modified'; exit 1; }
1726+
echo 'OK: a stale preimage SHA is refused; nothing written; the source tree is untouched'
17431727
17441728
# The OwnTS frontend spike (P-020 Own.React): the SAME OwnIR seam, fed from a
17451729
# React .tsx instead of C#. Two analyses over the one core: (1) a useEffect

0 commit comments

Comments
 (0)