Add bounded external placement comparison and preservation adapter

This commit is contained in:
2026-09-24 20:12:01 -07:00
parent ac5d7ef34a
commit 8a2010d8a6
10 changed files with 851 additions and 1 deletions
@@ -0,0 +1,36 @@
# Independent reference-adapter review
2026-09-25. Static review only; no engine/compiler/router/GUI execution or source edits. Parent owns JavaScript review and execution. Reviewed local experiment plan, Python supervisor, Rust importer diff and adjacent baseline/prototype observation contracts. Read the JavaScript output-check interface only to establish where importer preconditions are enforced.
**Verdict: no blocking source defect found for the frozen five-case diagnostic run, subject to consuming structurally passing outputs and the explicit post-import checks below.** This is readiness for experiment execution, not adapter fidelity, native traceability, or production acceptance. Compilation remains unverified here.
Reviewed SHA256:
- Plan `.local/connection-layout-reference-plan-20260925.md`: `41a420db7ee039fa424f60b61097bddc6eff787e2705004e1c88b19f8b97c371`.
- Supervisor `.local/run-elk-reference-20260925.py`: `114d2fd4dbc9a55cd57c4b5175989686dc413863fc2a62fa4a71278792937066`.
- Importer `crates/oa-core/examples/auto_layout_probe.rs`: `95666ce866111d0626e964483fc954276e258bd0ef61a4f96b9857a0ceab468e`.
- Rehashed frozen released geometry: `9a31a676f23dea2f41daec39497f569d68231fa0fd66e5a1a476be97d5d61ac2`, matching the parent's declared input.
## Concrete checks and result-consumption requirements
1. **Freeze pin improvement:** the supervisor asserts manifest and bundle pins and records all other bound hashes, including geometry, before/after. It does not assert the expected geometry SHA above. Add that single expected-input assertion before execution; otherwise a different preexisting geometry file can be consistently recorded without matching the intended frozen dataset. Current inspected bytes do match, so this is a narrow provenance improvement, not an observed wrong input. No generalized dependency framework is needed.
2. **Only import structurally passing output.** Rust `import_placement` rejects unsupported Scenario, schema mismatch, excess nodes/edges, missing/extra/duplicate IDs, nonfinite/nonpositive rectangles and translated nonfinite positions. It deliberately has no independent minimum-size/header/child-containment checks and no parent field in `PlacementPlan`; those gates are supplied by JavaScript inspection. Parent must require the per-case structural result and success receipt before extracting `{schema:1,nodes:[{id,position,size}]}`. Do not rank a transferred failed structural result as an ordinary candidate. Core validation is not a substitute for those geometric checks. Reprojected Rust bounds should also be compared with intended transferred bounds to detect core size normalization.
3. **Compare final physical ports, not only unchanged anchors.** Importer does not import ELK port positions; its final projected ports are therefore the right raw material for comparing owner/semantic-port identity and final intended tip to ELK's output. Unchanged authored fraction and unchanged ELK relative pixel offset can still diverge after compound resizing. Plan correctly declares this a transfer limitation requiring assessment. A candidate with such divergence is not evidence of faithful fixed-port optimization, even if both separate geometries validate.
4. **Use the matching baseline.** The existing `observe` result's `after` is released Layout without the new reset. The comparable baseline is `reset_only_control`, which clears effective routes after that Layout. The old prototype and importer clear using pre-operation eligibility, while the control's legacy helper uses post-Layout routed `!custom_suspended`. For the frozen unchanged-visibility cases this may agree, but verify actual eligible edge sets and resulting route records before attributing differences solely to placement. Do not relabel the old `after` metrics as reset-only.
5. **Conservation summary does not prove the route contract.** `conservation` masks routes and node geometry for broad semantic comparisons; `authored_routes_exact=false` is expected after reset. Independently compare complete per-edge records: entry/order, attachments, exact eligible reset, and retained `translated_routes` result. Roundtrip and conservation booleans are emitted observations, not asserted by the CLI; consuming analysis must inspect them. Exit0 alone does not pass them.
## Importer correctness within this scope
Eligibility is captured from the original projection before movement and requires actual owners equal projected endpoints with both visible. The importer clones the original, changes only selected-view node geometry, clears eligible pins/full labels, and applies the existing original-to-final `translated_routes` helper only to retained routes. Entries are updated in place; route order and endpoint attachments are preserved. Other views and semantic records remain untouched by construction.
The revised collapsed rule is appropriate: compare proposed size to the original compact projected size exactly, update position, preserve stored expanded size. Temporary unfolding is used only to recover core visual ancestry, including Allocation grouping; it is not returned or used to change visibility. Hidden nodes found under a visible collapsed ancestor receive that ancestor's translation, preserving their latent relative geometry. The ancestry walk is bounded and detects cycles/missing parents. This implementation does not establish a general policy for unrelated filtered descendants omitted even from the unfolded projection; the stated policy leaves unrelated omitted placements exact. Do not extrapolate beyond the frozen fixtures.
The importer cannot reverse semantic edges or accept replacement ports because neither exists in its plan schema. Its 256 stored-node /1024 visible-edge limits and Scenario refusal are explicit. Original-to-final positional deltas are calculated before retained-route translation; no intermediate Layout or independently validated Batch prefix is introduced. This remains an example-only geometry transfer, not a new production command or history implementation.
## Supervisor and attribution
The supervisor creates a fresh output directory and exclusive log files; each engine case has a separate subprocess with a90-second timeout. Python's `subprocess.run` timeout kills/waits for its child and the loop retains an explicit timeout/error receipt before proceeding. The reviewed contract uses the bundled engine in that Node process; this is not a general descendant-process supervisor. Before/after bindings cover Node, adapter script, geometry, manifest, bundle, dependency receipt, plan and supervisor. JavaScript receipts separately hash raw case/graph/mapping/result/observation files. Parent should retain both receipt layers.
The supervisor can exit0 even when individual cases fail or time out; its final record explicitly preserves those outcomes. Require every expected case receipt, no timeout, success plus structural checks before treating that case as transferable. Failed/unsupported cases stay visible in the comparison. Timing includes process work and is correctly disclaimed against earlier Rust route-only timings.
The plan correctly limits the claim to whether engine placement transfers usefully through released routing and label placement. Raw ELK routes/labels are diagnostic, never secretly turned into authored constraints. Estimated label extents, fixed-position port drift, prototype folded refusal and absent native trace acceptance remain real limitations. A failed placement transfer cannot reject ELK's complete coupled drawing architecture; a scalar gain cannot establish an engineer's correct trace or label association. No request is made for new fixtures, option sweeps, production dependencies or additional architecture before this bounded run.
@@ -0,0 +1,40 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const crypto = require('node:crypto');
const {adapt, inspect} = require('./reference_probe.cjs');
const nodes = [
{id:'parent', element:'ep', visual_parent:null, collapsed:false, position:{x:100,y:200},size:{width:300,height:300}},
{id:'child', element:'ec', visual_parent:'parent', collapsed:false, position:{x:124,y:264},size:{width:100,height:80}},
{id:'sink', element:'es', visual_parent:null, collapsed:true, position:{x:600,y:264},size:{width:100,height:80}},
];
const ports = [
{owner:'child',element:'out',side:'Right',position:{x:224,y:304}},
{owner:'sink',element:'in',side:'Left',position:{x:600,y:304}},
];
const edges = ['one','two'].map(id=>({id,oriented:true,source:'child',target:'sink',source_port:'out',target_port:'in'}));
const row = {fixture:'synthetic',diagram:'d',before_project:{diagrams:{d:{nodes:Object.fromEntries(nodes.map(n=>[n.id,n]))}}},
before:{projected_nodes:nodes,projected_ports:ports,projected_edges:edges,
routed_edges:edges.map(e=>({id:e.id,label:{text:e.id,size:{width:30,height:24}}}))}};
const {graph,mapping} = adapt(row);
assert.equal(mapping.ports.length,2);
assert.equal(graph.edges[0].sources[0],graph.edges[1].sources[0]);
const result = inspect(graph,mapping);
assert.equal(result.structural_checks_passed,true);
assert.deepEqual(result.nodes.find(n=>n.id==='child').position,{x:124,y:264});
assert.deepEqual(result.ports.find(p=>p.owner==='child').position,{x:224,y:304});
const badSize=structuredClone(graph); badSize.children.find(n=>n.id==='sink').height+=1;
assert(inspect(badSize,mapping).checks.some(c=>c.name==='collapsed_compact_size_exact'&&!c.passed));
const underHeader=structuredClone(graph); underHeader.children[0].children[0].y=2;
assert(inspect(underHeader,mapping).checks.some(c=>c.name==='child_inside_parent_body'&&!c.passed));
const reversed=structuredClone(graph); reversed.edges[0].sources=['sink'];
assert(inspect(reversed,mapping).checks.some(c=>c.name==='edge_orientation_and_endpoints'&&!c.passed));
const missing=structuredClone(row); missing.before.projected_edges[0].source_port='unknown';
assert.throws(()=>adapt(missing),/Missing projected semantic port/);
const undirected=structuredClone(row); undirected.before.projected_edges[0].oriented=false;
assert.throws(()=>adapt(undirected),/Unsupported undirected/);
const duplicate=structuredClone(row); duplicate.before.projected_nodes.push(duplicate.before.projected_nodes[0]);
assert.throws(()=>adapt(duplicate),/duplicate/);
console.log(JSON.stringify({checks:'passed',scope:'synthetic adapter mapping only; engine not invoked',
checks_exercised:['nested coordinate roundtrip','shared physical port reuse','collapsed size rejection','header intrusion rejection','edge reversal rejection','missing semantic port rejection','undirected rejection','duplicate node rejection'],
adapter_sha256:crypto.createHash('sha256').update(fs.readFileSync(require.resolve('./reference_probe.cjs'))).digest('hex')}));
@@ -0,0 +1,56 @@
# Connection-aware arrangement: independent criteria and next experiment
Proposed engineering decision for issue32, 2026-09-25; not an accepted requirement, production adoption, or native acceptance. Read the frozen independent routing results, decision-review and adoption-rubric under `harness/experiments/routing-independent-20260925` at local auto-layout HEAD `ac5d7ef34a567f45b72adbd30d1a243f5811ba73`, and reset `crates/oa-core/src/session.rs::layout` at `956e33e568c6b799d0330803c74927e205977dfd`. No code, compiler, router or GUI execution in this review.
**Recommendation: run the bounded ELK placement-reference comparison on the existing frozen five-case corpus.** Compare current Arrange/reset placement, the existing native layered prototype, and ELK-derived node placement through the unchanged released Rust router and label placement. Freeze one ELK configuration and adapter policy before results. This tests a missing architectural choice; further J/L coefficient or lane tuning does not.
## What the existing evidence establishes
Current Layout orders roots from projected connections, but places roots in rows of four and stacks children vertically; it does not use routed conflicts or label extents when placing them. Reset removes eligible authored bends and labels, making a new route possible; it does not make those node positions connection/label-aware.
The routing-only results are useful counterexamples to a scalar objective. L reduces workshop shared length1042→602 while adding a label/node and label/stroke collision. Reversed mixed IDs change132→233.5999756 shared length; sparse reduces496→372 but proper crossings24→25. Timing screens pass, and these finite outcomes do not identify either J or L as a general improvement. The retained released router remains a controlled reference, not a quality oracle.
Raw contacts, shared intervals, crossings and collision identities locate potential reading problems. They do not measure whether an engineer follows the intended connection: a short ambiguous junction at a decision point can matter more than a long common-terminal stub; a correct readable crossing can be preferable to coincident edges. Pairwise shared length also counts pair multiplicity, not just unique occupied ink. Area, bends and length expose costs; none identifies correct label-to-edge association or legible arrowheads. Keep raw quantities and concrete identities, with no weighted quality-pass total.
## Architecture choices and falsifiers
| Choice | Useful hypothesis | Smallest decisive contrary observation |
| --- | --- | --- |
| Connection-aware placement, then released routing and label placement | Better sibling order, layering and space allocation remove the dominant conflicts without a new routing architecture. | A faithful placement improves graph order but the released routing/label stage repeatedly recreates the named ambiguity, with no trace-task gain. This falsifies sufficiency on these cases, not every possible placement algorithm. |
| Bounded placement/routing/label feedback | A small number of specific gap/order corrections can repair conflicts left by the first pass. | A repair relocates ambiguity to another required task, or repeated passes oscillate/expand the scene without eliminating the conflict. Do not start this loop before identifying a concrete failed constraint from the placement comparison. |
| Coupled external engine output, including routes and labels | Jointly reserving routes and label space achieves benefits lost by independent downstream routing. | Engine output cannot faithfully express fixed ports, compound geometry or projection semantics, or its apparent visual gain depends on violating them. A placement-only failure is insufficient evidence against this choice. |
ELK Layered is a relevant reference because it explicitly supports ports, labels and compound graphs; this is a capability basis for the experiment, not a predicted win. [ELK Layered](https://eclipse.dev/elk/reference/algorithms/org-eclipse-elk-layered.html)
## One bounded comparison
1. Keep the existing five input Projects and their manifest hashes. Reuse the declared source-to-target tasks, including companion fan-out branches and the fixed ID permutation. Do not add favorable graphs or change names/IDs after viewing outputs.
2. Use one common reset policy for all three placement arms: eligibility captured from the original staged projection; eligible stored pins/full label cleared; attachments retained. Apply existing `translated_routes` to retained routes using each arm's actual final deltas. Current Arrange/reset is the baseline. Comparing an unreset baseline with a reset candidate would confound placement with stale-constraint removal.
3. Freeze engine/version, options, seed, input ordering, dimensions and mapping policy. Run the existing native prototype unchanged. Its folded/omitted-node rejection is an unsupported result, not permission to unfold the Project or omit the case from the table. Report supported subsets explicitly.
4. Emit the final cloned Project plus real Rust projection/routes/labels for each supported arm, and the unchanged raw ELK input/output. Preserve ELK edge sections and label locations for diagnosis, but do not store them as authored Project bends/labels in the placement-only arm. Engine labels can influence spacing while the displayed Rust label lands elsewhere; such a result must not be described as successful label placement.
5. Audit adapter invariants before quality ranking. Reuse independent contact metrics; inspect every changed collision/warning identity. Run the same predeclared trace tasks against the actual rendered result for any candidate that advances beyond geometric screening. Keep all failures and unsupported outputs.
6. Stop after this fixed comparison and concrete adapter-defect repairs. No option sweep, per-case best seed or penalty tuning. A faithful ELK placement gain motivates a production-placement decision; a faithful failure localizes what placement alone did not solve. If raw ELK output looks better but Rust rerouting loses the benefit, the next bounded question is transferring or reproducing that coupling, not more node-order heuristics. Raw output is diagnostic evidence only until rendered and audited with equal constraints.
## Adapter gates that can invalidate the experiment
- **Placement identity and hierarchy:** map by placement identity, not semantic element alone; a view can place the same semantic entity more than once. Use actual projected visual parents and visible endpoints, while preserving the authored containment model. Convert every nested local coordinate to Project coordinates exactly once. ELK distinguishes child shapes and edge sections; preserve its raw coordinate representation for diagnosis. [ELK JSON format](https://eclipse.dev/elk/documentation/tooldevelopers/graphdatastructure/jsonformat.html)
- **Cross-container edges:** explicitly configure hierarchy handling. Root default behavior separates children, whereas `INCLUDE_CHILDREN` allows one run across hierarchy levels. A silently flattened graph or independently laid-out children would not test compound connection-aware arrangement. [ELK hierarchy handling](https://eclipse.dev/elk/reference/options/org-eclipse-elk-hierarchyHandling.html)
- **Port identity:** one adapter port per physical projected placement/semantic-port identity, reused by its multiple edges. Duplicating shared ports per edge makes fan-out artificially easy; merging distinct ports changes the problem. Keep direction and semantic edge identity through any engine cycle reversal; never write a reversed semantic source/target back to Project.
- **Port position and resizing:** preserve authored side/fraction and dormant explicit attachments. Compare final Rust physical port tips to the adapter's final intended tips, not to original global coordinates that should move with nodes. ELK exposes both `FIXED_RATIO` and `FIXED_POS`; select a verified policy rather than treating them as synonyms. Fixed original pixel offsets on a resized container can cease to match our fractional anchor. Either demonstrate final-size fraction mapping, or mark that case unsupported; post-correcting only Rust ports makes the optimizer's assumed geometry stale. [ELK port constraints](https://eclipse.dev/elk/reference/options/org-eclipse-elk-portConstraints.html)
- **Node sizes and occupied content:** preserve leaf minimum sizes, container body/header/footer clearances, port extents and State row geometry where applicable. Compound resizing is an explicit geometric output and must contain children below the header. ELK supports fixed size constraints, but these do not by themselves supply ARCADIA's content rules. [ELK node-size constraints](https://eclipse.dev/elk/reference/options/org-eclipse-elk-nodeSize-constraints.html)
- **Labels:** supply the same declared label text/dimension policy across arms; ELK generally expects label dimensions from its caller. Estimated rectangles remain screening evidence. Preserve full text and use actual rendered ink for readability acceptance. Do not promote ELK label coordinates into persisted authored overrides to improve this arm. [ELK JSON labels](https://eclipse.dev/elk/documentation/tooldevelopers/graphdatastructure/jsonformat.html)
- **Folded and dormant geometry:** use promoted visible endpoints for planning, but do not replace actual semantic ownership. Retain dormant route metadata with the existing translation rule, and preserve collapse/filter state. If moving/resizing a folded owner requires a hidden-placement transform the adapter cannot represent faithfully, report unsupported rather than shifting hidden geometry opportunistically. Other views remain exact.
An identity import/export without layout should recover the exact Project; only explicitly permitted geometry/reset fields may differ after layout. Validate Projects and independently audit output IDs, finite geometry, container containment, endpoints and anchors. Passing `validate_candidate` alone cannot prove geometric containment or adapter fidelity.
## Proposed acceptance gates
**Preservation is a hard gate.** Semantic elements, relations, endpoints, roles, authored ownership, aliases/text/source meaning, projection/filter/fold state, route entry order and attachment data remain exact. Only selected-view arrangement geometry and eligible pin/full-label reset change; ineligible pins follow existing translation rules, including the State boundary exception. Hidden placement handling follows the declared Layout scope, not a blanket “hidden never moves” rule. Missing actual owners, filtered edges and promoted endpoints are ineligible for reset. Other views stay exact. After integration, enclosing Batch remains one transaction; a valid pending property Apply is a separate history action; invalid drafts block Arrange; clean Undo/Redo and persistence/source round trips retain the established contract. This experiment does not change Process/Scenario path behavior.
**Geometric screening is identity-aware.** Reject new missing routes, nonfinite geometry, invalid containment, unrelated node/header penetrations, or new warnings absent a specifically documented unavoidable constraint. Retain existing failures per edge. Report actual new/resolved label collisions and contacts, not net totals. Do not require zero crossings on arbitrary graphs or count intended ancestor-body containment as an obstacle defect.
**Traceability is the adoption gate.** For every frozen named task, the reviewer must identify the correct source, target, direction and associated connection label in the rendered diagram. Include both branches of shared ports and both distinct ports; inspect actual arrowheads and text. Record wrong turns, ambiguity, selections, pan/zoom and completion time. Normal selection is allowed but record whether unselected geometry was ambiguous and whether selection repaired it. Use the same declared readable scale and viewport, plus Fit for overview; unlimited zoom must not hide unusable navigation cost. Require an observed improvement in a declared task or correction of a baseline failure, with no new failed/ambiguous task. If both layouts already complete every task cleanly, do not manufacture a correctness win from a smaller scalar score; report clarity/action costs descriptively.
**Robustness and cost are separate.** Same-input repeats must be deterministic; ten repeated Arrange applications should settle rather than ratchet size or oscillate. The fixed edge-ID permutation must preserve semantics and task correctness; pixel-identical output for every graph symmetry is not required. Timing must cover placement plus Rust routing/label work, and report engine startup/serialization separately and as end-to-end cost. Existing route-only timing ceilings do not establish acceptable total Arrange latency. Declare any new end-to-end budget before timing; disclose unsupported cases and censored runs.
This comparison can reject an architecture assumption or justify one next prototype. It cannot establish universal optimality, all-Unicode label measurement, arbitrary graph scalability, or native usability from screenshots/metrics alone. It requires no production dependency on JavaScript and makes no recommendation to ship ELK before the adapter and trace evidence exist.
@@ -0,0 +1,32 @@
{
"package": "elkjs",
"version": "0.12.0",
"registry_url": "https://registry.npmjs.org/elkjs/0.12.0",
"tarball_url": "https://registry.npmjs.org/elkjs/-/elkjs-0.12.0.tgz",
"integrity": "sha512-YZcKynxVxYoKIOEpywEPwCFdg+BTbxQRNf3pbwdDCvc8O3kQD8bmIwSxKU1eOTVc4Xo+VG9Te+575mlfvOrhEQ==",
"tarball_sha256": "c1d7719723e020b10724e3ccbc935696a2884f1e78d361cdd03766309e8e8e2a",
"license": "EPL-2.0 OR GPL-3.0-or-later",
"members": [
{
"path": "lib/elk.bundled.js",
"bytes": 1609707,
"sha256": "1222e44f953ce7746af23801e723708f8e6f436b8b377a6a5fc7552f34a307b3"
},
{
"path": "package.json",
"bytes": 1687,
"sha256": "4614a8db12f1c46a1d988d55e92012286c5a25dde3ea69be7695013f4d23cf02"
},
{
"path": "LICENSE.md",
"bytes": 14443,
"sha256": "637e81f4a1b6b4079535c499fca05e238cf7605c1ff5b76d60d7da7ce96700c9"
},
{
"path": "README.md",
"bytes": 15957,
"sha256": "a83dbb420b0b4f335f4b9d2ca65c335be3fdcf83248bff49f81582b9c0c30712"
}
],
"scope": "Isolated reference experiment; no install scripts or production dependency."
}
@@ -0,0 +1,33 @@
# Connection-aware placement reference experiment
Prepared before reference-engine outputs. Owned scope remains issue32 under Codex-ytr83r/root. Gitea writes are currently failing; this plan and future results remain local-only until normal publication succeeds. No Linux checks or prerelease change.
## Question and decision
Can a mature port/hierarchy/label-aware placement engine produce node positions that materially improve the unchanged released Rust router's final drawing on the already frozen five-case corpus? Compare the old Rust layered prototype, reset-only released placement, and this external placement reference. This is a diagnostic comparison, not adoption of JavaScript into the product and not a test of ELK's complete drawing quality: returned ELK edge sections and label coordinates are deliberately not applied to the Project. Failure may show that placement and routing must stay coupled, or that this adapter is inadequate; it does not establish that ELK itself is poor.
The previous turn completed a verified explicit route-reset contribution. Prior J/P/PL/L router tuning stopped with realistic ID-order/label tradeoffs. Root cause inspection shows production Layout topologically orders roots then packs four per row; nested children are ordered by ID and stacked. Its isolated layered prototype uses neighbor node centers, ignoring fixed port order, and one diagram-wide maximum label gap. Repeating penalty tuning is not the next experiment.
Primary sources: [ELK Layered](https://eclipse.dev/elk/reference/algorithms/org-eclipse-elk-layered.html) supports ports, labels and compound graphs; [Schulze et al.](https://rtsys.informatik.uni-kiel.de/~biblio/downloads/papers/jvlc13.pdf) explains that crossing reduction depends on port order as well as node order. [ELK JSON](https://eclipse.dev/elk/documentation/tooldevelopers/graphdatastructure/jsonformat.html) defines nested nodes/ports and requires supplied label dimensions. [Official elkjs](https://github.com/kieler/elkjs) exposes the layout algorithms in JavaScript. These motivate a reference comparison, not an inferred implementation guarantee.
## Frozen inputs and engine
Use every existing case in `harness/experiments/routing-independent-20260925/corpus/manifest.json` at research commitac5d7ef3: workshop expanded/folded, mixed dispatch, its fixed edge-ID permutation, sparse40. Do not edit fixtures, select favorable routes or add new tuning cases after results. Manifest SHA256 `a58a6956e26b414e88fafd4fdb92e006050653a4149178f08df84128e3917717`. Reuse named trace tasks and report every case, including unsupported/error outcomes.
Reference engine elkjs0.12.0, official npm tarball SHA256 `c1d7719723e020b10724e3ccbc935696a2884f1e78d361cdd03766309e8e8e2a`, bundle SHA256 `1222e44f953ce7746af23801e723708f8e6f436b8b377a6a5fc7552f34a307b3`. Registry SHA512 integrity was verified. Isolated bundle extraction runs no npm lifecycle scripts. Dependency receipt and license retained. No application dependency or packaging decision follows.
One declared configuration: layered, RIGHT, orthogonal, hierarchy INCLUDE_CHILDREN, seed1, no edge/hierarchy merging. Projected visible hierarchy and dimensions; semantic port identities shared exactly where projection shares them. Ports use observed owner-relative coordinates, side and FIXED_POS. Compound header64/top padding and24 side/bottom padding reflect these architecture fixtures; they are not general State metrics. Current core-estimated edge label dimensions are supplied. Save complete input graph before calling the engine. Unsupported direction/port references fail explicitly. Record all output coordinates, sections and labels even though only node geometry is transferable.
## Adapter gates and attribution
Return exactly one finite positive node rectangle for each input projected node, with unchanged IDs and hierarchy. Detect extra/missing/duplicate nodes, changed identity, invalid fields, shrinkage and header/containment collisions. Do not change semantic endpoints, port anchors, relation direction, node visibility/collapse, authored endpoint attachments, or other views to fit the engine. Output port coordinates relative to resized containers must be audited: FIXED_POS can diverge from our final-size anchored fractions. Report that divergence; do not silently accept it as matching a fixed-fraction constraint or repair anchors. Reproject the imported Project through Rust to establish its actual final endpoints.
The placement importer starts from the original Project and replaces only returned visible node geometry. A visible collapsed node has a compact projected footprint distinct from stored expanded size: require the returned size to equal that compact footprint, apply only position, and preserve stored expanded size. A resized collapsed footprint is rejected in this first adapter, not silently written into expanded geometry. For a hidden placement under a visible collapsed ancestor, translate its original position by the nearest visible collapsed ancestor's delta, preserving latent relative geometry; unrelated out-of-projection nodes stay exact. This is an explicit reference-adapter policy, not a production amendment. Use existing actual-owner translation for retained routes; clear only pre-operation eligible visible actual-owner pins and full labels, preserving entries/order and attachments as in the verified reset contract. Validate the final Project, source roundtrip and conservation before geometric comparison. Scenario is excluded explicitly. The old prototype's folded refusal remains a failure to support that case, not a zero-score row.
## Observations and stopping rule
Parent bounds each engine process at90seconds; retain timed-out/missing output as failure. Engine timing is diagnostic startup+layout timing and is not directly compared to prior isolated Rust route-only timings. Freeze binaries/scripts/config/source before calls; verify input and tool hashes afterward. No concurrent compiler writes to shared target. Once compiled, preserve the observer executable before further worktree edits.
Report the existing full vector: proper crossings, all raw T/bend/endpoint contacts, pairwise and nonterminal shared length, node/header penetration, node and estimated-label collisions, unrelated label/stroke intersections, per-edge warnings, bends, length and area. No weighted aggregate pass. Compare each candidate to the same source/reset control; distinguish node positions from final routed geometry. Preserve source inputs and raw outputs. ID-permutation pair must stay explicit.
This finite experiment answers whether further native Rust placement design or an external-engine integration spike has stronger evidence. A simpler Rust candidate remains preferred if comparable. Reject a transfer as acceptable if it loses constraints, changes semantic records, produces invalid geometry or introduces new trace ambiguity. Scalar improvements alone cannot establish native traceability or shipped usability. Follow native inspection only for a candidate with a defensible full quality vector and faithful adapter; no automatic production adoption. Stop after this single configuration and analysis; repair concrete adapter defects with preserved failures, but do not tune options after viewing quality results.
@@ -0,0 +1,37 @@
"""Fetch pinned official reference engine, without install scripts or production changes."""
from pathlib import Path
import base64, hashlib, io, json, tarfile, urllib.request
root = Path(__file__).resolve().parent / 'elk-reference-20260925'
root.mkdir(exist_ok=False)
url = 'https://registry.npmjs.org/elkjs/0.12.0'
metadata_raw = urllib.request.urlopen(url, timeout=30).read()
metadata = json.loads(metadata_raw)
assert metadata['name'] == 'elkjs' and metadata['version'] == '0.12.0'
tar_url = metadata['dist']['tarball']
assert tar_url == 'https://registry.npmjs.org/elkjs/-/elkjs-0.12.0.tgz'
raw = urllib.request.urlopen(tar_url, timeout=30).read()
expected = 'sha512-YZcKynxVxYoKIOEpywEPwCFdg+BTbxQRNf3pbwdDCvc8O3kQD8bmIwSxKU1eOTVc4Xo+VG9Te+575mlfvOrhEQ=='
actual = 'sha512-' + base64.b64encode(hashlib.sha512(raw).digest()).decode()
assert actual == expected == metadata['dist']['integrity']
(root / 'registry-metadata.json').write_bytes(metadata_raw)
(root / 'elkjs-0.12.0.tgz').write_bytes(raw)
allowed = {'package/lib/elk.bundled.js', 'package/package.json', 'package/LICENSE.md', 'package/README.md'}
members = []
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:gz') as archive:
for member in archive.getmembers():
if member.name not in allowed:
continue
assert member.isfile()
data = archive.extractfile(member).read()
relative = Path(member.name).relative_to('package')
destination = root / relative
destination.parent.mkdir(exist_ok=True)
destination.write_bytes(data)
members.append({'path': relative.as_posix(), 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()})
assert len(members) == len(allowed)
receipt = {'package': 'elkjs', 'version': '0.12.0', 'registry_url': url, 'tarball_url': tar_url,
'integrity': actual, 'tarball_sha256': hashlib.sha256(raw).hexdigest(),
'license': metadata['license'], 'members': members, 'scope': 'Isolated reference experiment; no install scripts or production dependency.'}
(root / 'dependency-receipt.json').write_text(json.dumps(receipt, indent=2)+'\n', encoding='utf-8')
print(json.dumps(receipt))
@@ -0,0 +1,239 @@
'use strict';
// Isolated placement reference. No Project mutation or ELK route/label adoption.
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const REFERENCE_DIR = __dirname;
const SOURCE = path.resolve(REFERENCE_DIR, '../routing-corpus-runs-20260925/geometry-released/stdout.jsonl');
const SOURCE_SHA256 = '9a31a676f23dea2f41daec39497f569d68231fa0fd66e5a1a476be97d5d61ac2';
const BUNDLE_SHA256 = '1222e44f953ce7746af23801e723708f8e6f436b8b377a6a5fc7552f34a307b3';
const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
const binding = file => { const b = fs.readFileSync(file); return {path: file, bytes: b.length, sha256: sha(b)}; };
const write = (dir, name, value) => fs.writeFileSync(path.join(dir, name), JSON.stringify(value, null, 2) + '\n', {flag: 'wx'});
const requireThat = (ok, message) => { if (!ok) throw new Error(message); };
const finite = (...values) => values.every(Number.isFinite);
const unique = (values, key, label) => {
const result = new Map();
for (const value of values) {
requireThat(typeof value[key] === 'string' && !result.has(value[key]), `Missing/duplicate ${label} identity`);
result.set(value[key], value);
}
return result;
};
const portId = (owner, element) => `port:${owner}:${element}`;
const SIDES = {Left: 'WEST', Right: 'EAST', Top: 'NORTH', Bottom: 'SOUTH'};
const CONFIG = {
'elk.algorithm': 'layered', 'elk.direction': 'RIGHT',
'elk.edgeRouting': 'ORTHOGONAL', 'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
'elk.randomSeed': '1', 'elk.layered.mergeEdges': 'false',
'elk.layered.mergeHierarchyEdges': 'false',
};
function adapt(row) {
const metrics = row.before;
const nodes = unique(metrics.projected_nodes, 'id', 'node');
const definitions = unique(metrics.projected_edges, 'id', 'edge');
const routed = unique(metrics.routed_edges, 'id', 'routed edge');
const graph = {id: 'reference-root', layoutOptions: {...CONFIG}, children: [], edges: []};
const objects = new Map();
const mapping = {case: row.fixture, diagram: row.diagram, nodes: [], ports: [], edges: [],
excluded_source_placements: [], configuration: CONFIG,
scope: 'Transferable projected placement only. ELK routes/label positions are diagnostic and never applied to Project.',
port_policy: 'Zero-size FIXED_POS anchors at observed node-relative physical port positions. Drift and resizing require explicit assessment.',
compound_policy: 'Input size is minimum; top padding64, other padding24. Leaves retain input dimensions.'};
for (const node of nodes.values()) {
const parent = node.visual_parent === null ? null : nodes.get(node.visual_parent);
requireThat(node.visual_parent === null || parent, `Missing parent of ${node.id}`);
const seen = new Set([node.id]);
for (let p = parent; p; p = p.visual_parent === null ? null : nodes.get(p.visual_parent)) {
requireThat(!seen.has(p.id), `Hierarchy cycle at ${node.id}`); seen.add(p.id);
}
requireThat(finite(node.position.x, node.position.y, node.size.width, node.size.height)
&& node.size.width > 0 && node.size.height > 0, `Invalid node geometry ${node.id}`);
const object = {id: node.id, x: node.position.x - (parent?.position.x || 0),
y: node.position.y - (parent?.position.y || 0), width: node.size.width, height: node.size.height,
layoutOptions: {'elk.portConstraints': 'FIXED_POS'}, ports: [], children: []};
objects.set(node.id, object);
mapping.nodes.push({id: node.id, element: node.element, parent: node.visual_parent,
collapsed: node.collapsed, input_world: node.position, input_size: node.size});
}
for (const node of nodes.values()) {
const object = objects.get(node.id);
(node.visual_parent === null ? graph : objects.get(node.visual_parent)).children.push(object);
}
for (const object of objects.values()) {
if (object.children.length) Object.assign(object.layoutOptions, {
'elk.padding': '[top=64,left=24,bottom=24,right=24]',
'elk.nodeSize.constraints': '[MINIMUM_SIZE]',
'elk.nodeSize.minimum': `(${object.width},${object.height})`,
});
else object.layoutOptions['elk.nodeSize.constraints'] = '[]';
}
const portMap = new Map();
for (const port of metrics.projected_ports) {
const owner = nodes.get(port.owner), id = portId(port.owner, port.element);
requireThat(owner && !portMap.has(id) && SIDES[port.side], `Invalid/duplicate port ${id}`);
requireThat(finite(port.position.x, port.position.y), `Invalid port coordinates ${id}`);
const object = {id, x: port.position.x - owner.position.x, y: port.position.y - owner.position.y,
width: 0, height: 0, layoutOptions: {'elk.port.side': SIDES[port.side]}};
portMap.set(id, object); objects.get(port.owner).ports.push(object);
mapping.ports.push({id, owner: port.owner, element: port.element, side: port.side,
input_world: port.position, input_relative: {x: object.x, y: object.y}});
}
for (const edge of definitions.values()) {
requireThat(edge.oriented === true, `Unsupported undirected edge ${edge.id}`);
const endpoint = end => {
requireThat(nodes.has(edge[end]), `Missing ${end} node for ${edge.id}`);
const semantic = edge[`${end}_port`];
if (semantic === null) return edge[end]; // Explicit automatic node endpoint; never invent a port.
const id = portId(edge[end], semantic);
requireThat(portMap.has(id), `Missing projected semantic port ${id} for ${edge.id}`);
return id;
};
const source = endpoint('source'), target = endpoint('target');
const observed = routed.get(edge.id);
requireThat(observed && observed.label && finite(observed.label.size.width, observed.label.size.height)
&& observed.label.size.width >= 0 && observed.label.size.height >= 0, `Missing/invalid label ${edge.id}`);
const object = {id: edge.id, sources: [source], targets: [target], labels: [{id: `label:${edge.id}`,
text: observed.label.text, width: observed.label.size.width, height: observed.label.size.height}]};
graph.edges.push(object);
mapping.edges.push({id: edge.id, projected: edge, elk_sources: [source], elk_targets: [target],
observed_route: observed, label_measurement: 'Existing core estimated rectangle; no renderer measurement'});
}
requireThat(definitions.size === routed.size, 'Projected/routed edge inventory differs');
for (const node of Object.values(row.before_project.diagrams[row.diagram].nodes)) {
if (!nodes.has(node.id)) mapping.excluded_source_placements.push(node);
}
return {graph, mapping};
}
function inspect(result, mapping) {
const nodes = [], ports = [], edges = [], checks = [];
const add = (name, passed, detail) => checks.push({name, passed: Boolean(passed), detail});
function finiteTree(value) {
if (typeof value === 'number') return Number.isFinite(value);
if (Array.isArray(value)) return value.every(finiteTree);
if (value && typeof value === 'object') return Object.values(value).every(finiteTree);
return true;
}
add('all_engine_numeric_fields_finite', finiteTree(result));
function walk(container, ox, oy, parent) {
for (const node of container.children || []) {
const x = ox + node.x, y = oy + node.y;
nodes.push({id: node.id, parent, position: {x, y}, size: {width: node.width, height: node.height}});
add('finite_positive_node', finite(x, y, node.width, node.height) && node.width > 0 && node.height > 0, node.id);
for (const port of node.ports || []) {
ports.push({id: port.id, owner: node.id, position: {x: x + port.x, y: y + port.y},
relative: {x: port.x, y: port.y}, size: {width: port.width, height: port.height}});
add('finite_zero_size_port', finite(port.x, port.y, port.width, port.height)
&& port.width === 0 && port.height === 0, port.id);
}
walk(node, x, y, node.id);
}
edges.push(...(container.edges || []));
}
walk(result, 0, 0, null);
const actualNodes = unique(nodes, 'id', 'result node'), actualPorts = unique(ports, 'id', 'result port');
const actualEdges = unique(edges, 'id', 'result edge');
const sameIds = (actual, expected) => actual.size === expected.length && expected.every(v => actual.has(v.id));
add('node_inventory', sameIds(actualNodes, mapping.nodes));
add('port_inventory', sameIds(actualPorts, mapping.ports));
add('edge_inventory', sameIds(actualEdges, mapping.edges));
const resizing = [], portDrift = [];
for (const input of mapping.nodes) {
const output = actualNodes.get(input.id); if (!output) continue;
add('parent_identity', output.parent === input.parent, input.id);
const compound = mapping.nodes.some(n => n.parent === input.id);
const dx = output.size.width - input.input_size.width, dy = output.size.height - input.input_size.height;
resizing.push({id: input.id, compound, width_delta: dx, height_delta: dy});
add(compound ? 'compound_minimum_size' : 'leaf_size_exact', compound ? dx >= -1e-6 && dy >= -1e-6 : dx === 0 && dy === 0, input.id);
if (input.collapsed) add('collapsed_compact_size_exact', dx === 0 && dy === 0, input.id);
if (input.parent !== null) {
const parent = actualNodes.get(input.parent);
add('child_inside_parent_body', parent
&& output.position.x >= parent.position.x + 24 - 1e-6
&& output.position.y >= parent.position.y + 64 - 1e-6
&& output.position.x + output.size.width <= parent.position.x + parent.size.width - 24 + 1e-6
&& output.position.y + output.size.height <= parent.position.y + parent.size.height - 24 + 1e-6, input.id);
}
}
for (const input of mapping.ports) {
const output = actualPorts.get(input.id); if (!output) continue;
add('port_owner_identity', output.owner === input.owner, input.id);
portDrift.push({id: input.id, before: input.input_relative, after: output.relative,
dx: output.relative.x - input.input_relative.x, dy: output.relative.y - input.input_relative.y});
}
for (const input of mapping.edges) {
const output = actualEdges.get(input.id); if (!output) continue;
add('edge_orientation_and_endpoints', JSON.stringify(output.sources) === JSON.stringify(input.elk_sources)
&& JSON.stringify(output.targets) === JSON.stringify(input.elk_targets), input.id);
}
return {nodes, ports, checks, resizing, port_relative_drift: portDrift,
structural_checks_passed: checks.every(c => c.passed), adoption_accepted: false,
limitation: 'ELK sections/labels are only in raw result. Compound resize and fixed-port drift are measurements requiring explicit transfer review, not automatic acceptance.'};
}
async function main() {
const [caseName, destination, sourceArgument, bundleArgument, ...extra] = process.argv.slice(2);
requireThat(caseName && destination && extra.length === 0,
'Usage: node reference_probe.cjs CASE_NAME OUTPUT_NEW_DIR [SOURCE_JSONL [ELK_BUNDLE]]');
const sourcePath = sourceArgument ? path.resolve(sourceArgument) : SOURCE;
const bundlePath = bundleArgument ? path.resolve(bundleArgument) : path.join(REFERENCE_DIR, 'lib/elk.bundled.js');
const out = path.resolve(destination);
fs.mkdirSync(out, {recursive: false}); // Refuse overwrite, including any existing directory.
const receipt = {case: caseName, success: false, adoption_accepted: false,
started: new Date().toISOString(), timeout_policy: 'Parent supervisor supplies90s process limit; no in-process hard timeout.'};
try {
receipt.script = binding(__filename); receipt.input = binding(sourcePath);
receipt.engine = {package: 'elkjs', version: '0.12.0', ...binding(bundlePath)};
requireThat(receipt.input.sha256 === SOURCE_SHA256, 'Frozen source JSONL hash mismatch');
requireThat(receipt.engine.sha256 === BUNDLE_SHA256, 'Pinned elkjs0.12.0 bundle hash mismatch');
if (!bundleArgument) {
const dependencyPath = path.join(REFERENCE_DIR, 'dependency-receipt.json');
receipt.dependency_receipt = binding(dependencyPath);
const dependency = JSON.parse(fs.readFileSync(dependencyPath, 'utf8'));
requireThat(dependency.package === 'elkjs' && dependency.version === '0.12.0', 'Wrong dependency pin');
for (const member of dependency.members) {
const actual = binding(path.join(REFERENCE_DIR, member.path));
requireThat(actual.sha256 === member.sha256 && actual.bytes === member.bytes, `Dependency mismatch ${member.path}`);
}
} else {
receipt.dependency_receipt = null;
receipt.dependency_policy = 'External bundle admitted by exact pinned bundle SHA256; local extraction receipt not required.';
}
const rows = fs.readFileSync(sourcePath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
const cases = rows.filter(row => typeof row.fixture === 'string');
receipt.available_cases = cases.map(row => row.fixture);
requireThat(cases.length === 5 && new Set(receipt.available_cases).size === 5, 'Expected five distinct frozen cases');
const row = cases.find(value => value.fixture === caseName);
requireThat(row, `Unknown case ${caseName}`);
write(out, 'source-case.json', row);
const {graph, mapping} = adapt(row);
write(out, 'input-graph.json', graph); write(out, 'adapter-mapping.json', mapping);
receipt.input_graph = binding(path.join(out, 'input-graph.json'));
receipt.adapter_mapping = binding(path.join(out, 'adapter-mapping.json'));
const ELK = require(bundlePath);
const elk = new ELK();
const started = process.hrtime.bigint();
const result = await elk.layout(graph, {layoutOptions: CONFIG});
receipt.engine_elapsed_ns = (process.hrtime.bigint() - started).toString();
write(out, 'elk-result.json', result);
const observed = inspect(result, mapping);
write(out, 'placement-observation.json', observed);
receipt.success = observed.structural_checks_passed;
receipt.outputs = ['source-case.json','input-graph.json','adapter-mapping.json','elk-result.json','placement-observation.json'].map(name => binding(path.join(out, name)));
receipt.input_unchanged = binding(sourcePath).sha256 === receipt.input.sha256;
receipt.engine_unchanged = binding(bundlePath).sha256 === receipt.engine.sha256;
requireThat(receipt.input_unchanged, 'Frozen input changed during engine call');
requireThat(receipt.engine_unchanged, 'Pinned bundle changed during engine call');
if (!receipt.success) process.exitCode = 1;
} catch (error) {
receipt.success = false; receipt.error = {message: error.message, stack: error.stack}; process.exitCode = 1;
} finally {
receipt.finished = new Date().toISOString(); write(out, 'receipt.json', receipt);
}
}
module.exports = {adapt, inspect, CONFIG}; // Parent can exercise synthetic adapter cases without invoking ELK.
if (require.main === module) {
main().catch(error => { console.error(error.stack || error); process.exitCode = 1; });
}
@@ -0,0 +1,52 @@
"""Bound each frozen reference case in a separate process and preserve all outcomes."""
from pathlib import Path
import datetime, hashlib, json, subprocess, sys, time
local = Path(__file__).resolve().parent
root = local / 'elk-reference-20260925'
node = Path('C:/Users/User/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node.exe')
script = root / 'reference_probe.cjs'
source = local / 'routing-corpus-runs-20260925/geometry-released/stdout.jsonl'
manifest = local / 'auto-layout-20260925/harness/experiments/routing-independent-20260925/corpus/manifest.json'
output = root / sys.argv[1]
assert output.parent == root and output.name == sys.argv[1]
output.mkdir(exist_ok=False)
def record(path):
return {'path':str(path), 'bytes':path.stat().st_size,
'sha256':hashlib.sha256(path.read_bytes()).hexdigest()}
bound = [node, script, source, manifest, root/'lib/elk.bundled.js', root/'dependency-receipt.json',
local/'connection-layout-reference-plan-20260925.md', Path(__file__)]
before = [record(p) for p in bound]
assert before[2]['sha256'] == '9a31a676f23dea2f41daec39497f569d68231fa0fd66e5a1a476be97d5d61ac2'
assert before[3]['sha256'] == 'a58a6956e26b414e88fafd4fdb92e006050653a4149178f08df84128e3917717'
assert before[4]['sha256'] == '1222e44f953ce7746af23801e723708f8e6f436b8b377a6a5fc7552f34a307b3'
cases = [c['name'] for c in json.loads(manifest.read_text())['cases']]
receipts = []
for case in cases:
case_output = output / case
stdout = output / (case+'.stdout.log')
stderr = output / (case+'.stderr.log')
argv = [str(node), str(script), case, str(case_output)]
started = datetime.datetime.now(datetime.timezone.utc).isoformat()
start = time.perf_counter()
timed_out = False
with stdout.open('xb') as out, stderr.open('xb') as err:
try:
completed = subprocess.run(argv, cwd=root, stdout=out, stderr=err, timeout=90)
code = completed.returncode
except subprocess.TimeoutExpired:
timed_out = True
code = None
receipt = {'case':case, 'argv':argv, 'started_utc':started,
'elapsed_seconds':time.perf_counter()-start, 'timeout_seconds':90,
'timed_out':timed_out, 'exit_code':code, 'stdout':record(stdout), 'stderr':record(stderr)}
receipts.append(receipt)
(output/(case+'.receipt.json')).write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt), flush=True)
after = [record(p) for p in bound]
receipt = {'inputs_before':before, 'inputs_after':after, 'inputs_unchanged':before==after,
'cases':receipts, 'scope':'Reference-engine adapter execution; exit0 is not geometry or production acceptance.'}
(output/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n')
assert before == after, 'Bound inputs changed during experiment'
@@ -0,0 +1,73 @@
# #32 connection-aware placement: source audit and next design
Read-only audit at reset checkout HEAD `956e33e568c6b799d0330803c74927e205977dfd`; line references below are repository-relative at that pin. No source edits, compilation, routing execution or GUI actions. The finished reset behavior is separate from this proposal; neither the old layered prototype nor J/L routing experiments are adopted here.
## What current placement considers—and omits
| Source | Observed behavior | Consequence for #32 |
| --- | --- | --- |
| `crates/oa-core/src/session.rs:970–974` | Children come from the projection-parent forest into vectors in ordered-map traversal order. | Nested siblings remain placement-ID ordered; no internal exchange order or boundary-neighbor order is considered. |
| `session.rs:986–1012` | Port-aware minimum node size, State header/content sizing, then one vertical child column with48 horizontal padding and16 sibling gap; authored size remains a lower bound. | Valid enclosure scaffolding exists, but neither connection demand nor edge label dimensions affect child spacing or container aspect ratio. |
| `session.rs:1017–1040` | Every child starts at parent x+24, stacked beneath the header. | Reversed children in adjacent containers can force intersecting/shared routes even when a sibling permutation would separate them. |
| `session.rs:1052–1070` | Every visible edge is lifted to the two outermost roots; same-root edges disappear; duplicate root pairs collapse into a set. `edge.oriented` is not checked. | Some connectivity is used, but internal demands, multiplicity, ports and labels are discarded. Undirected serialized endpoint order also influences rank-like root ordering. |
| `session.rs:1072–1089` | Choose a remaining root with no remaining predecessor, tie by semantic alias/ID; if none, choose alias/ID anyway. | Deterministic cycle fallback exists, but this is a total topological order, not explicit layers or crossing minimization. |
| `session.rs:1111–1129` | Lay roots left-to-right with240 gaps; wrap after every fourth root with60 row gap. | A seven-node flow is broken into rows regardless of connections. Disconnected graphs and dense bipartite graphs receive the same packing policy. |
| `session.rs:1131–1159` | Apply final placement deltas, existing route translation, and pre-frozen eligible reset. | Useful commit boundary already exists; a better placement planner can replace candidate positions/sizes without redesigning history or route persistence. |
No call inside Layout computes edge-label geometry or scores its proposed final routed result. `routing.rs:1363–1395` derives visible text and estimates label width as7 per scalar plus16, height24; `1410+` tries automatic locations only after paths exist. Therefore source alone confirms that node arrangement is not label-aware. Core estimates must not be advertised as rendered glyph bounds; the independent #36 label work needs explicit reconciliation when integrated.
## Evidence that narrows the choice
The first prototype report, `harness/experiments/auto-layout-20260925/results-01.md` at the separate experiment checkout, documents useful placement effects: reversed children shared length148→0 and label/node collisions1→0; seven-node chain length2560→492 with8→0 bends; long-label collisions2→0. But diamond and bipartite shared strokes worsened. Its nine fixtures omitted folded/filtered topology, Allocation/State, cycles/undirected graphs and genuine other-view conservation. It explicitly rejected omitted/folded nodes, so it cannot be dropped into production Layout.
`harness/experiments/routing-independent-20260925/decision-review.md` retains the released router: J/L pass the finite timing guardrails but expose label and ID-order regressions, and native trace tasks were not met. The next placement experiment should therefore keep the released router fixed. Improving a routing penalty again would obscure whether ordering/space allocation solves the actual placement defect.
## Material alternatives
| Approach | Scope and tradeoff | Decision criterion |
| --- | --- | --- |
| Hierarchical sibling layers and boundary order | Replace vertical/4-per-row packing with connection-informed order, ranks and local corridor sizing, retaining current sizing/transaction machinery. Native Rust and bounded; compound boundary constraints are the real complexity. | Preferred first experiment: improves known nested ordering/label fixtures without corrupting latent folded geometry or worsening named trace tasks. |
| Bounded local improvement of existing positions | Treat current arranged boxes as a baseline; evaluate a small fixed set of sibling swaps, row breaks or gap changes using actual routed results. Preserves more organization and avoids a full layering engine. | Useful control, but reject as primary if it cannot discover the two-container sibling swap without excessive route evaluations or gets trapped by a needed rank change. |
| Compound layout engine adapter | Feed ports, nesting and label rectangles to a mature engine such as ELK via elkjs; validate and commit returned geometry. | Reopen only if native bounded candidates fail concrete compound cases. Additional runtime and mapping cost remain, and the app must still define folded/hidden scope, fixed anchors, measurement and transaction semantics. No engine adoption justified by these results alone. |
These differ in geometry construction/search strategy, not just router coefficients. Do not spend another iteration rebuilding the prototype's known unsupported scope without resolving it.
## Minimal proposed architecture
Keep one pure candidate computation inside the existing core Layout transaction. Inputs are the staged Project, target Diagram, actual projection-parent forest, visible projected connectivity, and deterministic core text-size estimates. Outputs are target-diagram positions/sizes; existing final route translation/reset remains authoritative. Do not call desktop font APIs from core commands or give GUI/API different layouts for identical inputs.
Use two views of the **same** forest: full stored placement ancestry for latent geometry/conservation, and visible connectivity for current arrangement goals. Reuse `projection::parents` (`projection.rs:81+`), which includes Allocation-specific allocation grouping, rather than substituting semantic parent fields. Folded rendering hides descendants and substitutes compact outer heights (`projection.rs:303–335`); that compact footprint is not permission to shrink/delete stored descendants. Preserve descendants' relative geometry when treating a collapsed subtree as one visible box, translating its latent members with the box. This is a proposed improvement over today's rearrangement of hidden descendants and requires an explicit contract choice before implementation. An alternative is retaining today's hidden-descendant reflow with full stored enclosure; it is simpler but may waste visible space. Do not silently mix these policies.
For each sibling group, lift visible edges to the two direct child subtrees at their lowest common ancestor. Use oriented edges for layer direction; undirected edges influence proximity/order only. Keep parallel demands and port-side/order information rather than reducing them to a set. External boundary connections should provide order constraints for the local children too: lifting only at the common ancestor would still miss the reversed-child case. Separate these boundary-order hints from rank constraints to avoid counting one edge as an independent rank demand at every ancestor. Use deterministic SCC handling for cycles, canonical ties, and explicit self-loop space.
Start with a fixed number of sibling order sweeps and one coordinate assignment. Compute container sizes bottom-up from frozen node minima plus content/header/footer/padding, using max constraints instead of repeatedly adding clearance to already enlarged sizes. Reserve corridor width/height using relevant visible label rectangles and edge demand per cut/group, not one global longest label inflated into every gap. Real fixed port anchors are hard constraints; automatic port choices remain derived and must be reprojected after placement, because their side/order and node capacity depend on new peer geometry (`projection/port_layout.rs:156+`). A final reprojection/enclosure check is mandatory before scoring.
Route only a small fixed number of complete candidates with the released router: baseline plus one main layered candidate, optionally one local-swap control. Keep actual path/label metrics and identity-preserving invariants, rather than accepting by straight-line crossing proxies alone. Bound topology preprocessing and routing work globally; on exhaustion return the declared baseline or fail atomically, never partially apply geometry. Selecting a baseline is a fallback outcome, not proof the new layout succeeded. Avoid a weighted scalar that hides new label collisions behind shorter paths.
## Non-negotiable invariants and evaluator changes
- Freeze reset eligibility before placement (`session.rs:955–969`). Route entry/order, attachments and port anchors follow the accepted reset contract; filtered/promoted metadata keeps existing actual-owner translation and State boundary rules. Other diagrams and all semantics remain exact. Generated elbows stay derived.
- Preserve staged Batch semantics and enclosing history. Apply one complete candidate with final validation; no new intermediate validation or hidden history entries. Keep Scenario's existing rejection (`session.rs:950–953`) and do not claim Process support from this work.
- Reuse reset tests in `tests/release_routing.rs:652+`, especially filtered/missing routes792+, folding864+, staged Batch930+. Add independent known-order witnesses for sibling crossing separation, seven-node flow without arbitrary wrapping, local long-label clearance, undirected endpoint reversal, cycle/self-loop, distinct/fixed/shared ports and disconnected components.
- Extend the placement evaluator with the frozen real workshop Allocation views, fold→Arrange→expand, hidden-edge sentinel and untouched other view. Record latent descendants as well as visible nodes; reject node/header penetration and lost port constraints. A visible-only geometry check misses broken expansion.
- Require exact repeat-input determinism and ten-call stability across save/reparse, including periods2 and cumulative container growth. Compare against current baseline and reset-only at the same router pin. Retain pairwise shared intervals, proper/T/bend contacts, label conflicts, warnings, area, route length and workload individually.
- Geometry remains a screen for failures. Parent must verify predeclared connection-tracing tasks and readable labels at normal zoom, then actual Arrange/Undo/Redo/Save/reopen. Current core text estimates and previous offline images do not establish these outcomes.
Recommendation: settle the folded latent-geometry policy, then prototype sibling ordering/layers with boundary hints and local label corridor space. Keep the reset candidate and released router intact until that experiment meets the larger scope. The first milestone is a falsifiable placement result, not a new framework or a claim that #32 is complete.
## Bounded ELK reference comparison: recommended now, dependency decision deferred
An external layout-only reference is feasible and worthwhile before another bespoke placement iteration. This does not contradict deferring a production compound-engine dependency: the comparison buys evidence about ordering and space allocation without committing the application to a JavaScript runtime. Pin elkjs 0.12.0, package integrity, Node executable and adapter/options; retain exact input/output. No ELK execution was performed in this audit.
Verified official capabilities: [ELK Layered](https://eclipse.dev/elk/reference/algorithms/org-eclipse-elk-layered.html) supports ports, labels, compound graphs and cross-hierarchy layout with the appropriate option. [JSON input](https://eclipse.dev/elk/documentation/tooldevelopers/graphdatastructure/jsonformat.html) supports nested children and explicit port references; callers supply label dimensions. [Port constraints](https://eclipse.dev/elk/reference/options/org-eclipse-elk-portConstraints.html) are node-level, including FIXED_POS and FIXED_RATIO. [Coordinate conventions](https://eclipse.dev/elk/documentation/tooldevelopers/graphdatastructure/coordinatesystem.html) default shapes to parent-relative coordinates; recursively flatten returned nodes before transfer. These capabilities do not establish adapter correctness or resulting Rust-route quality.
The frozen geometry rows have sufficient visible inputs: `before.projected_nodes` carries placement IDs, visual_parent, collapsed and allocation_summary; `projected_ports` carries semantic element plus owner placement, side and world position; `projected_edges` carries oriented, source/target placements and optional semantic ports; `routed_edges` supplies existing approximate label dimensions. Full `before_project` supplies authored anchor and dormant geometry records. Use `(owner placement, semantic port)` adapter identities, not semantic element ID alone. Shared ports must remain shared, not be cloned per edge. Use projected ancestry, not semantic parents (Allocation summaries are material here).
Proposed first reference: one fixed Layered configuration, compound hierarchy enabled, explicit direction and seed, fixed visible leaf dimensions, actual header padding for containers, existing label rectangle dimensions. For the initial diagnostic, fixing all imported port positions is an explicit overconstraint: automatic port choices will be recomputed by Rust after placement, and resized containers complicate absolute versus ratio anchors. Keep authored anchor records exact and report final per-port drift from the engine assumptions. Do not persist returned port coordinates to manufacture agreement. A mixed node containing manual and automatic anchors cannot be naively represented by one node-level FREE constraint. Reject unsupported mappings or document the stronger constraint before comparison.
Folded nodes are opaque visible boxes. Their compact projected height must never overwrite stored expanded size. Require planned compact size unchanged, move only their stored position, and translate hidden descendants with the nearest visible collapsed ancestor using full visual ancestry. Other omitted placements remain exact. This is an explicit experiment policy, not automatic adoption of new production folding semantics. Reprojection can enlarge expanded containers or move derived ports; record that difference, since it means the final Rust geometry differs from the engine's output assumptions.
Import only flattened node geometry into the original Project clone, without an initial Layout. Apply the same pre-operation visible/unpromoted actual-owner reset predicate to existing route entries; preserve attachments/order and use original-to-final translation for retained routes. Ignore ELK path sections and label positions. Therefore the result measures ELK-informed placement followed by released Rust routing, not full ELK routing quality; labels reserved by ELK may still collide when the Rust router selects different corridors.
Compare raw authored input, existing Layout/reset control, old layered candidate (unsupported rows explicit), and ELK placement with equivalent reset scope. Do not rank omitted folded rows as successes. Evaluate complete Project conservation with geometry/reset exceptions, compact and expanded containment, fixed anchors, final projected ports, actual Rust routes/labels, named trace tasks, and ID permutation sensitivity. One frozen-input repeated run establishes deterministic transfer only; repeated arrange/reproject feedback is a separate idempotence check. Reject nonfinite/missing/duplicate/out-of-scope nodes and invalid dimensions before any model mutation. A five-case benchmark can justify the next experiment, not production adoption.