ouroboros/scripts/carrier_rebase_helper.py
Ouroboros 7f0a112443 v7next F2.4: return the D34 carrier engine and re-split the update planner
Owner decisions 5.12-5.14=A (carrier resolver mandatory, before the M0 pin,
one lane with the re-split).

- release_sync: span-SSOT re-cut to THIS tree's carrier inventory - 25
  descriptors: the 8 reference spans + the README direct-download reference
  block + 8 release-download anchors on each public install page (derived
  from RELEASE_ASSET_TEMPLATES; underscore-safe id class).
- supervisor/update_carriers.py returned whole: span substitution + ordinary
  3-way per file, byte-exact BOUNDED git plumbing (own process group,
  whole-tree kill on timeout), standalone-importable for the operator helper.
- Three insertion points re-derived against the redesigned bodies: planner
  merge (preview AND stash-first build=True replan) before classify_conflicts;
  base re-merge before write-tree; live materializer before the Q8 projection
  and the M0 baseline pin. A conflict confined to declared carrier spans
  stays on the clean lane; anything else degrades to assisted exactly as
  before.
- update_merge.py 1593 -> 1193: planner/builder/materializer re-split into
  supervisor/update_merge_plan.py from the redesign's two-module form
  (parent-facade re-exports; _um()/_uc module-handle patch surfaces).
- update_merge_policy.carrier_guidance reads the span SSOT and describes the
  degraded remainder; RELEASE_INVARIANT_PATHS additive closure for the new
  leaves with parity pins; _POPEN_ALLOWLIST mirror row.
- Tests: carrier corpus + rebase helper + owner facade returned and
  re-proven; the materializer projection test fixture re-derived (a clean
  token conflict is now resolved by the planner); size-ratchet manifest
  regenerated (update_merge.py entered the band by extraction).
- docs/v7next/LEDGER_CORRECTIONS.md: F2.4 lane section (12 entries).

(cherry picked from commit 06f481dfbc415602a967218b6427955cd3818613)
2026-08-31 10:38:34 +00:00

107 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Span-substitution helper for version-carrier conflicts during tactical
rebases of the v7 branch (owner-ratified: spec §1.9-10, batch №8 answer 6=A).
Standalone operator tooling — NOT runtime. When a `git rebase` (or merge) of
the v7 branch stops on the release carriers (VERSION, pyproject.toml, uv.lock,
web/package.json, web/modules/api_types.js GATEWAY_CONTRACT_VERSION, the
README badge / Version History block / direct-download reference block, the
docs/ARCHITECTURE.md header, the release-download anchors of the two public
install pages), this helper resolves each conflicted carrier file by span
substitution: the preferred side — 'ours' by default, which during a rebase is
the side being rebased ONTO (index stage 2) — wins INSIDE the declared carrier
spans, and everything else in the file merges as an ordinary textual 3-way.
A file whose anchors are malformed or duplicated, or which conflicts OUTSIDE
its carrier spans, is left exactly as git left it, for manual resolution.
Non-carrier conflicted files are never touched.
The engine and the span descriptors are the SAME ones the managed-update
runtime uses: supervisor/update_carriers.py reading the SSOT in
ouroboros/tools/release_sync.py. The one liberty this launcher takes is
loading release_sync straight from its file and pre-registering it under its
canonical module name, so a standalone operator invocation never executes the
`ouroboros.tools` package __init__ (which drags in the full tool registry and
its runtime configuration).
Exit codes: 0 — every conflicted carrier file was resolved (non-carrier
conflicts may remain; they are the operator's ordinary rebase work);
1 — at least one carrier file degraded to manual resolution; 2 — git or
usage failure.
"""
from __future__ import annotations
import argparse
import importlib.util
import pathlib
import subprocess
import sys
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))
def _load_engine():
"""Import the shared resolver without executing ouroboros.tools.__init__."""
spec = importlib.util.spec_from_file_location(
"ouroboros.tools.release_sync",
REPO_ROOT / "ouroboros" / "tools" / "release_sync.py",
)
assert spec is not None and spec.loader is not None
release_sync = importlib.util.module_from_spec(spec)
spec.loader.exec_module(release_sync)
sys.modules.setdefault("ouroboros.tools.release_sync", release_sync)
from supervisor.update_carriers import resolve_carrier_conflicts
return resolve_carrier_conflicts
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"--worktree", default=".",
help="the mid-rebase checkout to operate on (default: current directory)",
)
parser.add_argument(
"--prefer", choices=("ours", "theirs"), default="ours",
help="which side wins INSIDE the carrier spans (default: ours — during "
"a rebase, the side being rebased onto)",
)
args = parser.parse_args(argv)
worktree = str(pathlib.Path(args.worktree).resolve())
listing = subprocess.run(
["git", "-C", worktree, "diff", "--name-only", "--diff-filter=U"],
capture_output=True, text=True,
)
if listing.returncode != 0:
print(f"error: could not list unmerged paths: {listing.stderr.strip()}",
file=sys.stderr)
return 2
conflicted = [line.strip() for line in listing.stdout.splitlines() if line.strip()]
if not conflicted:
print("nothing to do: no unmerged paths")
return 0
resolve_carrier_conflicts = _load_engine()
outcome = resolve_carrier_conflicts(worktree, conflicted, prefer=args.prefer)
resolved = list(outcome["resolved"])
kept = dict(outcome["kept"])
non_carrier = sorted(p for p, reason in kept.items() if reason == "not_a_carrier")
degraded = {p: reason for p, reason in kept.items() if reason != "not_a_carrier"}
if resolved:
print(f"resolved by span substitution ({args.prefer} inside the spans, "
f"3-way for the rest): {', '.join(sorted(resolved))}")
if degraded:
for path, reason in sorted(degraded.items()):
print(f"left for manual resolution: {path} ({reason})")
if non_carrier:
print(f"not carrier files — ordinary rebase work: {', '.join(non_carrier)}")
if not resolved and not degraded:
print("no carrier files among the conflicts")
return 1 if degraded else 0
if __name__ == "__main__":
sys.exit(main())