Phase 1 / Etch / M1.1.1-HF4 extension additive conflict reject#55
Merged
Conversation
Ratify reject as the normative policy for additive extension conflicts (M1.1.1-HF4). A conflict — two active extensions declaring the same component on one entity — was a non-fatal cook diagnostic no production code read, so the cook silently produced .scene.bin files the loader then refused to load. Fatalizing the static conflict establishes the invariant `cooked ⇒ loadable`. - diagnostics: reclassify `extension_additive_conflict` to a fatal E1797 (was a non-fatal warning); doc comment states reject/fatal - scene_cook: add `ExtensionAdditiveConflict` to CookError; the additive gate `fail`s on the first conflicting component (static message, short-circuit) and the dead warning infra is removed (Warning struct, Cooked.warnings, Builder.warnings, and all plumbing; unused token import) - tests: rewrite the conflict block to fatal semantics + add the cooked-is-loadable contract test and a runtime dynamic-reject test
Close M1.1.1-HF4 (extension additive conflict: ratify reject). CLAUDE.md: current-state HF4 mention, +1 Hotfixes-table row (untagged), the extension-conflict Open decision marked RESOLVED (reject ratified, E1797). Brief: G3 execution log, recorded deviations (none), closing notes, Status CLOSED. Language + drift audits clean.
The ratified `cooked ⇒ loadable` invariant is "base + active extensions conflict-free" — three forms. The prior gate covered only (a) two extensions declaring the same component; a post-G3 review found forms (b)/(c) and an accessor-contract gap still open (over-declared invariant). Completed here. - detectExtensionConflicts: seed `counts` with the entity's BASE component names at 1 each (from eb.comp_ids via registry.componentName), then count each extension — a name reaching 2 is fatal, unifying (a) ext-vs-ext and (b) ext-vs-base; drop the `ext_len < 2` early-out (a lone extension can conflict with base). `eb.comp_ids` threaded from the caller. - form (c): a repeated extension name now fails fatally (distinct static message) instead of silently dedup-continuing (which masked a load-time ExtensionAlreadyActive). - P1#3: validate.structure runs after verifyHash and before any accessor getter (failure → skip), closing a panic on a malformed-but-rehashed prefab. - BaseResolver comment: "warning (§30.5)" → "check (fatal E1797)". - tests: +3 (form b, form c, malformed-but-rehashed no-panic).
Form (c) (the same extension listed twice) was gated behind `base_resolver orelse return`, so `cook` — which always passes a null resolver — let `extensions: ["X","X"]` cook and then fail at load with `ExtensionAlreadyActive`. Form (c) is a pure lexical check and must not depend on the resolver. - detectExtensionConflicts split into two passes: pass 1 dedups the extension names (fatal E1797 on the first duplicate) with NO resolver; then the `base_resolver orelse return` guard; then pass 2 (base seed + extension schemas → forms a/b) with the resolver. - widen two lagging code comments to the full 3-form invariant (diagnostics enum + CookError.ExtensionAdditiveConflict doc). - test: cook(null resolver) on a duplicate extension → E1797 (the path the resolver-gated form-(c) test could not reach).
Post-review documentation close. The ratified invariant is "base + active extensions conflict-free" (three forms a/b/c); the docs from G1-G3 described only the two-extension case (form a). Aligns docs + drift-audit remediation. - CLAUDE.md: HF4 Hotfixes row + the extension-conflict Open decision rewritten to the full a/b/c invariant (widened E1797, two-pass detector, runtime ExtensionComponentConflict a/b + ExtensionAlreadyActive c); Last updated. - brief: Recorded deviations logs BOTH Codex rounds (1st: forms b/c + accessor contract R1 unspecified; 2nd: form c under null resolver + comment drift); Closing notes refreshed (best-effort scoped to a/b, form c always fatal, 23/23). - drift remediation (comment-only, zero logic): the test section header and the loader deactivate doc widened from the two-extension phrasing to "base or extension" declarants.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M1.1.1-HF4 — Extension additive conflict: ratify reject
Untagged hotfix (same convention as M1.1.1-HF1/HF2/HF3 — no merge, no tag from this branch; Guy merges). A dedicated post-HF3 spec session ratified reject as the normative, permanent policy for additive extension conflicts, and this aligns the implementation — establishing the invariant
cooked ⇒ loadable.Brief:
briefs/m1.1.1-hf4-extension-conflict-reject.mdThe invariant (complete — three forms)
The entity's {base components} ∪ {active extensions' components} must be conflict-free. Three rejected forms (never resolved by list order):
extensions:clause.Static conflict → fatal cook error
E1797 ExtensionAdditiveConflict(distinct static message per form; no output;weld checkfails in CI). Dynamic → runtimeerror.ExtensionComponentConflict(a/b) /error.ExtensionAlreadyActive(c), atomic. The runtime loader logic is UNCHANGED (reject was already implemented in HF3); only its conflict-policy comments were normalized.What changed (gate by gate)
loader.zigconflict-policy comments normalized (comment-only, zero logic): dropped "PROVISIONAL"/"open design surface"; state the policy as normative, citeengine-scene-serialization.md.extension_additive_conflictcodeW1791→E1797;ExtensionAdditiveConflictadded toCookError; deleted theWarningstruct,Cooked.warnings,Builder.warnings,emitAdditiveConflict,w1791_code, the now-unusedtokenimport.CLAUDE.md(HF4 Hotfixes row + Open decision RESOLVED) + closing audits.detectExtensionConflictsnow seeds the count with the entity's BASE components (unifies a/b) andvalidate.structure-guards each resolved prefab before any accessor getter (P1#3 — no panic on a malformed-but-rehashed prefab); duplicate extension names fail fatally.base_resolver orelse return, so the publiccook(null resolver) let["X","X"]cook and fail only at load.detectExtensionConflictssplit into a resolver-independent lexical pass 1 (form c) + a resolver-gated pass 2 (forms a/b). Two lagging code comments widened to the full invariant.CLAUDE.md, brief Recorded deviations logging both review rounds, Closing notes); drift + language audits clean.Review defects addressed
cookpath); two code comments still describing the two-extension-only case.Validation checklist
grep -rn "W1791" src/ tests/ tools/emptygrep -rniE "last.?extension.?wins|last extension in the list wins" src/ tests/emptyzig build testgreen — Debug and ReleaseSafe (every gate)test-extensions23/23 (8 cook conflict tests incl. forms a/b/c, resolver-less duplicate, disjoint-OK, fail-on-first,cooked ⇒ loadablecontract, malformed-but-rehashed no-panic; + a runtime dynamic-reject)zig fmt --checkgreenzig build lintgreencommit-msghook green on every commit