Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CLAUDE.md

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions briefs/m1.1.1-hf4-extension-conflict-reject.md

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions src/core/scene/loader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -568,11 +568,12 @@ fn extEntityArchetype(ext: Accessor) !Accessor.Archetype {
/// as load-time hooks, M1.1.1-HF1 / D2).
///
/// Conflict policy: a component the entity already carries is rejected with
/// `error.ExtensionComponentConflict` — the PROVISIONAL runtime policy recorded in
/// `briefs/m1.1.1-hf3-core-robustness-hotfix-3.md` (R6). Last-wins runtime
/// semantics (masking, restoration, out-of-order deactivate) are an open design
/// surface for a dedicated spec session; `etch-reference-part2.md §30.5` defines
/// no reject policy — do not cite it.
/// `error.ExtensionComponentConflict` — the normative runtime policy for additive
/// extension conflicts. The `extends` model is strictly additive: no two active
/// extensions may declare the same component on one entity. See
/// `engine-scene-serialization.md` (extension additive conflicts) as the
/// authority; the static cook counterpart is the fatal `E1797
/// ExtensionAdditiveConflict`, and together they guarantee `cooked ⇒ loadable`.
pub fn activateExtension(world: *World, gpa: std.mem.Allocator, entity: EntityId, name: []const u8, ext_bytes: []const u8) !void {
// Step 0 — refuse re-activation (R12(d)); works for hook-only (0-component)
// extensions too, where the component-conflict check below cannot fire.
Expand Down Expand Up @@ -619,8 +620,8 @@ pub fn activateExtension(world: *World, gpa: std.mem.Allocator, entity: EntityId
/// the bridge's `ExtensionResolver`). Reuses the shared `activateExtension`
/// path (atomic prevalidate → reserve → grouped add → record → `on_attach`).
/// Unknown name → `error.UnknownExtension`; a component the entity already
/// carries → `error.ExtensionComponentConflict` (the provisional reject policy
/// recorded in the R6 brief — not `§30.5`).
/// carries → `error.ExtensionComponentConflict` (the normative additive-conflict
/// reject policy — see `engine-scene-serialization.md`).
pub fn runtimeActivate(world: *World, gpa: std.mem.Allocator, entity: EntityId, name: []const u8, resolver: ExtensionResolver) !void {
const bytes = resolver.resolve(name) orelse return error.UnknownExtension;
try activateExtension(world, gpa, entity, name, bytes);
Expand All @@ -644,9 +645,9 @@ pub fn runtimeActivate(world: *World, gpa: std.mem.Allocator, entity: EntityId,
/// just fires `on_detach` then drops the record.)
///
/// Conflict policy: reject-on-conflict (see `activateExtension`) keeps the
/// component set unambiguous — no two active extensions share a component — so
/// removal needs no provenance tracking. Provisional, recorded in the R6 brief;
/// `etch-reference-part2.md §30.5` defines no such policy — do not cite it.
/// component set unambiguous — no two active declarants (base or extension) share a
/// component — so removal needs no provenance tracking. Normative; see
/// `engine-scene-serialization.md` (extension additive conflicts).
pub fn deactivateExtension(world: *World, gpa: std.mem.Allocator, entity: EntityId, name: []const u8, ext_bytes: []const u8) !void {
if (!world.hasEntityExtension(entity, name)) return error.ExtensionNotActive;
const ext = try openVerified(ext_bytes);
Expand Down
6 changes: 3 additions & 3 deletions src/etch/diagnostics.zig
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ pub const DiagnosticCode = enum {
prefab_component_field_type_invalid, // M0.8 E7 — E1795 PrefabComponentFieldTypeInvalid
prefab_component_redefined, // M0.8 E7 — E1796 PrefabComponentRedefined (RESERVED: variant/base component-shape merge is M0.9 runtime)
prefab_remove_base_component, // M0.8 E7 — W1790 PrefabRemoveBaseComponent (RESERVED: no `remove` syntax in the §24.1 grammar)
extension_additive_conflict, // M1.0.18 — W1791 ExtensionAdditiveConflict (cook: ≥2 active extensions on an entity declare the same component; §30.5, last-extension-wins)
extension_additive_conflict, // M1.0.18 → M1.1.1-HF4 — E1797 ExtensionAdditiveConflict (fatal cook error, strictly-additive `extends` → reject: (a) two extensions declare the same component, (b) an extension declares a component already carried by the base/an earlier extension, (c) the same extension is listed twice; guarantees `cooked ⇒ loadable`; runtime backstops `error.ExtensionComponentConflict` (a/b) / `error.ExtensionAlreadyActive` (c))

// ── async / effects (E09xx, M1.0.11 — etch-resolver-types.md §9.2) ──
async_call_in_non_async_context, // M1.0.11 E4 — E0901 AsyncCallInNonAsyncContext (async fn/method call, or `await`, in a non-async fn/rule)
Expand Down Expand Up @@ -560,7 +560,7 @@ pub const DiagnosticCode = enum {
.prefab_component_field_type_invalid => "E1795",
.prefab_component_redefined => "E1796",
.prefab_remove_base_component => "W1790",
.extension_additive_conflict => "W1791",
.extension_additive_conflict => "E1797",
.async_call_in_non_async_context => "E0901",
.await_not_statement_head => "E0904",
.unconsumed_async_effect => "E0905",
Expand Down Expand Up @@ -872,6 +872,6 @@ test "DiagnosticCode code and name are stable cross-version" {
try std.testing.expectEqualStrings("EventNotEntityScoped", DiagnosticCode.event_not_entity_scoped.name());
try std.testing.expectEqualStrings("E0909", DiagnosticCode.ambiguous_event_entity_target.code());
try std.testing.expectEqualStrings("AmbiguousEventEntityTarget", DiagnosticCode.ambiguous_event_entity_target.name());
try std.testing.expectEqualStrings("W1791", DiagnosticCode.extension_additive_conflict.code());
try std.testing.expectEqualStrings("E1797", DiagnosticCode.extension_additive_conflict.code());
try std.testing.expectEqualStrings("ExtensionAdditiveConflict", DiagnosticCode.extension_additive_conflict.name());
}
Loading