Specify complete-target mixed rename planning and resource bounds

This commit is contained in:
2026-09-24 16:02:54 -07:00
parent 852a79b566
commit dc87babf0f
6 changed files with 425 additions and 0 deletions
+23
View File
@@ -563,3 +563,26 @@ The contract explicitly handles synthetic brace-containing roots, complete
reference extents and the existing ordinary depth policy. It grants read-only
name-occurrence authority only. Rename composition and complete editing parity
remain separate required capabilities.
## 2026-09-24 — gate mixed rename intent against the complete desired model
Adopt the [mixed rename planning contract](analysis-mixed-rename-plan-contract-20260924.md)
after source mapping and independent criticism. Consume the complete authored
owner, immutably borrow desired Project, and retain only changed-name and physical
edit maps. Exhaustive shallow alias exceptions avoid Project clones, generic
diff machinery and duplicate legacy batches. Final namespaces are checked
simultaneously, so swaps work and collisions with unchanged names reject.
Existing whole-model PartialEq semantics define non-name equivalence, including
signed-zero equivalence across geometry, scalar and historical carriers. Original
non-name source bytes remain exact. The allocating iterative Expr comparator
requires a conservative E scratch reservation; independent type-graph and
frontier analysis bounds E by the retained original expression arena, including
all four root families. Revisit if new Expr carriers appear or conservative
headroom demonstrably blocks useful cases.
Deliberate later output parsing must preserve exact inherited per-snapshot policy
with separately reported new usage and aggregate live-storage accounting. An
independent fresh ledger must not hide retained output or old state. Execution
ownership/readmission seams remain separate required implementation; this
decision does not label a read-only plan as accepted rewritten source.
+5
View File
@@ -265,6 +265,11 @@ prior outcomes and failure bodies unchanged; formatting retains the identical
eight inherited blocks. Initial9-pass/4-fail fixture evidence and exact test-only
repairs are preserved. Both occurrence families now retain complete original
authority, but no rename composition or public edit capability follows yet.
The next [whole-target rename planning contract](analysis-mixed-rename-plan-contract-20260924.md)
is accepted after source mapping and independent critique. It specifies complete
non-name equality, simultaneous qualified namespaces, one original-coordinate
edit map and explicit comparison scratch. Implementation and output execution
remain unverified; live ownership and acceptance belong in issue7.
The preceding Project [Windows evidence](experiments/analysis-mixed-project-20260924/README.md)
distinguishes runtime source **71cadfa0cd0e112caa29588d2494753301f3cb6c** from
the formatting-only aaec successor. Strict core all-target Clippy, actual listing
@@ -0,0 +1 @@
*.md -text whitespace=cr-at-eol
@@ -0,0 +1,70 @@
# Independent critique: mixed rename planning
2026-09-24. Read-only source review at **`ee500908e3d77182ddd54e12524f9b6c6eb43b60`**. Reviewed [RENAME-PLAN-SOURCE-MAP.md](RENAME-PLAN-SOURCE-MAP.md), SHA256 `ed868523110b6eef996132589450042c6c649984cb870494d1c1e0c05d118c77`. No implementation, Cargo, commits or Gitea activity. Paths below begin at `crates/oa-core/src/`. Parent owns the accepted contract.
**Recommendation:** adopt the consumed-source/borrowed-target planning design, subject to the precise rules below. The original-expression-site scratch bound is valid for the complete current Project type graph. No hidden historical expression counterexample was found. Output emission and readmission require additional lifecycle seams; a verified plan alone must not be called accepted output.
## Whole-target gate and representation
The proposed whole-target exclusions cover the actual Project fields (`model.rs:1654–1672`), all global alias-bearing records, calculation parameter names, and nested diagram-node aliases (`:1518–1530,1582–1591`; `analysis/types.rs:29–41,78–89`). Require exhaustive borrowed struct destructuring without `..` at these exclusions. Compare collection lengths before zipped iteration, every ordered map key, each record's own ID, and each parameter's ID at the same Vec index. This rejects records moved between diagrams, substituted map keys with unchanged record IDs, parameter reorder/reidentity, body-variant changes and every non-name payload mutation. Do not identify target records by their proposed spellings.
Opaque payload equality is appropriate after these checks. No generic model diff, normalized clone, serde round trip or temporary target mutation is needed. Large target-only collections should fail lengths/variants before walking; diagnostics should describe the rejected field/category rather than debug-format an arbitrary target tree. Target memory is externally owned and is not silently admitted or counted as a new owned Project.
Consuming AuthoredMixedProject and borrowing immutable `&Project` avoids a duplicate model and guarantees target stability through safe Rust. A sparse `BTreeMap<Target,&str>` points only into that external target, never into the plan's own source field; a physical edit map owns only keys and qualified Targets. There is no self-reference. Original source/target references must remain read-only, with no loose-map constructor, legacy adapter, replacement Budget or partial-batch installation path.
Compare final names across **all** qualified catalog members, including unchanged names. The key is `(None,name)` for globals and `(Some(calculation_id),name)` for parameters; this matches existing `update.rs:41–74`. Equal parameter IDs in different calculations remain distinct. Validate changed byte lengths with checked arithmetic before scanning names, then existing `valid_alias`. Simultaneous swaps are legal; changed-to-unchanged collisions reject. Check sparse target membership against the authenticated catalog rather than relying on names alone.
Generate edits by streaming both authenticated occurrence families into one sorted physical map. Replacement authority is the whole alias Word only, never the reference's @/trivia extent. Exact-set verification can rescan the unique original families and compare required key/Target pairs plus the total count, without a second seen set. Check spans/source spelling and nonoverlap before export; count equality alone is insufficient.
## Expr equality scratch: complete reachability proof
At this source, Project reaches `analysis::Expr` through exactly four root families:
| Root | Definition / production |
| --- | --- |
| Calculation body | `analysis/types.rs:85–94`; `lower.rs:512–542` calls `self.binding`. |
| EngineeringProperty base | `model.rs:667–671`; `lower.rs:568–577`. |
| AnalysisCase override values | `model.rs:673–677`; `lower.rs:619–662`. |
| AnalysisCheck predicate | `model.rs:679–682`; `lower.rs:591–614`. |
`binding` (`lower.rs:686–699`) either returns Unbound or invokes expression lowering. Each `Work::Visit` emits exactly one ExpressionSite before constructing that node; all Expr variants' children are scheduled (`:768–858`). Resolver retains the resulting arena verbatim when merging typed definitions/elements (`mixed/project.rs:507–561`). Let **E** be this retained arena length, not the number of roots or occurrences.
The historical path is closed: Trace → TraceBaseline → SemanticBaseline → FacetReview (`model.rs:1634–1650`; `semantic/types.rs:84–134,164–174,258–260`). Its facet payload catalog contains no analysis Binding or Expr. Historical `Binding` is `LifelineBinding`, two IDs; annotation/message `expression` is String (`model.rs:526–536`). Defaults/real bounds/operation/contextual payloads likewise contain no Expr. Ordinary constraint/event/state expressions are strings. No arbitrary serde_json::Value or boxed Project payload introduces another analysis-expression root.
`Expr::eq` (`analysis/expression_lifecycle.rs:170–270`) starts with one pair and iteratively replaces a popped pair by corresponding child pairs. Pending pairs form a frontier of distinct nodes in the original tree. List lengths and call lengths/keys are checked before paired child insertion; fixed-arity variants push only their matching children; variant mismatch stops. It never recursively invokes Expr equality while that Vec is live. Sequential parent/container equality calls therefore cannot overlap these work vectors. Each work-vector cardinality is at most its original tree's node count, hence **≤E**, even for a huge/deep mismatching target.
Reserve E logical scratch entries before any such comparison and release after all comparisons have returned and their vectors dropped. With **E=0**, no original Expression variant exists: compare parent variants first and reject target-only Expression without entering Expr equality. `Exact` equality is bounded normalized structural equality, not arithmetic (`analysis_numeric/exact.rs:24–36`); no numeric meter charge is needed for it. The E proof concerns logical live pairs, not Vec capacity, heap bytes or RSS. Future additions to the payload type graph require renewing the reachability proof.
Controls should cover a wide matching list/call frontier, deep equal/mismatching trees, target-only deep expression against Unbound, E=0 historical traces and late refusal/drop. Conservative E reservation may reject an otherwise unchanged target when inherited auxiliary headroom is insufficient. Do not promise unconditional no-op success.
## Scalar equality decision
The source map's geometry example was incomplete: signed zero also occurs in DataValue::Real and DataTypeDefinition real bounds (`model.rs:310–344`), ordinary defaults (`:615–627`), and historical Data/Field reviews (`semantic/types.rs:94–100`). A geometry-only bitwise matcher would not enforce a universal bitwise rule.
Parent selected **existing whole-model PartialEq semantics everywhere**, including +0/-0 equivalence, while preserving every original non-name source byte. This is coherent and avoids a bespoke matcher. A target differing only in zero sign can therefore be semantically equal and produce no scalar edit; do not claim bitwise target representation identity. NaN targets do not compare equal to admitted finite originals. Include signed-zero controls in both geometry and an ordinary/historical real carrier so the policy remains explicit.
## Simpler candidates and accounting decision
| Candidate | Judgment |
| --- | --- |
| Borrow target; sparse names + physical map | Preferred: exact retained increment R+M entries, no name copies or typed-path clones; explicit lifetime. |
| Own target and copy changed names | Sound if independent ownership becomes necessary, but copies name bytes and owns a second full model. No current need justifies it. |
| Resolve every target name directly per edit | Saves R slots but repeated parameter Vec searches can become quadratic. Not the simpler bounded choice. |
| Pairwise final-name collision scans | Saves Q scratch slots at quadratic work cost. Prefer one borrowed qualified-name set. |
| Reuse both legacy producer batches or normalize/clone whole Project | Adds containers, provenance and accounting, or obscures unsupported mutations. Reject for this bounded task. |
| New metered Expr equality helper | Could reduce conservative scratch refusals, but duplicates/refactors proven matching logic. Defer unless E overreservation demonstrably blocks useful cases. |
For separated phases, the proposed incremental peak `max(E,R+Q,R+M)` is sound: equality reservation dies before changed-name collection; namespace set dies before edit collection. A combined implementation must account actual overlap. No-op planning remains subject to its actual phase reservations. Planning must preserve parse Usage, numeric units and frame metric; local storage checks do not recharge those counters (`budget.rs:139–165`).
## Concrete obligations before emission/readmission
These are contract requirements for the subsequent seam, not a proposed implementation now:
1. **Exact policy:** preserve all inherited Limits and ProjectLimits. `mixed.rs:223–268` currently reconstructs defaults or creates a new Budget; plain `parse()` would lose tightened policy. Deliberate output parsing should use the exact inherited **per-snapshot** profile, with fresh output usage explicitly reported separately. Never reset the original live witness or describe post-reparse numeric/parse counters as unchanged. This is not a cumulative old+new parsing budget unless separately specified.
2. **Complete preflight:** checked final file lengths and total include unchanged files. Compute per-file removed/replacement totals before final addition so transient growth does not defeat a valid combined shrink/grow result. Keep all edits in original coordinates and emit each original file once. Reserve F output-map entries, source bytes, copied canonical path keys and entry path before their allocations; optional length maps need additional slots. Output-size validation alone is not a simultaneous live-storage bound.
3. **Documented consumed transition:** decide exactly when old index/syntax/maps are dropped. Output buffers coexist with old B+R+M during emission and remain owned through verification. Dropping old auxiliary state can reduce the verification baseline, but does not make retained output strings/maps free. A carried ledger/baseline must include every surviving auxiliary allocation before parsing and resolving output. Current `resolve` starts `Ledger::default()` (`mixed/project.rs:438–442`), and parser metadata checks are separate (`mixed.rs:278–335`); merely wrapping those current entry points in two independently passing limits does not establish aggregate live storage.
4. **Actual verification:** parse/resolve/admit emitted bytes and compare the whole resulting Project with the immutable intended target. Retain E as a scalar if the old witness has been dropped and reserve equality scratch against the verification baseline too; the validated target's unchanged Expr shapes supply the same bound. A structurally invalid output and a valid-but-wrong output must reach their respective distinct refusals, with no partial usable result.
5. **Return lifetime:** a ResolvedMixedProject borrows its bundle; ordinary owning fields cannot safely return both bundle and borrowing witness. A later narrow consumed-parts result may own output/model/warnings after temporary syntax drops, or a caller-owned output can support a borrowing witness. Specify which guarantee is returned; do not imply a reusable index survives when it was discarded. No-op execution may return an explicit unchanged owner rather than pretend owned copies are free.
6. **Staleness and installation:** if execution accepts a current SourceBundle, compare entry, entire canonical file set, paths and exact bytes with the retained snapshot, including untouched files. This detects stale supplied snapshots; atomic disk/history/GUI adoption is separate and is not granted by a pure plan.
Proceed with a clearly named planning dependency once these planning rules are incorporated. Keep emission/readmission unimplemented until its policy, aggregate baseline and returned ownership are specified. This preserves the required whole-output gate without inventing a new framework or weakening full-edit parity requirements.
@@ -0,0 +1,134 @@
# 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 Project` containing 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:
1. Same schema_version, project ID, project name/description and every collection/key/category.
2. 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.
3. Elements: same ID/name/description/parent/body; only alias differs.
4. Relations: same ID/name/description/body; only alias differs.
5. 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.
6. 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
1. **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.
2. **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.
3. **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.
4. **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.
5. **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.
@@ -0,0 +1,192 @@
# Complete-target mixed rename planning contract
2026-09-24. Accepted successor to verified private source
`ee500908e3d77182ddd54e12524f9b6c6eb43b60`, after
[source mapping](analysis-mixed-rename-plan-20260924/RENAME-PLAN-SOURCE-MAP.md) and
[independent critique](analysis-mixed-rename-plan-20260924/RENAME-PLAN-CRITIQUE.md).
This document does not change user-owned requirements. Implementation and
runtime acceptance remain separate gates.
The user requires complete graphical/text/API editing parity. This dependency
verifies whole-model rename intent and complete original-coordinate token edits.
Output emission/admission, other scalar and structural edits, public source8,
history and checked persistence remain required subsequent capabilities.
## Inputs, ownership and result
Consume one opaque `AuthoredMixedProject` and immutably borrow one independently
supplied desired `Project` for the returned plan's lifetime. The original owner
retains its exact SourceBundle, canonical files, complete admitted model,
warnings, both occurrence families, qualified names and inherited limits/ledger.
Never accept replacement source, profile, occurrence maps or caller edit batches.
Failure returns diagnostics and drops the consumed attempt; no partial plan
escapes. The desired Project is neither changed nor normalized.
The returned private plan owns the original witness and exactly two maps:
- changed qualified `Target` to a borrowed desired-name `str`;
- original physical `(canonical_file, alias_start, alias_end)` to qualified Target.
The desired Project is an external immutable borrow, not a field that another
field borrows. No whole-Project/name clone, typed Path copy, separate per-family
batch, duplicate source table or legacy Index conversion is needed. Read-only
inspection derives replacement text from the retained name map and original
token text from the original witness. Inspection records cannot install a subset
or transfer authority to another source owner.
Borrowing the desired model avoids copying names and model state. Consuming a
desired model and copying changed names remains a justified alternative only if
a real integration owner needs independent target lifetime. A generic model diff,
serde conversion, alias-erased Project clone and two legacy producer adapters
introduce unnecessary storage or authority boundaries for this scope.
## Whole-target gate
Compare fixed collections by length, ordered key and each record's own identity.
Walk borrowed collections; do not allocate `Project::identities()` or clone
payloads. Use exhaustive struct destructuring without `..` for the records whose
alias/name fields are exempted, so added fields require an explicit decision.
Only global aliases and calculation-parameter names may differ. Require:
1. Project schema, ID, display name, description and complete collection sets.
2. Definitions' IDs, display names, descriptions and body variants. QuantityKind
and Unit payloads compare in full. Calculations retain parameter count and
each parameter's ID **at the same ordered position**, value type, result and
body. Only definition alias and scoped parameter name are exempted.
3. Elements' IDs, display names, descriptions, parents and full bodies; relations'
IDs, display names, descriptions and full bodies.
4. Diagrams' IDs, display names, descriptions, layer, projection, view options,
routes and node-key sets. Each node retains ID, element, position, size,
collapsed state and port anchors. Only diagram/node aliases are exempted.
5. Artifacts' and traces' full non-alias fields, including endpoints, historical
baselines, URI, revision, fragment and prose.
Equality follows existing whole-model PartialEq semantics. In particular,
positive/negative floating zero are equivalent in all existing carriers,
including scalar data and historical facets, not just geometry. This does not
authorize rewriting any non-name source bytes. The emitter must preserve those
bytes exactly. Nonfinite originals are excluded by existing admission.
Opaque unchanged payloads may use existing equality after checking variants and
container sizes. Their expression comparator is iterative **and allocates** a
work Vec. Reserve E scratch entry slots before this gate, where E is the total
retained original raw expression-node/site count. Sequential comparisons have
at most a frontier of E distinct old nodes. List/call sizes are checked before
pairing, and variant mismatches refuse before traversing a target-only tree.
When E=0, no Expr comparison may be invoked. Release this reservation only after
the equality calls and internal vectors finish. This conservative bound can
require headroom even for no-op intent; do not promise every accepted owner can
plan under an exactly exhausted profile.
Current reachability must be proved from all four roots: calculation body,
engineering-property base, analysis-case overrides and analysis-check predicate.
Historical semantic payloads use their distinct lifeline binding and string
expression fields. An added Expr carrier requires revisiting the bound. If that
proof fails, add a narrow budgeted equality seam rather than duplicating a full
expression matcher or treating its allocations as free.
## Final namespaces and exact edits
After releasing comparison scratch, collect only changed qualified names. Check
their cumulative UTF-8 byte lengths against the inherited source-byte limit
before validating their characters; borrowed external strings do not justify
unbounded scanning. Use the existing ASCII/reserved-word alias predicate.
Validate final names across the entire original catalog simultaneously. Global
aliases share one namespace. Parameters share a namespace only within their
owning calculation. Identity is qualified even if UUIDs or spellings coincide.
Every changed target must exist in that catalog. Use a borrowed temporary set of
`(scope, final_name)` with explicit entry reservations. Drop it before collecting
edits. A no-change result may reuse the already accepted original namespaces.
Walk both complete authenticated occurrence families. For each changed target,
insert its whole alias-token range and qualified identity into the single edit
map, reserving before insertion and refusing a duplicate physical key. Ordinary
records resolve to Global; typed records retain their existing qualified target.
Markers, trivia, prose, historical UUIDs, includes and unrelated bytes are never
replacement ranges.
Validate membership, original spelling, nonempty whole UTF-8 token spans and
monotone/nonoverlapping physical order. Rescan both original occurrence families
to require every changed occurrence exactly once with the right target and no
unchanged occurrence; compare total required count with edit-map length to rule
out extras without an extra seen set. A changed alias with only its header still
requires that header edit. Complete-set authority cannot be reduced to a caller's
selected edit list.
Expose a private complete-current-bundle validation seam for the eventual
emitter: entry, canonical path keys, exact file set and every file's bytes must
equal the original witness, including untouched files. A supplied snapshot check
alone does not promise an atomic filesystem/history installation.
## Storage and combined sizing
Let B be the inherited live ledger, Q the complete qualified-name count, R changed
targets, M changed physical occurrences and E the bound above. The selected phase
order has added peak `max(E, R + Q, R + M)` when namespace checking is required.
Successful retention is exactly R+M entry slots, zero occurrence slots and zero
copied text bytes. Count any implementation-added temporary container explicitly.
Reserve before growth; release after the logical entries/containers cease to be
live. Parsing/frame/reference/numeric Usage is unchanged by planning.
Compute each final file length while merging canonical files and sorted edits.
Sum that file's removed and replacement byte totals separately using checked
arithmetic, then subtract removed bytes before adding replacements. Sum all
final files, including untouched files, and
check the inherited complete output-byte limit. Do not enforce a separate typed
intermediate size: one family's shrink may offset the other's growth. A lengths
map is unnecessary; a later emitter may repeat the bounded sizing walk. Shrink
does not reduce the still-live B/R/M reservations.
## Required behavior and acceptance
Support complete project/ordinary/typed alias changes, simultaneous global swaps,
scoped parameter swaps, repeated/reordered call labels, cross-family references,
same spelling in distinct scopes and no-op intent. Reject collision with an
unchanged member in the same scope, invalid aliases, unknown/changed IDs,
parameter reorder/add/remove/type change and every non-name semantic mutation.
Reject the entire target before output; no partial success or repaired identity.
Use independently authored complete physical edit maps and final-name sets.
Include OA/SA, canonical cross-file order/coincident offsets, Unicode/CRLF,
line-comment-separated references, all ordinary categories and historical UUID
exclusion. Verify unchanged original/desired Projects, exact desired borrow,
original bundle authority, inherited Usage and R+M retention.
Attempt one unsupported mutation in every exempted-record field group; include
target-only deep/wide expression damage, mismatched list/call sizes and zero-E
variant changes. Verify the conservative equality bound with wide and deep old
trees and signed-zero equivalence in geometry/scalar/historical carriers.
Corrupt missing/extra/wrong-target edits, overlap, stale touched/untouched files,
entry/path/file-set and UTF-8 spans, requiring safe original diagnostics.
Hand-count realistic B+scratch peaks and cap/cap-minus-one cases that actually
pass predecessor phases. Exercise small-stack gate comparison, success/drop and
late refusal. Preserve existing legacy Plan/Composition tests, all private
baseline outcomes and explicit known full-suite/formatting failures.
## Mandatory successor: accepted rewritten output
An opaque plan is not accepted edited source. The next execution contract must
emit every file once from original coordinates, own complete output strings/path
keys/entry with F map slots and all copied bytes accounted, parse/resolve/admit
those actual bytes, and compare the entire resulting Project with desired intent.
It must reject admissible-but-wrong output as well as malformed output.
Preserve the exact inherited policy for each deliberate snapshot parse; do not
use a default profile or silently reset a live witness. Specify new-snapshot work
counters and one aggregate live-storage boundary. Output coexists with B during
emission. Before verification, either drop the consumed old auxiliary owner at a
documented transition while retaining output/intent/policy, or explicitly charge
both owners together. A fresh independent Ledger cannot hide live output storage.
Parser metadata and resolver storage need the carried surviving baseline. The
final whole-Project equality also needs its E scratch reservation even if the
old witness has already dropped; retain E as scalar verified metadata.
Returning an owning bundle alongside syntax that borrows it would be self-
referential. Prefer a narrow owning accepted result after temporary verification
syntax drops, or explicitly justify another lifetime design. No-op execution
should preserve the original owner through an explicit unchanged outcome. No
usable partial result or history/save adoption may escape failed verification.
These are constraints for the successor, not an already implemented execution API.