Skip to content

Commit 55ccbe9

Browse files
PhysShellclaude
andcommitted
feat(S2): step 8 — the canonical patch bundle (patch + manifest + postimage)
Turns the accepted rewriter's postimage into a deterministic, reviewable, hash-addressed artifact for the later step-9 self-gates and the 007 gate: python -m ownlang own-fix subscriptions apply \ --plan <validated-plan.json> --candidates <candidates.json> \ --root <source-root> --out <artifact-dir> [--rewriter <owen-rewrite-command>] <out>/change.patch <out>/apply-manifest.json <out>/postimage/<canonical-rel-path> Thin orchestration only — no model, no o7, no analyzer, no gate.toml. It re-runs the accepted validate_apply_inputs, invokes the accepted Owen.CSharp.Rewriter as an argv VECTOR (never a shell string, so nothing in a path can be read as syntax), verifies the transport output, then builds and publishes the bundle. rewriter-report.json is transport, and is verified rather than believed — a neighbouring executable produced it, which is not the same as it being true. Checked: exact key set, version/operation, input_bundle_sha256, validated_plan_sha256, target api, source path, pre-SHA, a RECOMPUTED post-SHA over the actual postimage bytes, applied/manual as sets with the expected partition, a postimage for the allowed path ONLY, and no unexpected file anywhere in the rewriter workdir. The validated plan stays the sole authority for action, the hash-bound candidates the sole authority for identity; the report contributes nothing to the manifest. change.patch is generated in-process (difflib over byte-lines, git headers) rather than shelled out to `diff`, so it cannot inherit a timestamp column, a temp path, a username or a cwd. Lines split on b"\n" ONLY — git's own notion of a line — because bytes.splitlines() also breaks on a lone \r (and \v, \f, \x1c), which would silently reflow a file whose content carries one. A missing EOF newline gets git's own marker on both sides. The result is a/<rel> -> b/<rel>, one file, no rename/mode/binary records, and `git apply` reproduces the postimage byte for byte. apply-manifest.json is the authority artifact: exact shape, sort_keys + compact separators + trailing newline, built ONLY from the validated plan (actions, in candidate order), the plan's own bytes, and the real postimage/patch bytes. No timestamps, no absolute or temp paths, no hostname, run id, command line or token counts — a re-run over the same pristine inputs is byte-identical. An all-manual_review plan is VALID, not a refusal: postimage == preimage, change.patch is zero length, patch_sha256 is the sha of no bytes, applied_findings is empty and every id stays in candidate order under manual_review_findings. Publication is atomic: everything is built in a sibling work directory and the finished staging bundle is renamed into place, so <out> never exists until every check has passed. An existing <out>, an <out> physically inside the source root (symlinks resolved), or any rewriter/patch/manifest/write/rename failure leaves no <out> at all, no work directory, and a byte-identical source tree. Tests: tests/test_patch_bundle.py (52 checks) covers the patch/manifest pure functions and every transport-tampering case — those live at the pure-function level ON PURPOSE, since forging a bad report must not require a test-only hook in the production rewriter. tests/patch_bundle_regressions.sh + a CI step drive the real chain: the three-part bundle, canonical headers with no temp/absolute path or timestamp, exact manifest shape and SHAs, `diff -r` determinism, a git apply round-trip on a pristine copy, mixed actions, the empty patch, and the refusal/no-output cases. Steps 9-12 are NOT started: no analyzer delta, no own-check re-run, no OWN001/OWN050 assertions, no gate.toml, no 007 evidence, no STS clone, no apply to a real branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JxKjqdGEFzq4UzZupw379G
1 parent d30c75d commit 55ccbe9

5 files changed

Lines changed: 900 additions & 3 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,16 @@ jobs:
17091709
- name: S2 owen-rewrite core (guards + regressions)
17101710
run: bash tests/rewriter_regressions.sh "$RUNNER_TEMP/s2"
17111711

1712+
# S2 step 8 — the canonical patch bundle over the same real chain, ending in
1713+
# `own-fix subscriptions apply`. Asserts the three-part bundle, canonical patch
1714+
# headers with no temp/absolute path or timestamp, the exact byte-deterministic
1715+
# manifest, a `git apply` round-trip that reproduces the postimage byte for byte,
1716+
# the mixed-action partition, the empty-patch contract, and that a refusal leaves
1717+
# no output. (The transport-tampering cases are pure-function tests in
1718+
# tests/test_patch_bundle.py — forging a report must not need a hook in the rewriter.)
1719+
- name: S2 step 8 — canonical patch bundle (patch + manifest + postimage)
1720+
run: bash tests/patch_bundle_regressions.sh "$RUNNER_TEMP/s8"
1721+
17121722
# A stale preimage SHA is the one refusal worth pinning in the workflow itself: it is
17131723
# the invariant the whole hash-bound chain rests on.
17141724
- name: S2 owen-rewrite — a stale preimage SHA is a hard refusal

‎ownlang/__main__.py‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,11 +559,46 @@ def _cmd_validate_plan(rest: list[str]) -> int:
559559
return rc
560560

561561

562+
def _cmd_apply(rest: list[str]) -> int:
563+
"""S2 step 8: `own-fix subscriptions apply` — the canonical patch bundle. Thin
564+
orchestration only: re-run the apply gate, invoke the accepted Owen.CSharp.Rewriter
565+
(as an argv vector, never a shell string), verify its transport output, and publish
566+
change.patch + apply-manifest.json + postimage/ as ONE atomic bundle. No model, no o7."""
567+
import shlex
568+
569+
from ownlang.fix_apply import ApplyError
570+
from ownlang.fix_bundle import apply_bundle
571+
572+
flags = {"--plan", "--candidates", "--root", "--out", "--rewriter"}
573+
parsed = _own_fix_parse(rest, flags, set())
574+
if parsed is None:
575+
return 2
576+
positional, opts = parsed
577+
if positional or not all(opts.get(k) for k in ("--plan", "--candidates", "--out")):
578+
print("usage: own-fix subscriptions apply --plan <validated-plan.json> "
579+
"--candidates <candidates.json> --root <source-root> --out <artifact-dir> "
580+
"[--rewriter <owen-rewrite-command>]", file=sys.stderr)
581+
return 2
582+
rewriter = shlex.split(opts.get("--rewriter") or "owen-rewrite",
583+
posix=(os.name != "nt"))
584+
if not rewriter:
585+
print("own-fix: --rewriter is empty", file=sys.stderr)
586+
return 2
587+
try:
588+
published = apply_bundle(opts["--plan"], opts["--candidates"],
589+
opts.get("--root") or ".", opts["--out"], rewriter)
590+
except ApplyError as exc:
591+
print(f"own-fix: refuse: {exc}", file=sys.stderr)
592+
return 2
593+
print(f"own-fix: wrote change.patch + apply-manifest.json + postimage -> {published}")
594+
return 0
595+
596+
562597
def cmd_own_fix(rest: list[str]) -> int:
563-
"""`own-fix subscriptions {candidates|render|validate-plan} ...`."""
598+
"""`own-fix subscriptions {candidates|render|validate-plan|apply} ...`."""
564599
if len(rest) < 2 or rest[0] != "subscriptions":
565600
print("usage: python -m ownlang own-fix subscriptions "
566-
"{candidates|render|validate-plan} ...", file=sys.stderr)
601+
"{candidates|render|validate-plan|apply} ...", file=sys.stderr)
567602
return 2
568603
verb, args = rest[1], rest[2:]
569604
if verb == "candidates":
@@ -572,8 +607,10 @@ def cmd_own_fix(rest: list[str]) -> int:
572607
return _cmd_render(args)
573608
if verb == "validate-plan":
574609
return _cmd_validate_plan(args)
610+
if verb == "apply":
611+
return _cmd_apply(args)
575612
print(f"own-fix: unknown subcommand {verb!r} "
576-
"(candidates | render | validate-plan)", file=sys.stderr)
613+
"(candidates | render | validate-plan | apply)", file=sys.stderr)
577614
return 2
578615

579616

0 commit comments

Comments
 (0)