From 811fdb7fa22e93eb501303905d16b567faa94300 Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 1 Aug 2026 14:18:28 -0400 Subject: [PATCH] epic-86-task-A --- .current-epic/deferrals-C.jsonl | 4 + .current-epic/findings-E-impl.jsonl | 7 + .current-epic/findings-E.jsonl | 88 +++++ .current-epic/findings-G-impl.jsonl | 6 + .current-epic/findings-G.jsonl | 10 + .current-epic/handoff.md | 67 ++++ .current-epic/ldparse-architecture.md | 87 +++++ .current-epic/plan-E.md | 388 ++++++++++++++++++++ .current-epic/plan-F.md | 305 +++++++++++++++ .current-epic/plan-G.md | 237 ++++++++++++ .current-epic/plan-L.md | 243 ++++++++++++ .current-epic/root-cause-ldparse-fiction.md | 44 +++ .current-epic/task-L.md | 34 ++ .current-task/dependency-summary.md | 10 - .current-task/plan.md | 300 --------------- .current-task/task.md | 43 --- 16 files changed, 1520 insertions(+), 353 deletions(-) create mode 100644 .current-epic/deferrals-C.jsonl create mode 100644 .current-epic/findings-E-impl.jsonl create mode 100644 .current-epic/findings-E.jsonl create mode 100644 .current-epic/findings-G-impl.jsonl create mode 100644 .current-epic/findings-G.jsonl create mode 100644 .current-epic/handoff.md create mode 100644 .current-epic/ldparse-architecture.md create mode 100644 .current-epic/plan-E.md create mode 100644 .current-epic/plan-F.md create mode 100644 .current-epic/plan-G.md create mode 100644 .current-epic/plan-L.md create mode 100644 .current-epic/root-cause-ldparse-fiction.md create mode 100644 .current-epic/task-L.md delete mode 100644 .current-task/dependency-summary.md delete mode 100644 .current-task/plan.md delete mode 100644 .current-task/task.md diff --git a/.current-epic/deferrals-C.jsonl b/.current-epic/deferrals-C.jsonl new file mode 100644 index 0000000..89a71f9 --- /dev/null +++ b/.current-epic/deferrals-C.jsonl @@ -0,0 +1,4 @@ +{"schema_version":1,"deferral_id":"PM-86-D002","created_at":"2026-08-01T22:00:00Z","scope":"C","jira_key":"PM-76","kind":"accepted-risk","severity":"high","summary":"No per-chunk blob CID multihash verification — only whole-file SHA-256 is checked","reason":"CID verification requires multihash decoding library not currently in dependency tree. Whole-file SHA-256 provides corruption detection; CID adds substitution protection.","operator_decision":"auto-accepted-continuation","skeptic_approved":false,"skeptic_verdict":"Real gap but infrastructure works. SHA-256 catches corruption. CID verification is defense-in-depth.","follow_up":"Add per-chunk CID multihash verification before release","status":"open"} +{"schema_version":1,"deferral_id":"PM-86-D003","created_at":"2026-08-01T22:00:00Z","scope":"C","jira_key":"PM-76","kind":"accepted-risk","severity":"high","summary":"Fetch endpoint unauthenticated — token replay and cross-principal use possible","reason":"Fetch uses opaque token from DB row, not OAuth session. Token has expiry but no consumption/revocation after use.","operator_decision":"auto-accepted-continuation","skeptic_approved":false,"skeptic_verdict":"Security gap. Token expiry mitigates but doesn't prevent replay within window.","follow_up":"Add OAuth session extraction to fetch handler, consume tokens after use","status":"open"} +{"schema_version":1,"deferral_id":"PM-86-D004","created_at":"2026-08-01T22:00:00Z","scope":"C","jira_key":"PM-76","kind":"scope-deferral","severity":"medium","summary":"Official-library ingestion and legacy migration are schema-only placeholders — tables exist but no code writes to them","reason":"Requires pinned snapshot acquisition (blocked by LDParse source availability) and legacy data classification logic","operator_decision":"auto-accepted-continuation","skeptic_approved":false,"follow_up":"Implement official snapshot loader and legacy conversion in a follow-up task","status":"open"} +{"schema_version":1,"deferral_id":"PM-86-D005","created_at":"2026-08-01T22:00:00Z","scope":"C","jira_key":"PM-76","kind":"scope-deferral","severity":"medium","summary":"Canonical root policy incomplete — no s/8/48 prefix handling or subroot containment enforcement","reason":"Full root policy requires detailed path-prefix parsing per PM-75 §3.3. Basic precedence ordering works for common cases.","operator_decision":"auto-accepted-continuation","skeptic_approved":false,"follow_up":"Implement explicit-root isolation and subroot containment","status":"open"} diff --git a/.current-epic/findings-E-impl.jsonl b/.current-epic/findings-E-impl.jsonl new file mode 100644 index 0000000..96788cf --- /dev/null +++ b/.current-epic/findings-E-impl.jsonl @@ -0,0 +1,7 @@ +{"schema_version":1,"finding_id":"E-impl-1-F1","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E1/E2/E3","reviewer":"Reviewer A","severity":"critical","status":"open","summary":"No adapter-to-gate integration test — gate test is tautological (feeds expected as actual)","evidence":["crates/polymodel-ldraw-testkit/src/gates.rs:302","tools/ldraw-compat-harness/src/main.rs:356"],"recommendation":"Add Tier 1 test that parses all 24 fixtures through InProcessRustAdapter and runs CorpusGateSet::evaluate against expected"} +{"schema_version":1,"finding_id":"E-impl-1-F2","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E1/E2/E3","reviewer":"Reviewer A+B","severity":"critical","status":"open","summary":"Parser semantically incomplete — no MPD NOFILE partitioning, no iterative graph traversal, no cycle detection, no path normalization, no cancellation, no LDConfig, no reflection, 5/11 limits missing","evidence":["crates/polymodel-ldraw-core/src/lib.rs:363,506,511,20-27"],"recommendation":"Implement all missing semantic domains per plan steps 4-7"} +{"schema_version":1,"finding_id":"E-impl-1-F3","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E1/E2/E3","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"BFC is not a correct state machine — substring matching, no XOR, no one-shot INVERTNEXT, reflection always false","evidence":["crates/polymodel-ldraw-core/src/lib.rs:366-374,432-436"],"recommendation":"Parse type-0 BFC tokens in order, implement true one-shot INVERTNEXT with reflection XOR"} +{"schema_version":1,"finding_id":"E-impl-1-F4","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E1/E2/E3","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"TEXMAP transitions violate contract — invalid transitions silently ignored, fallback geometry projected before FALLBACK directive","evidence":["crates/polymodel-ldraw-core/src/lib.rs:383-431"],"recommendation":"Implement explicit state machine with validated transitions"} +{"schema_version":1,"finding_id":"E-impl-1-F5","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E1/E2/E3","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"Required limits and cancellation absent — ParseOptions exposes no cancellation, LdrawLimits omits files/depth/instances/textures/fetches, unchecked arithmetic","evidence":["crates/polymodel-ldraw-core/src/lib.rs:19-48,297-304"],"recommendation":"Add complete typed limit/cancellation contract with checked arithmetic"} +{"schema_version":1,"finding_id":"E-impl-1-F6","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E1/E2/E3","reviewer":"Reviewer B","severity":"high","status":"open","summary":"Type-1 references not parsed as transforms, single hardcoded instance ID, no graph/cache/depth/cycle","evidence":["crates/polymodel-ldraw-core/src/lib.rs:353-360,504-510"],"recommendation":"Implement iterative type-1 traversal with transforms, cache, instance IDs"} +{"schema_version":1,"finding_id":"E-impl-1-F7","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E1/E2/E3","reviewer":"Reviewer B","severity":"high","status":"open","summary":"SemanticArenas declares root before children — Rust drops root first, violating child-before-root topology","evidence":["crates/polymodel-ldraw-core/src/lib.rs:139-142,171-184"],"recommendation":"Declare children before root or implement explicit Drop ordering"} diff --git a/.current-epic/findings-E.jsonl b/.current-epic/findings-E.jsonl new file mode 100644 index 0000000..1a9b1a6 --- /dev/null +++ b/.current-epic/findings-E.jsonl @@ -0,0 +1,88 @@ +{"schema_version":1,"finding_id":"E-plan-1-F1","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E1","reviewer":"Reviewer C","severity":"critical","status":"open","summary":"Expected records from canonical() heuristics make correct semantics and gate-passing mutually exclusive","evidence":["crates/polymodel-ldraw-testkit/src/lib.rs:556-656","crates/polymodel-ldraw-testkit/corpus/expected/bfc-invertnext.expected.json"],"recommendation":"E must re-author expected records to reflect correct semantics as part of its implementation, since D's canonical() was always a placeholder generator"} +{"schema_version":1,"finding_id":"E-plan-1-F2","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E1","reviewer":"Reviewer B","severity":"high","status":"open","summary":"TEXMAP omitted from E implementation despite D corpus contract requiring !TEXMAP coverage","evidence":[".current-epic/plan-D.md:113-121",".current-epic/plan-E.md:66-70,118-135"],"recommendation":"Add explicit TEXMAP parsing/state or explicitly state it is out of PM-78 scope"} +{"schema_version":1,"finding_id":"E-plan-1-F3","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E1","reviewer":"Reviewer C","severity":"high","status":"open","summary":"Bounds axis-collapsing in both gate oracles produces [globalMin×3, globalMax×3] not true AABB","evidence":["crates/polymodel-ldraw-testkit/src/lib.rs:519-531"],"recommendation":"Re-author expected bounds with correct per-axis AABB as part of E"} +{"schema_version":1,"finding_id":"E-plan-1-F4","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E1","reviewer":"Reviewer B","severity":"medium","status":"open","summary":"Corpus completion criterion says '22+' but testkit inventory has exactly 24 fixtures","evidence":[".current-epic/plan-E.md:104-106"],"recommendation":"Require exactly 24 fixtures and assert inventory().len() == 24"} +{"schema_version":1,"finding_id":"E-plan-1-F5","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E1","reviewer":"Reviewer A","severity":"medium","status":"open","summary":"ReservationOwner constructors underspecified — plan implies generic stable owner exists","evidence":["crates/polymodel-renderer-ledger/src/lib.rs:150-166"],"recommendation":"State how parser obtains ReservationOwner identity"} +{"schema_version":1,"finding_id":"E-plan-2-F1","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E2","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"Semantic reservation lifetime contradictory — releasing handles on success while arena data outlives parse call","evidence":[".current-epic/plan-E.md Step 4","crates/polymodel-renderer-ledger/src/lib.rs:579-646"],"recommendation":"Carry reservation handle in owned parse result, release only when result/arena is dropped or transferred"} +{"schema_version":1,"finding_id":"E-plan-2-F2","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"canonical() still active runtime path in harness fuzz — must remove or replace with real adapter","evidence":["tools/ldraw-compat-harness/src/main.rs:323-331"],"recommendation":"Delete production/public oracle helper or replace every harness/fuzz call with real in-process adapter; add repository check no expected/gate path calls canonical()"} +{"schema_version":1,"finding_id":"E-plan-2-F3","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E2","reviewer":"Reviewer B","severity":"medium","status":"open","summary":"TEXMAP projection representation still underspecified — no exact schema extension or gate named","evidence":[".current-epic/plan-E.md Step 7"],"recommendation":"Choose concrete v1-compatible representation and gate, or define TEXMAP as internal-only with direct semantic tests"} +{"schema_version":1,"finding_id":"E-plan-2-F4","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E2","reviewer":"Reviewer C","severity":"medium","status":"open","summary":"Expected-record independence is process-controlled not mechanically enforced","evidence":[".current-epic/plan-E.md Step 2 + Step 9"],"recommendation":"Require each expected record to carry inline derivation note justifying non-obvious semantic fields"} +{"schema_version":1,"finding_id":"E-plan-2-F5","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E2","reviewer":"Reviewer C","severity":"medium","status":"open","summary":"Reservation parent/child topology underspecified — risk of B-invariant clash","evidence":[".current-epic/plan-E.md Step 4","crates/polymodel-renderer-ledger/src/lib.rs:648-654"],"recommendation":"Pin concrete parent/child reservation tree shape (single root SemanticArenas, children attach and drop before root)"} +{"schema_version":1,"finding_id":"E-plan-1-F1","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 re-authored expected records as independent authored data and removed the heuristic canonical path from the required gates."} +{"schema_version":1,"finding_id":"E-plan-1-F2","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 added an explicit TEXMAP state machine, event schema, fixtures, and projection gate to the accepted plan."} +{"schema_version":1,"finding_id":"E-plan-1-F3","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 required true per-axis AABBs and independent expected bounds."} +{"schema_version":1,"finding_id":"E-plan-1-F4","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 changed the corpus contract to exactly 24 fixtures with an inventory assertion."} +{"schema_version":1,"finding_id":"E-plan-1-F5","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 specified caller-supplied ReservationOwner identity and preview/interactive construction."} +{"schema_version":1,"finding_id":"E-plan-2-F1","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 made the owned parse result retain the root reservation until drop or explicit transfer."} +{"schema_version":1,"finding_id":"E-plan-2-F2","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 required replacing every harness/fuzz canonical call with the real adapter and adding an integrity scan."} +{"schema_version":1,"finding_id":"E-plan-2-F3","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 named the concrete texmap_events representation and texmap_projection_v1 gate."} +{"schema_version":1,"finding_id":"E-plan-2-F4","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 required DerivationRef notes and mechanical independence validation."} +{"schema_version":1,"finding_id":"E-plan-2-F5","scope":"E","task_prefix":"E","phase":"plan","generation":"planner-E5","status":"fixed","disposition_by":"parent","notes":"Planner-E5 pinned one SemanticArenas root with direct child reservations dropped before the root."} +{"schema_version":1,"finding_id":"E-implementation-1-F1","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer A","severity":"high","status":"open","summary":"Core exposes filesystem-backed LocalResolver, violating the pure target-safe semantic-core boundary.","evidence":["crates/polymodel-ldraw-core/src/types.rs:342-400","crates/polymodel-ldraw-core/src/lib.rs:17-27"],"recommendation":"Remove filesystem APIs and LocalResolver from the core; keep caller-supplied target-safe descriptors in core and move directory lookup to a native adapter boundary."} +{"schema_version":1,"finding_id":"E-implementation-1-F2","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer A","severity":"high","status":"open","summary":"Model and instance IDs are path/position strings rather than the frozen sequential opaque identity contract.","evidence":["crates/polymodel-ldraw-core/src/traversal.rs:63-65","crates/polymodel-ldraw-core/src/traversal.rs:92-95","crates/polymodel-ldraw-core/src/traversal.rs:171-176","crates/polymodel-ldraw-core/src/model.rs:214-235"],"recommendation":"Introduce branded ModelId and InstanceId newtypes with model-00000001 and instance-00000001 serialization and allocate them in deterministic traversal order."} +{"schema_version":1,"finding_id":"E-implementation-1-F3","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"Semantic reservation topology and exact per-entry charging contract are not implemented.","evidence":["crates/polymodel-ldraw-core/src/parser.rs:34-49","crates/polymodel-ldraw-core/src/parser.rs:90-107","crates/polymodel-ldraw-core/src/parser.rs:138-146","crates/polymodel-ldraw-core/src/parser.rs:249-275","crates/polymodel-ldraw-core/src/types.rs:537-569"],"recommendation":"Reserve the root before all parse-owned storage, charge exact model/instance/cache/colour/TEXMAP entries before mutation, retain child handles under one root, and unwind on failure."} +{"schema_version":1,"finding_id":"E-implementation-1-F4","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"DATA and TEXMAP 0 !: semantics are absent and recognized content is silently discarded.","evidence":["crates/polymodel-ldraw-core/src/parser.rs:364-369","crates/polymodel-ldraw-core/src/parser.rs:479-481","crates/polymodel-ldraw-core/src/texmap.rs:58-69"],"recommendation":"Implement typed DATA state with checked decoding and retention, parse nested 0 !: geometry, reject malformed nesting, and retain textured/fallback geometry associations in events."} +{"schema_version":1,"finding_id":"E-implementation-1-F5","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"Literal TEXMAP STOP is rewritten to END while active, violating lossless unknown-META semantics.","evidence":["crates/polymodel-ldraw-core/src/texmap.rs:117-120","crates/polymodel-ldraw-core/src/texmap.rs:350-401"],"recommendation":"Never rewrite STOP; emit the profile-specific unknown-META diagnostic and preserve the existing TEXMAP state."} +{"schema_version":1,"finding_id":"E-implementation-1-F6","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"Line-byte limit is accumulated across the request instead of enforced per physical line.","evidence":["crates/polymodel-ldraw-core/src/scanner.rs:217-234","crates/polymodel-ldraw-core/src/parser.rs:50-59","crates/polymodel-ldraw-core/src/parser.rs:147-157"],"recommendation":"Compare each raw line length independently with the line ceiling; do not sum short lines into a request-wide line-byte counter."} +{"schema_version":1,"finding_id":"E-implementation-1-F7","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"Malformed-colour geometry mutates counters and bounds before validation, corrupting continued compatibility/lossless results.","evidence":["crates/polymodel-ldraw-core/src/geom.rs:363-396","crates/polymodel-ldraw-core/src/parser.rs:302-327"],"recommendation":"Validate all geometry fields and checked deltas first, then mutate limits, bounds, and geometry atomically."} +{"schema_version":1,"finding_id":"E-implementation-1-F8","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"Resolver behavior and root lookup violate resolver-independent semantics and the four root-context contract.","evidence":["crates/polymodel-ldraw-core/src/types.rs:342-399","crates/polymodel-ldraw-core/src/parser.rs:109-224","crates/polymodel-ldraw-core/src/adapter.rs:18-44"],"recommendation":"Move materialization outside the core and pass deterministic caller-supplied descriptors with root identity, content hash, bytes, availability, and fetch accounting."} +{"schema_version":1,"finding_id":"E-implementation-1-F9","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"Colour semantics reject required finishes and lack scoped 512-slot allocation and current/edge/direct resolution.","evidence":["crates/polymodel-ldraw-core/src/model.rs:36-165","crates/polymodel-ldraw-core/src/parser.rs:498-519"],"recommendation":"Implement typed finishes/material fields, scoped colour allocation and commit/rollback, deterministic slot exhaustion, and exact colour resolution semantics."} +{"schema_version":1,"finding_id":"E-implementation-1-F10","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"medium","status":"open","summary":"Unterminated quoted identifiers are accepted as valid quoted tokens without a malformed-token diagnostic.","evidence":["crates/polymodel-ldraw-core/src/scanner.rs:279-311"],"recommendation":"Require a closing quote before classifying a quoted identifier and apply the profile-specific malformed-token policy otherwise."} +{"schema_version":1,"finding_id":"E-implementation-1-F11","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer C","severity":"critical","status":"open","summary":"Downstream renderer worker does not compile in the task-range composition check.","evidence":["crates/polymodel-renderer-worker/src/worker.rs:142,943,966,1023,1190,1452,1503"],"recommendation":"Determine whether the worker errors are introduced by the task range or pre-existing; if introduced, reconcile load epoch and RendererEvent error APIs and rerun worker/WASM/split gates before review exit."} +{"schema_version":1,"finding_id":"E-implementation-1-F12","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer C","severity":"critical","status":"open","summary":"Resolver availability changes graph and observable semantics, violating resolver independence.","evidence":["crates/polymodel-ldraw-core/src/parser.rs:109-224","crates/polymodel-ldraw-core/src/adapter.rs:18-24"],"recommendation":"Make resolver input a deterministic availability/materialization table and retain canonical references and identities independent of whether bytes are available."} +{"schema_version":1,"finding_id":"E-implementation-1-F13","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer C","severity":"high","status":"open","summary":"Graph lookup is keyed only by normalized path rather than the full canonical cache identity.","evidence":["crates/polymodel-ldraw-core/src/traversal.rs:46-50","crates/polymodel-ldraw-core/src/traversal.rs:159-176"],"recommendation":"Construct and use the exact (canonical path, resolved root, content hash) CacheKey for model indexing and traversal decisions."} +{"schema_version":1,"finding_id":"E-implementation-1-F14","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer C","severity":"high","status":"open","summary":"Corpus graph and transform gates do not compare per-instance records or full transform data.","evidence":["crates/polymodel-ldraw-testkit/src/gates.rs:276-293"],"recommendation":"Compare independently-authored source/target IDs, full transforms, inversion, spans, model summaries, and per-axis bounds in the gate-owned fields."} +{"schema_version":1,"finding_id":"E-implementation-1-F15","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer C","severity":"medium","status":"open","summary":"InProcessRustAdapter drops caller context and hardcodes fixture preview budget/root assumptions.","evidence":["crates/polymodel-ldraw-core/src/adapter.rs:18-44"],"recommendation":"Preserve request provenance/context and expose a production byte-request path with explicit root and budget rather than fixture defaults."} +{"schema_version":1,"finding_id":"E-implementation-1-F16","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer C","severity":"medium","status":"open","summary":"Resolver-loaded buffers grow before resource and semantic reservation admission.","evidence":["crates/polymodel-ldraw-core/src/parser.rs:138-183","crates/polymodel-ldraw-core/src/parser.rs:251-255"],"recommendation":"Charge caller-supplied external bytes and per-file storage before allocation and make materialization part of the owned rollback topology."} +{"schema_version":1,"finding_id":"E-implementation-1-F17","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"critical","status":"open","summary":"Native compatibility harness does not compile, so corpus, fuzz, and differential commands cannot run.","evidence":["tools/ldraw-compat-harness/src/main.rs:65,130-132,285,297-300","crates/polymodel-ldraw-testkit/src/schema.rs:108-114"],"recommendation":"Fix harness compilation, validate required authored oracle projections, own the result through projection, and add command-level failure coverage."} +{"schema_version":1,"finding_id":"E-implementation-1-F18","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"high","status":"open","summary":"Core corpus integration test does not compare real parser output to authored expected records or execute gates.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:180-245","crates/polymodel-ldraw-testkit/src/gates.rs:381-397,493-508"],"recommendation":"Parse each fixture through the real adapter, evaluate all applicable gates against authored expected records, and fail on every mismatch."} +{"schema_version":1,"finding_id":"E-implementation-1-F19","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"high","status":"open","summary":"Rejected, limited, and cancelled outcome tests accept permissive or wrong result shapes and do not verify cleanup.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:195-241","crates/polymodel-ldraw-core/src/lib.rs:161-177"],"recommendation":"Table-drive all eleven limits, overflow, cancellation, exact diagnostic tuples, no partial publication, and ledger cleanup after errors and cancellation."} +{"schema_version":1,"finding_id":"E-implementation-1-F20","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"high","status":"open","summary":"real_models_match_oracle_ground_truth uses hardcoded loose minima instead of complete authored oracle projection comparison.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:608-691"],"recommendation":"Compare complete authored oracle projections or exact named gates with strict counts, order, identities, bounds, diagnostics, and transforms."} +{"schema_version":1,"finding_id":"E-implementation-1-F21","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"high","status":"open","summary":"Required BFC, TEXMAP, graph, limits, cancellation, and cleanup matrices are not covered by executable tests.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:101-177,413-473",".current-epic/plan-E.md:90,120,174,224,234"],"recommendation":"Add table-driven direct tests for every normative row and checkpoint, including reservation snapshots while alive and after drop."} +{"schema_version":1,"finding_id":"E-implementation-1-F22","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"high","status":"open","summary":"Fuzz smoke is unrunnable and lacks independent semantic/invariant validation.","evidence":["tools/ldraw-compat-harness/src/main.rs:353-405"],"recommendation":"Separate core mutation smoke from native differential execution, assert deterministic invariants and completion count, and add a command-level test."} +{"schema_version":1,"finding_id":"E-implementation-1-F23","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"medium","status":"open","summary":"Inventory, derivation, and oracle projection validation do not enforce the exact corpus contract.","evidence":["crates/polymodel-ldraw-testkit/src/inventory.rs:390-480","crates/polymodel-ldraw-testkit/src/schema.rs:108-114"],"recommendation":"Assert exact intended inventory cardinality/IDs, require complete derivation groups, and require one complete oracle projection per mandated fixture."} +{"schema_version":1,"finding_id":"E-implementation-1-F24","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer D","severity":"medium","status":"open","summary":"Testkit gate tests are tautological and do not prove real parser output or rejection of wrong fields.","evidence":["crates/polymodel-ldraw-testkit/src/gates.rs:493-508"],"recommendation":"Mutate each gate-owned field independently and add one real adapter-to-gate integration test over the authored corpus."} +{"schema_version":1,"finding_id":"E-implementation-2-F1","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer A","severity":"high","status":"open","summary":"Mandatory corpus/harness boundary does not compile, preventing corpus, fuzz, and differential validation.","evidence":["tools/ldraw-compat-harness/src/main.rs:57-68,130-132,271-300","crates/polymodel-ldraw-testkit/src/schema.rs:106-124"],"recommendation":"Remove or implement stale regenerate dispatch, handle optional oracle projections explicitly, and retain/own the parse result through projection before comparison."} +{"schema_version":1,"finding_id":"E-implementation-2-F2","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer A","severity":"high","status":"open","summary":"Materialization/cache contract exposes full identity but traversal and parser admission collapse descriptors by normalized path.","evidence":["crates/polymodel-ldraw-core/src/types.rs:343-352","crates/polymodel-ldraw-core/src/cache.rs:77-102","crates/polymodel-ldraw-core/src/parser.rs:107-121","crates/polymodel-ldraw-core/src/traversal.rs:46-49,159-164"],"recommendation":"Select deterministic materializations by full path/root/hash identity before traversal, index models by full key, and test same-path different-root/hash order independence."} +{"schema_version":1,"finding_id":"E-implementation-2-F3","scope":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer A","severity":"high","status":"open","summary":"Inventory validator hard-caps membership to a fixed static list, contrary to validating all present fixtures.","evidence":["crates/polymodel-ldraw-testkit/src/inventory.rs:390-435","tools/ldraw-compat-harness/src/main.rs:90-96,114-120"],"recommendation":"Validate all discovered/current inventory rows for uniqueness, provenance, derivations, and applicable gates; protect required baseline IDs as a subset rather than enforcing exact cardinality."} +{"schema_version":1,"finding_id":"E-implementation-2-F4","scope":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"Reservation charging remains coarse and does not precede required semantic growth.","evidence":["crates/polymodel-ldraw-core/src/parser.rs:45-105,227-255","crates/polymodel-ldraw-core/src/geom.rs:329-334","crates/polymodel-ldraw-core/src/model.rs:55-57","crates/polymodel-ldraw-core/src/texmap.rs:132-140,267-305,337-345,378-386","crates/polymodel-ldraw-core/src/traversal.rs:46-56,63-65,100-114,151-185,229-237"],"recommendation":"Reserve exact fixed charges before every required model/instance/cache/colour/TEXMAP growth, retain child handles under the single root, and prove failed admission leaves state unchanged."} +{"schema_version":1,"finding_id":"E-implementation-2-F5","scope":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"critical","status":"open","summary":"DATA state still discards recognized payload content without decoding, retention, diagnostics, or resource/semantic charging.","evidence":["crates/polymodel-ldraw-core/src/model.rs:268","crates/polymodel-ldraw-core/src/parser.rs:342-347,457-460"],"recommendation":"Implement typed DATA continuation decoding and retention with malformed/truncated diagnostics, decoded-byte limits, reservation admission, spans, cancellation, and cleanup tests."} +{"schema_version":1,"finding_id":"E-implementation-2-F6","scope":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"TEXMAP NEXT grammar and geometry association are incomplete; texture-limit insertion is not atomic.","evidence":["crates/polymodel-ldraw-core/src/texmap.rs:58-69,119-141,267-270","crates/polymodel-ldraw-core/src/parser.rs:348-384","crates/polymodel-ldraw-core/src/geom.rs:393-397"],"recommendation":"Enforce complete NEXT grammar and context rules, retain textured/fallback/NEXT geometry association, and reserve/check before texture identity insertion."} +{"schema_version":1,"finding_id":"E-implementation-2-F7","scope":"E","phase":"implementation","generation":"implementor-E2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"C++-compatible colour finish, scoped ColorScope, local-slot allocation, and current/edge/direct resolution remain incomplete.","evidence":["crates/polymodel-ldraw-core/src/model.rs:37-57,144-149","crates/polymodel-ldraw-core/src/parser.rs:481-484","crates/polymodel-ldraw-core/src/geom.rs:201-204,369-397"],"recommendation":"Implement ColorData finishes/materials, layered scope commit/rollback, deterministic 512-slot allocation and exhaustion, and C++ current/edge/direct/complement resolution with tests."} +{"schema_version":1,"finding_id":"E-implementation-2-F8","scope":"E","phase":"implementation","generation":"implementor-E2","status":"open","reviewer":"Reviewer D","severity":"high","summary":"All-fixture core loop is not an adapter-to-authored-corpus gate comparison.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:247-323","crates/polymodel-ldraw-testkit/src/gates.rs:381-397","tools/ldraw-compat-harness/src/main.rs:90-112,271-285"],"recommendation":"Parse each present fixture through InProcessRustAdapter, project with owned lifetime, evaluate all applicable gates against authored records, and fail on every mismatch."} +{"schema_version":1,"finding_id":"E-implementation-2-F9","scope":"E","phase":"implementation","generation":"implementor-E2","status":"open","reviewer":"Reviewer D","severity":"high","summary":"Limit/cancellation tests are metadata-only and do not exercise executable bounded inputs or cleanup.","evidence":["crates/polymodel-ldraw-testkit/corpus/semantic-limits.ldr","crates/polymodel-ldraw-testkit/corpus/adversarial-regressions.ldr","crates/polymodel-ldraw-core/src/lib.rs:229-244","crates/polymodel-ldraw-testkit/src/inventory.rs:138-147"],"recommendation":"Add executable exact/limit+1/overflow/cancellation tests for all eleven limits, assert exact diagnostics and no partial publication/ledger leakage."} +{"schema_version":1,"finding_id":"E-implementation-2-F10","scope":"E","phase":"implementation","generation":"implementor-E2","status":"open","reviewer":"Reviewer D","severity":"high","summary":"Normative BFC/TEXMAP/graph/transform/colour matrices are not executable and real-model oracle checks remain loose.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:168-226,491-579,655-798","crates/polymodel-ldraw-testkit/src/gates.rs:221-322"],"recommendation":"Table-drive every normative transition/error and graph/transform/colour boundary, then compare complete authored oracle projections with strict field/order assertions."} +{"schema_version":1,"finding_id":"E-implementation-2-F11","scope":"E","phase":"implementation","generation":"implementor-E2","status":"open","reviewer":"Reviewer D","severity":"high","summary":"Authored derivation/oracle validation is permissive and gate mutation coverage is tautological.","evidence":["crates/polymodel-ldraw-testkit/src/inventory.rs:461-478","crates/polymodel-ldraw-testkit/src/schema.rs:108-124","crates/polymodel-ldraw-testkit/src/gates.rs:493-509"],"recommendation":"Require non-empty derivations with mandatory field groups, validate oracle projection presence/fields, and add independent wrong-field mutations for each gate."} +{"schema_version":1,"finding_id":"E-implementation-2-F12","scope":"E","phase":"implementation","generation":"implementor-E2","status":"open","reviewer":"Reviewer C","severity":"medium","summary":"Oracle-integrity check is false-red because it matches legitimate serialize_canonical rather than forbidden generator calls.","evidence":["justfile oracle-integrity recipe","crates/polymodel-ldraw-testkit/src/schema.rs:190"],"recommendation":"Use a narrower identifier/token-aware scan for actual forbidden canonical/parse_syntax calls and scan all required expected/gate/harness/fuzz paths."} +{"schema_version":1,"finding_id":"E-implementation-2-F13","scope":"E","phase":"implementation","generation":"implementor-E2","status":"open","reviewer":"Reviewer D","severity":"medium","summary":"Oracle projection comparator sorts/deduplicates colour codes, erasing order, slots, and duplicates.","evidence":["crates/polymodel-ldraw-testkit/src/gates.rs:425-478","crates/polymodel-ldraw-testkit/src/schema.rs:219-224"],"recommendation":"Compare complete ordered colour code/slot data and all authored oracle fields without lossy normalization."} +{"schema_version":1,"finding_id":"E-implementation-2-F14","scope":"E","phase":"implementation","generation":"implementor-E2","status":"open","reviewer":"Reviewer C","severity":"critical","summary":"Renderer worker WASM remains uncompilable in composition checks; baseline attribution is not independently verified.","evidence":["crates/polymodel-renderer-worker/src/worker.rs:142,943,966,1023,1028,1190,1452,1503"],"recommendation":"Prove the failure against the task-base checkout. If task-owned, reconcile worker protocol migration; otherwise record the Epic-level baseline blocker and do not pass E’s parent test gate while it remains unresolved."} +{"schema_version":1,"finding_id":"E-implementation-3-F1","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E3","reviewer":"Reviewer D","severity":"high","status":"fixed","disposition_by":"parent","notes":"Verified in 806d55cb: TEXMAP expected projection, syntax-all-types projection, profile metadata, path identity snapshot, conditional-line projection, and rejected-outcome harness routing were corrected; oracle-integrity passes and inventory validates all 28 fixtures."} +{"schema_version":1,"finding_id":"E-implementation-3-F2","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E3","reviewer":"Reviewer D","severity":"high","status":"open","summary":"Strict OMR graph gate still fails because authored model identity and path-derived occurrence IDs do not match the actual C++-aligned parser projection, including repeated local-index IDs.","evidence":["crates/polymodel-ldraw-testkit/corpus/expected/omr-10030-1.expected.json:23753-26180","crates/polymodel-ldraw-core/src/traversal.rs:184-190","just ldraw-corpus output"],"recommendation":"Reconcile the graph gate and authored OMR projection with the authoritative C++ model/cache/traversal identity semantics; do not impose Rust-only global sequential IDs or weaken uniqueness without explicit C++ evidence."} +{"schema_version":1,"finding_id":"E-implementation-3-F3","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E3","reviewer":"Reviewer D","severity":"high","status":"open","summary":"Deterministic fuzz smoke treats expected INVALID_UTF8 rejection from mutation 0 as a harness failure.","evidence":["tools/ldraw-compat-harness/src/main.rs:353-405","just ldraw-fuzz-smoke output"],"recommendation":"Make fuzz smoke classify bounded parser rejections as valid outcomes while still failing on panic, nondeterminism, leaks, hangs, or unexpected diagnostics; keep differential/oracle checks separate and explicit."} +{"schema_version":1,"finding_id":"E-implementation-3-F4","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E3","severity":"critical","status":"open","reviewer":"Reviewer C","summary":"Parent workspace `just check` remains blocked by renderer-worker compile errors not touched by E3.","evidence":["crates/polymodel-renderer-worker/src/worker.rs:142,943,966,1023,1028,1190,1452,1503","just check output"],"recommendation":"Independently verify baseline attribution against the task-base checkout; if pre-existing, create/route an Epic integration blocker and do not claim the PM-86 task test gate passed."} +{"schema_version":1,"finding_id":"E-implementation-4-F1","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E4","severity":"critical","status":"open","reviewer":"Reviewer B","summary":"DATA/TEXMAP reservation admission occurs after allocation or state mutation and uses the wrong ledger class.","evidence":["crates/polymodel-ldraw-core/src/parser.rs:337-375","crates/polymodel-ldraw-core/src/types.rs:540-595","crates/polymodel-ldraw-core/src/texmap.rs:265-277,370-386","crates/polymodel-renderer-ledger/src/lib.rs:26-30,81-97"],"recommendation":"Admit decoded source/DATA storage before decode using DecodedSourceData, and reserve/check TEXMAP identity/event storage before mutating counters, sets, stacks, descriptors, or events; add failure-injection cleanup tests."} +{"schema_version":1,"finding_id":"E-implementation-4-F2","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer B","summary":"C++ colour finishes, layered scope, local slots, and current/edge/direct/complement resolution remain incomplete.","evidence":["crates/polymodel-ldraw-core/src/model.rs:12-34,104-165","crates/polymodel-ldraw-core/src/parser.rs:94,596-602","../LDParse-reference/include/LDParse/Color.hpp:38-89","../LDParse-reference/LDParse/Color.cpp:44-128"],"recommendation":"Port typed finish/material fields, layered ColorScope commit/rollback, deterministic local slot allocation/exhaustion, and C++ colour resolution with authoritative tests."} +{"schema_version":1,"finding_id":"E-implementation-4-F3","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer B","summary":"Materialization resolution sorts same-path descriptors by enum/hash rather than caller-resolved contextual root policy.","evidence":["crates/polymodel-ldraw-core/src/types.rs:347-356","crates/polymodel-ldraw-core/src/traversal.rs:159-174",".current-epic/plan-E.md:208-214"],"recommendation":"Carry caller-selected full CacheKey or explicit unresolved edge into each include, reject ambiguous tables, and test insertion-order-independent root/hash selection."} +{"schema_version":1,"finding_id":"E-implementation-4-F4","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer B","summary":"Distinct full cache-key models collapse into path-derived public IDs and summaries, losing root/hash provenance.","evidence":["crates/polymodel-ldraw-core/src/traversal.rs:46-52,63-65,92-115,182-195,239-241","crates/polymodel-ldraw-core/src/model.rs:268-275"],"recommendation":"Preserve C++-readable names while exposing full cache identity in summaries/occurrences and deduplicate by CacheKey, without inventing sequential opaque IDs."} +{"schema_version":1,"finding_id":"E-implementation-4-F5","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer B","summary":"Canonical path normalization rewrites forbidden syntax and loses exact spelling instead of rejecting per PM-75.","evidence":["crates/polymodel-ldraw-core/src/cache.rs:10-45",".current-epic/plan-E.md:208-214"],"recommendation":"Validate strict UTF-8/NFC/path syntax without rewriting; keep exact canonical spelling and separate root-specific ASCII-fold lookup/collision handling."} +{"schema_version":1,"finding_id":"E-implementation-4-F6","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer A","summary":"OMR graph gate enforces global uniqueness/legacy sequential compatibility unsupported by C++ child-tuple semantics.","evidence":["../LDParse-reference/include/LDParse/Model.hpp:40-52,68","../LDParse-reference/include/LDParse/ModelBuilder/ModelBuilderImpl.hpp:147-165","crates/polymodel-ldraw-testkit/src/gates.rs:287-314","just ldraw-corpus output"],"recommendation":"Remove the legacy sequential-ID uniqueness branch and compare ordered C++-representable source/target/position/BFC/inversion/transform edge data; re-author OMR accordingly."} +{"schema_version":1,"finding_id":"E-implementation-4-F7","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer C","summary":"Fuzz smoke treats bounded parser rejections as failures and cannot complete its deterministic mutation budget.","evidence":["tools/ldraw-compat-harness/src/main.rs:401-419","just ldraw-fuzz-smoke output"],"recommendation":"Classify typed bounded parse rejections as valid smoke outcomes, retain diagnostics, assert determinism/no panic/leak/hang, and keep oracle comparison explicit for accepted versus rejected cases."} +{"schema_version":1,"finding_id":"E-implementation-4-F8","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"critical","status":"open","reviewer":"Reviewer C","summary":"Renderer worker protocol migration in the PM-78 task range is uncompilable.","evidence":["crates/polymodel-renderer-worker/src/worker.rs:142,943,966,1023,1028,1190,1452,1503","crates/polymodel-renderer-protocol/src/lib.rs","crates/polymodel-renderer-protocol/src/load_state.rs","cargo check -p polymodel-renderer-worker --target wasm32-unknown-unknown"],"recommendation":"Complete load-state migration: maintain WorkerState.load_epoch, pass authoritative epoch through async closures, replace MeshError with current LoadFailed/LoadError events, and rerun worker WASM/split/check/test-all."} +{"schema_version":1,"finding_id":"E-implementation-4-F9","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"critical","status":"open","reviewer":"Reviewer D","summary":"All 28 expected records have empty derivations and no oracle_projection while validation reports pass.","evidence":["crates/polymodel-ldraw-testkit/corpus/expected/*.json","crates/polymodel-ldraw-testkit/src/inventory.rs:461-478","crates/polymodel-ldraw-testkit/src/schema.rs:108-124","tools/ldraw-compat-harness/src/main.rs:90-140"],"recommendation":"Require nonempty derivations with mandatory semantic field groups, require/validate authored oracle projections where applicable, and make validate/differential fail on missing evidence rather than treating Option::None as pass."} +{"schema_version":1,"finding_id":"E-implementation-4-F10","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer D","summary":"Adapter-to-authored corpus gates still do not cover all accepted/preserved/limited/cancelled behaviours or exact cleanup.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:247-323","tools/ldraw-compat-harness/src/main.rs:95-103,271-300","crates/polymodel-ldraw-testkit/src/inventory.rs:138-147"],"recommendation":"Run real adapter projection plus named gates for each publishing fixture and exact outcome/diagnostic/ledger cleanup assertions for rejected, limited, and cancelled fixtures."} +{"schema_version":1,"finding_id":"E-implementation-4-F11","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"high","status":"open","reviewer":"Reviewer D","summary":"Limits/cancellation/cleanup matrix and independent gate mutation coverage remain absent.","evidence":["crates/polymodel-ldraw-core/src/lib.rs:193-244","crates/polymodel-ldraw-testkit/src/gates.rs:221-322,493-509"],"recommendation":"Add executable all-eleven limit exact/limit+1/overflow/cancellation/cleanup tests and independent wrong-field mutations for every gate-owned projection field."} +{"schema_version":1,"finding_id":"E-implementation-4-F12","scope":"E","phase":"implementation","generation":"implementor-E4","severity":"medium","status":"open","reviewer":"Reviewer B","summary":"DATA continuation assembly grows unbounded and loses raw continuation semantics before validation.","evidence":["crates/polymodel-ldraw-core/src/parser.rs:409-421",".current-epic/plan-E.md:156-174"],"recommendation":"Validate raw tails incrementally, reserve checked encoded/decoded capacity before append/decode, checkpoint cancellation per chunk, and retain exact spans/chunk provenance."} +{"schema_version":1,"finding_id":"E-implementation-4-F6","scope":"E","phase":"implementation","generation":"implementor-E4","status":"fixed","disposition_by":"parent","notes":"Verified in graph tranche commit 1d2e22c8: removed synthetic sequential-ID/count/global-uniqueness fallback; graph gate now compares authored ordered source/target/transform/inversion tuples and fails closed with MissingAuthority when absent."} +{"schema_version":1,"finding_id":"E-implementation-4-F13","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E4","severity":"critical","status":"open","summary":"Current authored graph and OMR records lack ordered SceneRecord.instances edge authority required by the strict C++-aligned graph gate.","evidence":["crates/polymodel-ldraw-testkit/corpus/expected/graph-instances.expected.json","crates/polymodel-ldraw-testkit/corpus/expected/omr-10030-1.expected.json","crates/polymodel-ldraw-testkit/src/gates.rs:287-330","just ldraw-corpus output"],"recommendation":"Author ordered source/target/transform/inversion edges from fixture/C++ evidence for every graph fixture, including OMR, with derivation notes; do not fabricate records or restore synthetic-ID bypasses."} +{"schema_version":1,"finding_id":"E-implementation-4-F7","scope":"E","phase":"implementation","generation":"implementor-E4","status":"fixed","disposition_by":"parent","notes":"Verified in fuzz tranche commit 0db94364: bounded parser diagnostics/cancellation are classified as valid deterministic mutation outcomes, accepted parses are serialized twice for determinism, panics are caught, and ledger reservations are checked after drop. Native oracle schema failure remains explicit."} +{"schema_version":1,"finding_id":"E-implementation-4-F8","scope":"E","phase":"implementation","generation":"implementor-E4","status":"fixed","disposition_by":"parent","notes":"Verified in worker tranche commit dd384645 and independently by cargo check -p polymodel-renderer-worker --target wasm32-unknown-unknown plus just check. load_epoch propagation and stale event migration now compile; warnings remain only."} +{"schema_version":1,"finding_id":"E-implementation-1-F1","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"fixed","disposition_by":"parent","notes":"Verified in 5291366e: filesystem-backed LocalResolver and typed-arena were removed from the core; parser now accepts caller-supplied materialization descriptors. Core and testkit WASM checks pass."} +{"schema_version":1,"finding_id":"E-implementation-1-F5","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"fixed","disposition_by":"parent","notes":"Verified in 5291366e and focused regression test: literal STOP is not rewritten to END and does not pop TEXMAP state."} +{"schema_version":1,"finding_id":"E-implementation-1-F6","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"fixed","disposition_by":"parent","notes":"Verified in 5291366e and focused test: line-byte limit is enforced per physical line rather than cumulatively."} +{"schema_version":1,"finding_id":"E-implementation-1-F7","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"fixed","disposition_by":"parent","notes":"Verified in 5291366e and focused test: invalid geometry colour does not mutate geometry publication, counts, or bounds."} +{"schema_version":1,"finding_id":"E-implementation-1-F10","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"fixed","disposition_by":"parent","notes":"Verified in 5291366e and focused test: unterminated quoted tokens produce structured diagnostics."} +{"schema_version":1,"finding_id":"E-implementation-1-F11","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"open","disposition_by":"parent","notes":"Implementor reported the renderer-worker errors as pre-existing and outside the PM-78 touched files. This is not an operator-accepted risk; parent must independently verify baseline attribution and record the integration blocker before any test-gate pass."} +{"schema_version":1,"finding_id":"E-implementation-1-F2","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"rebutted-accepted","disposition_by":"operator","notes":"Authoritative LDParse C++ Model/Cache semantics govern model identity; no sequential opaque IDs are required without C++ evidence."} +{"schema_version":1,"finding_id":"E-implementation-1-F23","scope":"E","task_prefix":"E","phase":"implementation","generation":"implementor-E2","status":"rebutted-accepted","disposition_by":"operator","notes":"More than 24 fixtures is not inherently a test failure; all present fixtures must be validated independently."} +{"schema_version":1,"finding_id":"E-implementation-1-F2","scope":"E","task_prefix":"E","phase":"implementation","generation":"operator-correction-2026-08-03","status":"rebutted-accepted","disposition_by":"operator","notes":"The reviewer inferred sequential opaque model-/instance-ID semantics from the reworked plan. Operator directed that identity semantics must match the authoritative C++ library at ../LDParse-reference; implementor must verify Model/Cache/traversal semantics there and must not impose Rust-only sequential IDs without C++ evidence."} +{"schema_version":1,"finding_id":"E-implementation-1-F23","scope":"E","task_prefix":"E","phase":"implementation","generation":"operator-correction-2026-08-03","status":"rebutted-accepted","disposition_by":"operator","notes":"A fixture inventory larger than 24 is not itself a correctness failure. Operator directed retaining and validating all present fixtures; stale exact-24 assumptions must not delete coverage or turn extra fixtures into a false test failure."} diff --git a/.current-epic/findings-G-impl.jsonl b/.current-epic/findings-G-impl.jsonl new file mode 100644 index 0000000..481705f --- /dev/null +++ b/.current-epic/findings-G-impl.jsonl @@ -0,0 +1,6 @@ +{"schema_version":1,"finding_id":"G-impl-1-F1","scope":"G","task_prefix":"G","phase":"implementation","generation":"implementor-G1/G2","reviewer":"Reviewer A+C","severity":"critical","status":"open","summary":"Handshake state machine is dead code — HandshakeState/negotiate never wired into viewer bridge or worker entry","evidence":["crates/polymodel-renderer-protocol/src/lib.rs:155","src/viewer.rs:1403-1411","crates/polymodel-renderer-worker/src/worker.rs:1912-1915"],"recommendation":"Wire HandshakeState into both entry points, gate command dispatch behind Negotiated(session)"} +{"schema_version":1,"finding_id":"G-impl-1-F2","scope":"G","task_prefix":"G","phase":"implementation","generation":"implementor-G1/G2","reviewer":"Reviewer A","severity":"high","status":"open","summary":"Zero Tier-3 route tests for compound endpoint, fetch replay, wrong-principal, substitution","evidence":["src/appview/tests.rs"],"recommendation":"Add Axum/SQLite tests for owner success, wrong-principal denial, replay denial, substitution rejection"} +{"schema_version":1,"finding_id":"G-impl-1-F3","scope":"G","task_prefix":"G","phase":"implementation","generation":"implementor-G1/G2","reviewer":"Reviewer C","severity":"high","status":"open","summary":"StartCompoundLoad never referenced in viewer — app-side never acquires compound plan","evidence":["src/viewer.rs"],"recommendation":"Wire compound plan acquisition and StartCompoundLoad dispatch in viewer bridge"} +{"schema_version":1,"finding_id":"G-impl-1-F4","scope":"G","task_prefix":"G","phase":"implementation","generation":"implementor-G1/G2","reviewer":"Reviewer C","severity":"critical","status":"open","summary":"No LDraw mesh parser — worker routes .dat bytes to STL parser producing garbage","evidence":["crates/polymodel-renderer-worker/src/worker.rs:1301","crates/polymodel-mesh/src/parser.rs:102"],"recommendation":"Add LDraw MeshFormat variant or gate StartCompoundLoad behind format check"} +{"schema_version":1,"finding_id":"G-impl-1-F5","scope":"G","task_prefix":"G","phase":"implementation","generation":"implementor-G1/G2","reviewer":"Reviewer C","severity":"high","status":"open","summary":"Adding StartCompoundLoad mid-enum changes postcard discriminants for subsequent commands","evidence":["crates/polymodel-renderer-protocol/src/lib.rs:369"],"recommendation":"Append new variants at end or use explicit stable tags"} +{"schema_version":1,"finding_id":"G-impl-1-F6","scope":"G","task_prefix":"G","phase":"implementation","generation":"implementor-G1/G2","reviewer":"Reviewer C","severity":"high","status":"open","summary":"Compound cancellation doesn't abort spawned fetch loop — canceled loads continue consuming routes and reservations","evidence":["crates/polymodel-renderer-worker/src/worker.rs:1203"],"recommendation":"Associate AbortController with each load identity, check between descriptors"} diff --git a/.current-epic/findings-G.jsonl b/.current-epic/findings-G.jsonl new file mode 100644 index 0000000..ad81d75 --- /dev/null +++ b/.current-epic/findings-G.jsonl @@ -0,0 +1,10 @@ +{"schema_version":1,"finding_id":"G-plan-1-F1","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G1","reviewer":"Reviewer C","severity":"critical","status":"open","summary":"G assumes ResolvedResource has opaque fetch descriptor but shipped type has only ByteSource{resource_uri, blob_cids} — no route token/URL","evidence":["src/ldraw/resolve.rs:43-53",".current-epic/plan-G.md:92,94"],"recommendation":"Either reopen C to add descriptor or scope G to synthesize resource_uri→fetch URL explicitly"} +{"schema_version":1,"finding_id":"G-plan-1-F2","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G1","reviewer":"Reviewer C","severity":"critical","status":"open","summary":"C deferral D003 unauthenticated fetch makes G cross-principal rejection claims unachievable","evidence":[".current-epic/deferrals-C.jsonl:2",".current-epic/plan-G.md:94,227"],"recommendation":"Scope cross-principal claims to resolver/mint boundary, or block on D003"} +{"schema_version":1,"finding_id":"G-plan-1-F3","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G1","reviewer":"Reviewer C","severity":"high","status":"open","summary":"Wire format incompatibility — adding LengthUnit::LdrawUnit + version metadata to StartLoad breaks postcard bytes","evidence":["crates/polymodel-renderer-protocol/src/lib.rs:176-183",".current-epic/plan-G.md:75,112"],"recommendation":"State mandatory envelope cutover explicitly; freeze StartLoad shape before H/J"} +{"schema_version":1,"finding_id":"G-plan-1-F4","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G1","reviewer":"Reviewer B","severity":"high","status":"open","summary":"LengthUnit::LdrawUnit has no physical LDU scale/conversion semantics or renderer consumption defined","evidence":[".current-epic/plan-G.md:75,146-148"],"recommendation":"Define LDraw unit convention and conversion boundary with test"} +{"schema_version":1,"finding_id":"G-plan-1-F5","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G1","reviewer":"Reviewer A","severity":"medium","status":"open","summary":"Tier-type duplication — plan creates LdrawRootTier/LdrawRootSource instead of reusing PM-76 RootId","evidence":["src/ldraw/paths.rs:52-130"],"recommendation":"Reuse RootId, drop parallel tier taxonomy"} +{"schema_version":1,"finding_id":"G-plan-1-F6","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G1","reviewer":"Reviewer A","severity":"medium","status":"open","summary":"LengthUnit::LdrawUnit must be appended at END of enum to preserve postcard wire bytes","evidence":["crates/polymodel-renderer-protocol/src/lib.rs:67-77"],"recommendation":"Place variant after Unknown, not mid-enum"} +{"schema_version":1,"finding_id":"G-plan-1-F7","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G1","reviewer":"Reviewer C","severity":"high","status":"open","summary":"Negotiation restart/re-queue sequencing underspecified for mid-session worker restart","evidence":[".current-epic/plan-G.md:115,125"],"recommendation":"Address in-flight pending loads on worker restart and load_epoch interaction with fresh Created"} +{"schema_version":1,"finding_id":"G-plan-2-F1","scope":"G","task_prefix":"G","phase":"plan","generation":"planner-G2","reviewer":"Reviewer B","severity":"high","status":"open","summary":"Compound descriptor acquisition and companion enumeration underspecified — no concrete same-origin request/response boundary, route/API, or companion enumeration algorithm","evidence":[".current-epic/plan-G.md RouteIssuer/CompoundResourcePlan"],"recommendation":"Specify concrete acquisition contract: how MPD/manifest companions are enumerated and pre-resolved, route path, authentication flow, failure/retry behavior"} +{"schema_version":1,"finding_id":"G-plan-skeptic-1","scope":"G","task_prefix":"G","phase":"plan","generation":"skeptic-G3","reviewer":"Plan Deferral Skeptic","severity":"critical","status":"open","summary":"CID verification gap (D002) explicitly allowed to remain — adding multihash decoder is implementation work not impossibility","evidence":[".current-epic/plan-G.md:238,268"],"recommendation":"Either implement per-chunk CID verification (add multihash decoder dependency) or explicitly scope G acceptance to exclude CID claims without papering over"} +{"schema_version":1,"finding_id":"G-plan-skeptic-2","scope":"G","task_prefix":"G","phase":"plan","generation":"skeptic-G3","reviewer":"Plan Deferral Skeptic","severity":"high","status":"open","summary":"Root policy D005 not remediated — plan reuses incomplete C prefix parsing without implementing s/8/48 prefix handling","evidence":[".current-epic/plan-G.md root/path sections"],"recommendation":"Either implement explicit prefix parsing and subroot containment in G or explicitly scope acceptance to exclude root isolation claims"} diff --git a/.current-epic/handoff.md b/.current-epic/handoff.md new file mode 100644 index 0000000..d470234 --- /dev/null +++ b/.current-epic/handoff.md @@ -0,0 +1,67 @@ +# Epic PM-86 Runtime Handoff + +**Date:** 2026-08-01 +**Session:** 07f7s4-trash +**Continuations used:** 22 of 50 + +## Completed Tasks (Merged) + +### A (PM-87) — Viewer load/readiness state machine ✅ +- **Commit:** `mnnrlsvqmrrw` on epic-86 bookmark +- **Tests:** Protocol 21/21, App 197/197, WASM ✅ +- **Skeptic:** Rebutted (accessor methods are forward-looking API) +- **Jira:** Comment added to PM-87 + +### D (PM-77) — LDraw corpus and compatibility harness ✅ +- **Commit:** `951ab847` (merged first, before A) +- **Tests:** Testkit 5/5, differential exit 0, wasm ✅ +- **Skeptic:** PASS (gen-2) +- **Jira:** Comment added to PM-77 +- **Known limitations:** LDParse driver is standalone (not real library); expected records from canonical(); InProcessRustAdapter deferred to E + +## Tasks in Final Stages (Not Yet Merged) + +### B (PM-88) — Reservation ledger and hard budgets +- **Workspace:** ../epic-86-task-B (bookmark: epic-86-task-B) +- **Commits:** `xtmolxqmyzqm` (B3: worker charges) → `vxxxyoqkmxqy` (B2: ledger fixes) → `qzzwzzyuxzpy` (B1: initial ledger) +- **Tests:** Ledger 6/6 ✅, Worker wasm ✅ +- **Merge blocker:** worker.rs has 2 conflict regions from rebase onto A's commit. Both A and B modified worker.rs (different sections). Need manual conflict resolution. +- **Skeptic:** Not yet run (blocked on merge) + +### C (PM-76) — LDraw library projection +- **Workspace:** ../epic-86-task-C (bookmark: epic-86-task-C) +- **Commit:** `zkwprvvmxmlp` (includes E0004 fix, SQLx cache, verification lifecycle, resolver routing) +- **Tests:** Server 203/203 ✅, SQLx prepare ✅, just check ✅, just fix ✅ +- **Merge blocker:** Earlier merge attempt corrupted epic-86 bookmark (set to C's workspace marker). jj graph tangled. Need clean merge approach. +- **Skeptic:** REJECTED — accepted gaps as deferrals (unauthenticated fetch, no CID verification, no official ingestion, root policy incomplete). Deferrals in deferrals-C.jsonl. + +## Tasks Not Started (E-K) + +All have task caches written in `.current-epic/task-{E..K}.md`. + +Dependency status: +- **E** (PM-78): needs B+D → D ✅, B nearly ready +- **F** (PM-79): needs A+B+D+E → A ✅, D ✅, B nearly ready, E not started +- **G** (PM-80): needs A+B+C → A ✅, B nearly ready, C nearly ready +- **H** (PM-81): needs B+E+F+G → deep chain +- **I** (PM-82): needs B+H → very deep +- **J** (PM-83): needs A+C+F+G → needs C +- **K** (PM-84): needs everything → last + +## jj Workspace State Notes + +- **Epic workspace** (`../epic-86`): Has A+D source files. epic-86 bookmark may point to wrong commit (C's marker from failed merge attempt). Needs `jj bookmark set epic-86 -r mnnrlsvqmrrw --allow-backwards` to fix. +- **B workspace**: Clean, compiling code. B3's changes are committed and verified. Conflicts arise when rebasing onto A's commit (worker.rs both modified). +- **C workspace**: Clean, compiling code. C3's changes committed and verified. Merge mechanics are the blocker, not code quality. +- **A workspace**: May have stale working copy. A's changes are already in epic-86. +- **D workspace**: May have stale working copy. D's changes are already in epic-86. +- `.current-task/` added to `.gitignore` to prevent future merge conflicts. + +## Recommended Next Session Actions + +1. Fix epic-86 bookmark: `jj bookmark set epic-86 -r mnnrlsvqmrrw --allow-backwards` +2. Merge B: resolve worker.rs conflicts (take both A's load state machine AND B's reservation admission) +3. Merge C: use clean jj rebase from C's latest commit onto corrected epic-86 +4. Run B and C completeness skeptics +5. Start E and G planning (both become eligible after B+C merge) +6. Continue through dependency chain diff --git a/.current-epic/ldparse-architecture.md b/.current-epic/ldparse-architecture.md new file mode 100644 index 0000000..4d3823a --- /dev/null +++ b/.current-epic/ldparse-architecture.md @@ -0,0 +1,87 @@ +# LDParse C++ Architecture Summary (modernization branch 0fe78dc) + +Local clone: `/home/orual/Projects/LDParse-reference` +Source: elfprince13/LDParse, modernization branch `0fe78dcee22982f26cceced1b483340faa4d04e9` + +## Architecture overview + +``` +bytes → Lexer (line/token scanner) → MPD splitter → Parser (event/callback) → ModelBuilder (arena+cache+graph) +``` + +## Key types and semantics + +### Lexer (Lex.hpp, 482 lines) +- `TokenKind`: Zero..Five for line types 0-5; meta keywords (STEP, FILE, NOFILE, BFC, CERTIFY, etc.) +- `Winding`: bool-backed (CCW=false, CW=true) +- `Token`: kind + value + zero-based line/column span +- Keyword map is EXACT-CASE (no folding); lowercase variants become identifiers/garbage +- CRLF-safe line reading; quoted text becomes identifiers +- `lexModelBoundaries`: MPD pass recognizing `0 FILE ` and `0 NOFILE`, discard mode outside files + +### Model (Model.hpp, 66 lines) +- `Model`: name, source location, ColorTable ref, local colour maps, flattened LDMesh, default colour +- Child instances: tuples of (child model index, BFCStatus, TransMatrix, Model*) +- `mSubModelNames`: MPD name → model index map +- `mSubModels`: name → cached Model +- BFC: `tribool` certification (unknown/certified/uncertified) + `BFCStatus` (Off=-1, Standard=0, Invert=1) + +### ModelBuilder (ModelBuilderImpl.hpp, 266 lines) +- Callback-driven: handleMPDCommand, handleInclude, handleMetaCommand, geometry handlers +- `handleMPDCommand`: FILE creates/inserts submodel; NOFILE retargets to null +- `handleInclude`: resolves name through MPD cache, returns SwitchFile(index) for unresolved +- `handleMetaCommand`: STEP records index count; BFC state machine + - Certification starts unknown; geometry forces it false + - CERTIFY can set true only from unknown/true; enables clipping, defaults CCW + - NOCERTIFY sets false; CLIP/NOCLIP toggle only while certified + - CW/CCW update winding only while certified + - INVERTNEXT records one pending inversion only while certified +- Traversal is ITERATIVE: completed vector + scanStack, no recursion +- **No explicit cycle detection** — Rust port must add visiting/active-path set + +### Cache (Cache.hpp, 190 lines) +- `CacheNode`: compressed radix/prefix tree, case-insensitive (uppercase) comparison +- Caches named model ownership and MPD name→index +- Rust: HashMap is simpler and safer + +### Geometry (Geom.hpp, 97 lines) +- `TransMatrix`: translation (Position) + 9 float matrix coefficients (3x3 linear), NOT 4x4 homogeneous +- `Mesh`: per-attribute vectors + index vector + bfIndices (reverse copies for BFC) +- `mergeMesh`: appends/merges with optional per-attribute transform callbacks +- No explicit determinant/reflection in header — Rust port must compute det(linear) and carry reflection flag + +### Colors (Color.hpp/cpp) +- `ColorTable`: 512 numeric slots, local colour allocation via addLocalColour +- `ColorScope`: layered (committed stack + active map), record→commit lifecycle +- Edge colour: either code-based lookup or inline ColorData +- BFC certification NOT in Color — it's in Model + builder state + +### Parser (Parse.hpp, 314 lines) +- `ActionKind`: SwitchFile, SkipNLines, NoAction, StopParsing +- Callback/event-based: on_include, on_meta, geometry handlers +- `parseModels`: iterative multi-model driver with completed vector + switch/return stack +- Type 1: colour + 12-value transform matrix + filename → include callback +- Types 2-5: validated primitives → callbacks + +## Rust port architecture + +1. **Scanner**: line iterator → `Token { kind, value, span }` +2. **MPD splitter**: `0 FILE`/`0 NOFILE` → named model sources + root identity +3. **Parser**: validates line arity/types, emits typed events/actions +4. **Model arena**: `Vec` by stable `ModelId`, each owns mesh + child instances +5. **Name cache**: `HashMap`, shared per-MPD +6. **Traversal**: explicit stack + `visiting` set for cycles + `completed` for parse-once +7. **BFC/colour contexts**: explicit enums, one-shot `invert_next`, layered colour scope +8. **Geometry**: named affine transforms, determinant/reflection tracking, mesh merges + +## C++ → Rust translation + +| C++ pattern | Rust idiom | +|---|---| +| raw/shared pointer graph | arena + stable ModelId indices | +| boost::tribool | explicit enum (Unknown/Certified/Uncertified) | +| callback/member-pointer templates | traits/closures/event enum | +| tuple-heavy geometry | named structs (Vec3, Affine3, Mesh) | +| goto BFC parsing | state-machine methods | +| mutable iterator jumping | index-based work stack | +| assertions/logging for invariants | Result errors + tests | diff --git a/.current-epic/plan-E.md b/.current-epic/plan-E.md new file mode 100644 index 0000000..d38c042 --- /dev/null +++ b/.current-epic/plan-E.md @@ -0,0 +1,388 @@ +# PM-78 Plan — Thomas-compatible Rust LDraw semantic core (planner-E5 rework) + +> This is a from-scratch re-plan after the fictional/skeleton implementation. The required behavioral reference is the pinned C++ modernization branch at `/home/orual/Projects/LDParse-reference`. + +## Task identity + +- **Epic:** PM-86 — [LDraw/LEGO model support and flexible renderer](https://radiant-industries.atlassian.net/browse/PM-86) +- **Task:** PM-78 — [Thomas-compatible Rust LDraw semantic core](https://radiant-industries.atlassian.net/browse/PM-78) +- **Task prefix:** E +- **Task cache:** `.current-epic/task-E.md` +- **Epic workspace:** `../epic-86` +- **Epic bookmark:** `epic-86` +- **Base bookmark:** `main` +- **Task workspace:** `../epic-86-task-E` +- **Task bookmark:** `epic-86-task-E` +- **Plan output:** `.current-epic/plan-E.md` +- **Dependencies:** B/PM-88 reservation ledger; D/PM-77 corpus and compatibility harness +- **Downstream blockers:** F/PM-79, H/PM-81, K/PM-84 +- **Design source:** [PM-75 design](https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561) + +## Context and intent + +PM-78 creates the reusable pure Rust/WASM LDraw semantic core by porting the actual Thomas Dickerson LDParse architecture, not by extending the prior text heuristic. The required pipeline is line scanner → MPD virtual-file splitter → callback/event parser → model arena/cache builder → iterative graph traversal and canonical projection, with lossless syntax, typed semantics, diagnostics, transforms, BFC, colours, TEXMAP, steps, all hard limits, and reservation-owned lifetime. + +Before drafting this plan, the planner read `.current-epic/ldparse-architecture.md`, `.current-epic/root-cause-ldparse-fiction.md`, and every requested reference header: `Lex.hpp` (exact-case token map, CRLF-safe scanner, MPD boundary/discard state), `Parse.hpp` (typed arity validation, callbacks, `Action`, explicit switch stack), `Model.hpp` (flattened mesh and child-instance state), `ModelBuilderImpl.hpp` (FILE/NOFILE, BFC state machine, STEP, colours, includes), `Cache.hpp` (case-insensitive name cache), `Geom.hpp` (translation + 3×3 matrix and mesh merge), and `Color.hpp` (layered scope and 512-slot/local colour table). The prior failure—no real MPD partitioning, graph traversal, cache, transforms, cycle detection, TEXMAP transitions, reflection, or five limits—is therefore a hard corrective constraint. + +## Safety, workspace, and required skills + +1. In `../epic-86-task-E`, run `jj workspace root`, `jj status`, and `jj bookmark list`. Stop immediately when the workspace is the default workspace or the checked-out bookmark is `main`; the expected bookmark is `epic-86-task-E`. +2. After the parent copies task context, read `.current-task/task.md` and `.current-task/plan.md`; `.current-epic/task-E.md` remains the authoritative contract for this task. +3. Load `polytoken:investigating-a-codebase` before implementation and `reviewing-code-for-shipping-quality` before final review. +4. Re-read the cited C++ headers during implementation before choosing Rust behavior; do not infer semantics from this plan alone. Do not edit `.current-epic/manifest.txt`, `.current-epic/dependencies.dot`, `.current-epic/task-state.json`, `.current-epic/merge-gate.json`, generated `crates/polymodel-api`, or unrelated app/UI/renderer behavior. +5. Keep the core `#![forbid(unsafe_code)]`, target-safe, synchronous, deterministic, and free of filesystem, network, archive, process, Rayon, random/time/pointer identity, GPU, `three-d`, `three-d-asset`, `stl_io`, and `polymodel-mesh` dependencies. The core must not depend on the native LDParse harness. + +## Replace-vs-edit assessment + +| Area | Decision | Boundary and rationale | +|---|---|---| +| `crates/polymodel-ldraw-core/` | **Create** | No existing reusable owner covers the typed scanner, MPD table, graph/cache identity, BFC, colours, TEXMAP, diagnostics, profiles, canonical projection, or reservation-owned arenas. Create cohesive modules behind one small public API. | +| `crates/polymodel-ldraw-testkit/src/lib.rs` oracle and expected loader | **Replace the oracle path; edit the gate/schema boundary** | Delete the heuristic Rust `canonical()`/`parse_syntax()` authority. Load deterministic records generated exclusively by the pinned out-of-process LDParse oracle plus versioned mechanical projection rules; keep the Rust adapter solely as the candidate under test. | +| `crates/polymodel-ldraw-testkit/corpus/expected/*.expected.json` | **Regenerate all authoritative records from the pinned oracle** | Oracle-representable semantic payload is generated-only and byte-compared in CI. Humans author provenance and versioned deterministic rules only for demonstrably non-oracle fields; handwritten oracle-representable values and Rust-derived values are both critical defects. | +| Existing 28 fixture bytes/inventory | **Edit deliberately, preserving the authoritative set** | Keep exactly the current authoritative 28 rows and enforce exact inventory↔fixture↔expected equality. The named external row is `omr-10030-1`, sourced from the OMR catalogue entry `https://library.ldraw.org/omr/sets/308` and MPD `https://library.ldraw.org/library/omr/10030-1.mpd` (10030-1 Imperial Star Destroyer, Roland Dahl, CCAL 2.0 notice; retrieved SHA-256 recorded in provenance). Extend `steps-transforms.ldr` with complete `!TEXMAP START`, spec-shaped `NEXT`, fallback geometry, and `END` coverage. Use `END` in every valid fixture; retain literal `STOP` only in a lossless-profile unknown-META negative fixture, with no pre-parse vocabulary rewriting. Include the explicit malformed/unclosed profile rows below and update the matrix, tags, provenance, and expected record atomically. | +| `tools/ldraw-compat-harness/src/main.rs` | **Edit minimally** | Replace every in-process fuzz/parser call to `canonical()` with the real core `InProcessRustAdapter`/byte-request entry point. Keep LDParse differential execution native and out-of-process. | +| `crates/polymodel-renderer-ledger` | **Do not edit** | Consume B’s `ReservationLedger::reserve`, `reserve_child`, `Reservation`, and `ResourceClass::SemanticArenas`; do not duplicate arithmetic or change ledger ownership semantics. | +| `justfile` and repository checks | **Edit minimally** | Add focused core/corpus/WASM/oracle-integrity checks while preserving existing `ldraw-corpus`, `ldraw-wasm-check`, renderer split, and `test-all` gates. | +| `src/ldraw/*`, app, generated API, `polymodel-mesh`, renderer worker | **Do not edit** | Resolver lifecycle, scene IR, and viewer integration remain downstream tasks. | + +## Public boundary and fixed semantic contracts + +Create `polymodel-ldraw-core` with these public concepts: + +- `ParserProfile` and `ParseOptions` containing strict/compatibility/lossless policy, typed `LdrawLimits`, cancellation/checkpoint policy, root/source context, and an explicit caller-supplied `ReservationOwner` plus `ReservationLedger`. +- `LdrawParser::parse_bytes` returning `OwnedParseResult` or a typed parse error. The owned result retains semantic arenas and can be borrowed for projection without releasing retained-memory admission. +- A target-safe `InProcessRustAdapter` contract shared by core and testkit. It must have a fixture convenience method and a byte-request method carrying fixture identity, bytes, declared profile, authority/provenance metadata, and deterministic test owner; mutated harness bytes must be parsed by the real core, never by expected-record helpers. +- Internal typed structures for raw line spans/tails/line endings, commands/primitives, virtual files, paths, transforms, BFC, colours, TEXMAP events/state, graph/cache/instances, diagnostics, ordered steps, and arena ownership. Projection to PM-77 `CanonicalRecord` occurs at one boundary. + +### Reservation lifetime and topology (normative) + +The parser performs one root reservation for `ResourceClass::SemanticArenas` before creating any semantic arena, intern table, graph/cache table, decoded DATA/TEXMAP payload, or instance storage. The root admission is a **caller-configured memory budget** for semantic arena overhead. The default estimate is `bytes.len().checked_mul(8).and_then(|n| n.checked_add(8192)).ok_or(Overflow)` (8× input expansion plus an 8 KiB floor), which is sufficient for typical inputs. The caller may supply a larger budget for unusually dense valid inputs (e.g., many short FILE boundaries or minimal type-1 lines). The `SEMANTIC_ARENA_RESERVATION_LIMIT` diagnostic means the caller's configured budget was exceeded, not that the input is syntactically invalid — this is a resource limit enforced by B's ledger, analogous to WebGL or WASM memory limits. The corpus adapter uses a generous fixed budget (exactly 16 MiB) to avoid false rejections during testing. The returned `Reservation` is stored in the `OwnedParseResult` (or in a result-owned `SemanticArenas` owner) and is not released at successful parse return. Per-file and per-arena growth uses `ledger.reserve_child(&root_reservation, owner, ResourceClass::SemanticArenas, bytes)`, and the aggregate child amount must never exceed the root’s admitted envelope. If a child request would exceed that admitted amount, reconcile by rejecting the parse with the stable `SEMANTIC_ARENA_RESERVATION_LIMIT` limit diagnostic; do not resize, silently over-admit, or publish partial state. The result’s retained semantic data and the root charge therefore have identical ownership lifetime. + +Every bounded per-file, payload, or child allocation uses `ledger.reserve_child(&root_reservation, owner, ResourceClass::SemanticArenas, bytes)`. The tree is exactly one root `SemanticArenas` reservation with child reservations attached directly to that root; there are no sibling roots and no child-of-child reservations. `SemanticArenas` owns the child handles and drops them before the result drops the root handle. Borrowed projection does not release either. An explicit consuming transfer may move the owner/result to a downstream owner; otherwise dropping the owned result/arena releases children first and root last. Tests must inspect ledger snapshots after successful parse while the result is alive and after drop, proving the charge remains until drop and then returns to zero. + +**Per-allocation charging model:** The `bytes` argument to each `reserve_child` call is determined as follows: (1) raw source line text, token spans, and file partition boundaries are borrowed zero-copy from the input slice and are NOT charged; (2) per model arena entry: 256 bytes (name, BFC state, default colour, step-boundary vector header); (3) per type-1 instance: 128 bytes (12-float transform, BFC status, child ModelId, colour code); (4) per graph/cache table entry: 64 bytes (key fields + ModelId + visit state); (5) per local colour allocation: 32 bytes (code, slot, scope metadata); (6) per TEXMAP event record: 48 bytes. These fixed estimates are deliberately generous relative to the actual Rust struct sizes. The default budget (`bytes.len() * 8 + 8192`) is sufficient for typical inputs, but for unusually dense valid inputs (e.g., many short FILE boundaries at 9 bytes each charging 256 bytes per model), the caller may supply a larger budget. The corpus adapter uses a exactly 16 MiB fixed budget to avoid false rejections. + +The parser receives the exact caller owner and never derives one from bytes, paths, clocks, random values, pointer addresses, or a global singleton. Production callers construct `ReservationOwner::interactive(worker_id, load_epoch, scene_generation)`; the isolated corpus adapter uses the fixed `ReservationOwner::preview(1, 1, 1)` per isolated parse. Retired owners are rejected. All failure, limit, cancellation, and ownership-transfer paths have explicit drop/transfer tests. + +### BFC transition contract (normative) + +The implementation and expected records must distinguish **observed LDParse behavior** from **PM-75 corrections**. The observed column below is grounded in `/home/orual/Projects/LDParse-reference/include/LDParse/ModelBuilder/ModelBuilderImpl.hpp` (especially `handleMetaCommand` lines 89–143, `handleInclude` lines 147–165, geometry handlers lines 168–193, and `handleMPDCommand`/`handleEOF` lines 16–33 and 179–182). The corrected column is the PM-78 contract and is what the Rust core exposes; compatibility fixtures may record the observation but may not silently turn it into the normative rule. + +BFC certification is file-local and starts `Unknown` (the Rust equivalent of C++ indeterminate). Geometry and include events force `Unknown → Uncertified` in the observed builder. PM-75 §3.4 additionally makes the one-shot and file-boundary behavior explicit and corrects the unsafe leakage described below. `Certified` carries clipping and winding; `Uncertified` and `Unknown` do not. + +| Current state / input | Observed C++ LDParse | PM-78 normative correction / output | +|---|---|---| +| `Unknown` + `CERTIFY` | Accepts certification (`indeterminate(...) || certified`), enables clipping, defaults winding CCW; optional `CW`/`CCW` is parsed. | `Unknown → Certified`; enable clipping and default CCW, then apply one optional orientation. This is the only certification entry from `Unknown`. | +| `Certified` + `CERTIFY` | Accepts and reinitializes clipping/winding. | Remains `Certified`; re-enable clipping and apply the directive’s optional orientation. | +| `Uncertified` + `CERTIFY` | Rejects/stops parsing because the C++ condition disallows recertification. | Remains `Uncertified`; emit the PM-75/PM-78 diagnostic and do not enable clipping. | +| Any state + `NOCERTIFY` | Sets `mCertify = false`. | `Unknown` or `Certified → Uncertified`; `Uncertified` remains `Uncertified`; clear clipping, winding override, and pending inversion. | +| `Certified` + `CLIP` / `NOCLIP` | Toggles clipping only while certified. | Remains `Certified`; toggle clipping. `Unknown`/`Uncertified` ignore the toggle and emit a gated invalid-context diagnostic (no culling change). | +| `Certified` + `CW` / `CCW` | Changes winding only while certified. | Remains `Certified`; change winding. `Unknown`/`Uncertified` ignore it and emit the same invalid-context diagnostic. | +| `Unknown`/`Uncertified` + geometry (types 2–5) or include (type 1) | Geometry handlers and `handleInclude` force certification false. | `Unknown → Uncertified`; `Uncertified` remains so. A type-1 still creates/records its instance even if unresolved. | +| `Certified` + geometry/include | Does not demote certification in the reference. | Remains `Certified`; evaluate the primitive/include under current clip/winding state. | +| `Certified` + `INVERTNEXT` | Adds a per-model pending marker only when certified. | Set one pending bit only while `Certified`; a second `INVERTNEXT` re-arms the same bit, never a stack. | +| `Unknown`/`Uncertified` + `INVERTNEXT` | Silently ignores the directive because `target.mCertify` is false/indeterminate; no pending marker or error is produced. | State unchanged, no pending bit, and no diagnostic in the strict C++-compatible behavior; compatibility/lossless retain the raw command and may record only the frozen low-severity informational tuple below, never a state transition. | + +`INVERTNEXT` is consumed by the **next valid type-1 include** and applies inversion to that instance (including a cache hit); it is **not** consumed by a geometry primitive. A geometry primitive (type 2–5), any other non-BFC command, a `STEP`, a colour/meta command, malformed type-1, or a BFC command other than `INVERTNEXT` clears the pending bit before normal handling. Blank lines and BFC comments that do not constitute a command do not clear it. This makes the PM-75 §3.4 rule “only the immediately following valid type-1 subfile line” executable and explicitly answers the primitive case. + +The pending bit is part of the source-file BFC frame. In the observed C++ implementation, `handleInclude` erases `mInvertNext` only when no `SwitchFile` action is returned (`ModelBuilderImpl.hpp` lines 147–165), so a file switch can leave the source model’s marker live; MPD `FILE`/`NOFILE` creates/retargets models without an explicit marker reset. PM-75 §3.4 corrects that leakage: a file switch/include descent does not transfer pending state to the child frame and clears it at the switch boundary; a return cannot resurrect it. An MPD `FILE`/`NOFILE` transition creates/ends a file-local frame and clears pending state and file-local certification bookkeeping. A completed cache hit still consumes/applies the pending bit and receives a fresh occurrence/instance ID. EOF has no consumer: discard the pending bit while closing the frame, with no synthetic inversion and no additional diagnostic. Reflection is composed separately by XOR with the source-instance determinant, as required by PM-75 §3.4. + +Direct Tier 1 transition-table tests must cover every row, including `Unknown`/`Uncertified + INVERTNEXT` asserting unchanged state, no pending bit, and no strict diagnostic; compatibility/lossless info tuples must be asserted where selected. Cover both directive orders, geometry/include/blank/intervening-command clearing, file and MPD switches, cache hit, malformed include, EOF, and reflection XOR. Expected records must label C++ observations and PM-75 corrections separately. + +### Explicit malformed-directive diagnostic contract (normative) + +The following tuples are frozen as `(profile, code, severity, result)`; there is no caller-selected variation. `reject` means no `OwnedParseResult` is published; `preserve-and-continue` means the raw command/span remains in lossless syntax, semantic state is unchanged, and parsing continues; `preserve-without-transition` has the same retention rule and explicitly forbids a BFC/TEXMAP state change. `none`, `info`, `warning`, and `error` are stored diagnostic severities, not prose labels; `none` means no diagnostic record is emitted. + +| Malformed BFC case | Strict tuple | Compatibility tuple | Lossless tuple | +|---|---|---|---| +| BFC directive has unknown keyword, missing keyword, wrong arity, or malformed token | `(strict, BFC_INVALID_DIRECTIVE, error, reject)` | `(compatibility, BFC_INVALID_DIRECTIVE, warning, preserve-and-continue)` | `(lossless, BFC_INVALID_DIRECTIVE, warning, preserve-and-continue)` | +| `CERTIFY` has an invalid orientation/extra token | `(strict, BFC_INVALID_CERTIFY_ARGUMENT, error, reject)` | `(compatibility, BFC_INVALID_CERTIFY_ARGUMENT, warning, preserve-without-transition)` | `(lossless, BFC_INVALID_CERTIFY_ARGUMENT, warning, preserve-without-transition)` | +| `CLIP`, `NOCLIP`, `CW`, or `CCW` occurs while state is `Unknown`/`Uncertified` | `(strict, BFC_INVALID_CONTEXT, error, reject)` | `(compatibility, BFC_INVALID_CONTEXT, warning, preserve-without-transition)` | `(lossless, BFC_INVALID_CONTEXT, warning, preserve-without-transition)` | +| `CERTIFY` occurs while state is `Uncertified` | `(strict, BFC_RECERTIFY_UNCERTIFIED, error, reject)` | `(compatibility, BFC_RECERTIFY_UNCERTIFIED, warning, preserve-without-transition)` | `(lossless, BFC_RECERTIFY_UNCERTIFIED, warning, preserve-without-transition)` | +| BFC numeric/enum field is non-finite or otherwise not representable | `(strict, BFC_INVALID_VALUE, error, reject)` | `(compatibility, BFC_INVALID_VALUE, warning, preserve-without-transition)` | `(lossless, BFC_INVALID_VALUE, warning, preserve-without-transition)` | +| `INVERTNEXT` while BFC state is `Unknown` or `Uncertified` | `(strict, BFC_INVERTNEXT_OUTSIDE_CERTIFIED, none, preserve-without-transition)` — no diagnostic is emitted, matching C++ | `(compatibility, BFC_INVERTNEXT_OUTSIDE_CERTIFIED, info, preserve-without-transition)` | `(lossless, BFC_INVERTNEXT_OUTSIDE_CERTIFIED, info, preserve-without-transition)` | + +| Malformed TEXMAP case | Strict tuple | Compatibility tuple | Lossless tuple | +|---|---|---|---| +| START/NEXT is missing method, required parameters, or pngfile, has extra tokens, or has invalid GLOSSMAP shape | `(strict, TEXMAP_INVALID_ARITY, error, reject)` | `(compatibility, TEXMAP_INVALID_ARITY, warning, preserve-without-transition)` | `(lossless, TEXMAP_INVALID_ARITY, warning, preserve-without-transition)` | +| START/NEXT names a method outside PLANAR/CYLINDRICAL/SPHERICAL | `(strict, TEXMAP_INVALID_METHOD, error, reject)` | `(compatibility, TEXMAP_INVALID_METHOD, warning, preserve-without-transition)` | `(lossless, TEXMAP_INVALID_METHOD, warning, preserve-without-transition)` | +| START/NEXT has missing, non-finite, or invalid numeric parameters | `(strict, TEXMAP_INVALID_VALUE, error, reject)` | `(compatibility, TEXMAP_INVALID_VALUE, warning, preserve-without-transition)` | `(lossless, TEXMAP_INVALID_VALUE, warning, preserve-without-transition)` | +| NEXT’s first nonblank line is type 0 | `(strict, TEXMAP_NEXT_TYPE0, error, reject)` | `(compatibility, TEXMAP_NEXT_TYPE0, warning, preserve-without-transition)` | `(lossless, TEXMAP_NEXT_TYPE0, warning, preserve-without-transition)` | +| NEXT occurs while START/FALLBACK is active | `(strict, TEXMAP_NEXT_NESTED, error, reject)` | `(compatibility, TEXMAP_NEXT_NESTED, warning, preserve-without-transition)` | `(lossless, TEXMAP_NEXT_NESTED, warning, preserve-without-transition)` | +| `0 !:` is nested inside another `0 !:` geometry command | `(strict, TEXMAP_NESTED_BANG_COLON, error, reject)` | `(compatibility, TEXMAP_NESTED_BANG_COLON, warning, preserve-without-transition)` | `(lossless, TEXMAP_NESTED_BANG_COLON, warning, preserve-without-transition)` | +| FALLBACK occurs without an active same-file START | `(strict, TEXMAP_FALLBACK_OUT_OF_SCOPE, error, reject)` | `(compatibility, TEXMAP_FALLBACK_OUT_OF_SCOPE, warning, preserve-without-transition)` | `(lossless, TEXMAP_FALLBACK_OUT_OF_SCOPE, warning, preserve-without-transition)` | +| FALLBACK occurs a second time for the same START | `(strict, TEXMAP_DUPLICATE_FALLBACK, error, reject)` | `(compatibility, TEXMAP_DUPLICATE_FALLBACK, warning, preserve-without-transition)` | `(lossless, TEXMAP_DUPLICATE_FALLBACK, warning, preserve-without-transition)` | +| END has extra/malformed tokens | `(strict, TEXMAP_INVALID_END, error, reject)` | `(compatibility, TEXMAP_INVALID_END, warning, preserve-without-transition)` | `(lossless, TEXMAP_INVALID_END, warning, preserve-without-transition)` | +| END occurs in a different file from its START | `(strict, TEXMAP_CROSS_FILE_END, error, reject)` | `(compatibility, TEXMAP_CROSS_FILE_END, warning, preserve-without-transition)` | `(lossless, TEXMAP_CROSS_FILE_END, warning, preserve-without-transition)` | +| `0 !:` has malformed or missing type 1–5 geometry | `(strict, TEXMAP_INVALID_BANG_COLON_GEOMETRY, error, reject)` | `(compatibility, TEXMAP_INVALID_BANG_COLON_GEOMETRY, warning, preserve-without-transition)` | `(lossless, TEXMAP_INVALID_BANG_COLON_GEOMETRY, warning, preserve-without-transition)` | +| Literal STOP is encountered as unknown META (never rewritten to END) | `(strict, TEXMAP_UNKNOWN_META_STOP, error, reject)` | `(compatibility, TEXMAP_UNKNOWN_META_STOP, warning, preserve-and-continue)` | `(lossless, TEXMAP_UNKNOWN_META_STOP, warning, preserve-and-continue)` | + +The `TEXMAP_NEXT_TYPE0` diagnostic span targets the `NEXT` command line, not the type-0 line that cancelled it. Missing END at EOF/file end is **not** a malformed case and therefore has no diagnostic tuple: it closes the active effect, retains ordered events, and emits no synthetic END. The table is covered by Tier 1 tests asserting exact profile, code, severity, result, span, and diagnostic order for every row. + +### TEXMAP state machine matrix (normative) + +TEXMAP is **not present in the cited C++ reference**; it is LDraw-spec/PM-75 behavior only. The core recognizes the official grammar `0 !TEXMAP (START | NEXT) [GLOSSMAP pngfile]`, plus `FALLBACK` and `END`. `NEXT` has no standalone count token: it applies to the immediately following nonblank valid type 1–5 geometry line, and a first nonblank type-0 line invalidates/cancels it. Valid fixtures use `END`; literal `STOP` is not grammar and is retained only as lossless unknown-META negative coverage. The parser never rewrites `STOP` to `END` before parsing. + +The four states are `Inactive`, `AwaitingNext`, `Active`, and `Fallback`. A frame also stores the owning file, previous texture for nested START/END, mapping mode, finite parameters, and the fallback association. The transition matrix is: + +| State | Input | Next state | Required behavior and PM-75/spec anchor | +|---|---|---|---| +| `Inactive` | valid `START` | `Active` | Push no prior texture, install the new texture immediately, and associate subsequent pre-FALLBACK geometry. PM-75 §3.5 and official START semantics. | +| `Inactive` | valid `NEXT` with spec-shaped method/parameters/pngfile | `AwaitingNext` | Store the texture descriptor; do not activate it until the immediately following nonblank line is inspected. No standalone count token is accepted. PM-75 §3.5 and official NEXT grammar. | +| `Inactive` | `FALLBACK` | `Inactive` | Diagnose with `TEXMAP_FALLBACK_OUT_OF_SCOPE` per the frozen tuple above (strict rejects; compatibility/lossless warn and preserve without transition). State remains Inactive. | +| `Inactive` | `END` | `Inactive` | Silent no-op: no diagnostic and no state change because there is no active same-file START. Official END semantics. | +| `AwaitingNext` | blank | `AwaitingNext` | Blank lines are skipped while looking for the immediately following nonblank line. Official NEXT semantics. | +| `AwaitingNext` | first nonblank type 0, including META/STEP/END | `Inactive` | Cancel NEXT, retain the raw command, and emit `TEXMAP_NEXT_TYPE0` whose span targets the prior `NEXT` command line; NEXT cannot seek past a type-0 line. Official NEXT semantics. | +| `AwaitingNext` | first nonblank valid type 1–5 geometry | `Active` for that geometry, then `Inactive` | Apply the stored texture to that immediately following valid geometry line only; no FALLBACK is legal for NEXT, and the temporary association ends after that geometry. Official NEXT semantics. | +| `AwaitingNext` | first nonblank malformed type 1–5 line | `AwaitingNext` | Keep NEXT pending and do not consume/cancel it; emit the geometry line’s own parse diagnostic, then continue waiting for the next valid geometry. The malformed line is not treated as the first valid geometry or as type 0. PM-75 recovery rule: the official spec does not define malformed-geometry handling during NEXT; PM-75 §3.5 prescribes this recovery behaviour. | + +| `AwaitingNext` | EOF/file end | `Inactive` | Cancel without synthetic geometry or END; no missing-END diagnostic is emitted. PM-75 §3.5 file-end rule. | +| `Active` | `0 !:` geometry | `Active` | Associate `0 !:` geometry with the current texture until FALLBACK/END/STEP/file end; it is texture geometry, not fallback. Official FALLBACK semantics. | +| `Active` | standard type 1–5 geometry | `Active` | Associate standard type 1–5 geometry with the current texture until FALLBACK/END/STEP/file end. Official FALLBACK semantics. | +| `Active` | valid `FALLBACK` | `Fallback` | Freeze geometry already seen as textured; subsequent fallback geometry is associated with this START and ignored by TEXMAP-aware projection. Official FALLBACK semantics. | +| `Active` or `Fallback` | valid `START` | `Active` | Legal nested START; push current texture and install the nested one. Nested texture frames are permitted. Official START nesting semantics. | +| `Active` or `Fallback` | valid `NEXT` | unchanged | NEXT is not nested into an existing START/FALLBACK frame: reject/diagnose it, preserve the current texture and fallback association, and do not create a temporary scope. PM-75 §3.5 NEXT. | +| `Fallback` | geometry | `Fallback` | Associate with the current START’s fallback section; do not emit it as textured geometry in the TEXMAP-aware view. Official FALLBACK semantics. | +| `Active` or `Fallback` | same-file `END` | previous texture’s `Active`/`Fallback`, or `Inactive` | Pop the matching START frame and restore the previous texture. A START after END is legal and starts a fresh frame. Official END semantics. | +| any state | cross-file `END` | unchanged | END cannot close a START from another file; apply the explicit cross-file diagnostic row below and preserve the current frame. Official END semantics. | +| any active state | `STEP` or owning-file end | prior texture frame or `Inactive` | Terminate the frame at STEP/file end; do not carry it into a new MPD file. PM-75 §3.5 lifetime rule. | +| any state | START/NEXT with missing, non-finite, or method-invalid parameters | unchanged | Retain raw syntax, never create a texture association from invalid values, and apply the explicit diagnostic row below. Official method grammar plus PM-75 §2 safety. | +| `Fallback` | second `FALLBACK` | `Fallback` | Retain the first fallback association and apply the explicit duplicate-marker diagnostic row below. Official FALLBACK section model. | + +Missing END at EOF/file end closes the effect as specified, preserves ordered START/fallback events, and emits no synthetic END. A malformed/nested `!:` command is retained losslessly and diagnosed; it must not silently become fallback geometry. Resolver availability never changes this state machine: the adapter supplies texture descriptors/payloads (or an unavailable marker), and the event remains in the projection either way. + +`SemanticRecord` carries additive `texmap_events: Vec`. Each event contains kind (`start`, `next`, `fallback`, `end`—not `stop`), source span, owning-file identity, mapping mode, finite canonical parameters, and associated textured/fallback geometry or texture reference where applicable. Non-TEXMAP records carry an empty list. The `texmap_projection_v1` gate compares ordered events, spans, transitions, parameters, file ownership, fallback association, and unavailable-payload retention. Direct Tier 1 tests cover every matrix cell, nested START/END, START after END, literal STOP, invalid numerics, EOF/missing END, cross-file END, STEP, and resolver-present/absent invariance. + +### PM-77 limits accounting (normative) + +The following are the eleven PM-77 logical limits (values from PM-75 §5.6 and `.current-epic/plan-D.md` §3.9). `LdrawLimits` exposes all eleven as typed `u64` ceilings. Every increment is a checked `current.checked_add(delta)` performed before mutating the counter or allocating. A checked overflow is the same fail-closed `LimitExceeded` outcome as crossing the configured ceiling; parse-owned graph/cache/arena state is unpublished and dropped, and cancellation uses the same cleanup path. Fetches and textures are resolver-independent: the core receives caller-supplied descriptors/bytes and a caller-supplied fetch count at the adapter boundary; it never performs fetches or discovers textures itself. + +| Limit (default) | Exact unit | Increment point | Overflow / rejection behavior | Resolver independence | +|---|---|---|---|---| +| Resource bytes (100 MiB) | input byte count (`u64` bytes) | Once per `parse_bytes` request, before scanning; DATA decoded bytes are charged at decode output as resource bytes, not twice as source text | Checked total and ceiling admission before retaining bytes; reject request and publish nothing | Caller supplies the byte request; no resolver | +| Line bytes (1 MiB) | one raw physical line’s bytes, excluding the line terminator but including raw fields/tail | At each line boundary after locating the terminator and before retaining line data | Checked line-length arithmetic; reject that line/request with stable diagnostic; no truncation | Caller-supplied bytes only | +| Files (16,384) | count of virtual MPD files, including root | When a validated `FILE` creates a new virtual-file entry; root is counted once | Checked count before insertion; reject MPD and discard partial table | File partition is input-only; no resolver | +| Include depth (256) | active type-1 descent depth, not total references | Before pushing a child frame for a type-1 include; root depth is 0 | Checked `depth + 1` and ceiling check before push; reject/suppress descent, preserve reference and diagnostic | Target resolution is supplied by caller/cache; no fetch | +| Commands (5,000,000) | count of parsed semantic command events (type 0–5 plus recognized META/DATA/TEXMAP controls) | Once per accepted line event, before event storage/dispatch; blank physical lines do not count | Checked increment before event mutation; reject/cancel parse on overflow/ceiling | No resolver | +| Instances (5,000,000) | count of emitted type-1 occurrence/instance IDs, including unresolved and cycle-cut occurrences | Before assigning each occurrence ID, including cache hits and cycle edges | Checked increment before ID publication; reject/cancel if exhausted | Caller supplies target identity/cache result | +| Triangles (20,000,000) | emitted type-3 triangles plus type-4 quads after deterministic triangulation (two triangles per quad) | Before appending each resulting triangle to semantic geometry | Checked count and capacity estimate before append/reservation; reject/cancel with no partial publication | Geometry is caller-supplied bytes | +| Lines (20,000,000) | emitted type-2 lines plus type-5 conditional-line segments | Before appending each line/conditional primitive to semantic geometry | Checked count/capacity before append; reject/cancel without silently dropping the primitive | Geometry is caller-supplied bytes | +| Textures (1,024) | distinct texture descriptors/texture identities retained in the parse result, including unresolved descriptors; repeated references reuse one identity | At first insertion into the per-result texture table, before descriptor/event retention | Checked count before insertion; reject the new association/parse with diagnostic and no partial publication | Descriptor and optional payload are caller-supplied; lookup/fetch is never performed by core | +| Fetches (8) | count of resolver/fetch attempts requested by the adapter for external targets or texture payloads | At the adapter boundary immediately before each caller-issued fetch; never incremented by core cache hits | Checked increment and ceiling check; caller cancels/rejects the fetch and reports the bounded diagnostic; core remains deterministic | Entirely caller-supplied; core has no network/filesystem resolver | +| Diagnostics (10,000) | count of stored diagnostic records, including limit/cancellation/invalid-input diagnostics | Immediately before each diagnostic append; suppressed duplicate diagnostics do not bypass the counter | Checked increment before append; when exhausted, emit/return the single bounded `DiagnosticLimitExceeded` terminal error without unbounded additional diagnostics | No resolver | + +Tier 1 tests must instantiate each limit at a tiny value, prove exact success and limit+1 rejection, exercise `u64` arithmetic overflow at each increment/estimate, and verify no counter or reservation mutation after rejection. Separate tests prove resolver-present/absent equal-byte requests have equal syntax, identities, state transitions, limits, and diagnostics; only caller-provided fetch/texture payload metadata may differ. + +### Expected-record authority and independence (normative) + +The pinned external Thomas Dickerson LDParse implementation is the semantic authority for this compatibility contract. Every expected semantic value that LDParse emits, or that can be mechanically projected from its emitted model/mesh/child/tokenized/resolved data, is generated deterministically from the pinned out-of-process oracle. Such values MUST NOT be handwritten and MUST NOT be sourced from `polymodel-ldraw-core`, `InProcessRustAdapter`, runtime actual records, snapshots of Rust output, or existing expected payloads. + +Human-authored material is limited to: (1) provenance identifying fixture bytes, oracle revision, wrapper/schema revision, and source clauses; and (2) explicit versioned deterministic projection rules for fields that LDParse demonstrably cannot emit. Each canonical field is classified in a checked-in field-authority table as `oracle_direct`, `oracle_mechanical`, `input_mechanical`, or `polymodel_policy`. There is no `human_authored_semantic` class. Before classifying a field as unavailable, inspect the LDParse API and preserve all values exposed by model mesh, children/submodels, winding, default/local colours, model boundaries, tokenized geometry, and resolved geometry. + +The oracle-only generator runs the pinned binary out of process, writes to a temporary directory by default, fails on unknown/missing oracle fields, runs twice byte-identically, and byte-compares generated records against committed expected records. It must not link/import/call the Rust semantic core. A deliberate update command may replace committed generated records only from this oracle-only pipeline. CI/source-integrity checks reject both Rust-derived expectations and handwritten changes to oracle-representable generated fields. + +`ExpectedRecord` carries generated semantic payload plus human provenance/projection-rule metadata (`DerivationRef` or its replacement). The loader verifies fixture hash, oracle revision, wrapper/schema/rule versions, field-authority classification, and complete traceability. The gate integration independently parses fixture bytes through the real Rust core adapter and compares the candidate projection to the committed oracle-generated expectation while the owned reservation remains live. + +## Implementation steps + +### 1. Establish the crate, typed boundary, and workspace checks + +Create the core crate and modules for the reference-shaped scanner/token map, MPD/DATA splitter, callback parser/actions, model arena/cache, iterative traversal, transforms/geometry, BFC, colours, TEXMAP, diagnostics/profiles, limits, reservations, and projection. Add one target-safe adapter request/result seam and make the harness consume the real core adapter. Add a repository oracle-integrity check that searches expected/gate/harness/fuzz source for calls matching `canonical(` or `parse_syntax(` and fails on any match; remove the public `canonical` and `parse_syntax` functions. Keep the native LDParse command only as an out-of-process differential boundary. + +**Done when:** core, testkit, and harness compile against one adapter definition; no `canonical(` or `parse_syntax(` call exists in expected/gate/harness/fuzz paths; the harness fuzz loop parses mutated bytes through the core; dependency inspection shows no forbidden renderer/parser dependency. + +### 2. Build oracle-backed corpus records and integrity validation + +Expand the pinned LDParse wrapper and projection to preserve every authoritative value the library exposes. Implement the oracle-only deterministic generator and checked-in field-authority/projection-rule table described above. Regenerate all 28 expected records from exact fixture bytes and pinned oracle output; mechanically derive AABBs, graph/mesh/child relationships, BFC/winding, colours, steps, geometry and other fields wherever the emitted data supports them. Apply explicit versioned input/spec/Polymodel rules only for genuinely unavailable fields. Preserve the authoritative 28-row inventory and assert exact inventory↔fixture↔expected equality. + +**Done when:** two temporary generations are byte-identical; generated output byte-matches all 28 committed records; every field has an authority classification and trace; fixture hashes, oracle/wrapper/schema/rule revisions, provenance, matrix and tags validate; the generator structurally cannot call the Rust core; unknown oracle fields fail closed; handwritten or Rust-derived oracle-representable values fail integrity checks. + +### 3. Implement lossless scanner and typed primitive syntax + +Scan raw bytes line locally, retaining raw tails, fields, LF/CRLF/no-ending mode, byte spans, one-based line/column, and first type token. Recognize types 0–5 and typed comments/meta/directives, subfile references, lines, triangles, quadrilaterals, and conditional lines. Retain unknown/malformed type-0 text and diagnose invalid UTF-8, malformed counts/coordinates, short fields, BOM/blank/whitespace boundaries, and unterminated final lines under the selected profile. Keep original numeric spelling separate from finite numeric values; reject NaN/infinity from geometry. + +**Done when:** Tier 1 table-driven scanner tests match authored syntax for applicable fixtures; repeated scans are byte-identical; strict, compatibility, and lossless profile outcomes have stable diagnostic code/severity/span/order; no whole-file substring search determines semantics. + +### 4. Implement profiles, diagnostics, limits, cancellation, and reservation-owned growth + +Define stable diagnostics and profile policy tables. Enforce every PM-77 limit with checked arithmetic and deterministic limit/limit+1 outcomes for bytes, lines, files, include depth, commands, instances, triangles, lines, textures, fetches, and diagnostics. Check cancellation at line scan, file partition, include traversal, TEXMAP payload handling, and projection boundaries; errors/cancellation discard unpublished graph/cache state. + +Before every semantic growth, calculate the checked estimate, reserve against the exact caller owner, then allocate. Create exactly one root `SemanticArenas` reservation with initial admitted amount the caller-configured SemanticArenas budget (default: `bytes.len().checked_mul(8).and_then(|n| n.checked_add(8192))`) (checked before parsing) and attach every per-file/per-arena child through `reserve_child(&root, ...)`; the aggregate child charge must not exceed the root admission. If a child would exceed it, reject the parse with `SEMANTIC_ARENA_RESERVATION_LIMIT` and publish no partial result; do not silently resize or over-admit. Children are stored in `SemanticArenas` and dropped before the root handle in `OwnedParseResult`. Keep the successful result alive through projection tests. Never release the root merely because parsing returned `Ok`. + +**Done when:** Tier 1 tests cover every limit and limit+1 vector, overflow, cancellation checkpoint, rejection, cleanup, and stale-owner case; a successful parse snapshot shows the SemanticArenas charge and reservation count still present until `OwnedParseResult` drop, then zero; child handles cannot outlive root; no partial publication, panic, hang, or leak occurs. + +### 5. Implement MPD/DATA and resolver-independent path identity + +Partition `0 FILE`/`0 NOFILE` into virtual files while preserving DATA payload lines and boundaries. Handle duplicate/missing terminators, unreachable files, malformed/truncated DATA/base64, and decoded-size limits without filesystem/archive access. Implement canonical path normalization and the four PM-75 root contexts (current MPD table, uploaded manifest, uploaded LDraw roots, pinned official library), including precedence, shadowing, exact/ASCII-fold collisions, explicit-prefix no-fallback, forbidden syntax, and out-of-root diagnostics. + +A type-1 reference always retains its canonical target and unresolved status. Resolver availability cannot change syntax, identities, or diagnostics for equal bytes and root context. + +**Done when:** MPD/DATA/path gates pass all applicable rows; malformed DATA and decoded limits are diagnosed; all four roots have deterministic happy/shadow/collision/out-of-root/forbidden tests; dependency checks show no filesystem/network/process/archive symbol in core. + +### 6. Implement iterative graph/cache identity, transforms, and instances + +Traverse type-1 references with an explicit stack containing source-file identity, canonical target, transform, BFC context, TEXMAP context, and depth. Freeze the graph/cache key exactly as `(canonical_path: NormalizedPath, resolved_root: RootId, content_hash: Sha256)`—no additional source-context category, profile field, resolver address, pointer, random value, or fetch timing participates in the key. Allocate `ModelId` sequentially in first-seen order during deterministic traversal, starting at `model-00000001`; serialize it as exactly `model-<8 decimal digits>`. Allocate a fresh `InstanceId` sequentially for every type-1 occurrence in that same traversal order, starting at `instance-00000001`; serialize it as exactly `instance-<8 decimal digits>`, including unresolved references, cache hits, and cycle-cut edges. A completed cache entry permits parse-once reuse, but every reference occurrence allocates its own instance ID before resolution/descent decisions, subject to the instances limit. + +The operational cycle invariant is: when a frame is about to descend, construct its full graph identity key and insert it into `visiting`; the key remains present for the entire frame lifetime; on normal completion, limit rejection, cancellation, or error unwind, remove it exactly once before popping the frame. If a child key is already in `visiting`, emit the deterministic cycle diagnostic, retain the reference and its newly allocated instance ID (with cycle-cut metadata), and suppress only descent into that child; do not recurse, duplicate the active frame, or discard the edge. A cache hit outside `visiting` reuses completed semantic contents while still creating an occurrence instance; a cache hit whose key is active is a cycle and follows the same diagnostic/suppression rule. Include depth is checked before frame push. This invariant is required for MPD self-cycles, mutual cycles, repeated references, cache hits, resolver-unavailable targets, and every cancellation/error path. + +Apply finite right-handed 3×3-plus-translation transforms with checked numerics, determinant/reflection tracking, ordered steps, primitive counts, and true per-axis bounds. Carry BFC, colour, and TEXMAP state explicitly into each instance. + +**Done when:** Tier 1 graph tests assert `visiting` membership at push and absence after every completion/error/cancel path; assert the exact key `(canonical_path: NormalizedPath, resolved_root: RootId, content_hash: Sha256)` and no other key fields; assert `ModelId` allocation is first-seen sequential `model-00000001` and `InstanceId` allocation is occurrence-order sequential `instance-00000001`, both serialized with eight decimal digits; assert cycle edges receive IDs and diagnostics while descent is suppressed; and cover repeated references, parse-once reuse, distinct instances, cache hits, self/mutual cycles, depth, unresolved/malformed refs, and deep non-recursive chains. Transform/bounds/step outputs are deterministic and match re-authored expected records. + +### 7. Implement BFC, colours, TEXMAP state, and v1 projection + +Implement file-local BFC certification/winding and exact one-shot `INVERTNEXT`: only the next valid type-1 include consumes and applies it; a type-2–5 geometry primitive and every intervening command clear it without consuming/applying it, while blank lines do not clear it. Cover nested transitions, malformed refs, EOF, file/MPD switches, cache hits, reflection, and PM-75 XOR semantics. Implement colour 16/current, 24/edge/direct encodings, scoped `!COLOUR` definitions, alpha/luminance/finish/material fields, unknown-colour diagnostics, and deterministic LDConfig state. + +Implement the fixed TEXMAP state machine (`Inactive`, `AwaitingNext`, `Active`, `Fallback`) from the matrix above. Parse START/NEXT mode and finite parameters, accept NEXT only from `Inactive`, associate fallback geometry with the owning START frame, terminate only on same-file END/STEP/file end, and emit the concrete ordered `TexmapEvent` projection described above. Literal STOP is not a state transition. Unavailable texture resolution preserves event state. The `texmap_projection_v1` gate compares all projected event data, spans, file ownership, and fallback association. + +Project syntax, semantic identities, BFC/colour/TEXMAP state, steps/limits, graph IDs, counts, six-axis bounds, reflection, diagnostics, provenance, and canonical decimal spelling in declaration order. + +**Done when:** BFC, colour, TEXMAP, steps/transform, projection, and all applicable corpus gates pass; direct tests cover every BFC/TEXMAP transition/error including silent `Unknown`/`Uncertified + INVERTNEXT`, inactive `END`, nested `0 !:`, NEXT span targeting, malformed type 1–5 pending behavior, and separate `0 !:` versus standard type 1–5 active geometry; every expected record serializes the additive event list; repeated serialization is byte-identical; no recognized construct disappears without a diagnostic. + +### 8. Wire focused commands, WASM checks, and documentation + +Add adapter-to-gate tests that parse exactly all 28 authoritative embedded fixtures under their declared profiles and report fixture/gate/field failures. Add oracle-generation tests that run independently of the Rust core, generate twice to temporary directories, and byte-compare against committed records. Extend `just ldraw-wasm-check` to compile core and testkit for `wasm32-unknown-unknown`. Add the oracle-integrity check to the repository validation surface and ensure `ldraw-fuzz-smoke` uses the real core adapter. Document the API, profiles, resolver-independent identity, exact TEXMAP projection, derivation-note workflow, reservation owner source, root/child lifetime topology, and downstream boundaries. + +**Done when:** one focused command exercises exactly 28 fixtures and all named gates; oracle-only generation is deterministic and byte-matches committed records without importing/calling the Rust core; core/testkit compile for WASM; fuzz smoke tests the Rust candidate against oracle-backed expectations; documentation states the authority, ownership, and projection contracts. + +### 9. Review, validate, and hand off + +Load `reviewing-code-for-shipping-quality`, inspect every expected JSON diff as semantic data, and remove process residue, duplicate parsing, unbranded values where a branded type exists, unsafe/hidden fallback, generated churn, and comments that only narrate code. Run the exact validation sequence below, inspect `jj diff`, fix all Critical/High findings in follow-up commits, and commit the task change with the required trailers exactly once. + +**Done when:** every acceptance item and all ten prior findings map to code plus a concrete test; `jj diff` contains only PM-78-owned files; the change is on `epic-86-task-E`; the parent receives commit ID, changed-file summary, exact validation output, and reproducible remaining risks. + +## Test strategy + +This task has no provider, daemon, browser, TUI, or UI behavior. Tier 1 pure Rust tests cover semantic-core, ledger-backed ownership, corpus, and projection behavior; the native compatibility harness is a separate native integration boundary. Tier 2 TestProvider, Tier 3 daemon subprocess, and Tier 4 TestBackend do not apply. + +| Behavior | Tier/infrastructure | Required cases | +|---|---|---| +| Workspace/oracle safety | Tier 1 repository check plus workspace commands | Stop on default workspace/main; oracle generator links/calls only the pinned external wrapper and mechanical rule code; reject any Rust-core/adapter/actual-record dependency; one real in-process adapter exists only for candidate evaluation. | +| Oracle-generated expected integrity | Tier 1 generator/golden/testkit tests | Exactly the authoritative 28 inventory rows; exact inventory↔fixture↔expected equality; fixture/oracle/wrapper/schema/rule revisions; field-authority coverage; generate twice byte-identically; temporary generation byte-matches committed records; handwritten or Rust-derived oracle-representable fields fail. | +| Line syntax types 0–5 | Tier 1 scanner tables and syntax gate | Raw tails/spans; line endings; BOM/blank/whitespace/UTF-8; malformed counts/primitive fields; stable diagnostics. Include exact-case meta-keyword fixtures proving `STEP` maps to the keyword while lowercase `step` and mixed-case `Step` remain identifiers/garbage, plus quoted-identifier fixtures asserting one quoted identifier token with its contents/span preserved. | +| Numeric handling | Tier 1 unit/property tests | Signed zero, decimal/exponent spelling, finite extremes, overflow/underflow, NaN/infinity, canonical decimal output and diagnostic order. | +| Profiles/diagnostics | Tier 1 profile matrix | Strict rejection, compatibility acceptance, lossless preservation, code/severity/span/message/order, happy/error/boundary/adversarial rows. | +| MPD/DATA | Tier 1 virtual-file tests and MPD gate | FILE/NOFILE, duplicate/missing terminators, unreachable blocks, payload/base64, malformed/truncated DATA, decoded limits. Add explicit discard-state fixtures asserting bytes before the first `FILE`, after `NOFILE`, and between malformed boundary markers are discarded and never assigned to a virtual file/model. | +| Paths/resolution | Tier 1 path tables and path gate | Four roots; precedence/shadowing; exact/ASCII-fold collisions; explicit prefixes/no fallback; forbidden/out-of-root; resolver present/absent invariance. | +| Graph/instances | Tier 1 iterative graph tests and graph gate | Parse-once repeated models, distinct stable instances, cache identities, cycles, depth, unresolved/malformed refs, deep chains. | +| BFC | Tier 1 transition tables and BFC gate | Observed-vs-corrected CERTIFY/NOCERTIFY/CW/CCW; certified gating; reflection XOR; one-shot INVERTNEXT consumed only by valid type-1 include; explicit `Unknown`/`Uncertified + INVERTNEXT` unchanged/no-pending/no-strict-diagnostic case plus compatibility/lossless info tuple; primitive/intervening-command clearing, blank preservation, malformed ref, file/MPD switch, cache hit, and EOF. | +| Colours/LDConfig | Tier 1 colour state tests and colour gate | 16/current, 24/edge/direct, direct encodings, alpha/luminance/finish/material, scoped definitions, unknown colours. Add 512-slot boundary and exhaustion fixtures, deterministic local-colour allocation-order assertions, and scope commit/rollback assertions (committed definitions survive scope close; rolled-back definitions do not). | +| TEXMAP state/projection | Tier 1 state tests plus `texmap_projection_v1` corpus gate | Official START/NEXT/FALLBACK/END grammar (NEXT has no count token and applies to the immediately following nonblank valid type 1–5 line), four-state transitions, mode/finite params/spans, every explicit malformed tuple by profile, `TEXMAP_NEXT_TYPE0` span on the NEXT line, nested `0 !:` tuple, inactive END silent no-op, malformed type 1–5 preserving pending NEXT, literal STOP unknown-META retention with no rewrite, missing END/EOF without diagnostic, cross-file END, unresolved payload retention, empty list for non-TEXMAP records. | +| Steps/transforms/bounds | Tier 1 matrix/geometry tests and steps gate | STEP/ROTSTEP boundaries, finite right-handed transforms, determinant/reflection, no early scaling, independent six-axis AABB, ordered steps. | +| Limits/cancellation | Tier 1 bounded PM-77 vectors | Every limit and limit+1; checked overflow; cancellation at all named checkpoints; no partial publication. | +| Reservation lifetime/topology | Tier 1 ledger-backed core tests | Exact caller owner; fixed adapter owner; root-before-growth; all children via `reserve_child(&root, ...)`; child drop before root; successful charge remains until result drop; error/limit/cancel cleanup; stale owner and transfer. Add a ledger snapshot assertion that the root’s admitted amount equals exactly the caller-configured SemanticArenas budget (default: `bytes.len().checked_mul(8).and_then(|n| n.checked_add(8192))`) for representative inputs, plus a checked-add overflow assertion and a child-over-admission rejection assertion with `SEMANTIC_ARENA_RESERVATION_LIMIT` and no published result. | +| Canonical projection | Tier 1 golden/serialization tests | `ldraw-canonical-v1`, provenance, declaration order, canonical decimals, BFC/colour/TEXMAP/steps/diagnostics, byte-identical repeat serialization, no recognized loss. | +| Full PM-77/PM-78 corpus | Tier 1 adapter-to-gate integration | Parse exactly 28 authoritative fixtures under declared profiles; all applicable named gates; exact field failures; expected semantics generated from pinned oracle independently of the Rust candidate. | +| WASM/dependency safety | `cargo check` target plus dependency-tree assertions | Core/testkit tests compile for `wasm32-unknown-unknown`; no forbidden renderer/parser dependencies or host APIs; renderer split remains green. | +| Native differential boundary | Existing standalone native harness | LDParse stays out-of-process, offline, and separate from core; fuzz smoke invokes only the real in-process adapter. | + +Run focused validation in this order: + +```text +jj workspace root +jj status +jj bookmark list +cargo test -p polymodel-ldraw-testkit +cargo test -p polymodel-ldraw-core +cargo test -p polymodel-ldraw-core --tests -- --nocapture +cargo check --tests -p polymodel-ldraw-core --target wasm32-unknown-unknown +cargo check --tests -p polymodel-ldraw-testkit --target wasm32-unknown-unknown +just ldraw-corpus +just ldraw-wasm-check +just ldraw-fuzz-smoke +just verify-renderer-split +just fix +just check +just test-all +``` + +`just test-all` is the final merge gate and must visibly execute corpus, WASM, fuzz, differential, renderer-ledger, renderer, and workspace checks. Report the exact command and output for any environment failure; do not replace a failed gate with a weaker assertion. + +## Rework-specific implementation guardrails + +The prior implementation must not be patched by adding a few branches. Replace the semantic parser/model-builder path where its structure is fundamentally incompatible with the requirements (whole-line BFC matching, single hardcoded instance, no virtual-file table, no active-path state, no transform propagation, or silent TEXMAP transitions). Edit only stable testkit schema/provenance and command seams where their ownership is correct. + +The implementation review must reject a result that has any of these observable symptoms: `0 FILE` merely stripped instead of partitioned; content outside active MPD files assigned to a model; one fixed instance ID; recursive include descent; no `visiting`/active-path cycle state; cache lookup absent or case behavior accidental; type-1 transform values discarded; determinant/reflection always false; BFC determined by substring search; `INVERTNEXT` consumed by blanks or silently ignored; `CERTIFY`/winding transitions accepted while uncertified; TEXMAP transitions ignored; fewer than all 11 limits; unchecked count/byte arithmetic; or a successful parse whose semantic reservation is released before its owned result drops. + +The actual adapter-to-gate test is mandatory: it must feed each final fixture’s bytes and profile into the Rust core candidate, project the returned owned result while its reservation remains alive, and compare it with the independently generated pinned-oracle expected record. A test that passes expected data as both arguments, generates expected values from Rust actuals, or substitutes handwritten values for oracle-representable fields is invalid. + +## Required commit trailers + +Commit the implementation on the task change with a concise PM-78 message and exactly these trailers: + +```text +Epic: PM-86 +Task: PM-78 +``` + +## Review expectations + +Implementation reviewers must verify: + +- The implementation covers the complete task cache and all 28 authoritative fixtures. +- Every expected semantic payload is reproducibly generated from the pinned out-of-process LDParse oracle plus classified versioned mechanical rules; human edits are limited to provenance/rules, and no oracle generator path calls the Rust core/adapter/actual-record code. +- Reviewers must reject both Rust-derived expectations and handwritten oracle-representable values. Temporary generation runs twice byte-identically and byte-matches committed records. +- The harness fuzz path uses the real core candidate and LDParse remains the independent out-of-process authority. +- BFC expected outcomes and bounds are sourced from oracle-direct/mechanical data where exposed, with explicit PM-75/Polymodel rule classification only for genuine reference gaps. +- Exactly the authoritative 28 inventory rows exist and match fixture/expected sets; TEXMAP coverage is present without dropping another required category. +- TEXMAP is a typed state machine and its exact ordered `texmap_events` projection is checked by `texmap_projection_v1`. +- The shared adapter has one target-safe definition and the core remains independent of native harness code. +- Raw spans/tails/line endings, numeric spelling, diagnostic order, canonical decimals, schema, and provenance are stable. +- MPD/DATA boundaries, all four path contexts, resolver independence, cycles, limits, and iterative traversal are correct. +- Repeated models parse once while every occurrence has a distinct stable identity. +- `ReservationOwner` is explicit, caller-derived, and the deterministic adapter owner is documented and tested. +- There is exactly one root `SemanticArenas` reservation; all children use `reserve_child` against it; children drop before root; a successful parse retains the reservation until owned-result/arena drop or explicit transfer. +- No panic, silent recognized-construct loss, filesystem/network/process/archive access, recursion-based include overflow, random identity, or hidden fallback exists. +- The core WASM dependency tree stays free of `three-d`, `three-d-asset`, `stl_io`, and `polymodel-mesh`, and `just verify-renderer-split` is green. +- `jj diff` contains only PM-78-owned crate/API seam, testkit schema/expected/corpus updates, harness adapter change, justfile, tests, and documentation; generated API and unrelated app/UI files are absent. +- Critical/High findings are fixed before handoff and trailers are present. + +## Ready-for-parent-merge handoff + +Before returning to the parent orchestrator, provide: + +1. Task workspace `../epic-86-task-E`, bookmark `epic-86-task-E`, and committed change ID. +2. Changed-file summary; public crate/API names; profile, TEXMAP, field-authority/projection-rule, and reservation configuration; exact authoritative fixture count `28`; oracle/wrapper/schema/rule revisions; confirmation that temporary oracle-only generation ran twice byte-identically and byte-matched every committed expected record. +3. Exact focused output for testkit/core tests, projection/corpus gates, core/testkit WASM checks, fuzz smoke, and `just verify-renderer-split`. +4. Exact `just test-all` output proving the full merge gate ran, or the reproducible environment failure if it did not. +5. Confirmation that `jj diff` was reviewed and no prohibited state/source/generated files were modified. +6. A finding-disposition summary and any remaining risk stated as a reproducible command/failure, never an undocumented assumption. + +## Finding dispositions + +- **E-plan-1-F1 — superseded by operator authority correction.** All 28 expected semantic records are generated deterministically from the pinned external LDParse oracle. Humans author only provenance and versioned mechanical rules for demonstrably unavailable fields. Both Rust-derived expectations and handwritten oracle-representable values are prohibited and structurally tested. +- **E-plan-1-F2 — fixed and clarified in gen-3.** TEXMAP is a PM-75/LDraw-spec-only four-state machine (`Inactive`, `AwaitingNext`, `Active`, `Fallback`) with START/NEXT/FALLBACK/END, malformed/EOF behavior, literal-STOP diagnostics, ordered events, a concrete projection field, and a named `texmap_projection_v1` gate. +- **E-plan-1-F3 — fixed.** Expected bounds and projection use independent per-axis `[min_x,min_y,min_z,max_x,max_y,max_z]` values, with direct non-collapsing AABB tests. +- **E-plan-1-F4 — superseded by intentional corpus expansion.** The authoritative inventory is exactly 28, enforced by exact inventory↔fixture↔expected equality and exact-28 adapter/corpus commands; useful coverage is not deleted to satisfy the stale 24 numeral. +- **E-plan-1-F5 — fixed.** `ReservationOwner` is caller-supplied from worker/load/scene identity; production uses `interactive(...)`, the isolated adapter uses documented `preview(1,1,1)`, and the parser never invents owner identity. +- **E-plan-2-F1 — fixed.** `OwnedParseResult`/`SemanticArenas` owns the root reservation through successful parse and projection; release occurs only at owned-result/arena drop or explicit transfer. A snapshot test proves the charge remains after successful parse until drop. +- **E-plan-2-F2 — superseded by operator authority correction.** Heuristic/Rust-backed expected helpers are deleted, but a deterministic oracle-only generator is mandatory. It invokes pinned LDParse out of process, cannot depend on Rust core/adapter actuals, generates twice identically to a temporary directory, and byte-compares with committed expectations. +- **E-plan-2-F3 — fixed.** TEXMAP uses the exact additive `SemanticRecord.texmap_events: Vec` representation and the named `texmap_projection_v1` gate, with field contents and non-TEXMAP empty behavior fixed here. +- **E-plan-2-F4 — superseded by field-authority model.** Every field across all 28 records is classified as oracle-direct, oracle-mechanical, input-mechanical, or Polymodel-policy and carries exact oracle/rule revision traceability. There is no human-authored semantic class. +- **E-plan-2-F5 — fixed.** The reservation tree is one root `SemanticArenas` reservation; every child attaches directly through `reserve_child(&root, ...)`, children are stored/dropped before root, and topology is tested. +- **E-impl-1-F1 (Critical): superseded by operator authority correction.** A real adapter-to-gate integration test parses every fixture and evaluates the Rust candidate against independently generated pinned-oracle expectations; expected-as-actual, Rust-derived expected values, and handwritten oracle-representable values are explicitly invalid. +- **E-impl-1-F2 (Critical): fixed by plan.** The implementation steps require real MPD/DATA partitioning, normalized paths, iterative graph/cache traversal, active-path cycles, cancellation, LDConfig, reflection, all 11 limits, and cleanup; the guardrails reject the old skeleton symptoms. +- **E-impl-1-F3 (Critical): fixed by plan.** BFC is token/event-driven with explicit `Unknown`/`Certified`/`Uncertified` state, legal transitions, one-shot `INVERTNEXT`, and determinant reflection XOR; whole-line substring matching is prohibited. +- **E-impl-1-F4 (Critical): fixed and clarified in gen-3.** TEXMAP is a validated PM-75/LDraw-spec-only four-state machine with ordered typed START/NEXT/FALLBACK/END events, explicit nested/cross-file/EOF/invalid-numeric behavior, literal-STOP diagnostics, and deterministic unavailable-resolution retention; silent transition loss is prohibited. +- **E-impl-1-F5 (Critical): fixed by plan.** `LdrawLimits` names and tests all 11 PM-77 limits with checked arithmetic, limit/limit+1, overflow, cancellation checkpoints, and no partial publication. +- **E-impl-1-F6 (High): fixed by plan.** Type-1 transforms, stable model/instance IDs, normalized cache, explicit traversal stack, visiting cycle detection, completed parse-once reuse, and depth limits are mandatory. +- **E-impl-1-F7 (High): fixed by plan.** `SemanticArenas` stores child reservations so they drop before the retained root; ledger snapshots test successful retention, cleanup, and transfer/drop ordering. +- **E2rw-B-1 (High — BFC transition table): fixed in gen-3.** The normative table separates observed C++ behavior from PM-75 corrections, cites `ModelBuilderImpl.hpp`, defines `Unknown`/`Certified`/`Uncertified`, gates CLIP/NOCLIP/CW/CCW, and specifies INVERTNEXT consumption/clearing across primitives, commands, file/MPD switches, cache hits, and EOF. +- **E2rw-B-2 (High — TEXMAP matrix): fixed in gen-3.** The plan states TEXMAP is absent from the C++ reference, anchors transitions to PM-75 §3.5, and gives a total matrix for `Inactive`/`AwaitingNext`/`Active`/`Fallback`, START/NEXT/FALLBACK/END, nested START, invalid numerics, cross-file END, STEP, EOF, missing END, and literal STOP. +- **E2rw-B-3 (High — limits accounting): fixed in gen-3.** All eleven PM-77 limits have exact defaults, units, increment points, checked overflow/rejection behavior, and resolver-independent caller-supplied fetch/texture boundaries. +- **E2rw-B-4 (Medium — graph cycles): fixed in gen-3.** The graph step defines `visiting` insertion/removal on frame lifetime, the full cache identity key, cycle instance allocation and diagnostic preservation, and descent suppression without recursion. +- **E-plan-3-F1 (High — TEXMAP NEXT syntax): fixed in gen-4.** The plan now uses the official `0 !TEXMAP (START | NEXT) [GLOSSMAP pngfile]` grammar with no count token; NEXT applies to the immediately following nonblank valid type 1–5 geometry line, and a first nonblank type-0 line cancels it. Tests and fixtures assert this exact behavior. +- **E-plan-3-F2 (High — STOP pre-parse mapping): fixed in gen-4.** Valid fixtures use `END`; literal `STOP` appears only as lossless unknown-META negative coverage, and the parser performs no pre-parse vocabulary rewrite. +- **E-plan-3-F3 (High — graph/cache identity): fixed in gen-4.** The key is exactly `(canonical_path: NormalizedPath, resolved_root: RootId, content_hash: Sha256)`; model IDs are first-seen sequential `model-<8 decimal digits>` values and occurrence IDs are traversal-order sequential `instance-<8 decimal digits>` values. +- **E-plan-3-F4 (High — reservation root sizing): fixed in gen-4, revised gen-6.** The root SemanticArenas admission is a caller-configured memory budget. The default estimate is `bytes.len().checked_mul(8).and_then(|n| n.checked_add(8192))` — a policy default sufficient for typical inputs, not a mathematical proof of universal sufficiency. For unusually dense valid inputs, the caller supplies a larger budget. The corpus adapter budget is exactly 16 MiB. Exceeding the admitted amount rejects with `SEMANTIC_ARENA_RESERVATION_LIMIT` without partial publication. +- **E-plan-3-F5 (High — diagnostic tuples): fixed in gen-4.** Every malformed BFC/TEXMAP case has an explicit strict/compatibility/lossless `(profile, code, severity, result)` row, including NEXT type-0 invalidation, invalid method/value/arity, nested/cross-file/duplicate controls, malformed `!:` geometry, and literal STOP unknown META. +- **E4-BFC-001 (High — INVERTNEXT outside Certified): fixed in gen-5.** The BFC table explicitly defines `Unknown`/`Uncertified + INVERTNEXT` as unchanged state with no pending bit and no diagnostic in the strict C++-compatible behavior; compatibility/lossless have frozen low-severity `BFC_INVERTNEXT_OUTSIDE_CERTIFIED` info tuples that preserve syntax without transitioning state. Direct table-driven tests assert all profile outcomes. Verified against `/home/orual/Projects/LDParse-reference/include/LDParse/ModelBuilder/ModelBuilderImpl.hpp:89-97`. +- **E4-R1 (Medium — NEXT type-0 diagnostic span): fixed in gen-5.** The diagnostic contract and matrix state that `TEXMAP_NEXT_TYPE0` targets the `NEXT` command line, not the cancelling type-0 line; the test strategy asserts the exact span. +- **E4-R2 (Medium — nested `0 !:` tuple): fixed in gen-5.** The malformed-TEXMAP table freezes `TEXMAP_NESTED_BANG_COLON` as strict error/reject and compatibility/lossless warning/preserve-without-transition. +- **E4-TEXMAP-001 (Medium — inactive END): fixed in gen-5.** The matrix splits inactive FALLBACK and END; inactive END is an explicit silent no-op with no diagnostic and no state change, with malformed END still covered by `TEXMAP_INVALID_END`. +- **E4-TEXMAP-002 (Medium — malformed geometry after NEXT): fixed in gen-5.** The matrix adds `AwaitingNext + first nonblank malformed type 1–5` → `AwaitingNext`; the geometry diagnostic is emitted and NEXT remains pending for the next valid geometry. +- **E4-RESERVATION-001 (Medium — root sizing assertion): fixed in gen-5.** The reservation test strategy asserts exact root admission the caller-configured SemanticArenas budget (default: `bytes.len().checked_mul(8).and_then(|n| n.checked_add(8192))`), checked-add overflow, and child-over-admission rejection with no published result. +- **E4-SCANNER-001 (Medium — scanner casing/quotes): fixed in gen-5.** Scanner tests explicitly cover exact `STEP` versus lowercase/mixed-case forms and quoted-identifier token/span preservation. +- **E4-MPD-001 (Medium — MPD discard state): fixed in gen-5.** MPD tests explicitly cover and assert discarded content before first FILE, after NOFILE, and between malformed boundaries. +- **E4-COLOUR-001 (Medium — 512-slot colour boundaries): fixed in gen-5.** Colour tests cover 512-slot boundary/exhaustion, deterministic local allocation order, and scope commit/rollback. +- **E4-R6 (Low — split Active→Active geometry): fixed in gen-5.** The matrix has separate Active rows for `0 !:` geometry and standard type 1–5 geometry, both remaining Active and associating with the current texture. diff --git a/.current-epic/plan-F.md b/.current-epic/plan-F.md new file mode 100644 index 0000000..227540f --- /dev/null +++ b/.current-epic/plan-F.md @@ -0,0 +1,305 @@ +# PM-79 / Task F — Minimal LDraw vertical slice and renderer scene IR + +## Contract + +- **Epic:** PM-86 — [https://radiant-industries.atlassian.net/browse/PM-86](https://radiant-industries.atlassian.net/browse/PM-86) +- **Task:** PM-79 — [https://radiant-industries.atlassian.net/browse/PM-79](https://radiant-industries.atlassian.net/browse/PM-79) +- **Task prefix:** F +- **Task cache:** `/home/orual/Projects/epic-86/.current-epic/task-F.md` +- **Epic workspace/bookmark:** `/home/orual/Projects/epic-86`, `epic-86` +- **Manifest/dependencies/state:** `/home/orual/Projects/epic-86/.current-epic/manifest.txt`, `dependencies.dot`, `task-state.json` +- **Base bookmark:** `main` +- **Expected task workspace/bookmark:** `/home/orual/Projects/epic-86-task-F`, `epic-86-task-F` +- **Integrated baseline:** `/home/orual/Projects/epic-86-task-G`, `epic-86@99436d09` + +## Context and outcome + +F owns the sole LDraw-to-renderer/world boundary for the normal Thing detail/viewer product flow plus ordinary-format parity. The acceptance composition is an ordinary Thing→Model→Part→File record: the normal publish UI/record contract uploads an LDraw multipart/MPD root and all companion sources, projection and G’s compound resolver discover verified descriptors, E parses them, and F renders them in that Thing’s existing viewer endpoint. There is no special viewer route, demo-model entry, separate upload flow, or direct-URL LDraw path. E owns parsing, semantic records, BFC, inclusion traversal, colour/material state, canonical projection, and semantic reservations. G is completed and integrated at `99436d09` and owns verified content-bound compound resource acquisition plus worker protocol/session/epoch/cancellation plumbing. + +The operator-visible composition uses authoritative LDraw primitives exported by E: type 1 occurrences, type 2 explicit lines, type 3 triangles, type 4 quads, and type 5 conditional lines. Generic IR point rendering is separately proved with a renderer-owned lower-level fixture; F does not invent a type-0 LDraw point syntax. H/I/J/K remain out of scope. + +## Safety check — stop on `main` + +Before any workspace or source operation, run: + +```sh +pwd +jj workspace root +jj workspace list +jj bookmark list +jj log -r 'epic-86@' -n 1 --no-graph +jj log -r 'main@' -n 1 --no-graph +jj status +``` + +Stop if the workspace root is not `/home/orual/Projects/epic-86-task-F`, the active bookmark is `main`, the workspace is the default workspace, or the task is not based on the parent’s integrated Epic tip. Create/use the expected sibling workspace and start a fresh change: + +```sh +jj workspace add /home/orual/Projects/epic-86-task-F -r epic-86 +cd /home/orual/Projects/epic-86-task-F +jj new -m 'PM-79: add minimal LDraw renderer scene boundary' +``` + +Do not reset unrelated changes. Do not edit `.current-epic/manifest.txt`, `.current-epic/dependencies.dot`, `.current-epic/task-state.json`, `.current-epic/merge-gate.json`, `events.log`, branches, or workspaces. + +## Grounded replace-vs-edit decisions + +| Area | Decision | Plan | +|---|---|---| +| `crates/polymodel-renderer-protocol/src/lib.rs`, new `src/scene.rs` | **Edit/add** | Keep integrated `CompoundLdraw`, `StartCompoundLoad`, `CompoundSceneSource`, `CompoundResourcesReady`, `InstallSucceeded`, and `LdrawUnit`; add a dependency-light serde-safe IR. Protocol remains an app leaf independent of E, mesh, and `three-d`. Preserve wire bytes; explicitly version unavoidable changes. | +| `crates/polymodel-ldraw-core/src/geom.rs`, `model.rs`, `lib.rs`, new export module | **Focused edit/add; do not replace parser** | Extend the existing `GeometryRecord`/`InstanceRecord`/`OwnedParseResult` seam with an owned render export. Keep scanner, parser, BFC, colours, traversal, limits, canonical projection, and reservation ownership. Never reconstruct geometry from counts or parse source in F. | +| New `crates/polymodel-renderer-worker/src/scene.rs` | **Add** | Define a worker-private installed-scene abstraction with two close-parallel payload modes: the existing rich ordinary `ModelMesh`/`three_d::Model` path, preserving transformed parts, PBR colours/materials/textures/hierarchy and triangle/point geometry; and a richer structured-primitive path carrying surfaces, explicit edges, conditional edges, points, occurrences, and materials. Adapt E's LDraw export into the structured mode, but name and shape the mode by renderer capability rather than LDraw so another structured/CAD source can use it later without parser knowledge. | +| `crates/polymodel-renderer-worker/src/worker.rs` | **Focused renderer generalization** | Generalize installed/pending payload, admission, draw dispatch, and Preview to operate on the two scene modes under one lifecycle contract. Preserve the existing ordinary conversion and PBR payload intact. Both modes share reducer transitions, ownership/swap, camera fitting, controls, frame scheduling, resize, render target/readback, Preview encoding, disposal, and context retention; only resource construction and per-primitive draw dispatch differ. | +| `crates/polymodel-renderer-worker/Cargo.toml` | **Minimal edit** | Add E core for production. Add `three-d`'s `window` feature only to the native Preview render test target/dev dependency; production worker features remain unchanged. Keep protocol leaf-safe and keep `three-d`, `three-d-asset`, and mesh parsing worker-only. | +| Native Preview runner and render test | **Add** | Add one native `three-d/window` Preview runner backed by a shared render function. Interactive invocation opens a visible window for operator inspection and writes the rendered PNG; the automated test invokes the same function with a hidden window/offscreen `RenderTarget`, reads pixels, and compares the deterministic image. Run the automated mode under Xvfb when no display server is available; do not add EGL/OSMesa or a software renderer. | +| `src/thing_detail.rs`, `src/viewer.rs`, appview/indexing publish/projection seams | **Focused edit/add** | Exercise the normal Thing detail viewer for an ordinary Thing→Model→Part→File LDraw composition; do not add a viewer route, DEMO_MODELS entry, special source, upload flow, or product surface. Preserve ordinary format behavior. | +| `e2e/tests/viewer.spec.ts` | **Focused edit** | Exercise normal Thing detail wiring, installed stats, source identity, and no-fallback behaviour; keep existing context-retention tests. Pixel correctness belongs to the native Preview render test, not browser e2e. | +| `justfile` | **Minimal edit** | Add `preview-ldraw` to launch the visible native runner and a focused automated native Preview test recipe if the existing test command cannot address it directly. Do not alter unrelated recipes. | +| `.current-epic/*`, branches, manifests | **Do not edit** | Preserve orchestration state and repository rails. | + +## Scene IR contract to freeze + +- Branded deterministic IDs: `SceneId`, `OccurrenceId`, `GeometryId`, `MaterialId`, `TextureId`, provenance and diagnostic IDs. IDs are opaque, never public vector indices. +- `Scene` owns `Units`, provenance, diagnostics, root, ordered geometries/materials and flat occurrences, bounds, and full `SceneStats`; LDraw uses `Units::declared(LengthUnit::LdrawUnit)`. +- `Occurrence` is flat for this slice: stable `OccurrenceId`, source/target geometry identity, source span/material/BFC as needed, and one composed `world_from_local` transform to root. Repeated subfiles share geometry but retain distinct stable occurrence IDs/transforms. Nested parent/children and E parent IDs are not required; hierarchy generalization is not implemented here. +- `Geometry` owns authoritative type-2/3/4/5 payloads: generic `Point`, `Triangle`, `Quad`, `Line`, and `ConditionalLine { endpoints, controls }`. Generic points are renderer-fixture-only; no LDraw type-0 syntax is added. LDraw topology and occurrence material/BFC state remain available through the adapter. +- Material carries E-resolved colour/edge/finish/material state with deterministic fallback. TEXMAP remains provenance/diagnostics only; F does not sample it. +- Bounds and counts come from actual emitted geometry after hierarchy transforms, in native LDU; stats include vertices, triangles, quads, lines, conditional lines, and points. +- Sole world basis: `(x,y,z) -> (x,z,-y)`, matrix `[[1,0,0],[0,0,1],[0,-1,0]]`, determinant `+1`; preserve winding. Metric is declaration/provenance only: `1 LDU = 1/2500 m = 0.4 mm`. +- Ordinary `ModelMesh` inputs retain current STL/OBJ/glTF/3MF units, hierarchy, materials, textures, camera behavior, and output. + +## Integrated baseline and exact G handoff + +This plan is grounded in `/home/orual/Projects/epic-86-task-G` at `epic-86@99436d09`, where G is completed and integrated. Consume these exact symbols, not a parallel URL path: + +- `MeshFormat::CompoundLdraw`, `LengthUnit::LdrawUnit`. +- `RendererCommand::StartCompoundLoad`, `CompoundLoadPlan`, `WorkerFetchDescriptor`, `validate`, `encoded_size`. +- `CompoundSceneSource { primary, descriptors: Vec<(WorkerFetchDescriptor, Vec)> }` and `handoff_compound_scene`. +- `RendererEvent::CompoundResourcesReady { namespace, identity, source }` (verified-resource boundary, not readiness). +- Worker `handle_compound_load`, `load_compound_source`, `compound_is_current`, `fetch_descriptor_bytes`, `finish_compound_source`. +- `RendererEvent::InstallSucceeded`, `LoadEvent::InstallSucceeded`, `LoadIdentity { load_epoch, scene_generation }`, `LoadReducer`, `ResourceToken`, `TransitionOutcome`. +- Viewer `ViewerMeshSource`, `CompoundLogicalIntent`, `PendingCompoundLoad`, `/app/ldraw/compound-resources`, and acquisition/restart epoch checks. + +Old G review claims that G lacked compound handoff, parser boundary, or integration are **rebutted-stale** by this baseline. F consumes G’s verified bytes/resource set; `CompoundResourcesReady` must never be treated as renderer readiness. + +## Authoritative LDU and transform rules + +PM-75/PM-79 is authoritative: **`1 LDU = 1/2500 m = 0.4 mm`**. Integrated protocol prose saying `1/64 inch = 0.396875 mm` is stale documentation; correct it when touching that comment, but do not convert coordinates. `Units::declared(LengthUnit::LdrawUnit)` records declaration only. + +Retain native finite `f64` LDU coordinates. Sole basis: + +```text +(x,y,z) -> (x,z,-y) +B = [[1,0,0], [0,0,1], [0,-1,0]], det(B) = +1 +``` + +Compute normals from LDU triangles before basis; use inverse-transpose for non-rigid/non-uniform occurrence linear transforms, then apply `B` once. Preserve winding—no second flip. Compute bounds after primitive points, composed occurrence transforms, then basis. Test stud vectors independently from the metric. + +## Dependency graph and E-owned export contract + +```text +protocol::scene (leaf IR; app-safe) + ^ ^ + | | +E LDraw core (export) mesh (ordinary input) + \ / + renderer-worker (adapter + three-d/GPU) + ^ + app/viewer +``` + +The protocol remains independent of E, mesh, and `three-d`; E remains independent of renderer protocol. Put the narrow serde/postcard-safe IR in `polymodel-renderer-protocol/src/scene.rs`. The worker depends on E and owns adaptation/GPU resources; E does not depend on the renderer protocol. No speculative scene crate or format registry. + +G descriptor roots are the exact appview strings `mpd`, `manifest`, `models`, `parts`, `p`, `external`, `official-parts`, and `official-p` (`src/appview/ldraw.rs:341-352`). Map them deterministically before E parsing: `mpd -> RootId::CurrentMpd` and current-MPD `Materialization`; `manifest -> RootId::UploadedManifest` and manifest/project `Materialization`; `models|parts|p|external -> RootId::UploadedLdraw` and uploaded-LDraw `Materialization`; `official-parts|official-p -> RootId::OfficialLibrary` and official-library `Materialization`. Unknown roots fail closed. For each verified descriptor construct `Materialization::new(NormalizedPath::new(descriptor.path), mapped_root, bytes, true, Some(descriptor.key/root provenance))`; reject invalid path, length/digest mismatch, duplicate `(mapped_root,path)`, missing/duplicate primary, or route root/path mismatch. The unique descriptor whose key equals `source.primary` supplies `ParseOptions.root_name` and `resolved_root`; use the worker owner and an explicit empty `target_selections: &[]` because the integrated plan has no selection table. Never guess by vector order or synthesize E parent IDs. + +Add a public E export module (for example `polymodel-ldraw-core/src/render_export.rs`) with exact owned types: + +```rust +pub struct RenderExport { + pub source: RenderSourceIdentity, + pub units: RenderUnits, // native LDU + pub geometries: Vec, + pub occurrences: Vec, + pub materials: Vec, + pub diagnostics: Vec, + pub provenance: RenderProvenance, + pub bounds: Option, + pub stats: RenderStats, +} +pub struct RenderGeometry { + pub id: GeometryIdentity, + pub model_id: String, + pub primitives: Vec, + pub bfc: BfcFrame, + pub material: MaterialIdentity, +} +pub enum RenderPrimitive { + Triangle { vertices: [Point3; 3], winding: Winding }, + Line { endpoints: [Point3; 2] }, + ConditionalLine { endpoints: [Point3; 2], controls: [Point3; 2] }, +} +pub struct RenderOccurrence { + pub id: OccurrenceIdentity, + pub source_geometry: GeometryIdentity, + pub target_geometry: GeometryIdentity, + pub world_from_local: Transform, + pub bfc: BfcFrame, + pub colour: ColourCode, + pub source_model: String, + pub target_model: String, +} +``` + +Also own `RenderSourceIdentity` (canonical path/root/content hash), `RenderUnits`, deterministic branded `GeometryIdentity`/`OccurrenceIdentity`/`MaterialIdentity`, `RenderMaterialState` (resolved `ColourData`, edge/finish/material state and fallback), `RenderProvenance`, `RenderDiagnostic` (severity/code/message/span), and checked `RenderStats` (vertices, triangles, quads, lines, conditional lines, points). `OwnedParseResult::into_render_export(self) -> Result` is the sole ownership transfer; `OwnedRenderExport` retains E `SemanticArenas`/reservation until drop or adapter consumption. Preserve `OwnedParseResult::project()`, `OwnedProjection`, and `CanonicalRecord` unchanged as compatibility projections. + +The existing `GeometryRecord` retains type-2/3/4/5 arrays in `ModelData::geometry`/`ModelSummary::geometry`; `InstanceRecord` retains type-1 transform, inversion, BFC, colour, and target identity. Extend this seam to copy owned payloads and flat occurrences with composed transforms and source/target geometry identity. Split type 4 deterministically as triangles `(0,1,2)` and `(0,2,3)` with E-resolved BFC winding exactly once. Do not parse source lines again in F. + +**Point contract:** do not invent an LDraw type-0 point syntax. The protocol IR includes generic `Primitive::Point`, tested only with a renderer-owned lower-level fixture. The normal Thing MPD/companion composition uses authoritative types 1–5; if point acceptance is required, prove generic point drawing separately and state the distinction. H owns broader LDraw point semantics. + +## Renderer IR, adapter, and GPU ownership + +`scene.rs` owns branded `SceneId`, `OccurrenceId`, `GeometryId`, `MaterialId`, `TextureId`, provenance/diagnostic IDs; `Scene`, flat `Occurrence`, `Geometry`, `Material`, `TextureRef`, `Bounds`, `SceneStats`, `Units`, and `Primitive`. `Scene` owns ordered geometry/material tables, flat occurrences, units, provenance, diagnostics, bounds, and full stats. Each occurrence owns source/target geometry identity and a composed `world_from_local` transform; no nested parent/children hierarchy is required in F. `Geometry` owns indexed triangles/indices/normals, explicit lines, conditional endpoints/controls, and point positions. IDs/order are deterministic from content identity and traversal order. + +The existing ordinary parser continues producing rich `ModelMesh` values: transformed parts, PBR colours/materials, textures, hierarchy, triangle geometry, and point clouds. STEP continues reaching this path through its existing glTF conversion. Preserve that payload and conversion intact. + +Generalize the worker's installed scene into two close-parallel capability modes: `Ordinary` retains the existing `three_d::Model` representation; `Structured` owns surface batches, explicit edge batches, conditional-edge controls, points, occurrence transforms, and resolved materials. E's LDraw export adapts into `Structured`, but the mode itself contains no LDraw parser concepts and is reusable by a future structured/CAD source. Both modes implement one worker-private behaviour contract for bounds/stats, admission, camera fit, draw submission, Preview, readback, controls, resize, lifecycle swap, and disposal. The WASM viewer, native visible runner, and native automated Preview test invoke this same behaviour; only mode-specific resource construction and primitive draw dispatch differ. TEXMAP remains metadata only; F does not sample it. + +Worker-only `PendingScene`/`InstalledScene` own `GpuSceneResources`: triangle batches as `three_d::CpuMesh` uploaded to `three_d::Mesh` and drawn via `three_d::Gm`; explicit lines via `three_d::Line`; points via `three_d::Points`; conditional endpoints/controls in a worker-owned conditional batch, reduced to visible endpoint segments submitted through `three_d::Line`. The deterministic conditional visibility rule projects endpoints and controls and draws the endpoint iff control points are on opposite sides of the projected endpoint line (cross-product signs product `<= 0`, zero visible). A scene is valid with triangles **or** lines/conditional lines/points; empty bounds are an install error. Camera framing uses transformed bounds and existing worker camera/control setup. Reserve before allocation; build pending completely; swap only after reducer-accepted `LoadEvent::InstallSucceeded`; drop pending/old resources and reservations exactly once on failure, stale, cancel, resize, or dispose. No TEXMAP sampling. Keep wire `MeshStats { vertices, triangles }` unchanged; full primitive counts remain internal/browser-visible stats. No implicit wire field additions. + +Expose per-kind counts and bounds only through one narrow test/debug-gated payload for the normal Thing browser proof and worker tests; never claim a general browser API: + +```text +DebugFrameReadback { + identity: LoadIdentity, + viewport_width: u32, + viewport_height: u32, + dpr: u32, + clear_rgba: [u8; 4], + non_clear_pixels: u32, + triangles: u32, + lines: u32, + conditional_lines: u32, + points: u32, + bounds_min: [f32; 3], + bounds_max: [f32; 3], +} +``` + +Emit it only after an actual worker frame draw/readback on the fixed viewport/DPR/clear colour. `non_clear_pixels > 0` is the viability sentinel; DOM readiness, canvas existence, or counts alone are insufficient. + +## Numbered implementation steps + +### 1. Freeze protocol IR and workspace safety + +From `/home/orual/Projects/epic-86-task-F`, verify workspace/bookmark safety and `epic-86@99436d09`. Add the leaf IR, generic point primitive, and unit declaration while preserving all integrated G symbols and existing wire fixtures. In `crates/polymodel-renderer-protocol/src/lib.rs`, explicitly replace the stale LDU comment at lines 326-327 and the stale numeric assertion at lines 1054-1056 that encode `0.396875`; preserve the `LdrawUnit` discriminant and assert metadata `1/2500 m = 0.4 mm` without scaling coordinates. + +**Done when:** native/WASM protocol builds, mixed-scene serde/order/ID tests pass, existing wire golden is unchanged, and protocol has no E/mesh/three-d dependency. + +### 2. Add E render export, not a second parser + +Extend `GeometryRecord`/`InstanceRecord`/`OwnedParseResult` with the exact export contract. Retain type-2/3/4/5 arrays, type-1 hierarchy/transforms, identities, BFC, colour/material state, TEXMAP metadata, diagnostics, bounds/stats, and reservation lease. Split quads deterministically in E; do not add type-0 parsing. + +**Done when:** exact primitive-array export and repeated transformed-child identity tests pass; canonical projection remains stable; dropping `OwnedRenderExport` releases E reservation. + +### 3. Add the structured scene mode in close parallel + +Generalize pending/installed scene behaviour around `Ordinary` and `Structured` payloads. Keep ordinary `ModelMesh` conversion and `three_d::Model` construction unchanged. Implement the E-export adapter into the structured mode; validate finite/checked/non-empty payloads, map E materials/fallbacks, and construct surface, explicit-edge, conditional-edge, and generic-point resources. Compute LDU normals first, inverse-transpose where needed, then apply the determinant-`+1` basis once with no winding flip and compute post-transform bounds. Reuse common camera fitting, controls, render targets, Preview encoding, lifecycle, and disposal while dispatching each mode's actual drawable resources. + +**Done when:** structured adapter tests prove topology/endpoints/controls/point resources, point/line-only acceptance, basis/stud vectors, normals, transformed bounds, materials, and typed errors; ordinary rich PBR/texture/hierarchy payloads remain unchanged; both modes pass the same behaviour contract for install, frame, resize, Preview, and disposal. + +### 4. Consume G handoff through E to install + +Retain G’s `load_compound_source` path: validate plan; reserve network/retained/decoded/WASM classes before allocation; fetch each descriptor route; verify exact length/SHA-256 via `verify_part_file`; check `compound_is_current`; construct `CompoundSceneSource`; call `handoff_compound_scene`; release fetch reservation. Never derive a direct LDraw URL. + +Then construct E `Materialization` entries from descriptor path and verified bytes, select `source.primary`, use the current `ReservationOwner`, invoke `LdrawParser::parse_bytes` exactly once, and call `into_render_export`. The worker keeps the verified source/bytes; the main thread never receives bytes or sends them back. Check negotiated session, namespace, load epoch, scene generation, acquisition/restart epoch, source identity, descriptor integrity, current identity after parse/adapt, and `Installing(identity)` before output. Check abort between descriptors and before parse/adapt/output. Clean every E/fetch/scene/GPU reservation and candidate on error, cancel, supersession, stale, resize, and dispose. + +Both `RendererNamespace::Interactive` and `RendererNamespace::Preview` use the same verified-source → E parse/export → LDraw scene-adapter → worker-private `PendingScene` pipeline. In `finish_compound_source`, no `CompoundResourcesReady` event may be posted before this shared parse/export/adapt stage. Interactive reserves/allocates GPU resources, reducer-commits `LoadEvent::InstallSucceeded`, swaps the installed scene, and publishes `RendererEvent::InstallSucceeded` with wire-stable `MeshStats`; after an actual draw it may emit the narrow gated debug readback. Preview uses the same candidate scene resources and camera to render the fixed requested output to PNG, then publishes the existing `RendererEvent::PreviewImageRendered { request_id, png }` protocol shape; it does not publish Interactive `InstallSucceeded`. Ordinary Preview PNG behavior remains unchanged for non-compound formats, and no parallel parser/raw URL path is added. + +`CompoundResourcesReady` remains an informational verified-source event only and must be posted after the shared parse/export/adapt stage and after the namespace-specific output has been accepted: for Interactive, after reducer-committed `InstallSucceeded`, installed swap, and the actual draw/readback; for Preview, after the shared scene has rendered and `PreviewImageRendered { request_id, png }` has been accepted for posting. It is never readiness and must not precede parse/export/adapt. Exactly three lifecycle coupling cases cover both namespaces without a Cartesian matrix: failure/cancel cleanup, superseding stale completion, and resize/dispose ownership cleanup. + +**Done when:** Interactive reaches `Ready` only through reducer-accepted `InstallSucceeded`; Preview renders the same E export through the ordinary PNG path and publishes `PreviewImageRendered` with a nonempty valid PNG; `CompoundResourcesReady` is posted only afterward and is never readiness; stale/cancel/error paths leak nothing; ordinary formats/textures remain green. + +### 5. Prove Preview output natively and exercise the normal Thing flow + +Use the normal publish UI/record contract to compose an ordinary Thing→Model→Part→File record containing an LDraw multipart/MPD root at `models/main.ldr` plus every companion source. Seed those ordinary records in the test database, including actual type-2/3/4/5 geometry, BFC/material state, a repeated transformed child, and the real CID/SHA-256/byte-length values in file metadata and manifest edges. Assert projection and G’s `/app/ldraw/compound-resources` resolver discover the primary and companions; the worker must verify source identity/boundaries before passing bytes to E. Navigate to that Thing’s normal detail viewer endpoint. Do not add a fixture/product route, DEMO_MODELS entry, separate upload flow, direct URL, or fallback source. + +Add a native Preview runner compiled with test/tool-only `three-d/window`. Its normal invocation opens a visible window, loads the seeded verified compound inputs through E export and the same F scene adapter/Preview camera path, renders continuously for operator inspection, and writes the current Preview PNG on startup or an explicit capture action. Provide a documented `just preview-ldraw` command that launches this runner directly, without the app server, browser, database service, or Playwright. + +Make the deterministic renderer oracle browser-free by reusing the runner's render function in an automated mode: create a hidden `WindowedContext`, render into an offscreen `RenderTarget`, read pixels, and compare the produced image at a fixed viewport against the committed expected PNG (exactly, or with one explicitly justified tight pixel tolerance if the native GL implementation requires it). Run automated mode under Xvfb in display-less environments. The test must assert valid PNG dimensions/encoding, non-clear pixels, expected primitive stats/bounds, and image comparison; scene snapshots alone are insufficient. The visible runner and automated test must not maintain separate scene construction, camera, or draw implementations. + +Browser e2e proves product wiring and straightforward live-renderer behaviour rather than duplicating the native pixel baseline: the normal Thing detail endpoint exposes the expected Thing/source/current-load identities, reaches reducer-accepted `InstallSucceeded` with nonzero installed stats, and draws a non-clear frame without falling back to STL or a special route. Apply one deterministic orbit input and one resize; assert the same retained worker/context and load identity produce another non-clear frame with updated viewport dimensions, without reacquisition, reinstall, or context recreation. Compound Preview uses the same E export/scene adapter, renderer core, camera/frame path, and returns a valid nonempty PNG through the existing Preview event. + +**Done when:** `just preview-ldraw` opens a visible native window showing the seeded multipart model and writes its Preview PNG; the shared hidden-window automated mode produces the expected image without a browser; normal Thing detail navigation reaches compound LDraw with no fallback and correct identities/readiness; compound Preview returns the same-scene PNG; no special route or product surface exists. + +## Minimal functional test set + +Do not add fuzzing, performance suites, publishing registry work, TEXMAP sampling, broad visual baselines, or H/I/J/K scope. Add only these focused tests: + +1. **E export (Tier 1):** exact type-2/3/4/5 primitive arrays, deterministic quad split, repeated transformed child occurrence IDs/shared geometry identity, BFC/material metadata, provenance, bounds/stats, and reservation drop. +2. **IR/protocol (Tier 1):** serde/order/ID stability for triangle, explicit line, conditional line, and generic point; one pre-existing wire golden and unit compatibility check. +3. **Math (Tier 1):** exact declaration `1/2500 m = 0.4 mm`, no scaling, basis/stud vectors, determinant `+1`, winding preservation, normal inverse-transpose timing, and bounds order. +4. **Adapter (Tier 1):** E export to triangle/line/conditional draw resources; renderer-owned generic point fixture to a point draw resource; point/line-only acceptance; malformed/non-finite/empty errors; pending/installed ownership and full internal stats. +5. **Ordinary parity (Tier 1/worker):** compact STL/OBJ/glTF/3MF adapter smoke, retained hierarchy/material behavior, and existing glTF texture assertion. +6. **Exactly three real-ledger lifecycle coupling cases (Tier 1 worker/reducer, no Cartesian matrix):** (a) parse/adapter/install failure or cancel returns reservations and disposes pending resources; (b) superseding identity makes late completion stale and unable to replace the scene; (c) resize/dispose swaps or clears ownership without leaking the old scene. +7. **Native Preview runner/render test:** `just preview-ldraw` opens a visible native `three-d/window` runner for operator inspection and PNG capture from deterministic multipart inputs. The automated mode reuses the exact scene/camera/draw function with a hidden `WindowedContext` (Xvfb in display-less CI), renders through the real Preview camera and offscreen `RenderTarget`, reads pixels, encodes PNG, and compares with the committed expected image at a fixed viewport. Assert dimensions, non-clear pixels, primitive stats/bounds, and expected image; this is the primary renderer proof and uses no browser. +8. **Live renderer browser behaviour:** navigate to the seeded Thing’s normal detail endpoint and assert projection/G compound discovery, source identity/no STL fallback, reducer-accepted `InstallSucceeded`, nonzero installed stats, and one non-clear frame. Apply one deterministic orbit and one resize, then assert another non-clear frame with updated viewport while worker/context/load identity remain unchanged and no reacquisition or reinstall occurs. Request ordinary compound Preview and assert valid nonempty PNG delivery. This is a focused behaviour test, not a visual baseline; do not add a special route or DEMO_MODELS entry. + +## Commands and validation order + +Run from `/home/orual/Projects/epic-86-task-F` on `epic-86-task-F`: + +```sh +pwd +jj workspace root +jj log -r 'epic-86@' -n 1 --no-graph +jj status +just check +just fix +cargo test -p polymodel-renderer-protocol +cargo test -p polymodel-renderer-ledger +just preview-ldraw --help +cargo test -p polymodel-ldraw-core +cargo test -p polymodel-renderer-worker --lib +just verify-renderer-split +just build-renderer-worker +just test-renderer +cd e2e && npm install && npm test -- viewer.spec.ts +cd .. +just test-all +jj diff +jj status +``` + +Use the repository’s documented generated-file prerequisite if a fresh workspace lacks `src/env.rs`; never hand-edit generated output. Resolve command failures; do not weaken acceptance. + +## F1 finding dispositions + +1. **Stale premise that G is incomplete/no integrated handoff — rebutted stale:** baseline is `epic-86@99436d09`; use `CompoundResourcesReady`, `CompoundSceneSource`, `handoff_compound_scene`, `load_compound_source`, and the reducer seam. +2. **LDU ambiguity — fixed:** PM-79 freezes `1 LDU = 1/2500 m = 0.4 mm`; integrated `1/64 inch` prose is stale documentation and cannot control geometry. +3. **E export underspecified — fixed:** exact `RenderExport`, `OwnedRenderExport`, primitive/occurrence/material/provenance/diagnostic/units/stats contracts and reservation lifetime are specified; canonical projection remains. +4. **Parser payload loss/worker duplication — fixed:** export from existing `GeometryRecord`/`InstanceRecord`/`OwnedParseResult`; retain type 2–5 arrays in E, split quads in E, parse once, no F parser. +5. **Invented LDraw point syntax — fixed:** generic IR point plus renderer-owned fixture only; operator LDraw fixture uses authoritative types 1–5; H owns broader point semantics. +6. **IR dependency inversion — fixed:** IR is a narrow protocol leaf; E does not depend on protocol/mesh/three-d; worker owns adaptation. +7. **GPU/stats/line-only behavior — fixed:** pending/installed resources own primitive buffers, reservations, bounds, full internal stats, compatibility `MeshStats`, and swap/dispose rules. +8. **Normals/basis/bounds — fixed:** LDU normals first, inverse-transpose where needed, determinant-`+1` basis once, no winding flip, explicit bounds order. +9. **G-to-install lifecycle — fixed:** verified resource -> content-bound handoff -> E materialization/parser/export -> adapter -> GPU install -> reducer `InstallSucceeded`, with checks and cleanup at every boundary. +10. **Special-route/product-flow ambiguity — corrected by operator:** the earlier `/viewer/ldraw-vertical-slice` and demo/STL-switch choices are removed. Acceptance uses an ordinary Thing→Model→Part→File MPD composition, normal publish/projection/G compound discovery, and that Thing’s normal detail viewer endpoint; no new route, DEMO_MODELS entry, upload flow, or direct URL. +11. **Interactive-only/Preview ambiguity — corrected by operator:** the earlier Interactive-only disposition is removed. Interactive and compound Preview share verified source→E parse/export→F adapter; they diverge only at output (`InstallSucceeded` versus ordinary preview PNG-ready event). +12. **Test sprawl/weak viability/H-I-J-K leakage — fixed:** focused E/IR/math/worker/ordinary-parity tests, exactly three lifecycle cases without a namespace Cartesian matrix, normal Thing browser flow with actual Interactive readback and same-scene Preview PNG, and explicit exclusions. + +## Review expectations + +Block protocol/app dependency on E/mesh/three-d, parser duplication, direct LDraw URL derivation, separate upload/route/demo product surfaces, bypass of G bytes, an LDraw-named or parser-aware installed-scene abstraction, separate renderer lifecycle/frame/Preview implementations by mode, replacement or flattening of ordinary `ModelMesh` transformed-part/PBR/colour/texture/hierarchy payloads, speculative format registry work, or duplicated camera/render-target/readback logic between native Preview and the WASM viewer; missing type-2/3/4/5 payloads, fabricated type-0 syntax, lost flat occurrence identity/transforms, nondeterministic IDs/order, or canonical projection breakage; hidden scale conversion, wrong LDU value, negative determinant, second winding flip, wrong normal timing, or pre-transform bounds; allocation before reservation, leaks, pending/live swap before reducer acceptance, readiness on `CompoundResourcesReady`, stale replacement, or broken cancel/resize/dispose cleanup; ordinary STL/OBJ/glTF/3MF or texture regression; Preview not sharing E export/adapter or not publishing a valid PNG; second WebGL context; DOM-only e2e; or H/I/J/K/fuzz/performance/publishing/TEXMAP/visual-baseline scope creep. + +## Commit and ready-for-parent-merge handoff + +Commit on `/home/orual/Projects/epic-86-task-F` at bookmark `epic-86-task-F`: + +```text +PM-79: add minimal LDraw renderer scene boundary + +Epic: PM-86 +Task: PM-79 +``` + +Before parent merge, report task workspace/bookmark/commit; grouped file list; exact command results including `just test-all`; clean `jj diff`/`jj status`; confirmation that no runtime state/manifest/dependency files changed; integrated baseline and exact G symbols consumed; exact normal Thing→Model→Part→File MPD/companion composition and real CIDs/digests/lengths; root mapping/materialization decisions; authoritative LDU value and both corrected stale sites; proof G’s verified `CompoundSceneSource` bytes were parsed once by E inside the worker and never round-tripped through main; proof Interactive `InstallSucceeded` is readiness while `CompoundResourcesReady` is informational; proof compound Preview used the same E export/adapter and produced a valid PNG; normal Thing detail endpoint/no fallback; fixed-viewport pixel/readback and nonzero gated stats; zero reservation snapshots for the exactly three lifecycle cases; and all reviewer dispositions. Parent merges only after trailers, focused/full gates, measured real-flow pixels and Preview PNG, and review clearance. \ No newline at end of file diff --git a/.current-epic/plan-G.md b/.current-epic/plan-G.md new file mode 100644 index 0000000..2fd8a23 --- /dev/null +++ b/.current-epic/plan-G.md @@ -0,0 +1,237 @@ +# PM-80 — Compound LDraw resolver and versioned worker protocol (planner-G4) + +## Task identity and source of truth + +- **Epic:** PM-86 — LDraw/LEGO model support and flexible renderer +- **Epic URL:** https://radiant-industries.atlassian.net/browse/PM-86 +- **Task:** PM-80 — Compound LDraw resolver and versioned worker protocol +- **Task URL:** https://radiant-industries.atlassian.net/browse/PM-80 +- **Task prefix:** G +- **Task cache:** `.current-epic/task-G.md` +- **Epic workspace:** `../epic-86` +- **Base bookmark:** `main` +- **Task workspace:** `../epic-86-task-G` +- **Task bookmark:** `epic-86-task-G` +- **Dependencies:** A/PM-87, B/PM-88, C/PM-76 — completed +- **Blocks:** H/PM-81, J/PM-83, K/PM-84 +- **Design source:** [PM-75 LDraw/LEGO model support and flexible renderer architecture](https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561) + +The cached task contract in `.current-epic/task-G.md` is authoritative. Jira is the workflow source of truth; this plan is the implementation handoff for the isolated task workspace. + +## Context + +G consumes PM-76’s typed verified resolver, PM-87’s identity/reducer state, and PM-88’s reservation ledger. It must provide a URL-free, principal-bound compound LDraw plan and replace the current bare postcard stream with a negotiated, versioned app↔worker protocol. PM-76 currently exposes `ResolvedResource::ByteSource { resource_uri, blob_cids }`; G must add its adjacent descriptor/route seam without duplicating PM-76’s schema, route table, or verification state machine. + +LDraw scale is exact: `1 LDU = 1/64 inch = 0.396875 mm = 0.000396875 m`; `0.4 mm` is display rounding only. Convert exactly once at the worker scene/IR boundary. + +## Isolation and safety gate + +Before editing source, run: + +```sh +jj workspace root +jj workspace list +jj bookmark list +jj status +jj log -r @ -n 1 +``` + +Stop before editing when the root is not `../epic-86-task-G`, the workspace is the default/Epic workspace, `@` is on `main`, or the bookmark is not `epic-86-task-G`. Start a fresh change: + +```sh +jj new -m "PM-80: compound LDraw resolver and versioned worker protocol" +``` + +Do not edit or commit on `main` or `../epic-86`. Do not edit parent-owned `.current-epic/manifest.txt`, `dependencies.dot`, `task-state.json`, `merge-gate.json`, or runtime logs. The task workspace consumes the copied contract and plan through `.current-task/task.md` and `.current-task/plan.md`. + +## Frozen dependency contracts + +1. Reuse PM-76 `LdrawResourceResolver`, `SqliteResolver`, `ResolvedResource`, `ResolveError`, `CacheKey`, `SourceIdentity`, `TargetIdentity`, `CanonicalPath`, and `RootId`. Do not create another resolver trait, cache tuple, root taxonomy, or verification state machine. Replace PM-76's stateful public mint/token route seam with deterministic stateless content-bound routes; do not retain or add a public route-token table merely for compatibility. +2. Public projected resources are anonymously viewable and require neither OAuth nor a bearer capability. The compound plan returns deterministic same-origin resource routes bound to the complete immutable project/publisher/source/root/canonical-path/ordered-target-CID and digest identity. The GET route canonicalizes and re-resolves that identity, serves exactly the bound content, or fails closed if it is malformed, out of scope, changed, or unavailable. Authentication and capability issuance are outside this public-resource task and belong only to a future explicitly permissioned/account-scoped resource policy. +3. The compound endpoint is an app-internal anonymous route, not a new lexicon and not a replacement for the generated one-resource XRPC procedure: + `POST /app/ldraw/compound-resources` with `{"project":"at://...","path":"models/main.ldr"}`. +4. The endpoint returns one complete plan containing `version`, `primary`, and ordered descriptors with canonical `key`, deterministic relative same-origin `route`, `path`, PM-76 `root`, byte length, SHA-256, MIME, typed source/binding metadata, and ordered target CIDs. It returns no PDS/publisher URL and gives the worker no ability to invent or broaden resource identities. +5. The server enumerates the complete MPD/manifest/transitive companion set before responding. It resolves every path through PM-76, requires verified bytes, derives one deterministic content-bound route per descriptor, deduplicates by canonical key in first-seen order, terminates cycles, and enforces the 16,384 descriptor and byte/metadata bounds. Any failure returns no partial plan; no route rows, tokens, rollback, or revocation are involved. +6. The worker fetches only supplied relative routes. It never constructs an absolute URL, sends an authorization token, calls the acquisition endpoint, or performs filename-to-network lookup. +7. PM-87 remains the sole owner of `LoadIdentity { load_epoch, scene_generation }`. Worker restart invalidates the old protocol session, events, and reservations; retains only a typed logical intent; bumps the epoch; replaces/remounts the canvas; renegotiates/resizes; and may reuse or deterministically reacquire the stateless plan after the new session is usable. +8. PM-88 reservations are acquired in its defined order before response buffering or parser entry. Reservations are bound to worker/load epoch/generation and released or transferred on every success, error, cancellation, stale result, supersession, revocation, disposal, and restart. +9. `LengthUnit::LdrawUnit` is appended after `Unknown`; no existing postcard discriminant may move. LDU conversion is performed once at the worker scene/IR boundary. + +### G-owned security controls that must be implemented, not deferred + +**Per-chunk CID verification (skeptic G-plan-1).** PM-76’s aggregate length/SHA-256 check is not sufficient to claim blob-CID substitution protection. Add a direct server dependency on the already lockfile-present CID/multihash implementation (prefer the compatible `cid`/`multihash` versions after checking Jacquard’s API; keep it server-only and out of the leaf protocol/worker dependency tree). In the shared verified-byte fetch path used by G, decode each stored CID, read its multihash code and digest, compute the supported digest over that complete fetched chunk, and reject malformed, unsupported, or mismatched CIDs before appending the chunk. Then verify exact aggregate length and SHA-256. Do not claim CID protection until Tier-1 vectors and real route tests cover substitution, malformed CID, wrong codec, truncation, and reorder cases. + +**Explicit subroot parsing and containment (skeptic G-plan-2).** Reuse PM-76’s `RootId` for source identity, but add a typed compound path-policy seam for standard LDraw subroots. Parse `s/`, `8/`, and `48/` as explicit first-segment prefixes only where the selected PM-76 root permits them; preserve the prefix in canonical lookup identity; reject malformed/unknown prefixes; and enforce that the resolved entry remains inside the requested root/subroot scope. An explicit prefix must never fall through to another root or resolve an unprefixed sibling. Add tests for valid `s`, `8`, `48`, case policy, missing prefix, cross-root candidate, and path escape. Do not describe root isolation as covered by generic `CanonicalPath` validation alone. + +## Grounding files and implementation skills + +Re-read these before implementation: + +- `src/ldraw/paths.rs`, `src/ldraw/resolve.rs`, `src/ldraw/mod.rs`, `src/appview/ldraw.rs`, and `src/appview/mod.rs` — PM-76 paths, resolver/cache/error contracts, current verification/fetch behavior, route mounting. +- `src/appview/tests.rs`, `migrations/009_ldraw_resources.sql`, and existing auth/router helpers — real SQLite and session fixtures. +- `lexicons/library/ldrawManifest.json`, `lexicons/library/ldrawResource.json`, and generated mint types — authored/generated boundary; do not hand-edit generated code. +- `crates/polymodel-renderer-protocol/src/lib.rs`, `src/load_state.rs`, `src/viewer.rs`, and `crates/polymodel-renderer-worker/src/worker.rs` — bare postcard contract, PM-87 state, bridge lifecycle, direct URL behavior. +- `crates/polymodel-mesh/src/companions.rs`, parser/reference-walker APIs, and PM-78/PM-79 plans — safe MPD and type-1 reference enumeration. +- `.current-epic/plan-A.md`, `.current-epic/plan-B.md`, `.current-epic/plan-C.md`, `.current-epic/deferrals-C.jsonl`, `justfile`, and the relevant crate `AGENTS.md` files. + +Load `jira-epic-task-execute`, `polytoken:investigating-a-codebase`, and `reviewing-code-for-shipping-quality` during implementation. Do not load `frontend-design`; this task has no Dioxus layout/CSS surface. + +## Replace-vs-edit assessment + +| Area | Decision | Reason | +| --- | --- | --- | +| `src/ldraw/paths.rs` / `resolve.rs` | Edit minimally; add a typed subroot-policy/parser seam and expose the shared CID-verification helper only where PM-76’s verified-byte path needs it. | PM-76’s `RootId`, precedence, cache key, SQL, and verification state remain authoritative; replacing these modules would duplicate security policy. | +| `src/appview/ldraw.rs` | Replace the stateful public mint/token handlers with a stateless deterministic content-bound GET handler while retaining shared verified-byte streaming. | Public immutable resources do not need bearer capabilities; preserving token storage would add lifecycle failure modes without enforcing a product requirement. | +| `src/appview/mod.rs` | Narrow edit to mount the compound POST route. | No route architecture replacement is needed. | +| `crates/polymodel-renderer-protocol` | Replace the bare wire contract with one envelope/handshake module and migrate all consumers together. | Adding fields to `StartLoad` would preserve incompatible postcard bytes and cannot enforce negotiation. | +| `src/viewer.rs` / `load_state.rs` | Edit the bridge/reducer lifecycle. | Keep PM-87 identity allocation and reducer ownership; add session/plan/restart gates rather than a second state machine. | +| `crates/polymodel-renderer-worker/src/worker.rs` | Replace only direct-URL LDraw orchestration. | The lookup/fetch path is fundamentally incompatible with exact descriptor plans; retain renderer setup, scene installation, preview, input, and frame loop. | +| Tests/fixtures/justfile | Add focused fixtures/tests; edit justfile only if a required existing command is not exposed. | Avoid broad build-script or generated-code churn. | + +## Numbered implementation steps + +### 1. Verify workspace and dependency contracts + +Run the safety gate, inspect the copied task/plan, PM-76/87/88 contracts, current tests, route/auth helpers, generated boundary, and `cargo tree`. Add direct server dependency declarations for CID/multihash verification only after confirming the compatible API and feature flags; do not rely on transitive lockfile presence. Confirm the dependency does not enter `polymodel-renderer-protocol` or the app’s wasm dependency tree. + +**Done when:** the isolated workspace and fresh jj change are confirmed, `main` is untouched, exact symbols/fixtures are identified, and the dependency graph proves the CID implementation is server-only. + +### 2. Implement explicit LDraw subroot policy + +Add pure typed parsing for `s/`, `8/`, and `48/` prefixes and a containment check layered on PM-76 `RootId`/`CanonicalPath`. Define which PM-76 uploaded and official roots accept each prefix, preserve canonical prefix identity, reject unknown/malformed prefixes, reject cross-root fallback, and ensure relative companion joins cannot escape the containing root/subroot. Keep MPD/manifest exact-case and uploaded/official ASCII-case rules as PM-76 defines them. + +**Done when:** Tier-1 tests prove valid standard prefixes, case behavior, out-of-root and cross-root rejection, explicit-prefix no-fallback, collision handling, and relative-join containment without a parallel `RootId` taxonomy. + +### 3. Implement per-chunk CID and aggregate byte verification + +Refactor the shared verified-byte fetch path so each chunk is fully buffered only within PM-88’s reservation limits, decoded as the stored CID, and checked against its multihash code/digest using the direct CID/multihash dependency. Reject malformed/unsupported/wrong-codec/mismatched CID before reassembly; then assert exact total length and SHA-256 over the raw ordered bytes. Preserve PM-76 verification CAS semantics: no mismatch may publish `verified`, and failed verification transitions through the existing rejection path. + +**Done when:** pure vectors and server tests cover valid CID/hash combinations, malformed CIDs, unsupported codes, substituted chunks, truncation, reorder, aggregate length mismatch, aggregate SHA mismatch, and no verified-state publication after any failure. + +### 4. Build the versioned protocol envelope and handshake + +Implement postcard envelope/version range negotiation, required/optional capability rules, deterministic selection, session metadata, and `Created → Negotiating → Negotiated | Failed`. Reject malformed, duplicate, conflicting, stale-session, pre-negotiation, and out-of-order messages. Migrate every app/worker command/event, current formats, and preview to the envelope in one cutover; leave only the established worker-ready/canvas bootstrap outside postcard and reject old bare scene-message bytes. + +**Done when:** Tier-1 golden fixtures cover success, no-overlap, required/optional capability failure, malformed/duplicate/conflict, out-of-order/pre-negotiation, stale session, and old raw-message rejection; no scene-dependent command bypasses `Negotiated`. + +### 5. Freeze wire metadata and LDU semantics + +Append `LengthUnit::LdrawUnit` after `Unknown`; carry negotiated version, namespace, full PM-87 identity, units, and either current-format source or a complete compound plan. Add independent golden bytes for negotiation, current-format, LDraw, malformed, stale, and old-message cases. Implement exact LDU conversion only at worker scene/IR handoff and retain raw LDU values in parsing/protocol payloads. + +**Done when:** discriminants are fixture-protected; 0/1/64/1000 LDU vectors and no-double-conversion tests pass; current STL/OBJ/glTF/GLB/3MF envelope round trips remain green. + +### 6. Add anonymous-capable all-or-nothing compound acquisition + +Mount `POST /app/ldraw/compound-resources`; deserialize typed `AtUri` and immediately parse `CanonicalPath`. Public projected Things and their resources must resolve without an OAuth session. Implement complete MPD and manifest enumeration: primary resolution, verified MPD `FILE`/type-1 first-seen traversal, manifest ordinal universe, relative canonical joins, explicit subroot containment, PM-76 precedence, exact-key dedupe, cycle termination, descriptor/byte bounds, and deterministic content-bound route derivation. Return no partial plan on any failure; write no route rows and mint no tokens. + +**Done when:** Tier-1 enumeration/property tests and Tier-3 Axum/SQLite tests prove logged-out public success, deterministic complete descriptor order and routes, MPD/manifest transitive coverage, prefix containment, malformed/missing/collision/source-change failure, no partial response, and no publisher/PDS URL-bearing response field. + +### 7. Implement stateless content-bound GET routes + +Replace the public mint/token redemption flow at `/xrpc/space.polymodel.library.getLdrawResource` with a deterministic GET identity supplied by the plan. Parse and canonicalize project/resource/publisher/source/root/path plus immutable ordered target-CID/digest identity, re-resolve through PM-76, compare the complete binding, and either stream exactly those publisher-owned bytes or fail closed. Use the verified-byte path from step 3. Do not require OAuth, cookies, browser identity, bearer tokens, route rows, expiry, revocation, atomic consumption, or replay state. + +**Done when:** Tier-3 real route tests prove repeated logged-out public success, signed-in parity, deterministic route stability, publisher/source/root/path/target/project substitution rejection, stale-projection/content-change rejection, per-chunk CID substitution rejection, canonical containment, and uniform fail-closed responses. + +### 8. Integrate bridge acquisition, negotiation, restart, retry, and cancellation + +Add handshake/session gates, typed pending logical intent, complete-plan acquisition before worker load, one fresh-batch retry for transport/5xx acquisition failures, typed route-failure handling, and PM-87 restart sequencing. Discard all old descriptors on retry/restart; release reservations; replace/remount canvas; renegotiate and resize; then acquire/reissue with a newer identity. Check cancellation before every fetch/parser/publication step and preserve preview/interactive namespace isolation. + +**Done when:** bridge/reducer tests prove bootstrap → negotiation → resize → complete plan → load ordering, stale-event rejection, fresh epoch and deterministic stateless-plan reuse/reacquisition after restart, deterministic canvas failure, cancellation, and reservation release. + +### 9. Replace worker LDraw filename lookup with exact plan loading + +Make the negotiated LDraw payload carry the complete plan. Validate descriptor key/route/metadata, look up primary and companions by exact canonical key, fetch only relative same-origin routes with credentials, verify response length/SHA-256 and target metadata, and pass in-memory bytes to the parser. Remove filename-to-URL construction, route minting, publisher/PDS access, and arbitrary parser network callbacks. Keep non-LDraw formats under the negotiated envelope and convert LDU exactly once at scene/IR handoff. + +**Done when:** Tier-4 wasm/worker tests prove unknown keys, absolute/publisher URLs, route substitution, missing companions, mismatched bytes, malformed metadata, arbitrary filename activation, and pre-negotiation loads fail closed before parser/scene publication. + +### 10. Integrate PM-88 admission and run cross-boundary coverage + +Reserve the full primary/companion workload before buffering or parsing in deterministic PM-88 order. Release/transfer on success, failure, cancellation, stale identity, supersession, revocation, disposal, and restart. Run configured browser/e2e coverage for MPD, manifest, prefixes, precedence, digest/CID/auth/replay/revocation, stale routes, protocol mismatch, cancellation, reservations, preview/interactive parity, LDU conversion, and current-format parity using deterministic interception/checkpoints rather than sleeps. + +**Done when:** Tier-1 ledger tests, Tier-3 route tests, Tier-4 worker/browser tests, and final renderer split checks assert zero premature parser entry, zero reservation leaks, exact route decisions, final reducer state, scene readiness, and complete-plan reacquisition. + +### 11. Validate, review, and commit + +Run from the isolated task workspace: + +```sh +just check +just fix +cargo test -p polymodel-renderer-protocol +cargo test -p polymodel-mesh +cargo test -p polymodel --lib +cargo nextest list -p polymodel --features server +just test-server +just build-renderer-worker +just verify-renderer-split +just test-renderer +cd e2e && npm test -- tests/viewer.spec.ts +cd .. && just test-all +``` + +Run `just sqlx-prepare` if production SQL/migrations changed. Review only necessary generated/SQLx changes. Load `reviewing-code-for-shipping-quality`, inspect `jj diff`, `jj diff --stat`, and `jj status`, and resolve all Critical/High findings. + +**Done when:** all relevant commands pass; direct CID/multihash verification is tested; root prefix/containment tests pass; the worker dependency split contains no `three-d`, `three-d-asset`, `stl_io`, or `polymodel-mesh` in the app target; browser coverage passes; and the task commit is on `epic-86-task-G` with exact trailers: + +```text +Epic: PM-86 +Task: PM-80 +``` + +## Test strategy by behavior + +| Behavior | Tier and concrete infrastructure | +| --- | --- | +| Envelope/handshake/wire bytes | Tier 1 protocol unit/state tests with committed independent postcard fixtures for success and every malformed, capability, ordering, stale, and old-byte failure. | +| LDU semantics | Tier 1 exact enum/discriminant/conversion vectors; Tier 4 worker scene test proves one conversion and no double conversion. | +| PM-87 identity/restart | Tier 1 bridge/reducer tests for stale session/epoch/generation, cancellation, supersession, and fresh identity; Tier 4 delayed browser test proves complete-plan reacquisition after worker replacement. | +| Canonical path and subroot policy | Tier 1 pure table/property tests for NFC, traversal, percent/backslash/control rejection, `s/`/`8/`/`48/`, root permission, containment, collisions, case rules, and explicit-prefix no-fallback. | +| MPD/manifest enumeration | Tier 1 parser/reference-walker tests for FILE/type-1 transitive traversal, ordinal order, relative joins, dedupe, cycles, out-of-universe references, bounds, and root containment; Tier 3 endpoint tests use real projected SQLite data. | +| Batch descriptors/routes | Tier 1 descriptor tests prove required binding metadata, deterministic content-bound relative routes, exact key map, verified-only construction, and no partial plan; Tier 3 tests prove repeated stateless GET resolution against PM-76 identity/verification. | +| CID and byte integrity | Tier 1 known multihash/CID vectors, malformed/unsupported/wrong-codec and chunk substitution/truncation/reorder tests; Tier 3 route/verification tests prove no bytes after per-chunk CID, length, SHA, or stale-binding failure. | +| Public route/substitution | Tier 3 real Axum/SQLite tests prove repeated logged-out public viewing, signed-in parity without identity dependence, deterministic route stability, publisher/source binding, complete project/root/path/target substitution rejection, and stale projection/content rejection. | +| Worker no-lookup boundary | Tier 4 wasm/bridge tests prove only supplied relative routes are fetched, no absolute/publisher URL is accepted, exact keys are required, and missing companions never trigger fallback lookup. | +| PM-88 admission/cleanup | Tier 1 deterministic ledger/rollback tests plus Tier 4 worker/browser tests for reserve-before-buffer/parse and zero leaks across success/error/cancel/stale/revocation/supersession/disposal/restart. | +| Current formats and preview | Tier 1 envelope round trips; Tier 4 configured renderer/browser tests for STL, OBJ, glTF/GLB, 3MF, preview/interactive separation, and readiness ordering. | +| Renderer split/regression | `just verify-renderer-split`, explicit wasm cargo-tree assertion, and final `just test-all`. | + +No provider/daemon or TUI tier applies to this task: its behavior is protocol, appview, resolver, wasm-worker, and browser behavior. + +## Implementation review expectations + +Reviewers must verify: + +1. PM-76 `RootId`, resolver, frozen errors, cache identity, and verification semantics are reused; the public token/route-row lifecycle is removed rather than duplicated. +2. The compound POST route supports logged-out public viewing and remains complete, deterministic, bounded, explicit-prefix contained, and no-partial-result. +3. The worker receives only exact-key deterministic content-bound same-origin route descriptors and cannot synthesize URLs, broaden resource identities, or access publisher/PDS URLs. +4. GET publisher/source resolution, canonical identity parsing, complete immutable binding comparison, stale-content rejection, and uniform fail-closed errors are tested at the real Axum boundary. +5. Every fetched chunk’s CID multihash is decoded and compared with the complete chunk bytes before aggregate length/SHA verification; no CID-substitution protection claim rests on a string parse alone. +6. Protocol envelope cutover is mandatory, old bare postcard bytes are rejected, and exact golden bytes are committed. +7. `LengthUnit::LdrawUnit` follows `Unknown`, exact LDU semantics are tested, and conversion occurs once. +8. Restart invalidates old sessions/routes/events, releases work, bumps the epoch, reacquires a complete plan, renegotiates/resizes, and deterministically reissues or fails. +9. PM-88 reservations precede buffering/parser entry and clean up on every terminal/race path; preview remains separate. +10. Generated APIs/assets and the app/worker dependency split remain clean. + +Resolve all Critical and High findings before parent handoff. Remaining risk must be an observed command/tool failure, not an accepted CID-verification or root-isolation gap. + +## Ready-for-parent-merge handoff + +Return the task workspace/bookmark and final jj change ID; `jj status`; `jj diff --stat`; complete diff-review result; changed-file list; exact results for every validation command; golden-wire, path/subroot/precedence, batch enumeration, CID/byte-integrity, deterministic stateless route, substitution/stale-content, restart, cancellation, reservation-cleanup, preview/interactive, logged-out browser, and ordinary-format results; confirmation that PM-76 resolver/cache/verification semantics were reused while its public token lifecycle was removed; reviewer findings/dispositions; and concrete remaining risks. Confirm the commit message contains: + +```text +Epic: PM-86 +Task: PM-80 +``` + +## Prior finding dispositions + +- **G-plan-1-F1 — fixed and retained:** The plan distinguishes PM-76’s `ResolvedResource::ByteSource` from G’s adjacent typed descriptor/route seam, specifies the concrete POST response, relative opaque route, no-URL worker rule, and tests. +- **G-plan-1-F2 — superseded by operator product correction:** Public Things must remain viewable while logged out and public immutable files need no bearer capability. The plan replaces token mint/redemption with deterministic stateless content-bound routes, complete publisher/project/source/root/path/target binding re-resolution, stale-content and substitution rejection, and repeated anonymous GET tests at the real route boundary. +- **G-plan-1-F3 — fixed and retained:** The plan mandates one versioned envelope cutover, migrates all app/worker postcard consumers together, rejects old raw scene bytes, and freezes independent golden fixtures. +- **G-plan-1-F4 — fixed and retained:** The plan defines exact LDU physical scale, display-rounding distinction, one scene/IR conversion boundary, and exact/no-double-conversion tests. +- **G-plan-1-F5 — fixed and retained:** The plan reuses PM-76 `RootId`; the new subroot policy is a path-containment policy, not a parallel LDraw source/tier taxonomy. +- **G-plan-1-F6 — fixed and retained:** The plan requires appending `LengthUnit::LdrawUnit` after `Unknown` and protects existing postcard discriminants with fixtures. +- **G-plan-1-F7 — fixed with stateless-route simplification:** The plan specifies logical-intent retention, reservation cleanup, fresh epoch/session, old-event invalidation, deterministic stateless-plan reuse/reacquisition, replacement canvas/resize, renegotiation, and deterministic failure on restart; no token revocation or route reissuance lifecycle remains. +- **G-plan-2-F1 — fixed with operator product correction:** The plan names `POST /app/ldraw/compound-resources`, typed request/response semantics, anonymous public acquisition, deterministic content-bound descriptor routes, six-stage complete enumeration behavior, ordering/dedupe/cycle/bounds, no-partial-result behavior, retry behavior, and worker no-lookup rules. +- **G-plan-skeptic-1 — fixed:** G now explicitly implements per-chunk CID multihash decoding and digest verification with a direct server dependency, tests malformed/unsupported/substituted/truncated/reordered chunks, and forbids claiming CID protection from aggregate SHA alone. +- **G-plan-skeptic-2 — fixed:** G now explicitly parses `s/`, `8/`, and `48/` prefixes, enforces permitted-root/subroot containment, rejects cross-root fallback, and includes pure, route, and browser acceptance vectors; generic path validation is not treated as root isolation. diff --git a/.current-epic/plan-L.md b/.current-epic/plan-L.md new file mode 100644 index 0000000..593437f --- /dev/null +++ b/.current-epic/plan-L.md @@ -0,0 +1,243 @@ +# PM-89 — Ledger-backed WASM memory enforcement + +## Task identity and execution boundary + +- **Epic:** PM-86 — LDraw/LEGO model support and flexible renderer +- **Epic URL:** https://radiant-industries.atlassian.net/browse/PM-86 +- **Task:** PM-89 — Ledger-backed WASM memory enforcement +- **Task URL:** https://radiant-industries.atlassian.net/browse/PM-89 +- **Task prefix:** L +- **Task cache:** `.current-epic/task-L.md` +- **Epic workspace:** `.` (the provisioned epic workspace; runtime manifest records `../epic-86`) +- **Base bookmark:** `main` +- **Expected task workspace:** `../epic-86-task-L` +- **Expected task bookmark:** `epic-86-task-L` +- **Dependency:** B / PM-88, merged into `epic-86` +- **Plan output:** `.current-epic/plan-L.md` + +This task changes the worker’s real WASM memory-growth boundary. The ledger crate remains dependency-light and `#![forbid(unsafe_code)]`; the Talc `Source` implementation belongs in the WASM worker, where the unsafe allocator integration is already isolated. + +## Safety and grounding checks + +Before editing in the provisioned task workspace, run: + +```sh +jj workspace root +jj bookmark list +jj status --no-pager +``` + +Stop immediately if the workspace is the default workspace on `main`, the current bookmark/change is not `epic-86-task-L`, or the workspace is not `../epic-86-task-L`. The implementor must then read `.current-task/task.md` and `.current-task/plan.md` in that task workspace. Do not edit `.current-epic/manifest.txt`, `.current-epic/dependencies.dot`, `.current-epic/task-state.json`, `.current-epic/merge-gate.json`, or other Epic runtime files. + +The implementor must use jj, start from a fresh task change, and must not create or move workspaces/bookmarks. The parent owns task-runtime state and merge operations. + +## Context + +`polymodel-renderer-ledger` currently admits caller-supplied WASM estimates at representative worker boundaries, but the global allocator still uses `WasmGrowAndClaim`, so an unanticipated Rust allocation can invoke `memory.grow` without consulting the ledger. Talc 5.0.4 calls `Source::acquire` before each growth request; PM-89 must replace the source with a ledger-backed wrapper around `WasmGrowAndExtend`, rejecting growth before delegation when the worker-wide `WasmPages` budget is exhausted. + +The source callback runs while Talc’s allocator lock is held. The existing generic `ReservationLedger::reserve` uses mutex-protected `HashMap` state and may allocate reservation metadata, so calling it directly from `Source::acquire` would risk allocator re-entry/deadlock. The implementation must add an allocator-safe, fixed-state page-growth admission path in the ledger (or an equivalent preallocated path) and make the worker’s normal `ReservationLedger` view share that exact page state. The plan must not claim enforcement while retaining an independent source-side counter. + +## Replace-vs-edit assessment + +- **`crates/polymodel-renderer-worker/src/worker.rs` allocator block — replace the allocator block, do not add branches to it.** The current two-line `WasmGrowAndClaim` static is fundamentally the wrong integration point. Replace it with a dedicated `allocator` module/static using `LedgerBackedSource`, while preserving the worker state and message/frame-loop architecture. Extracting the source into `src/allocator.rs` is preferred because more than half of the allocator integration will be new and must be reviewed independently from renderer behavior. +- **`crates/polymodel-renderer-ledger/src/lib.rs` page accounting — extend the ledger with a dedicated allocator-safe page state, not a wholesale rewrite.** Preserve the generic synchronized ledger API and existing policy tests. Add a fixed-capacity/atomic or otherwise preallocated `WasmPageLedger`/page-growth handle whose reservation and release operations cannot allocate or call the global allocator. Its snapshot integration must reflect the same worker-wide page total used by the source. Do not route the Talc callback through the existing dynamically growing reservation map. +- **Worker WASM estimate call sites — edit, removing duplicate `ResourceClass::WasmPages` estimates from load/scene composites.** Once actual growth is charged by the source, retaining caller estimates would double count the worker bucket and cause false rejection. Keep the other class reservations and their cleanup unchanged; their estimates remain complementary admission controls, not substitutes for actual page enforcement. +- **`crates/polymodel-renderer-ledger/README.md` — rewrite the WASM growth strategy section, not a wording patch.** The current section explicitly says low-level growth cannot be enforced and calls representative pre-admission “equivalent”; that premise is obsolete and dishonest after PM-89. Replace it with the actual Talc source boundary, the no-reentrancy constraint, worker-wide high-water semantics, and the remaining limits of the boundary. +- **Existing worker tests — extend the current WASM test module and add focused allocator tests.** Do not replace renderer lifecycle tests. Add a small deterministic source/ledger test surface plus browser-backed `wasm_bindgen_test` coverage that drives the production source or an exact production-configured `TalcCell` instantiation. +- **`Cargo.toml`, `justfile`, generated files, and app/UI code — edit only if validation exposes a required dependency/recipe change.** Talc 5.0.4 and the worker ledger dependency already exist. Do not introduce renderer dependencies into the ledger leaf or edit generated API/assets. + +## Required implementation skills + +- Load `reviewing-code-for-shipping-quality` for the final implementation/diff pass. This is allocator safety code; remove process residue and ensure comments explain invariants rather than narrating the task. +- No `frontend-design` skill is needed; this task has no UI surface. + +## Numbered implementation steps + +### 1. Verify isolation and reconstruct the Talc contract + +Read the task-local cache/plan after the safety checks, then inspect the pinned Talc 5.0.4 source used by Cargo. Confirm these signatures and invariants before coding: + +- `unsafe trait talc::source::Source`; +- `Source::acquire(&mut Talc, Layout) -> Result<(), ()>`; +- `Source::resize(&mut self, chunk_base, heap_end, is_heap_base) -> *mut u8`; +- `WasmGrowAndExtend`’s exact page calculation and its `memory_grow` → `extend`/`claim` sequence; +- Talc’s warning that a source must not use its parent `TalcLock` or indirectly invoke the global allocator; +- `TalcLock::new` const initialization and the worker’s `#[global_allocator]` target. + +Do not copy an assumed `heap_end: *mut u8` acquire signature from an outdated document; the installed Talc API passes `&mut Talc` and `Layout`. + +**Done when:** the implementation notes/tests use the installed 5.0.4 API, the source never logs/allocates/re-enters Talc, and the worker remains a `wasm32-unknown-unknown`-only global allocator. + +### 2. Add allocator-safe shared WASM page accounting to the ledger + +Extend `polymodel-renderer-ledger` with a narrowly scoped page-growth state/API that can be used from a Talc source without allocation. The design must satisfy all of the following: + +1. The page state is initialized in a const-safe way for a worker static and can also be attached to the runtime `ReservationLedger` used by `WorkerState`, so source and snapshots cannot diverge into two budgets. +2. Admission and release use fixed storage/atomics or preallocated metadata and cannot allocate, grow a `HashMap`, format an error, or call any path that could re-enter the global allocator. A `Mutex` call from `Source::acquire` is not acceptable. +3. The page budget is the PM-88 worker-wide `ResourceClass::WasmPages` bucket: one worker owner bucket, `768 MiB` per worker, `1 GiB` global class ceiling, 64 KiB page units, and no per-load multiplication. The source uses a fixed worker owner for the physical high-water counter; load/generation owners remain diagnostic inputs to ordinary reservations only. +4. The page state exposes an idempotent release/drop path suitable for a source-owned reservation token. If a source tracks one cumulative high-water reservation, update it without creating per-growth heap metadata. The page ledger/snapshot must make the actual committed page count inspectable in bytes/pages. +5. The generic ledger’s existing `Reservation`/composite behavior remains correct. Remove or prohibit duplicate manual `WasmPages` reservations from worker call sites once actual source growth is bound. If generic tests retain a direct WASM reservation API, define clearly how it shares the page state and cannot double count it. +6. Accounting must be monotonic with WebAssembly memory: `memory.grow` cannot shrink the physical memory. A Talc logical heap trim is not permission to release a committed-page reservation unless the delegated source demonstrably returns/reclaims the underlying committed region. For `WasmGrowAndExtend`, retain the high-water charge across normal `resize` callbacks; release it only when the source is dropped/teardown makes the allocator unusable. Document this explicitly. + +Use checked page/byte conversion at the ledger boundary (`pages * 65_536`) and reject overflow without mutation. Preserve `#![forbid(unsafe_code)]` in this crate. + +**Done when:** a source callback can reserve/release page budget with no dynamic allocation or global-allocator re-entry; the runtime ledger and source share one page counter; snapshots report actual page bytes; exact-limit/limit+1, overflow, idempotent release, and shared-worker cases have deterministic tests. + +### 3. Implement `LedgerBackedSource` around `WasmGrowAndExtend` + +Create the worker-only allocator module (or an equivalently isolated module in `worker.rs`) containing `LedgerBackedSource`, with `Inner` instantiated as `WasmGrowAndExtend` in production. + +Implement `Debug`/required bounds and the unsafe `Source` contract with comments tied to Talc 5.0.4. The source must: + +1. Calculate the same required page delta as Talc’s pinned `WasmGrowAndExtend` implementation, including chunk overhead and alignment padding, using checked `usize` arithmetic. Keep the calculation in one helper and add boundary vectors around one-page and page-minus-overhead layouts so a future Talc upgrade cannot silently drift. +2. Attempt page admission before delegating to `Inner::acquire`. A rejected admission returns `Err(())` and does not call the inner source or `memory_grow`. +3. Delegate only after admission succeeds. If delegation succeeds, retain the committed page reservation. If delegation fails before physical growth, release the tentative reservation; if the inner source has already grown WebAssembly memory before reporting failure, reconcile from the observed page delta and retain the physical high-water charge rather than undercounting. The implementation must define and test this distinction rather than blindly dropping every failed reservation. +4. Never create a `Vec`, `Box`, `String`, formatted error, `Reservation` backed by a growing map, tracing event, or other allocating object inside `acquire`, `resize`, `Drop`, or any helper reachable from those methods. +5. Set `TRACK_HEAP_END` deliberately. Implement `resize` only according to the installed Talc contract and delegate to the inner source; do not release committed WASM pages merely because Talc reports a smaller logical heap. If a future inner source actually reclaims pages, release only the verified delta with checked arithmetic. The production `WasmGrowAndExtend` path must preserve high-water accounting. +6. Implement `Drop` for source-owned page state so a local/test allocator releases its reservation exactly once. Explain that a process global normally lives for the worker lifetime; drop is still required for local tests and any explicit allocator teardown. + +**Done when:** `LedgerBackedSource` compiles as the global allocator, rejected growth returns allocator failure before `memory_grow`, accepted growth delegates normally, failed delegation cannot leak/undercount a known physical grow, and resize/drop semantics preserve the actual committed-page invariant. + +### 4. Replace the worker global allocator and wire the shared runtime ledger + +Replace the `WasmGrowAndClaim` static with `TalcLock, WasmBinning>`. Define one worker-wide page state/static and pass the same state into `LedgerBackedSource` and `WorkerState`’s `ReservationLedger` view. Initialization must be const-safe for the allocator static and must not depend on a runtime `WorkerState` existing before early allocations. + +Update worker ownership and cleanup so: + +- physical page growth is charged to the worker bucket, not separately to each load epoch/scene generation; +- load and scene composites no longer include estimated `ResourceClass::WasmPages` entries, preventing double counting; +- all non-WASM class reservations retain their existing owner/epoch/generation cleanup behavior; +- worker disposal retires ordinary identities and releases ordinary reservations, while page high-water state remains until allocator teardown/worker lifetime ends; +- the page snapshot used by diagnostics/tests is the exact source-backed value. + +Do not make the source consult `WorkerState`, `Rc>`, browser objects, or a mutable load identity: the callback can occur during any Rust allocation and must remain independent of the message loop. + +**Done when:** no production worker path charges the same physical page growth both as an estimate and as a source reservation; the static source and `WorkerState.ledger` expose the same page total; normal renderer operations still use the existing ledger for all other classes and lifecycle behavior. + +### 5. Rewrite the ledger README honestly + +Replace the current “WASM growth strategy record” with documentation that states: + +- Talc 5.0.4’s `Source::acquire` is now the real pre-`memory.grow` enforcement boundary for the worker’s Rust global allocator; +- `LedgerBackedSource` reserves the required aligned page delta before delegating to `WasmGrowAndExtend` and returns allocator failure when the worker-wide page ceiling rejects it; +- the physical counter is a monotonic committed/high-water page budget because WebAssembly memory does not shrink through normal Talc heap trimming; +- allocator callbacks use an allocation-free fixed page path because the callback executes under Talc’s lock; +- other classes (network, parser/source, scene, GPU, canvas, and texture) remain explicit caller-owned reservations and are not magically inferred from raw allocator growth; +- the boundary covers Rust allocations using this worker global allocator, not arbitrary JS/host allocations or a different WebAssembly memory/module; +- rejection is allocator failure/OOM, not silent growth, and callers must preserve error/cleanup semantics. + +Remove the old imported-memory/instrumentation “equivalent enforcement” justification. If toolchain/runtime limitations are mentioned, distinguish them from the now-enforced allocator boundary and do not claim representative pre-admission alone enforces unanticipated allocations. + +**Done when:** a reader can identify exactly what is enforced, what remains estimate-based, how high-water release works, and what is outside the boundary; no README sentence says that representative caller admission is equivalent to allocator enforcement. + +### 6. Add focused unit and browser-WASM tests + +Add tests at the lowest tier that proves each behavior, while ensuring the integrated proof uses the production source configuration. + +**Tier 1 / dependency-light tests:** + +- page conversion and required-delta helper: exact one-page, page-minus-overhead, alignment-padding, overflow, and large-layout vectors; +- fixed page state: exact budget succeeds, limit+1 rejects without mutation, release restores capacity, duplicate release is harmless, and two load identities consume one worker bucket; +- source state machine with a non-growing/mock inner source where possible: reserve-before-delegate ordering, no delegation after rejection, successful delegation retention, failed delegation rollback, and drop cleanup; +- resize semantics: logical heap-end callbacks never release committed WASM high-water for `WasmGrowAndExtend`; a verified reclaiming inner source releases only the checked shrink delta; +- shared view: the ledger snapshot used by worker code reports the source’s page total and no manual estimate remains in the page class. + +**Tier 3/worker browser-WASM tests via existing `wasm_bindgen_test`:** + +- instantiate a production-shaped `TalcCell`/source fixture using `LedgerBackedSource` and a deliberately small test page budget, not the process global static’s default ceiling; +- allocate a small object below the budget and assert success plus page accounting equal to the observed `memory_size` growth delta (allowing only documented Talc bootstrap pages in the fixture’s baseline); +- exhaust the page budget, attempt an allocation that requires another growth, assert a null/allocator failure or `AllocError`/panic according to the chosen API, and assert the observed WebAssembly page count did not increase after rejection; +- assert an admitted allocation delegates and grows normally, and that deallocation/heap trimming does not falsely reduce the physical high-water counter; +- exercise a realloc path that triggers a new growth, verifying the same gate and no double charge when Talc grows in place without invoking `acquire`; +- run a shared-worker scenario with two load identities/ledger views and prove the second cannot obtain a second worker entitlement. + +Use the existing `just test-renderer` recipe/runner and add a focused recipe only if the implementation adds a distinct test target. Do not use TestProvider, daemon integration, TUI `TestBackend`, or a fake allocator in place of the browser-WASM source test. + +**Done when:** tests fail if rejected allocation increases `memory_size` or reaches the inner growth call, pass for admitted allocation, prove actual page accounting, cover rollback/drop/resize, and exercise the production source type rather than only a look-alike counter. + +### 7. Validate, review, and commit + +Run the repository’s normal sequence after the first build has generated any required environment module: + +```sh +just check +just fix +cargo test -p polymodel-renderer-ledger +cargo check -p polymodel-renderer-ledger --target wasm32-unknown-unknown +RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo check -p polymodel-renderer-worker --target wasm32-unknown-unknown +just test-renderer +just lint +just test-all +``` + +Run the focused worker browser command separately if the new tests are not covered by `just test-renderer`: + +```sh +RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo test -p polymodel-renderer-worker --target wasm32-unknown-unknown +``` + +Inspect `jj diff`, the changed-file list, and the final worker dependency tree. Confirm no generated files, public worker artifacts, unrelated renderer behavior, or process notes were added. Load `reviewing-code-for-shipping-quality` and remove AI/process residue before commit. + +Commit on `epic-86-task-L` with a concise message such as `PM-89: enforce WASM growth through ledger` and these required trailers: + +```text +Epic: PM-86 +Task: PM-89 +``` + +**Done when:** `just check`, `just fix`, focused ledger tests, worker WASM check, relevant browser-WASM tests, lint, and `just test-all` pass (or any failure is reported as an observed command failure); `jj diff` is limited to the planned files; the commit contains both required trailers. + +## Test strategy matrix + +| Behavior | Tier and concrete proof | +| --- | --- | +| Required page delta matches Talc 5.0.4 | Tier 1 helper vectors copied from the pinned `WasmGrowAndExtend` formula: chunk overhead, alignment, one-page boundaries, and overflow. | +| Page budget exact/limit+1 | Tier 1 fixed-page-ledger tests with a small configured budget; exact succeeds, one page over rejects with byte-for-byte unchanged state. | +| Allocation-free callback path | Tier 1 source tests plus code review: no dynamic containers, formatting, tracing, generic HashMap reservation, or parent Talc access reachable from `acquire`, `resize`, or `Drop`. WASM compilation validates bounds and cfg wiring. | +| Reserve before real growth | Browser-WASM Tier 3 production-source test with an inner-call/growth observation; rejected acquire leaves `memory_size` unchanged and never delegates. | +| Accepted allocation | Browser-WASM Tier 3 allocation below the budget succeeds, delegates to `WasmGrowAndExtend`, and records the observed growth/pages. | +| Exhausted-budget OOM | Browser-WASM Tier 3 consumes the test budget, attempts a growth-requiring allocation, and asserts allocator failure plus no additional `memory_size` pages. | +| Realloc growth | Browser-WASM Tier 3 forces a realloc that cannot fit in place; the new growth is gated and charged once. In-place realloc that does not call `acquire` does not create a page reservation. | +| Delegate failure | Tier 1 mock-inner test distinguishes failure before growth from a known physical grow followed by claim failure; reservations are rolled back/retained according to observed physical state. | +| Resize/high-water | Tier 1 and browser-WASM tests prove logical Talc heap trimming does not release monotonic WebAssembly committed pages; only a verified reclaiming source may release a checked delta. | +| Drop/release | Tier 1 fixed-state tests and local source drop test prove idempotent release and no stale page charge after allocator teardown. | +| Shared worker semantics | Tier 1 two-owner tests and browser-WASM worker fixture prove all load epochs share one worker-wide 768 MiB bucket and cannot multiply the entitlement. | +| Ledger/source identity | Tier 1 snapshot test plus worker integration test prove `WorkerState` sees the exact source-backed page total, with no duplicate manual `WasmPages` estimate. | +| Existing worker lifecycle | Existing worker browser suite, `just test-renderer`, and `just test-all` cover load/scene/preview/canvas behavior; changed page estimates must not break ordinary non-page reservations or cleanup. | +| Dependency/target boundary | `cargo tree -p polymodel-renderer-ledger --target wasm32-unknown-unknown --edges normal` remains dependency-free; `just check` compiles worker WASM; `just verify-renderer-split` remains clean. | +| Documentation honesty | Review README against the actual source and tests: it names the Talc boundary, high-water semantics, allocation-free constraint, and out-of-bound host/JS allocations without “equivalent” pre-admission claims. | + +This task does not use provider, daemon, or TUI test tiers because the behavior is inside a browser WebAssembly allocator and must be tested through Talc/WASM memory growth. + +## Implementation review expectations + +Reviewers must inspect the complete diff and verify: + +1. `WasmGrowAndClaim` is absent from the worker global allocator and the production type is `LedgerBackedSource`. +2. `Source::acquire` reserves before delegation, returns `Err(())` before `memory_grow` on rejection, and cannot allocate or re-enter Talc. +3. The source and `WorkerState` use one page state; no independent source-side budget or duplicate manual `WasmPages` charge can drift from the snapshot. +4. Required page calculation is checked and locked to Talc 5.0.4 behavior, with boundary tests. +5. Inner failure, resize, and drop paths cannot leak or falsely release physical high-water accounting. +6. The worker-wide page bucket is shared across load epochs and uses PM-88’s 768 MiB worker / 1 GiB global ceilings. +7. The ledger crate retains `#![forbid(unsafe_code)]`, no forbidden renderer/browser dependency enters it, and allocator-only unsafe code is isolated to the worker source module. +8. Browser-WASM tests prove both admitted growth and exhausted-budget failure without an increase in actual WebAssembly pages. +9. The README no longer defends estimate-only admission as equivalent to allocator enforcement and clearly states the remaining boundary limits. +10. No generated artifacts, unrelated UI/renderer changes, AI/process residue, or weakened validation checks are present. + +## Final ready-for-parent-merge handoff + +Before returning to the parent, report: + +- task bookmark `epic-86-task-L` and commit/change ID; +- complete changed-file list; +- exact results for ledger native tests, ledger WASM check, worker WASM check, browser-WASM tests, `just check`, `just fix`, `just lint`, and `just test-all`; +- confirmation that the static Talc source and runtime ledger share page accounting; +- confirmation that exhausted-budget allocation does not call `memory.grow` and admitted allocation does; +- confirmation of README scope/boundary wording and no stale “equivalent” claim; +- `jj diff` review result and any observed blockers. + +The task is merge-ready only when the allocator gate is real, the page state is shared and allocator-safe, the physical high-water invariant is tested, and the implementation is not a scaffold depending on a later task. + +## Prior finding dispositions + +No prior reviewer findings were supplied for planner-L1. There are no finding dispositions to carry forward. diff --git a/.current-epic/root-cause-ldparse-fiction.md b/.current-epic/root-cause-ldparse-fiction.md new file mode 100644 index 0000000..9cd5daa --- /dev/null +++ b/.current-epic/root-cause-ldparse-fiction.md @@ -0,0 +1,44 @@ +# Root Cause: LDParse Fiction (2026-08-02) + +## What happened + +PM-77 (Task D) and PM-78 (Task E) were planned, implemented, reviewed, and merged/progressed on a false premise: that Thomas Dickerson's LDParse was a Python library. In reality, LDParse is a C++ library at `elfprince13/LDParse`. + +## Failure chain + +1. **PM-75 design** (Confluence) correctly references `elfprince13/LDParse` with two pinned revisions. No language claim. +2. **D's planner** assumed Python: built `ldparse_driver.py`, `uv.lock`, `pyproject.toml`, CPython 3.12.8 pin. No verification that the referenced library was Python. +3. **Provenance** used wrong GitHub username (`taliesin` instead of `elfprince13`) → HTTP 404. +4. **"Blocked" status** in `artifact-provenance.json` was accepted by every gate: plan review (3 reviewers), plan deferral skeptic, D implementation review (4 reviewers), D completeness skeptic, orchestrator test gate, and parent merge. +5. **Expected records** were generated from a naive heuristic `canonical()` function using substring matching, not from LDraw spec or reference behavior. +6. **D merged** with fictional harness and tautological expected records. +7. **E's planner** built on D's broken foundation. E's implementation was a skeleton parser built from plan text without studying any reference source. +8. **Multiple E review cycles** (3 plan generations, 4 implementation reviewers) identified symptoms (parser incomplete, BFC wrong, no graph traversal) but never traced the root cause to the missing reference. + +## What would prevent this + +### 1. External reference verification gate +Before a plan references an external library/repository, the planner must: +- Fetch or clone the referenced source +- Cite specific files, types, and functions that inform the plan +- If the reference cannot be accessed, mark the task `blocked-failure`, not "blocked" with a silent skip + +The plan deferral skeptic should verify that cited external references resolve. A 404 on a core reference is not an acceptable "blocked" status. + +### 2. Provenance integrity +A "blocked" acquisition in provenance metadata should be a hard blocker, not a note that gets accepted. If a pinned reference returns 404, the task cannot proceed until the reference is resolved or explicitly re-scoped by the operator. + +### 3. Reference study requirement for port/mirror tasks +When a task says "mirror architecture/behaviour of X", the planner's first step must be to study X and produce an architecture summary citing specific source files. This summary becomes a required plan input. If the planner cannot study X, the task is blocked. + +### 4. Foundation verification for dependent tasks +When task E depends on task D's corpus/gates, E's planner should verify D's corpus was built from real reference data, not from a heuristic generator. The plan review should check that expected records trace to spec/reference, not to a function in the same crate. + +## Corrective actions taken + +- PM-77 reopened in Jira with corrected description citing C++ reference +- PM-78 updated with C++ source file list and local clone path +- PM-80 commented noting dependency on E rework +- LDParse cloned to `/home/orual/Projects/LDParse-reference` (modernization branch `0fe78dc`) +- D, E, G reset to pending in task-state +- C++ source study dispatched to inform new plans diff --git a/.current-epic/task-L.md b/.current-epic/task-L.md new file mode 100644 index 0000000..698ee66 --- /dev/null +++ b/.current-epic/task-L.md @@ -0,0 +1,34 @@ +# Task L — Dynamic: Ledger-backed WASM memory enforcement + +**Created by:** Orchestrator (dynamic task) +**Depends on:** B (PM-88, merged) +**Epic:** PM-86 + +## Goal + +Replace the talc allocator's `WasmGrowAndClaim` growth source with a `LedgerBackedSource` that checks the reservation ledger before every `memory.grow`. This makes the ledger a real enforcement boundary instead of a cooperating counter. + +## Context + +The current ledger (`polymodel-renderer-ledger`) is `#![forbid(unsafe_code)]`, std-only, and has no connection to the actual allocator. It checks caller-supplied estimates at representative call sites but cannot prevent unanticipated allocations from growing memory. + +Talc 5.0.4 is the global allocator: `TalcLock`. Talc exposes a `Source` trait where `Source::acquire` is called on every memory growth request. By implementing a custom `Source` that wraps `WasmGrowAndExtend` and checks the ledger before delegating, we get real enforcement: if the budget is exceeded, `memory.grow` is never called and the allocation fails. + +## Scope + +1. Create a `LedgerBackedSource` implementing talc's `Source` trait +2. On `acquire`: calculate the page delta, check `ledger.reserve(owner, ResourceClass::WasmPages, bytes)`, delegate to inner source on success, return `Err(())` on failure +3. Track reservations for release on `resize`/drop +4. Replace `WasmGrowAndClaim` with `LedgerBackedSource` in the worker's `#[global_allocator]` +5. Update the ledger README to remove the "cannot enforce" justification and document the real enforcement boundary +6. Add tests proving that allocation fails when the ledger budget is exhausted +7. Keep the ledger crate itself `#![forbid(unsafe_code)]`; the `LedgerBackedSource` lives in the worker crate (allocator code, `unsafe` is appropriate) + +## Acceptance + +- When the ledger's `WasmPages` budget is exhausted, new allocations fail (panic-on-OOM or alloc_error, not silent growth) +- When the budget is not exhausted, allocations succeed normally +- The ledger's accounting matches actual memory pages grown (not estimates) +- The README documents the real enforcement boundary honestly +- `just check` and `just fix` pass +- Worker wasm check passes diff --git a/.current-task/dependency-summary.md b/.current-task/dependency-summary.md deleted file mode 100644 index f28333a..0000000 --- a/.current-task/dependency-summary.md +++ /dev/null @@ -1,10 +0,0 @@ -# Dependency Summary — Task D - -Epic: PM-86 (epic-86 workspace) -Base bookmark: main -Epic workspace: ../epic-86 - -Dependencies: Task D has NO predecessors. -Blocks: See .current-epic/dependencies.dot - -This task can start implementation immediately — all dependencies are satisfied. diff --git a/.current-task/plan.md b/.current-task/plan.md deleted file mode 100644 index fbc5263..0000000 --- a/.current-task/plan.md +++ /dev/null @@ -1,300 +0,0 @@ -# PM-77 Plan — LDraw corpus and compatibility harness (generation 3) - -## Task - -- **Epic:** PM-86 — [LDraw/LEGO model support and flexible renderer](https://radiant-industries.atlassian.net/browse/PM-86) -- **Task:** PM-77 — [LDraw corpus and compatibility harness](https://radiant-industries.atlassian.net/browse/PM-77) -- **Task prefix:** D -- **Task cache:** `.current-epic/task-D.md` -- **Epic workspace:** `../epic-86` -- **Base bookmark:** `main` -- **Task workspace expectation:** `../epic-86-task-D` -- **Task bookmark expectation:** `epic-86-task-D` -- **Plan output:** `.current-epic/plan-D.md` -- **Dependencies:** none -- **Downstream blockers:** E/PM-78, F/PM-79, K/PM-84 -- **Design source:** [PM-75 design](https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561) - -## Context and intent - -PM-77 is the reproducible test foundation for the LDraw semantic and renderer work. It adds a dependency-light, target-safe Rust testkit, authored specification corpus, versioned canonical records, and a native-only compatibility harness. The corpus is normative only where expectations are derived from ratified LDraw/OMR requirements or an explicit Polymodel safety decision; outputs from LDParse, Thomas fixtures, weldr, ldr_tools, OMR, and official-library artifacts remain observations until a compatibility-ledger decision adopts them. - -The plan is written so E can implement its semantic core against a machine-checkable gate contract and so F/H/K can consume stable v1 records without depending on native parser tooling. - -## Safety, workspace, and implementation skills - -1. In the task workspace, run `jj workspace root`, `jj status`, and `jj bookmark list`. Stop immediately if the workspace is the default workspace or the checked-out bookmark is `main`. -2. Confirm the workspace is `../epic-86-task-D` and the bookmark is `epic-86-task-D`. Do not create workspaces or bookmarks from this plan. -3. After the parent copies the task cache into the implementation workspace, read `.current-task/task.md` and `.current-task/plan.md`; the cached task contract is authoritative. -4. Do not edit `.current-epic/manifest.txt`, `.current-epic/dependencies.dot`, `.current-epic/task-state.json`, `.current-epic/merge-gate.json`, `.current-epic/events.log`, generated `crates/polymodel-api`, or renderer production behavior. -5. Load `polytoken:investigating-a-codebase` before implementation and use the repository's existing test/just conventions. No frontend-design skill applies. Load `reviewing-code-for-shipping-quality` before the final implementation review. - -## Replace-vs-edit assessment - -| Area | Decision | Reason and boundary | -|---|---|---| -| `crates/polymodel-ldraw-testkit` | **Create** | No existing crate owns cross-parser fixtures, provenance, profiles, canonical records, or E-facing gates. Keep it independent of parser/renderer implementations and compileable for wasm. | -| Corpus, expected records, ledger, and provenance files | **Create** | Existing `public/models/` files are STL/STEP assets and must not be relabeled as LDraw fixtures. Add behavior-named, exact-byte files under the new testkit-owned corpus. | -| `tools/ldraw-compat-harness` | **Create** | No existing compatibility tool exists. A standalone native tool is structurally safer than adding Python/process dependencies to the renderer or app. | -| LDParse driver/environment | **Create** | LDParse is a Python library, not a binary. Commit a hash-pinned driver and lockfile/environment rather than patching an assumed executable. | -| Fuzz/property smoke harness | **Create** | No existing LDraw fuzz surface exists. Add a bounded deterministic command at the test boundary; do not add branches to unrelated production code. | -| Root `Cargo.toml` and `justfile` | **Edit minimally** | The workspace already uses `members = [".", "crates/*"]`; the new crate is auto-included, so add no explicit member entry. Add only required dependencies and recipes, including new recipes as `test-all` prerequisites. Preserve existing commands and renderer split checks. | -| `polymodel-renderer-protocol`, `polymodel-mesh`, app, generated API | **Do not edit** | PM-77 supplies test contracts and fixtures; it must not introduce a heavy dependency path or production parser behavior. | - -## Required deliverable layout - -The implementor may choose equivalent module names, but the following boundaries and artifacts are mandatory: - -- `crates/polymodel-ldraw-testkit/`: dependency-light Rust crate with embedded/compile-time fixture inventory, public profiles, expected/canonical records, provenance validation, matrix validation, corpus gates, and deterministic serialization. It must not depend on `three-d`, `three-d-asset`, `stl_io`, `polymodel-mesh`, filesystem, network, archive, process, or parser implementation crates. -- `crates/polymodel-ldraw-testkit/corpus/` (or a documented testkit-owned `testdata/ldraw/`): authored inputs, expected records, matrix, ledger, manifests, provenance/license records, semantic-limit vectors, and the H-owned conditional-line family. -- `tools/ldraw-compat-harness/`: native-only Rust harness and adapter modules, plus `ldparse_driver.py`, `.python-version`, `pyproject.toml`, `uv.lock`, vendored/pinned LDParse source or verified source archive, and README/build evidence. Tests are offline and deterministic. -- `fuzz/` or a testkit-owned property target: bounded seeded smoke/regression command with committed malformed/adversarial inputs and reports. -- `justfile`: `ldraw-corpus`, `ldraw-differential`, `ldraw-fuzz-smoke`, and `ldraw-wasm-check`; all four are prerequisites of `test-all`. -- Root `Cargo.toml`: only dependency changes required by the new crate/tool. Do not add a redundant workspace-member line because `crates/*` already includes it. - -## Cross-task contracts that this task must publish - -### 1. Versioned canonical record contract - -D commits canonical syntax, semantic, and scene record schema version `v1`, with stable field ordering, explicit integer widths, canonical decimal/float/non-finite tokens, normalized separators, raw bytes/tails/spans, diagnostic order, provenance, and schema compatibility rules. E and F may add fields only through a documented compatible extension; they must not change committed v1 golden meaning or expected records without a schema-version bump and a reviewed migration. The v1 schema and representative golden snapshots are therefore a cross-task artifact, not disposable test scaffolding. - -### 2. Machine-checkable semantic corpus gates - -A **corpus gate** is a named, profile-scoped Rust assertion over one fixture's authored expected record and the adapter-produced actual record. The testkit exposes a wasm-safe trait and result types, for example: - -```rust -pub trait CorpusGate { - fn name(&self) -> &'static str; - fn profile(&self) -> Profile; - fn assert_case( - &self, - fixture: &FixtureCase, - expected: &ExpectedRecord, - actual: &CanonicalRecord, - ) -> Result<(), GateFailure>; -} -``` - -The testkit also exposes a `CorpusGateSet` that rejects duplicate gate names, missing profile coverage, or a fixture with no applicable gate. E implements the adapter/semantic side of this contract in its wasm-compatible crate and runs the gates against its parsed records; the testkit owns the assertion definitions and fixture expectations. Process execution and oracle differential reporting are separate native-only code and are never required by E's gate evaluation. - -The minimum v1 gate set is named and profile-scoped as follows: - -1. `syntax_types_0_to_5`: each primitive/meta line is classified correctly, with raw tail, source span, line-ending, and malformed-field behavior checked for strict, compatibility, and lossless rows where applicable. -2. `profile_acceptance_and_diagnostics`: strict/compatibility/lossless accept, reject, preserve, diagnostic code, severity, and deterministic diagnostic-order expectations are exact. -3. `mpd_and_data_boundaries`: `FILE`/`NOFILE`, `!DATA`, duplicate/missing terminators, payload concatenation/base64, unreachable blocks, and decoded limits match the expected record. -4. `path_and_resolution_independence`: canonical names, forbidden syntax, root identity, shadowing, and unresolved-resource behavior are represented without requiring a filesystem or resolver side effect; E's parser result remains deterministic when resolver availability changes. -5. `bfc_state_and_invertnext`: file-local certification, winding, XOR inversion, one-shot `INVERTNEXT`, blanks, intervening commands, malformed references, EOF, and stable diagnostic codes match the profile row. -6. `colours_and_ldconfig`: colour 16/24 semantics, direct colours, alpha/luminance/finish/material fields, unknown colours, and scoped `!COLOUR` output are preserved or diagnosed exactly. -7. `graph_identity_and_instances`: iterative include traversal, stable file/model IDs, cycle detection, cache reuse, repeated-model parse-once behavior, and distinct instance identity match expected graph records. -8. `steps_and_transforms`: STEP boundaries, finite right-handed LDU transforms, reflection state, bounds inputs, and no-early-scaling expectations match canonical records. -9. `numeric_finiteness_and_raw_preservation`: signed zero, decimal/exponent spelling, finite extremes, overflow/underflow, non-finite rejection, invalid counts, and lossless raw representation match profile expectations. -10. `semantic_limits_and_cancellation`: each D-owned LDraw semantic limit has limit and limit+1 expected outcomes, deterministic diagnostics, cancellation checkpoints, and post-cancellation cleanup. - -Each gate has at least one happy, error, and boundary/adversarial fixture where the profile permits it. Gate IDs and profile names are serialized in failure reports so E's acceptance claim “all PM-77 semantic corpus gates” is mechanically checkable rather than a prose promise. Gate definitions, their pure evaluation tests, and E-facing traits must compile with `cargo check --tests --target wasm32-unknown-unknown`; only native oracle/process tests are target-gated. - -### 3. Ownership of limits - -D owns **LDraw-semantic** limits and vectors: maximum resource/file bytes (100 MiB/resource), line length (1 MiB/line), files (16,384), include depth (256), commands (5,000,000), instances (5,000,000), triangles (20,000,000), lines (20,000,000), textures (1,024), fetches (8), diagnostics (10,000), plus malformed/overflow and cleanup behavior at each limit and limit+1. D does not duplicate reservation formulas. - -B/PM-88 owns **reservation-arithmetic** vectors: every class/domain/load/global/unified ceiling, checked u64 overflow, `ceil_div`, `align_up`, WASM/GPU/texture/canvas formulas, invalid dimensions, and resize old+new overlap. D's adapter contract can carry reservation outcomes, but B's implementation and arithmetic corpus remain authoritative. - -## Implementation steps - -### 1. Establish inventory, profiles, records, and gate API - -Define fixture identity, exact bytes, relative path, line-ending mode, behavior tags, authority class, profile (`strict`, `compatibility`, `lossless`), expected outcome, source spans/raw tails, diagnostics, semantic/scene records, adapter observations, and provenance. Embed the inventory and fixture bytes or use another target-safe representation; runtime filesystem/network access is forbidden in the reusable crate. - -Implement the `CorpusGate`/`CorpusGateSet` contract above, the ten named v1 gates, profile applicability validation, deterministic `GateFailure`, and machine-readable coverage reporting. Keep expected records separate from observations. Reject duplicate IDs, duplicate gate/profile rows, missing expected records, missing required categories, unreferenced files, and incomplete provenance. - -**Done when:** `cargo test -p polymodel-ldraw-testkit` validates the inventory and gate set, intentionally malformed inventory/gate cases fail, all ten gates have named profile rows, and `cargo check --tests -p polymodel-ldraw-testkit --target wasm32-unknown-unknown` passes without forbidden dependencies. - -### 2. Author the complete ratified-spec corpus - -Create exact-byte, behavior-named fixtures and independently reviewed expected records for: - -- types 0–5, comments/meta, subfile references, lines, triangles, quads, conditional syntax, whitespace, CRLF/LF, blank lines, BOM/UTF-8, unterminated final lines, raw tails, byte/line spans, and malformed boundaries; -- MPD `FILE`/`NOFILE`, `!DATA`, duplicate names, missing terminators, unreachable blocks, payload decode and embedded-resource limits; -- BFC `CERTIFY`, `NOCERTIFY`, `CCW`, `CW`, `INVERTNEXT`, nested/subfile state, blanks/intervening commands, malformed references, EOF, and exact diagnostic outcomes; -- colour definitions, colour 16/24, direct colours, alpha/material/finish/luminance, unknown colours, and LDConfig-derived values; -- `!TEXMAP` start/next/fallback/stop, points, explicit edges, and conditional-line syntax; -- numeric spelling and finiteness boundaries, invalid counts, overflow/underflow, and stable diagnostics; -- semantic limit and limit+1 cases from the ownership list, cancellation checkpoints, and cleanup after aborted work. - -Hand-author expectations from ratified specifications and PM-75; an upstream output must never rewrite an expected file. Record fixture hashes and authority/source citations. - -**Done when:** the inventory report shows every acceptance category with happy, error, boundary, and adversarial cases; every fixture has an independently reviewed expected record and exact input-byte hash; all ten gate families can select at least one fixture. - -### 3. Add all four canonical path/root contexts - -Build a dedicated path-policy fixture family for PM-75 §3.3. For **each** context below, include happy resolution, shadowing/precedence, collision, out-of-root, and forbidden-syntax cases (literal `%`, backslash, empty/dot/dot-dot segment, leading/trailing slash, controls, colon, query, and fragment): - -1. current MPD virtual-file table: exact-case lookup, duplicate names, and MPD shadowing of external roots; -2. uploaded manifest: relative directory then manifest root, exact-case keys, duplicate canonical keys, and explicit-prefix behavior; -3. uploaded LDraw roots: `MODELS → PARTS → P`, with explicit `s/`, `8/`, and `48/`, ASCII-fold lookup/collision, and no fallback after an explicit prefix; -4. pinned official library: `PARTS → P`, explicit `s/`, `8/`, and `48/`, canonical spelling, ASCII-fold collision, and ingestion invalidation. - -Expected records must include canonical spelling, root identity, target identity, cache identity, and resolver-independent parser behavior. Include repeated-reference and cycle cases that exercise canonical identity without using a real filesystem. - -**Done when:** matrix validation proves all four contexts and all five sub-families are represented, MPD shadowing and explicit-prefix no-fallback are asserted, and no fixture can activate URI/network access. - -### 4. Add H-owned conditional-line classifier fixtures - -Reserve a complete `h-conditional-*` fixture family in the D corpus and mark H/PM-81 as the owner of the hand-computed coordinates and expected packed outcomes. D supplies the fixture schema, hashes, profile/matrix rows, and expected-record versioning; H consumes these vectors without redefining the classifier contract. - -Include at minimum one hand-computed vector for each of the six PM-75 §3.5 outcomes: - -- `Visible` — same-side controls; -- `SuppressedInterior` — strictly opposite-side controls; -- `SuppressedOffscreen` — endpoint segment has no surviving clip interval; -- `SuppressedAmbiguous` — projectable control has unsafe/non-positive `w`; -- `SuppressedDegenerate` — clipped endpoint segment is shorter than epsilon; -- `Invalid` — non-finite homogeneous input with the stable diagnostic. - -Also include epsilon boundary vectors immediately below, exactly at, and immediately above segment `epsilon` and oriented-area `±epsilon`; `w_epsilon` below/exactly/above and `w <= 0`; near/far/side-plane clipping; off-frustum but projectable controls; unsafe control `w`; behind-camera endpoint; reflection; perspective and orthographic projections; same/opposite/collinear controls. Expected records must assert that only `Visible` emits a segment and suppressed outcomes emit no diagnostic unless the contract says otherwise. - -**Done when:** all six outcomes and every epsilon/`w_epsilon` boundary have committed coordinates, hand-computed expected records, owner metadata `H/PM-81`, and matrix rows; H can consume them through the v1 record format without adding a new corpus schema. - -### 5. Pin external artifacts and provenance - -Add minimized, legally attributable artifacts for Thomas fixtures, OMR and official-library examples, weldr, and ldr_tools. Every non-authored artifact records source URL, exact commit/blob/archive, SHA-256, license or operator-attested permission, original test identity, modifications, and authority class. Keep selected files and notices in the repository; acquisition is a separate maintenance operation and normal tests are offline. - -Use PM-75's exact LDParse revisions: - -- master: `877667417d6cab57c04aed9cb3d5bfe7efa0589c`; -- modernization: `0fe78dcee22982f26cceced1b483340faa4d04e9`. - -Record both revisions, their source URLs, source/archive SHA-256, and license evidence in the ledger. The provenance validator recomputes every committed artifact hash and rejects altered, unlisted, or under-attributed files. - -**Done when:** all selected artifacts and permission/license records are committed, provenance tests pass offline, both exact LDParse revisions are source-verified, and a clean checkout can reproduce the pinned inputs without floating branches or network access. - -### 6. Build the reproducible native LDParse and oracle adapters - -Model adapter classes explicitly: - -- `InProcessRustAdapter`: crate-pinned, target-safe Rust implementation adapter for E/F/K records; -- `OutOfProcessAdapter`: subprocess adapter with explicit executable/driver identity, environment, timeout, termination, stdout/stderr, exit status, and version evidence. - -LDParse is **only** an `OutOfProcessAdapter`; it is not treated as a binary. Commit `tools/ldraw-compat-harness/ldparse_driver.py`, a pinned `.python-version` specifying **CPython 3.12.8**, `pyproject.toml`, and `uv.lock` with hashes. Vendor the two source revisions or commit verified source archives and their SHA-256; the driver must import only the source matching the selected revision and print a deterministic JSON record. The README must document `uv sync --locked` and the offline invocation. The harness must reject a wrong interpreter, lockfile drift, source hash/revision mismatch, missing version evidence, network-enabled acquisition, timeout, crash, malformed driver output, or normalization failure. - -The required LDParse oracle surface is fixed, not discovered at implementation time: the driver and corpus must exercise line types 0–5 with raw tails/spans, MPD `FILE`/`NOFILE` boundaries, include/model graph and cache/instance identity, BFC certification/winding/`INVERTNEXT`, STEP boundaries, finite numeric/diagnostic outcomes, and deterministic acceptance/rejection for those cases. Every invocation maps to a fixture and expected record. TEXMAP, conditional-line classification, renderer batches, and any other feature not in this minimum surface are not silently claimed as LDParse coverage; they are marked `unsupported-by-ldparse` in the ledger and tested through authored expectations or another independently pinned adapter. - -For all process adapters, load only inventory-listed fixtures, enforce wall-clock/input/resource budgets, capture complete evidence, normalize without erasing raw tails/spans/order/diagnostics/BFC/paths/colours/geometry distinctions, and emit machine-readable mismatches linked to fixture and ledger IDs. weldr/ldr_tools expected values must be independently derived, never copied from one another. - -**Done when:** `uv sync --locked` succeeds under CPython 3.12.8 offline with the committed source pins; the native driver produces deterministic JSON for every required LDParse fixture; the harness tests wrong pin/interpreter, timeout, crash, unsupported-surface, and malformed-output failures; no required surface is covered by an escape clause. - -### 7. Implement canonical v1 serialization and compatibility ledger - -Serialize syntax/semantic/scene records with stable field order, stable non-semantic collection ordering only, explicit integer widths, canonical finite decimal/float formatting, explicit non-finite tokens, lowercase hex digests, normalized path separators, and explicit spans/raw bytes/diagnostics/provenance. Do not sort semantically ordered statements, geometry indices, BFC transitions, or diagnostic order. - -Add golden snapshots for syntax, MPD graph, BFC/colour state, points/edges/conditional geometry, scene hierarchy/instances/bounds, all ten gate families, and representative limit/cancellation outcomes. Add a compatibility ledger row for each disagreement with specification citation, observations, selected behavior, rationale, owner/date, and regression fixture; validation requires every oracle output and disagreement to link to a row. - -**Done when:** repeated serialization is byte-identical, valid insertion-order independence is tested, ordered data remains ordered, v1 schema/version rules are documented, and no upstream observation is accepted as normative without a ledger decision. - -### 8. Add bounded fuzz/property/adversarial smoke - -Create a deterministic native smoke command with committed malformed inputs and minimized regressions, fixed seeds, fixed iteration/case budgets, byte/depth/time limits, and killable process execution. Cover whitespace/line endings, malformed numerics, path traversal and collisions, MPD duplicates/cycles/deep chains, DATA/TEXMAP truncation, BFC transitions, semantic limits and limit+1, cancellation/cleanup, and canonicalization stability. Reports record seed, iterations, budgets, toolchain, adapter identity, and pass/fail. - -Expose the adapter seam for E's in-process parser and K's later expansion without making the testkit native-only. Add no unbounded or networked fuzz job. - -**Done when:** `just ldraw-fuzz-smoke` runs from a clean checkout with fixed seeds and deterministic output, every named adversarial class has a committed regression input, and cancellation/limit behavior cannot hang or panic. - -### 9. Wire commands, target checks, documentation, and merge gate - -Add these commands: - -- `ldraw-corpus`: inventory, provenance, matrix, gate, and snapshot tests; -- `ldraw-wasm-check`: `cargo check --tests -p polymodel-ldraw-testkit --target wasm32-unknown-unknown` plus the reusable E-facing gate path; -- `ldraw-fuzz-smoke`: bounded property/adversarial regression; -- `ldraw-differential`: offline native harness with `uv run --locked` and pinned source/toolchain evidence. - -Update `test-all` so these four recipes are explicit prerequisites in addition to the existing `fix check lint test test-server test-renderer`. This makes corpus, fuzz, differential, and wasm checks part of the ordinary merge gate rather than documentation-only commands. Keep native-only process work out of wasm recipes and preserve `verify-renderer-split`. - -Document fixture authoring, expected-record review, gate implementation for E, H vector ownership, provenance/permission review, pin updates, ledger decisions, Python setup, offline commands, and schema versioning. - -**Done when:** every recipe is runnable and documented; `just test-all` actually invokes all four new recipes; wasm compilation succeeds for testkit/gate code; native-only boundaries are explicit; no app dependency tree gains forbidden renderer/parser crates. - -## Test strategy - -This task has no provider, daemon, TUI, browser, or UI behavior. Tier 2 TestProvider, Tier 3 daemon integration, and Tier 4 TestBackend do not apply. - -| Behavior | Tier and infrastructure | Required cases | -|---|---|---| -| Inventory, profiles, and named corpus gates | Tier 1 pure Rust tests in `polymodel-ldraw-testkit` over embedded bytes/records | happy path; duplicate/missing gate; missing profile row; missing expected record; every one of ten gates; strict/compatibility/lossless applicability | -| E-facing gate evaluation and wasm safety | Tier 1 pure gate tests plus `cargo check --tests --target wasm32-unknown-unknown` for testkit and E adapter path | all gate names; no filesystem/process/network; wasm compile; expected gate failure details | -| Authored syntax/semantic expectations | Tier 1 corpus tests through adapter trait | types 0–5; raw tails/spans; MPD/DATA; BFC; colours; four path contexts; cycles; TEXMAP; numeric boundaries; diagnostics | -| Path/root policy | Tier 1 table-driven fixture tests | all four contexts, each with happy/shadow/collision/out-of-root/forbidden syntax; exact-case, ASCII-fold collision, explicit-prefix no-fallback, MPD shadow | -| H conditional-line family | Tier 1 fixture/schema validation in D; H-owned downstream classifier tests consume v1 records | six outcomes; epsilon and `w_epsilon` below/exact/above; clip planes; unsafe controls; reflection; perspective/orthographic; hand-computed coordinates | -| Provenance and integrity | Tier 1 deterministic hash tests | valid artifact; altered bytes; missing source/revision/license; modification/authority mismatch; unlisted file | -| Canonical v1 records | Tier 1 golden/snapshot tests | repeated serialization; valid insertion-order independence; ordered BFC/diagnostics/geometry preservation; raw bytes/spans; floats/non-finite; schema version | -| Semantic limits and cancellation | Tier 1 bounded adapter/property tests | each D-owned semantic limit and limit+1; malformed/overflow; depth/cycles; cancellation; cleanup; no hang/panic. Reservation arithmetic is covered by B/PM-88, not duplicated here | -| LDParse differential | Native standalone integration command, Tier outside Tiers 2–4 | CPython 3.12.8/uv.lock; both exact revisions; required minimum surface; deterministic JSON; wrong pin/interpreter; source mismatch; timeout; crash; unsupported surface; malformed output | -| Thomas/weldr/ldr_tools/OMR evidence | Native standalone integration plus Tier 1 ledger/provenance tests | attribution; independent expected values; source/hash; disagreement requires ledger decision; no oracle becomes normative implicitly | -| Fuzz/property/adversarial | deterministic seeded native smoke command | committed regressions for whitespace, numerics, paths, cycles, MPD/DATA/TEXMAP, BFC, limits, cancellation, serialization; fixed budget/timeout/kill path | -| Workspace and renderer split | target checks and existing recipes | `just check`; wasm testkit check; `just verify-renderer-split`; app tree remains free of `three-d`, `three-d-asset`, `stl_io`, `polymodel-mesh` | - -Run focused validation in this order, then the complete merge gate: - -```text -just check -just ldraw-corpus -just ldraw-wasm-check -just ldraw-fuzz-smoke -just ldraw-differential -just fix -just test -just test-server -just test-renderer -just test-all -``` - -`just test-all` is the final merge gate and must visibly execute `ldraw-corpus`, `ldraw-wasm-check`, `ldraw-fuzz-smoke`, and `ldraw-differential`. A failure must be fixed in the task diff or reported with the exact failing fixture/tool and reproducible command; no undocumented assumption is an acceptable result. - -## Finding dispositions from planner-D2 - -- **D-plan-2-C1 (critical): fixed.** The plan defines `CorpusGate`, named/profile-scoped assertions over fixture expected and actual canonical records, a gate set, wasm-safe E implementation boundary, and ten minimum v1 gates. -- **D-plan-2-C2 (critical): fixed.** PM-75's exact master and modernization revisions are named; the required LDParse surface is enumerated; the plan does not defer build-surface discovery or offer a “fully exercised or limited” escape clause. -- **D-plan-2-H1 (high): fixed.** The `h-conditional-*` family covers all six outcomes, hand-computed coordinates, epsilon/`w_epsilon` boundaries, clipping, control, reflection, and projection cases and is owner-allocated to H. -- **D-plan-2-H2 (high): fixed.** Path fixtures explicitly cover all four PM-75 §3.3 contexts, with happy/shadow/collision/out-of-root/forbidden-syntax sub-families and precedence/no-fallback checks. -- **D-plan-2-H3 (high): fixed.** D's LDraw-semantic limits and limit+1 vectors are enumerated and explicitly separated from B's reservation-arithmetic boundaries. -- **D-plan-2-H4 (high): fixed.** LDParse is explicitly modeled as a Python library behind a committed `ldparse_driver.py`, not as a binary; source pins, driver, interpreter, and lockfile are part of the deliverable. -- **D-plan-2-H5 (high): fixed.** `test-all` is required to depend on all four new corpus/wasm/fuzz/differential recipes, and the plan requires verifying that they actually run. -- **D-plan-2-H6 (high): fixed.** CPython 3.12.8, `.python-version`, `pyproject.toml`, `uv.lock`, `uv sync --locked`, and offline/hash-pinned behavior are specified. -- **D-plan-2-M1 (medium): fixed.** Gate definitions/evaluation and E's adapter path are wasm-compileable; only native subprocess/oracle differential code is target-gated. -- **D-plan-2-M2 (medium): fixed.** Canonical syntax/semantic/scene records are explicitly a committed v1 cross-task contract with golden snapshots and schema-bump rules. -- **D-plan-2-M3 (medium): fixed.** In-process Rust and out-of-process adapters are separate abstractions; LDParse is only out-of-process. -- **D-plan-2-L1 (low): fixed.** The root uses `members = [".", "crates/*"]`; the plan tells the implementor not to add a redundant member declaration. - -## Commit and review requirements - -Commit the implementation on the task change with a concise PM-77 message and exactly these trailers: - -```text -Epic: PM-86 -Task: PM-77 -``` - -Implementation reviewers must verify: - -- every task acceptance category and every required happy/error/boundary/adversarial case has committed input and expected output; -- all ten named semantic corpus gates are machine-validated, profile-scoped, wasm-safe, and consumable by E; -- v1 canonical records, snapshots, and compatibility rules are stable for E/F/H/K; -- all four path contexts and the complete H conditional-line family are present with hand-computed expectations; -- D/B ownership of semantic versus reservation-arithmetic limits is not blurred; -- provenance is hash-validated, legally attributed, and records source/blob, original test, modifications, and authority; -- LDParse uses both exact PM-75 revisions, CPython 3.12.8, `uv.lock`, the committed driver, and the fixed minimum oracle surface; -- upstream observations are separated from authored expectations and every disagreement has a ledger decision; -- weldr/ldr_tools expectations are independently derived; -- the testkit remains dependency-light and wasm-compatible, and the renderer split is unchanged; -- fuzz/property execution is bounded, seeded, killable, and included in `just test-all`; -- `jj diff` contains fixture bytes, expected outputs, provenance/permission records, pins, commands, documentation, and the `test-all` wiring, with no generated API or unrelated production/UI files. - -## Ready-for-parent-merge handoff - -Before returning to the parent orchestrator, provide: - -1. task workspace/bookmark and commit ID; -2. changed-file summary, corpus byte/file counts, schema version, and all pinned upstream revisions/toolchain identifiers; -3. the verified LDParse minimum surface and the explicit ledger rows for features outside it; -4. exact focused command output, including `cargo check --tests --target wasm32-unknown-unknown` and `just test-all`, proving the four new recipes ran; -5. confirmation that `jj diff` was reviewed and no prohibited state/source/generated files were modified; -6. any remaining risk as a reproducible failing command or missing external permission, never as an undocumented assumption. diff --git a/.current-task/task.md b/.current-task/task.md deleted file mode 100644 index f04af2f..0000000 --- a/.current-task/task.md +++ /dev/null @@ -1,43 +0,0 @@ -# Task D — PM-77: Implement: LDraw corpus and compatibility harness - -**Jira:** [PM-77](https://radiant-industries.atlassian.net/browse/PM-77) -**Status:** To Do -**Priority:** Medium -**Labels:** compatibility, fixtures, implement, ldraw, tests -**Execution Prefix:** D | Sort Order: 40 -**Epic:** [PM-86](https://radiant-industries.atlassian.net/browse/PM-86) -**Design source:** [PM-75 design](https://radiant-industries.atlassian.net/wiki/spaces/PM/pages/5570561) - -## Dependencies - -Blocks: E (PM-78), F (PM-79), K (PM-84) -No predecessors. - -## Goal - -Create the reproducible parser/semantic/visual test foundation before production implementation. - -## Scope - -Authored ratified-spec micro-fixtures and strict/compatibility/lossless matrix; buildable pinned LDParse differential harness; attributed Thomas fixtures; pinned OMR/official-library artifacts; minimized weldr/ldr_tools parser/MPD/resolver/BFC/geometry tests with independently derived expectations; fuzz/property/adversarial budget harness; deterministic semantic/scene serialization. - -Fixture provenance records source URL, exact commit/blob, SHA-256, licence or operator-attested permission, original test, modifications and authority class. Upstream implementation output is never normative without a compatibility-ledger decision. - -## Acceptance - -Coverage includes types 0–5, raw tails/spans, MPD/DATA, exact BFC state, colours, canonical/root path vectors, cycles, TEXMAP, points/edges, diagnostics, numerical boundaries, resource limits and cancellation. The LDParse oracle is reproducibly buildable in its pinned environment or explicitly limited to the verified buildable surface with fixtures proving each oracle use. - -## Merge-ready tests - -* `just check` -* `just fix` -* focused corpus/harness tests -* fuzz/property corpus smoke/regression command established by this task -* wasm32 check for reusable harness/core fixtures where applicable -* `just test-all` before handoff - -The isolated jj workspace must be green and independently merge-ready; fixtures, permissions, expected outputs and harness invocation are committed, not described for later. - -## Comments - -None. -- 2.54.0