16 KiB
Private emitted-unverified rename transition — proposal
2026-09-25. Design only; not an accepted contract, implementation or edited-output acceptance. Source pin fa7a980418ef99d772e67c66c28dfa7b5181f875. Paths below are relative to crates/oa-core/src/. No source changes, builds or Gitea operations.
Minimal outcome and privacy
Parent selection supersedes the earlier enum/callback candidates: use two named consuming methods, with callers selecting from immutable changed_names().is_empty() before consumption. Both methods call validate_current and the existing validate internally, then reject the wrong branch. No common result enum, callback adapter, heap indirection or lint suppression is needed. This is accepted design direction, pending implementation/runtime gates.
Use a narrow rename::emission child owning the transition and an opaque pending owner:
// Fields private to rename::emission. Not Clone/Copy/Serialize/AsRef/Deref.
struct PendingRename<'t> {
bundle: SourceBundle,
limits: Limits,
project_limits: ProjectLimits,
retained: Slots,
expression_nodes: usize, // E, verified original count for later equality
desired: &'t Project,
}
impl<'s, 't> RenamePlan<'s, 't> {
fn into_unchanged(
self, current: &SourceBundle,
) -> Result<AuthoredMixedProject<'s>, Failure>;
fn emit_changed_unverified(
self, current: &SourceBundle,
) -> Result<PendingRename<'t>, Failure>;
}
Use the repository's existing restricted visibility (pub(in crate::language::analysis_source)) only where the private caller must name types/methods. Implementation in a child of rename can read its parent's private plan fields; no mutable plan/model/token authority is exposed. current is only a freshness comparison input; emission reads the retained original bundle. It cannot substitute the source to emit.
For this bounded step, PendingRename owns the bundle/policy/Slots/E directly. Do not widen carried::RetainedSnapshot with a sibling-callable new(bundle, slots, policy). The future sole consuming transition can take an actual sealed PendingRename and transfer its verified fields into RetainedSnapshot after its execution contract is accepted. Until then the pending type exposes no production bundle getter/extractor, no borrowed carried input, no verification/adoption method and no conversion to SourceBundle/Project/session state. cfg(test) read-only inspection suffices to verify emitted bytes/counts/intent/policy. Existing RetainedSnapshot fixture constructors and carried parse APIs remain unchanged.
This is an explicit staging choice: the current result is an emitted-unverified owner, not yet a RetainedSnapshot usable by production carried parsing. Its retained formula is exactly the future snapshot formula; eventual transfer can move fields without copying. If parent instead requires constructing RetainedSnapshot in this slice, the alternative is a carried-owned factory accepting the sealed RenamePlan itself and performing the emission transition internally, with minimal read-only/consuming plan access. An arbitrary-parts constructor is not justified. The pending-only option changes fewer existing privacy boundaries and creates no premature verification entry.
An owning pending result with inspection only in fixtures is sufficient for an independently testable producer: consumers can hold/drop it but cannot save, install or treat its raw bytes as accepted model state through its API. The private name and absence of extraction are intentional authority restrictions, not runtime cryptographic authentication. A future trusted private implementation can add its sole verifier; no hash/token duplication is needed now.
No-op and unchanged authority
First run existing validate_current(current) (rename.rs:161–169) so even no-op execution refuses a stale complete snapshot. If the sealed plan's names/edits are empty, consume its empty maps and return the original AuthoredMixedProject unchanged. No output String/key/entry copies, new snapshot parsing or new storage reservation occur. The source's original exact bundle borrow, maps, warnings, Project and policy survive.
The unchanged owner remains an already-admitted original owner, not a newly emitted result. The whole-target gate already established alias-only equality; signed-zero-equivalent desired representation does not authorize rewriting scalar bytes. The no-op branch can end the desired borrow because it does not need later output equality.
Do not implement a no-op by allocating an identical owned SourceBundle and calling that free. Do not run a second whole-model comparator merely to reconfirm the sealed plan.
Validation and emission sequence
- Consume the opaque plan. Complete-current-bundle validation compares entry, all keys, exact file set and every byte, including untouched files.
- Rely on plan construction's
validate()proof for whole token membership, original spelling, UTF-8 ranges and nonoverlap (rename.rs:178–239). No API mutates the maps afterward. Reuse its sizing logic and source authority; do not re-lex/lower/admit or build another seen/token/map inventory. Calling the same private validator once at emission can support internal fault-injection tests, but is redundant for sealed production authority and is not required as a new authentication layer. Parent should choose deliberately; prefer the sealed proof and cheap checked emission invariants below. - If changed, reject every explicit carried source (syntax.carried.is_some()), including borrowed-zero mode, before output reservation. Its external retained owner would survive dropping the source guard; O alone would then omit live C. No-op preserves any carried guard without rebasing. For this prerequisite changed emission therefore starts only from legacy syntax. Then compute checked complete output length S using the existing subtract-removed-before-add-replacements formula (
rename.rs:261–308). Include every original file. Sum copied canonical key byte lengths K and entry byte length J using checked arithmetic; let F be the existing file count. Capture exact Budget::limits, syntax ProjectLimits, desired borrow and E as scalar metadata, without cloning the model or names. - Reserve the entire output increment
O=(F,0,S+K+J)against the still-live plan ledger before allocating/copying any output String, map key or entry. The current ledger already includes old B plus R+M; do not add those again. Failure maps through existing source diagnostics while the old witness is alive and returns no pending owner. - Emit each file exactly once into one final String from original coordinates, traversing canonical files and the sorted physical edit map. Repeated sizing iteration is allowed; a new lengths map is unnecessary. For each file compute its final length with a scalar pass over that file's edits, allocate that String capacity, then append unchanged original slices and replacement names in order, followed by the untouched suffix. Clone that canonical path once into the final map. Files with no edits still get one exact full copy. Clone entry once. Markers/comments/includes/prose/UUIDs remain in unchanged slices.
- Every cursor/range calculation is checked; exact token boundaries are inherited from the plan. At each file require produced logical length equal its computed length, final cursor within source and all file edits consumed. Require complete file count and total S after assembly. These are inexpensive producer invariants, not a new independent syntax authenticator or guarantee of intended semantic validity.
- No partial map escapes on any failure. After complete assembly, snapshot the surviving O/policy/E/desired scalars, destructure and explicitly drop the old names/edit maps and AuthoredMixedProject before constructing and returning PendingRename. The output bundle moves once into pending. Drop the old ledger with its owner; pending retained state is O, not B+R+M+O.
Rust's return/drop order should be visible in code: use a narrow local owning output, capture scalar/reference metadata, drop(names); drop(edits); drop(source); then construct pending. Borrowed original SourceBundle remains caller-owned after the source auxiliary owner drops, exactly as before; this transition does not claim to free or charge that external borrowed input. The external desired Project also remains borrowed and unchanged.
Counts and lifetimes
Let B be the admitted authored owner's existing live auxiliary Slots, R changed targets and M changed physical edits. The plan already retains L=B+(R+M,0,0). During emission the output reservation establishes peak L+O in each existing dimension:
O.entries = F
O.occurrences = 0
O.text = S + sum(canonical_path_utf8_bytes) + entry_utf8_bytes
The entry is a separately owned String even when it equals a map key. Unchanged files still contribute their complete output bytes to S. Old source Strings are externally borrowed, not copied into B; all newly owned output bytes are counted once. Existing copied old syntax/name/warning text remains live in L throughout production and cannot be released early merely because a replacement shrinks a file.
A whole-output upfront reservation avoids per-file partial reservation bookkeeping and an extra lengths map. Final file capacity uses its exact precomputed length, but accounting is logical bytes, not allocator capacity/BTree node overhead/RSS. Temporary String then map insertion transfers the same reservation, not two copies. Checked reserve before creation does not promise recovery from operating-system allocator failure; existing logical resource-failure behavior remains the scope.
Once the old owner/maps drop, pending stores O and the exact inherited policy, with E retained only as original expression-count provenance for a future whole-Project equality reservation; it is not a proven bound on arbitrary faulty emitted syntax. Future carried verification must still account O plus parser/resolver allocations and decide required producer timing; the successful emission reservation proves none of those later checks. It also does not prove output is syntactically admissible or semantically equal to desired.
Avoiding another stack-layout regression
Do not add Box/Rc/Arc or clone the old owner/model to simplify the outcome. Such allocation would require its own design/accounting and is unnecessary. Enum size is the maximum variant payload plus tag/alignment, not the sum of both payloads; nevertheless debug return temporaries can amplify a large unchanged owner. Keep helper layering shallow and do not retain duplicate inline policy in witnesses that already own it. Pending needs its one policy because the old owner actually drops.
The selected split consuming API avoids a common large enum and requires no callback framework. Each method enforces its branch; the two explicit wrong-branch errors are acceptable bounded API checks. Ordinary Result layouts and generated stack usage still require parent gates; this source-level choice is not a measured stack guarantee.
Independent acceptance cases
- No-op: pointer identity of original bundle and retained owner state, identical Slots/Usage/numeric/warnings, no output allocation/copy; stale unchanged-file or entry mismatch still refuses. No desired mutation.
- Whole bytes: rich OA/SA global+scoped swaps, nested/reordered/repeated labels, canonical multi-file coincident spans, Unicode/CRLF and category fixture with line-comment-separated references. Independently authored entire expected file strings/maps, not reconstruction through an emitter-like expected helper. Every non-token byte must match original at its corresponding unchanged segments; historical UUIDs/prose untouched.
- Combined sizing: the existing31-byte node shrink plus typed f growth example emits924 bytes from930, despite a hypothetical typed-only954; the933-byte negative refuses the inherited930-byte output-size cap before copying. Owned-text overlap may legitimately refuse under that same cap even when final S fits: use sufficient inherited text allowance for a successful emission test and distinguish planning-size versus live-owned-text caps.
- Exact overlap: independently hand-count F/S/K/J and old plan L. A cap fitting L and O separately but below L+O refuses before the first output copy. Exact combined cap succeeds if all actual predecessors fit; test entries and text independently. Assert final pending retained exactly O, not L+O or O twice. Do not tighten a live owner's Budget after admission to manufacture a boundary.
- Complete stale input: touched and untouched source bytes, file removal/addition, path rename and entry changes reject before allocation. Equal independent current bundle content succeeds; emitted authority still comes from the retained original.
- Failure lifecycle: malformed internal fixtures must enter existing plan-validation tests before a sealed plan exists; do not fabricate externally installable edited maps. Where emission has its own checked-arithmetic/range invariants, targeted private fault injection can assert no partial pending owner escapes. Overflow beyond valid profile ceilings is explicitly labeled helper/fault-injection evidence.
- Provenance limitation: changed sources from both real retained C and explicit borrowed-zero carried mode refuse before output allocation; no-op preserves each original guard. No numerical zero bypass. A future consuming carried-owner transition is required before repeated owned edits.
- Ownership/API: desired remains externally borrowed, old owner is dropped before Pending creation, output is independently owned and immutable through available API; only cfg(test) inspections reveal bytes/policy/O/E. No production extraction, parse/admission, session adoption or persistence path is added.
- Regressions: preserve all existing planner/carried outcomes and unchanged small-stack controls, inherited failure dispositions and formatting exclusions. Emitted-unverified success is reported separately from future output acceptance.
Minimal files and next decisions
mixed/project/ordinary_index/rename/emission.rs: emission outcome, pending owner, sole consumed-plan producer and its tests as a child module. Access existing private plan/source fields through lexical ancestry, not public mutable accessors.rename.rs: module wiring and, if needed, narrowly factor existing per-file checked-length iteration for use by sizing/emission without allocating a lengths table. Preserve the sealed constructor validation and existing inspection APIs.- Scoped implementation note/tests.
mixed/carried.rsneed not change for pending-only delivery; no arbitrary RetainedSnapshot constructor is opened.
Read against the parent draft harness/analysis-mixed-emission-contract-20260925.md: parent selected pending-only staging, split consuming methods, changed legacy-only provenance, one upfront O reservation and no production extraction. The draft requests reuse of complete edit validation and malformed-plan fixture coverage; invoking the existing validate once before construction is compatible and avoids inventing a second authenticator. No duplicate streaming byte comparator is proposed. Parent retains API selection and contract acceptance authority. A later accepted contract must specify the sole Pending-to-RetainedSnapshot/verifier transition and cannot mistake this owner for accepted rewritten output.
Minimal independent oracle supplied by reviewer: empty project p renamed next has authored A=(14,0,9), plan L=(16,0,9), output O=(1,0,105), peak L+O=(17,0,114). Exact entries 17/text 114 and predecessor-compatible minus-one caps are the preferred first actual-path acceptance cases. These are independent hand counts, not new runtime results.
Superseding parent decision: both split methods rerun the existing complete validator after stale-current validation, enforce the correct no-op/changed branch, and refuse changed carried input before O allocation. Earlier proposal preferences for relying only on construction proof or callback delivery are not the implementation selection. Await parent claim/contract publication before source edits.