26 KiB
Mixed whole-target rename planning: source map and proposed dependency
2026-09-24. Read-only research against clean private source ee500908 (Repair ordinary-index fixtures to exercise intended validation) in .local/as3batch. No source edits, Cargo, runtime experiments, commits or Gitea actions. Parent owns adoption, compilation and integration. Paths/line numbers below refer to that private commit and begin at crates/oa-core/src/ unless stated otherwise. This report is a proposal, not an accepted contract or implementation claim.
The accepted root harness/analysis-ordinary-index-contract-20260924.md requires a whole independently supplied target Project, allowed global/scoped renames only, simultaneous namespace validation, original-coordinate edits from both families, actual-output parse/resolve/admission and whole-target equality. Full structural/scalar editing, identity changes, migration and GUI/save parity remain later requirements. A rename-planning dependency must not claim that these are completed.
What exists and what does not
| Source seam | Verified behavior and consequence |
|---|---|
language/analysis_source/mixed/project/ordinary_index.rs:27–77 |
AuthoredMixedProject owns the typed owner and ordinary map; read-only views return the entire Project, original SourceBundle/files, complete qualified names, typed occurrence/expression maps and physical ordinary records. This is the correct consumed input. It has no rename authority or legacy adapter. |
language/analysis_source/mixed/project.rs:18–26 |
The retained resolved owner holds original syntax/Budget, complete Project, warnings, static admission metadata, raw expression/occurrence arenas and the aggregate Ledger. A narrow child inside the owner can split borrows during planning without exposing replacement sources or budgets. |
language/analysis_source/update.rs:15–19,23–75 |
Existing typed Plan borrows legacy Index, owns a supplied rename-name map and edit Vec. Final namespace keys are (None, name) for globals and (Some(calculation_id), name) for parameters; this already permits simultaneous swaps and shared parameter spelling across calculations. It validates alias syntax via valid_alias. It does not compare a complete target Project. |
language/analysis_source/update.rs:78–111 |
The legacy producer filters unchanged targets, copies typed keys (including shared Path references), sorts locations and clones names. This is a useful behavioral reference, but new mixed map/vector entries must use aggregate entry-slot accounting rather than this producer's local occurrence ceiling. |
language/analysis_source/update.rs:119–168 |
Existing validation compares all numeric source files, checks membership/spelling/uniqueness/order and verifies the exact complete qualified edit set. Mixed provenance additionally contains original entry and canonical paths; do not project it down to the old numeric-map input. |
language/analysis_source/update.rs:173–222 |
Legacy output sizing includes every supplied file and checked arithmetic; application emits each original source once in order. Its lengths map and new buffers are not reserved against retained mixed storage. Reuse behavior, not these local budget assumptions unchanged. |
language/analysis_source/update.rs:227–323 |
Composition holds the inherited Budget by reference, exports only an intact complete borrowed batch and intentionally postpones standalone typed-output sizing. Its input remains legacy Index, not the complete mixed witness. OriginalEdit is inspection, never independently transferable authority. No current helper accepts both occurrence families. |
model.rs:1654–1672,1763–1781 |
Project contains project alias plus definitions, elements, relations, diagrams/nested node placements, artifact links and traces. identities() allocates a Vec; streaming these fixed collections avoids another uncounted temporary catalog. |
analysis/types.rs:29–41,78–89 |
Definition alias is global. Calculation parameters are an ordered Vec with scoped ParameterId, name and value_type. Parameter name alone may change; parameter ID, order, type, result and body must remain fixed. |
model.rs:241–248,1092–1098,1518–1530,1582–1591,1602–1612,1642–1651 |
All other global alias-bearing records have a shallow alias field; their remaining payloads are immutable under rename-only intent. Diagram node maps need a separate alias-aware walk. Trace baselines and artifact owner/URI/revision/fragment are not names. |
analysis/expression_lifecycle.rs:170–270 |
Expr::PartialEq is iterative but allocates pending: Vec<(&Expr,&Expr)>; it is not allocation-free. List/call lengths are checked before adding paired children; call keys are checked. Parent bodies' derived PartialEq invokes this implementation. |
model.rs:671,677,682 and analysis/types.rs:85–89 |
Current analysis Bindings live in calculation bodies and engineering property/check/case payloads. Their original expression trees have retained raw-site arenas. This offers a source-derived upper bound for the existing Expr equality work Vec, subject to an explicit proof/test rather than ignoring its allocation. |
model.rs:1739–1746, language.rs:1205,1282, language/analysis_source/cursor.rs:67–78 |
Both ordinary and typed names ultimately use the same ASCII identifier/reserved-name predicate. Renaming cannot introduce punctuation, Unicode names, empty strings or reserved aliases just because replacements are UTF-8. |
language/analysis_source/budget.rs:6–14,36–64,139–165 |
Budget owns private exact Limits, parse Usage, numeric meter and failure latch. New Budget starts Usage/numeric work at zero. Local lowering/text checks do not mutate cumulative parse counters. No current read-only exact-profile getter or budget-transfer parse seam exists. |
language/analysis_source/mixed.rs:223–268 |
parse reconstructs a default source8 profile from ProjectLimits; parse_with_profile permits only tightening and then always calls Budget::new(profile). Neither is an already-accounted continuation of the original Budget. Reusing parse would lose private tightened profile values. |
language/analysis_source/mixed/project/accounting.rs:6–70 |
Ledger checks additions in entry, occurrence and text domains before allocation, then releases explicitly. It is an auxiliary logical-live-storage policy, not model heap or RSS. Rename state must continue this baseline. |
source_project.rs:640–755,868–893 |
Public update recompiles mutable public state, formats declarations, reparses old/new text, applies ordinary edits, performs a later-coordinate structural pass and recompiles output for complete Project equality. This is neither the private original-coordinate producer nor a replacement for the whole-target rename gate. The final equality requirement is the right acceptance precedent. |
Smallest useful next owner
Recommendation for parent adoption: consume AuthoredMixedProject, borrow one immutable complete desired Project for the plan's lifetime, produce an opaque read-only MixedRenamePlan<'source,'target>. Names are illustrative. Construction either returns the complete plan or diagnostics; failure consumes/drops the attempted source owner. The desired Project is not mutated, normalized or partially applied. Its immutable borrow prevents safe caller mutation while the plan exists; its pointer/lifetime alone is not edit authority.
Suggested fields are only:
- owned original
AuthoredMixedProject<'source>; &'target Projectcontaining the independent whole intended state;- a sparse
BTreeMap<Target, &'target str>for changed final names; - one
BTreeMap<(file,start,end), Target>containing every changed ordinary/typed alias token in original coordinates.
No field borrows another field of the returned object. Names borrow the external desired Project, while map keys/targets own only small values. No cloned Project, cloned name Strings, cloned typed paths, occurrence-family adapter arrays or duplicated original file table is needed. Read-only inspection can derive old token text from the owned original witness and replacement text from the borrowed desired-name map. No independently fabricable constructor, mutable map, Deref-to-legacy-Index or external batch installation method should exist.
This is a proposed planning dependency, not permission to omit subsequent emission/readmission. Planning can include a full combined output-length preflight without emitting. If parent chooses to implement emission/readmission in the same milestone, settle the verification-lifetime and budget questions below first, and keep the planning invariants unchanged.
Representation candidates
| Candidate | Assessment |
|---|---|
| Consumed source + borrowed whole target + sparse borrowed final names + physical edit map | Recommended minimum. No self-reference or duplicate Project; changed names and edits have exact entry-only storage. Immutable target lifetime is explicit. Whole-source physical uniqueness already exists, so a second typed-path key representation is unnecessary for token-only edits. |
| Consumed source + consumed target + owned copied changed names + physical map | Sound alternative if owning target intent is operationally necessary. Avoid self-references by copying R changed names; explicitly charge their UTF-8 bytes while the whole owned target remains live. Simpler external lifetime but duplicated name bytes. Parent wording should state whether target consumption is actually required. |
| Consumed source/target, no name map; look up every target directly in the desired model | Saves R slots but repeatedly searches global collections and linearly searches parameter Vecs for each occurrence. Could become quadratic for many repeated calls/large signatures. Prefer a bounded sparse lookup over this accidental cost. |
| Reuse two legacy producer batches and a collector | Adds different provenance wrappers, typed-key/path clones, multiple containers and duplicated completeness work. No need to materialize two independent producers for this same sealed snapshot. Preserve legacy behavior separately; do not weaken its nominal API. |
| Clone/serde-convert Project, erase or replace aliases, then ordinary equality | Rejected default: duplicates large model/JSON structures, obscures unsupported-field checks and adds allocation/drop work. Mutating the caller's target temporarily is also unnecessary. |
| New generic AST/model diff or structural-edit framework | Not justified for fixed shallow alias exceptions. The accepted input already provides semantic identity and original occurrence ownership. |
Complete target gate
Walk corresponding old/desired fixed model collections with borrowed iterators. Check each map length and each ordered map key before comparing records. Check record IDs as fields, not merely map keys. Do not collect Project::identities(). Require:
- Same schema_version, project ID, project name/description and every collection/key/category.
- Definitions: same ID, display name, description and DefinitionBody variant. QuantityKind/Unit bodies equal in full. Calculations have same parameter length and same parameter ID at each Vec position; compare each parameter value_type, result and body. Only definition.alias and each scoped parameter.name may differ.
- Elements: same ID/name/description/parent/body; only alias differs.
- Relations: same ID/name/description/body; only alias differs.
- Diagrams: same ID/name/description/layer/projection/view_options/routes and same node-map keys. Each node keeps ID/element/position/size/collapsed/port_anchors; only diagram/node aliases differ.
- Artifacts and traces: every field including endpoints, baseline, history, URI and prose equal except alias.
Implement the shallow exclusions with exhaustive struct destructuring without .. where practical, so adding a field to Project/Definition/Parameter/Element/Relation/Diagram/NodePlacement/ArtifactLink/Trace causes the gate to require a decision at compilation. Delegate unchanged opaque payloads to existing equality rather than duplicate every enum. A false comparison is an unsupported-change diagnostic, never a silently ignored diff. Target is not admitted independently first: shape/payload equivalence to admitted original plus final-name checks is the bounded plan gate; actual emitted output still needs its own full admission.
Equality allocation and scalar semantics
Using body == invokes the existing Expr work Vec. A proportionate candidate is to reserve E scratch entry slots, where E is the retained original raw expression-site count, before this gate and release after all equality calls have returned/dropped their internal vectors. Comparisons are sequential, and each simultaneous Expr pair belongs to an original tree; current Eq checks paired child counts before growth, so pending pairs cannot exceed that original tree's node count <=E. Empty E means no old Expr comparison is invoked; mismatched Binding/DefinitionBody variants must refuse before trying to traverse a target-only tree. The reservation is a conservative bound, not an exact live-pop meter. Require an independent proof/control for wide lists/calls and deep trees; do not claim that an iterative equality method allocates nothing. If parent rejects conservative reservation, a small budgeted reuse seam in expression_lifecycle is preferable to copying that full matcher into rename code.
Other derived payload equality and model allocations remain under the established logical auxiliary/model-heap distinction, but the new gate must not quietly put its own temporary collections outside accounting. Very large desired maps/parameter Vecs should fail their length comparisons before traversal. Check candidate replacement name length sums against inherited bounds before scanning/copying long strings; borrowed raw target memory is not a reason to permit unbounded replacement-validation work.
Specify equality meaning explicitly: existing Project::PartialEq treats +0.0 and -0.0 equal in geometry. If “no scalar change” requires bitwise floating identity rather than existing semantic equality, node/route geometry needs a stricter gate. NaN/nonfinite originals are already rejected by admission. Do not silently present structural PartialEq as byte-identical model representation.
Names and exact edit completeness
After the gate, stream names by stable identity to collect only changes. Global Target(id) covers the complete global catalog. Parameter Target(calculation_id,parameter_id) stays separate even when its UUID equals a global or another calculation's parameter.
Validate final names simultaneously across the entire qualified catalog, not only renamed members. Use existing valid_alias and a temporary borrowed set (scope,final_name), where scope is global None or owning calculation ID. Drop this set before edit collection when possible. A changed name cannot collide with an unchanged name in the same namespace. A global and a parameter may share spelling; separate calculations may reuse parameter spelling. Every target in the sparse map must be present in the original catalog; target-gate identity equality should make absence an invariant refusal.
Then iterate both authenticated occurrence families. For a changed target, insert its exact physical alias location into the one edit map; ordinary targets are Global(record.target), typed targets use their existing qualified Target. Reject existing keys before insertion. The original indexes already enforce nonempty full-token spans and cross-family physical uniqueness. Retain neither ordinary full extent as replacement authority nor a copied typed path. @/$ markers, whitespace, line comments, display text, historical UUIDs, includes and all unselected bytes remain outside edits.
Before export/emission, validate the exact required set: scan both source families and check each required physical key has the correct qualified target; unchanged occurrences must have no edit. Compare total required count with edit-map len to exclude extras. This avoids a separate seen set because the source set and map are each physically unique. Recheck original spelling against original qualified names, whole-token membership and monotone/nonoverlapping physical order; any corruption must fail before output. A borrowed inspection record cannot install a subset, pair with another source owner or replace desired names.
Supported and rejected examples
- Rename project p->mission, ordinary activity w->work and typed property v->value in one target: update all their header/reference occurrences in both families, including ordinary node.element@v and typed property.subject@w.
- Swap global f/g and swap parameters x/y inside calculation f simultaneously: allowed when final namespaces are unique. Apply all replacements against original spans; never sequentially resolve intermediate spellings.
- Rename one calculation's x while another calculation's x and global x remain unchanged: permitted; IDs/scopes determine occurrences.
- Whole target equal to original: successful empty plan, unchanged parse/numeric/frame Usage, no emitted bytes during planning. A sparse entry saying the same name need not exist at all because intent comes from the whole target.
- Rename f->g while global g remains g: reject collision, even if g has no reference uses.
- Rename one parameter to an unchanged sibling's name: reject; the same name in a different calculation is allowed.
- Rename plus display-name edit, body value 1->2, changed unit scale, parameter reorder/insertion/type, relation endpoint, node position, diagram ownership, trace baseline or schema/ID change: reject the whole target before any output. Reidentity, missing-ID stabilization and movement have no route through this API.
- Retained source entry/path/file-set or untouched file differs in a supplied current snapshot: reject stale snapshot, even if every touched token still matches.
- Same semantic desired Project but source style changes requested implicitly: source style is not part of target rename intent; all original trivia remains exact.
Ledger and output preflight
Let B be inherited retained ledger; Q all qualified global+parameter names; R changed qualified targets; M changed physical tokens across both families; E old expression sites; F original files. Under the recommended borrowed-target/two-map plan:
- added retained storage: R+M entry slots, zero occurrence slots, zero copied name bytes;
- equality scratch: explicitly reserved E upper-bound entries, released before name collection;
- namespace scratch: Q entries while R changed-name entries exist; release after the set drops;
- no typed Path allocation/clone, extra source table, identity Vec, family adapter table or seen-edit set;
- no new Budget::frame, parse/reference/Exact charges in planning.
A clean phase order gives a conservative added peak max(E, R+Q, R+M) with no copied replacement text, provided gate/name passes really do not overlap those temporaries. Any implementation that combines phases must count the actual overlap instead. The owned-target/owned-name variant additionally retains sum UTF-8 bytes of R copied final names. Reservation checks must precede growth; failure drops the whole attempt. Test caps with fixtures where this phase's bound actually exceeds all earlier parser/resolver/index peaks—the ordinary-index work demonstrated why stage-local arithmetic alone is insufficient.
For combined sizing, walk canonical files and sorted edit keys together. Checked subtraction/addition computes each final file length; sum all output file lengths, including unchanged files. The scalar maximum/total can be computed without a retained F-entry lengths map; emitting later can repeat the same bounded calculation. Validate the final combined output bound, not a hypothetical typed-only intermediate, so one producer's shrink can compensate another's growth. Shrink does not reduce currently live B/R/M storage.
Actual SourceBundle construction adds F map entries plus all new source bytes, canonical path-key copies and the entry-path copy. Those owned path/text allocations need explicit reservations too; model/validator heap exclusion does not make a copied SourceBundle free. If lengths are retained, add F slots. If Vec sorting is chosen instead of a BTreeMap, ordinary stable sort_by_key may allocate sort scratch; choose an in-place unstable order or reserve its scratch. There is no need for that choice with the proposed physical map.
A future pure emitter can require a complete current &SourceBundle and compare it to the retained original bundle (entry, canonical path keys, exact file set and bytes) immediately before use. No caller-supplied replacement Budget/profile. This detects stale supplied snapshots; it does not by itself provide atomic disk/history/GUI installation, which belongs to later owner integration.
Emission/readmission questions that must not be hidden
- Same policy versus cumulative work: parse_with_profile currently starts a new Budget even when its Limits match. Reusing parse() loses tightened profile values; passing the old charged Budget to a new parser would count original+output bytes/tokens/Exact work cumulatively and can reject an otherwise in-profile near-limit rename. Parent must define whether re-admission is a same-policy per-snapshot parse with a separately bounded transaction peak/work total, or a cumulative operation budget. Recommendation: preserve exact inherited Limits/caller policy, keep all live allocations in one transaction accounting boundary, and explicitly specify work counters for the deliberate new snapshot parse. Do not silently reset counters or claim unchanged numeric work after actual-output parsing. Planning alone must leave all Usage untouched.
- Live overlap: output strings coexist with the old witness until emission finishes; actual verification may also create new syntax/arenas/maps while old state survives. Current resolve creates its own fresh Ledger and has no external baseline reservation seam. Either drop old auxiliary state at a documented consumed transition before compiling (retaining only needed intent/policy/output) or add a narrow accounted verification seam that includes the retained old baseline. Do not add two individually passing ledgers and call that an aggregate bound.
- Returned ownership: a new ResolvedMixedProject borrows the emitted SourceBundle. Returning both as ordinary fields of one owning struct would be self-referential. A narrow acceptance result can own output SourceBundle plus the resolved Project/warnings/metadata after dropping temporary syntax/index, via a private consuming parts seam; it must not claim to retain a reusable source index. Another option is a callback-based borrow after installing output ownership, but that is a wider integration choice. Reindexing the accepted output for the next edit is a separate lifecycle/work cost to state, not a hidden free operation.
- No-op execution: an empty plan can preserve the original owner without parsing/emitting identical bytes. If the downstream API always returns a newly owned bundle, that entails copies and their bounds even for no-op. Prefer an explicit unchanged outcome for eventual execution; settle its type before promising zero-allocation no-op apply.
- Target/equality policy: decide borrowed versus consumed desired Project, and existing structural PartialEq versus bitwise geometry equality. These affect API/storage and exact supported/rejected examples, not rename token logic.
These are concrete seam decisions, not reasons to weaken the whole-output admission/equality gate. A planning-only dependency can be implemented before them if its output is honestly only a read-only verified plan. An emitted result cannot be called accepted until actual original-coordinate output is parsed, completely resolved/admitted and compared with the whole independent target, with no usable partial result after failure.
Discriminating acceptance before edit execution
Require independent exact plans for whole-project/scoped swaps, both reference directions, repeated/reordered calls, all ordinary categories, same parameter UUID across calculations/global, comment-separated markers and Unicode/CRLF. Include no-op and changed-to-unchanged collision. Reject one mutation from each non-rename field group and map/parameter identity/order change; include target-only deep/wide payload damage to prove bounded equality behavior. Check entire old and desired Projects remain unchanged and the exact whole target is retained by the plan lifetime.
Corruption controls should distinguish missing/extra edit, wrong qualified target, duplicate/overlapping location, stale touched and untouched files, changed entry/path/file set, invalid UTF-8 location and safe diagnostic fallback. Existing sealed original completeness plus exact plan equality supports missing planned-edit rejection; it does not claim arbitrary coordinated corruption of the original lowerer/projection.
Hand-count B plus R/M/E/Q peaks, candidate replacement-byte checks and eventually output F/path/text growth. Assert planning parse/numeric/frame usage unchanged, with actual later reparse counters separately specified. Use exact cap/cap-minus-one cases that demonstrably pass predecessors. Small-stack tests must exercise equality, success/drop and representative late refusal rather than only parsing.
Preserve legacy update/Composition oracles in language/analysis_source/update/tests.rs:134,212,257,275,304,327,508,563,622,688,714; do not rewrite them to accept mixed authority. Subsequent emitter tests need exact whole-file outputs, combined growing/shrinking bound cases including untouched files, malformed and admissible-but-wrong output controls reaching actual Project equality, and no history/save adoption on late failure.