Skip to content

Phase 0 / Core / RTTI + Resources + Events + Bindgen unifié + Plugin loader squelette#14

Merged
guysenpai merged 23 commits into
mainfrom
phase-0/core/rtti-resources-events-bindgen
May 23, 2026
Merged

Phase 0 / Core / RTTI + Resources + Events + Bindgen unifié + Plugin loader squelette#14
guysenpai merged 23 commits into
mainfrom
phase-0/core/rtti-resources-events-bindgen

Conversation

@guysenpai
Copy link
Copy Markdown
Contributor

Summary

M0.2 — Tier 0 foundations milestone complete. Delivers the comptime
RTTI reflection runtime + the singleton-entity resource system + the
heterogeneous MPMC event bus + the unified bindgen pipeline +
the plugin loader skeleton. Closes Phase −1 debts D-S6-RTTI (S6
schema_hash swap) and D-S2-bindgen (vk_gen / wayland_gen fusion).

  • RTTI Tier 0 (src/core/rtti/) : TypeId / SchemaHash /
    FieldDesc / TypeInfo + comptime builder + xxHash32/64 hashes
    • runtime registry. POD validator + cross-build determinism.
  • S6 IPC swap : messages.schemaHash delegates to
    rtti.computeSchemaHash. Voie 2 (protocol bump 1→2) retenue —
    Wyhash legacy et xxHash64 structurellement incompatibles + bump
    est mécanisme prévu par engine-ipc.md §5.2. Golden values RTTI
    committées dans ipc_compat_test.zig. Bug latent S6
    framing.zig:196 (hardcoding @as(u16, 1)) découvert + corrigé
    en sous-produit.
  • Resources (src/core/resources/) : singleton entities
    (engine-spec.md §2.9) avec flag is_singleton sur Archetype
    (option (a)), 4 fichiers de tests (20 tests). Lifecycle inferred
    comptime via convention pub const lifecycle: Lifecycle.
  • Events (src/core/events/) : Vyukov-pattern MPMC ring
    buffer + cursor epoch invalidation + drain par lifetime. 4
    fichiers de tests (18 tests). Intégré scheduler aux 3 boundaries
    (.phase / .tick / .frame).
  • Bindgen unifié (tools/bindgen/) : adapters
    vk_xml / wayland_xml portés 1:1 depuis les anciens gen
    tools. Critère mécanique « diff vide » atteint
    zig build bindgen-verify retourne EC=0. Squelette
    core/{api_description,validator,resolver,emitter}.zig posé
    pour Phase 1+ (premiers consommateurs : keepers).
  • Plugin loader (src/core/plugin_loader/) : Loader au
    dessus de std.DynLib, table WeldAPI + 7 sous-APIs (ECS 24 +
    Resource 8 + Event 6 + Service 2 + Memory 8 + Editor 17 +
    Platform 14 = 79 callbacks) toutes stubbées
    WELD_ERR_NOT_IMPLEMENTED. Validation au load
    weld_plugin_entry + api_version_min. 3 stubs cross-platform
    Zig pur + 2 fichiers de tests (18 tests).
  • Freeze partiel C0.5 : 47 marqueurs FROZEN sur 13 fichiers de
    surface publique RTTI / Resources / Events (100 % couverture).

Test plan

  • zig build EC=0
  • zig build test EC=0 (300+ pass, 10 skipped Windows-only,
    aucun test M0.1 / S6 modifié)
  • zig build lint EC=0 (custom Zig linter clean sur production
    tree)
  • zig fmt --check EC=0
  • zig build bindgen-verify EC=0 (critère mécanique diff vide
    E5)
  • Bench S1 dev-mode (non-opposable, médiane ~74 µs vs gate 65
    µs — sub-systèmes M0.2 ne touchent pas hot path ECS, re-bench
    cold-isolé planifié pré-tag)
  • Bench C0.1 dev-mode : 3.21 ms / gate 17.5 ms — GO 5.5×
  • Bench IPC RTT dev-mode : p50 5 µs / baseline 6 µs — GO
  • Audit FROZEN : grep -rln "FROZEN" retourne 13 fichiers
    (100 % couverture surface-publique)

Notable items for review

  • Décision technique E5 (i) : les adapters bindgen
    court-circuitent l'ApiDescription intermédiaire pour préserver
    le critère « diff vide ». Le squelette core/*.zig reste posé
    code-complete mais non exercé jusqu'à Phase 1+. Documenté en
    brief §Notes.
  • Décision E2 protocol bump : WELD_IPC_PROTOCOL_VERSION 1→2.
    Casse handshake S6 acceptée (aucun binaire en prod). Documentée
    en brief §Déviations actées.
  • 79 callbacks plugin API stubbées — signatures finales gelées
    pour C0.5, câblage réel Phase 3.
  • Plugin loader log.warn (pas log.err) sur chemins erreur —
    l'erreur est portée par le retour LoaderError, évite false
    fails du test runner Zig 0.16 sur tests négatifs.
  • Bench S1 cold-isolé re-run pré-tag : nécessaire pour
    verdict opposable. Architecture argument robuste, mais le
    chiffré non encore archivé en mode opposable.

Out-of-scope (Phase 3+)

  • Implémentation fonctionnelle des 7 sous-APIs plugin
  • Capability enforcement runtime
  • Hot-reload de plugins, sandboxing process plugin
  • Headers C exportés include/weld_api.h
  • platform.dynamic_loader wrapper (M0.3)
  • ApiDescription pipeline complet (Phase 1+ via keepers)

Closes M0.2.

🤖 Generated with Claude Code

guysenpai and others added 23 commits May 22, 2026 10:01
Lands the standalone RTTI subsystem under src/core/rtti/. No metier
consumer wired yet — the S6 IPC swap is E2, resources are E3, events
are E4.

Source layout:
- type_info.zig — TypeId, SchemaHash, Category, Lifecycle, FieldKind,
  FieldDesc, TypeInfo + engine composites (Vec2/3/4, Quat, Mat3/4,
  Color, Entity, AssetHandle).
- hash.zig — computeTypeId (xxHash32 on @typename), computeSchemaHash
  (xxHash64 on (name, fields)), comptime-deterministic. Schema hash
  is sensitive to type name (decision documented inline).
- comptime_builder.zig — buildTypeInfo (gates non-POD via
  @CompileError), buildFields, classifyField, isPOD predicate.
- registry.zig — Registry with idempotent register on
  (type_id, schema_hash); SchemaMismatch on conflict; lookup +
  lookupByName.
- rtti.zig — public surface re-export, pinned in src/core/root.zig
  comptime block for lazy-analysis-guard.

Tests (tests/core/rtti/):
- comptime_builder_test.zig — 8 tests covering primitive kinds,
  engine composites, nested struct + nested_type_id, fixed_array
  count, string_inline sentinel, optional, enum_tag, isPOD negative
  path.
- hash_test.zig — 6 tests covering type_id determinism, name-based
  parity with raw xxHash32, schema_hash field-order sensitivity,
  type-name sensitivity, stability, rename detection.
- registry_test.zig — 6 tests covering register/lookup,
  lookupByName, idempotence, SchemaMismatch, round-trip
  component↔bytes via FieldDesc (Position scalar, Velocity with
  nested Vec3).

build.zig wired to include the three test files alongside the M0.1
suite. zig build / zig build test / zig fmt --check / zig build lint
all green.

Refs: briefs/M0.2-rtti-resources-events-bindgen.md E1.
Align with the codebase convention used by the ECS module
(src/core/ecs/root.zig, no parallel src/core/ecs.zig). The brief
listed src/core/rtti.zig and src/core/rtti/ as separate entries —
landed verbatim in dc76dc0, renamed here per Guy verbal direction
2026-05-22.

Path changes:
- src/core/rtti.zig -> src/core/rtti/root.zig (git mv preserves
  history)
- imports inside the re-export drop the rtti/ prefix
  ("type_info.zig" instead of "rtti/type_info.zig")
- src/core/root.zig: @import("rtti.zig") becomes
  @import("rtti/root.zig")

build.zig untouched — the three tests/core/rtti/*_test.zig entries
do not reference the re-export path.

Deviation tracked in briefs/M0.2-rtti-resources-events-bindgen.md
section 'Déviations actées'. CI gates verts (build, test, fmt,
lint).
BLOCKED on byte-compat divergence. See briefs/ for diagnosis.

Implements the D-S6-RTTI swap as specified by E2 §1-§5:
- tests/core/rtti/ipc_compat_test.zig captures 5 Wyhash legacy
  schema_hash values (ProtocolHello, SpawnEntity, ModifyComponent,
  Heartbeat, LogMessage) hardcoded from a pre-swap one-shot capture.
- src/core/ipc/messages.zig: schemaHash() delegates to
  rtti.computeSchemaHash() — minimal swap, no other IPC file touched.
- engine-spec.md §25.3 NOT updated (the swap is not effective until
  the byte-compat reconciliation is tranched).

State at checkpoint:
- zig build: clean
- tests/ipc/: 240/255 pass, 10 skipped — IPC heritage suite intact,
  the swap is consistent editor+runtime so wire-level tests still
  pass.
- tests/core/rtti/ipc_compat_test.zig: 5/5 FAIL — the bytes emitted
  by RTTI E1 diverge structurally from the Wyhash legacy bytes
  (different hash function XxHash64 vs Wyhash, different input
  format typed tuple vs concatenated key string).

Per E2 directive section 2 (Guy 2026-05-22): blocage Cas 2 obligatoire,
retour Claude.ai. No touch to src/core/rtti/hash.zig (commit dc76dc0)
to preserve the registry built in E1. No autonomous choice between
the two reconciliation paths (helper local in messages.zig vs
protocol version bump).

Refs: briefs/M0.2-rtti-resources-events-bindgen.md sections
Journal d'execution + Blocages rencontres.
BLOCKED on framing.zig:196 inline test hardcoding old version.

Applies voie 2 of the E2 unblocking (Guy 2026-05-22): adopt the
RTTI E1 xxHash64 algorithm as the canonical schema_hash, bump the
protocol version, retire the Wyhash legacy byte-compat check.

Changes:
- src/core/ipc/protocol.zig: WELD_IPC_PROTOCOL_VERSION 1 -> 2 +
  rationale doc comment.
- tests/core/rtti/ipc_compat_test.zig: rewritten as golden value
  stability test. 5 RTTI hashes (ProtocolHello, SpawnEntity,
  ModifyComponent, Heartbeat, LogMessage) captured against the
  post-swap branch and pinned. Any future refactor of
  rtti.computeSchemaHash or message layout surfaces as a deliberate
  diff.
- briefs/M0.2-rtti-resources-events-bindgen.md: journal entries for
  the unblocking + deviation E2-bump tracking the voie 2 choice +
  blocage entry for the framing.zig inline test.

Standalone tests pass:
- zig build clean
- zig fmt --check clean
- zig build lint clean
- tests/core/rtti/ipc_compat_test.zig 5/5 pass
- tests/ipc/ heritage suite untouched

Full suite blocked:
- src/core/ipc/framing.zig:196 hardcodes 'expectEqual(@as(u16, 1),
  h.version)'. The bump 1 -> 2 fails this assertion. Per directive
  E2 §5, this triggers blocage Cas 2 — STOP, do not modify the test
  unilaterally. The fix is trivially aligning the line with the
  surrounding pattern (use 'protocol.WELD_IPC_PROTOCOL_VERSION'
  like line 183) but it must be authorized by Guy before
  application.

Spec edits NOT applied locally — they live in the KB. Patch
snippets are queued for Guy to apply in Claude.ai:
- engine-spec.md §25.3: note swap effectif M0.2.
- engine-ipc.md §5.2: note bump effectif M0.2.

Refs: briefs/M0.2-rtti-resources-events-bindgen.md sections
Journal + Blocages rencontres + Deviations actees E2-bump.
Latent S6 bug surfaced by the M0.2 / E2 protocol version bump
(1 -> 2, commit d2e48d8): line 196 hardcodes 'expectEqual(@as(u16,
1), h.version)' while lines 183 and 199 of the same file already
use 'protocol.WELD_IPC_PROTOCOL_VERSION'. The literal was a
copy-paste oversight in S6 — there is no test intent on the value
1 itself, only on the round-trip of the header version field.

Substitute the literal by the constant. Aligns with the majority
pattern of the file. No other line touched.

E2 §5 directive triggered a Cas 2 blocage at the discovery of the
hardcoded literal; this commit lands under explicit Guy
authorization 2026-05-22.

Refs: briefs/M0.2-rtti-resources-events-bindgen.md section
'Deviations actees'.
Closes the E2 entry in the milestone brief:

- Journal entries timestamped 11:38 (Guy unblock #2 received),
  11:40 (full suite green EC=0), 11:43 (bench RTT archived).
- Deviation E2-framing-fix tracks commit 1d9d186 (framing.zig:196
  literal -> protocol.WELD_IPC_PROTOCOL_VERSION).
- Blocage entries closed with cross-references to the unlock
  commits.

bench/reports/ipc_rtt_2026-05-22.md archives the post-swap RTT
measurement. 5 runs in a dev-mode session (no cold-isolated
protocol per engine-phase-0-criteria.md § Methodologie bench) show
p50 median around 8 us vs baseline 6 us with massive variance on
upper percentiles - signature of session noise. The swap is purely
comptime (schema_hash baked-in at compile, no runtime hash call on
the hot path) so no structural regression is possible. Report
flagged non-opposable; cold-isolated re-bench scheduled for E6.

Refs: briefs/M0.2-rtti-resources-events-bindgen.md sections
Journal + Blocages rencontres + Deviations actees E2-bump +
E2-framing-fix.
Lands the resource subsystem under src/core/resources/ following
the ecs/root.zig pattern (single canonical entry point, no parallel
src/core/resources.zig). Resources are singleton-entity components
per engine-spec.md §2.9 — exactly one value of each resource type
lives in the world, exposed via setResource / getResource /
getResourceMut / hasResource / removeResource / resourceChanged.

Module layout:
- src/core/resources/registry.zig — ResourceRegistry mapping
  rtti.TypeId to EntityId + 1-byte ResourceMarker component that
  keeps the resource archetype distinct from any user [T]
  archetype.
- src/core/resources/api.zig — public API. setResource spawns a
  singleton entity carrying [T, ResourceMarker] and flips
  Archetype.is_singleton. getResourceMut routes through
  world.get_mut so the M0.1 tick-based change detection picks up
  every write automatically. resourceChanged reads
  arch.changedTick(...) on the resource slot.
- src/core/resources/root.zig — re-export public surface, pinned
  in src/core/root.zig for the lazy-analysis guard.

ECS wiring:
- Archetype gets is_singleton: bool = false (decision option (a)
  from brief § Notes — 1 bool per archetype, no bit reserved in
  the component mask, archetype count plafonné < 100).
- Query.maybeRescan and ComptimeQuery.next skip is_singleton
  archetypes so user queries never see resource entities.
- World gets singleton_resources: ResourceRegistry alongside the
  existing resources: ResourceStore (the S4 byte-keyed map the
  Etch interpreter still consumes — the two stores coexist until
  a later milestone unifies them).

RTTI extension:
- buildTypeInfo(T, .resource) now populates lifecycle via the new
  inferLifecycle(T, category) helper. Reads T.lifecycle if
  declared (struct convention), defaults to .transient (least
  durable). Non-resource categories keep lifecycle null. The E1
  test 'lifecycle is null for ... resources' is replaced by
  'lifecycle defaults to .transient for resources, null otherwise'
  to reflect the new contract.

Tests (tests/core/resources/):
- api_test.zig — 6 tests (set/get round-trip, overwrite,
  remove invalidates get, has flips, getMut visible via get, two
  resources coexist).
- change_detection_test.zig — 5 tests (getResourceMut bumps
  changed_tick, since=before returns true, since=after returns
  false, absent resource returns false, initial set marks
  added_tick).
- query_exclusion_test.zig — 3 tests (singleton invisible to
  comptime query, two resources have distinct entities, user
  entity with same component type coexists via marker-distinct
  archetype).
- lifecycle_test.zig — 6 tests (config / state / transient
  declared via T.lifecycle, default transient for unannotated
  resources, null for non-resource categories, inferLifecycle is
  comptime-foldable).

Standalone: 20/20 resources tests pass. Full suite EC=0, all CI
gates green (build, test, fmt, lint).

Refs: briefs/M0.2-rtti-resources-events-bindgen.md sections
Journal + Notes (decisions techniques E3).
Lands the event subsystem under src/core/events/ following the
ecs/root.zig / rtti/root.zig / resources/root.zig pattern. Single
canonical entry point, no parallel src/core/events.zig.

Module layout:
- src/core/events/lifetime.zig — Lifetime enum (.tick / .phase /
  .frame).
- src/core/events/cursor.zig — EventCursor POD (type_id +
  last_read + epoch).
- src/core/events/queue.zig — EventQueue(T) bounded MPMC ring
  buffer via the Vyukov pattern (power-of-two cap, per-slot
  atomic seq, claim via CAS on head, publish via release store of
  slot.seq). Saturation = drop-oldest (overwrite the slot, bump
  drops_since_last_drain). Cursors track last_read independently;
  poll snaps forward when overrun. drain() bumps an atomic epoch
  so stale cursors fail next poll with error.CursorInvalidated.
- src/core/events/bus.zig — EventBus indexes typed queues by
  rtti.TypeId. Vtable monomorphised per T for type-erased drain /
  drops accounting. register / emit / subscribe / poll /
  drainAtBoundary surface. DROPS_WARN_THRESHOLD = 10 — drains
  above the threshold emit a std.log.scoped(.events).warn.
- src/core/events/root.zig — re-export public surface, pinned in
  src/core/root.zig for the lazy-analysis guard.

ECS wiring:
- World gets event_bus: EventBus field (decision option (ii) from
  brief § Notes — direct field rather than scheduler-injected via
  ModuleContext; matches the E3 singleton_resources pattern and
  engine-tier-interfaces.md §0 which lists event_bus as a Tier 0
  service).
- src/core/ecs/scheduler.zig dispatches drainAtBoundary(.phase)
  after every phase, drainAtBoundary(.tick) and (.frame) at end of
  dispatchFrame. The three boundaries are distinct call sites
  even though tick=frame collapse to a single dispatch in Phase 0
  (ready to diverge Phase 0.4+).

Tests (tests/core/events/):
- queue_test.zig — 6 tests (emit + poll, FIFO ordering, MPMC
  concurrent 4 producers × 1000 emits with no loss, isolation
  between event types, emit without register fails,
  queueCount + AlreadyRegistered).
- saturation_test.zig — 3 tests (cap=4 emit 6 keeps the last 4
  plus drops counter = 2, drains reset the counter, warning
  threshold = 10 exercised end-to-end).
- lifetime_test.zig — 5 tests (per-lifetime drain isolation,
  phase queue survives tick drain, frame queue survives
  phase+tick drains, cursor invalidated after drain,
  re-subscribe yields a fresh cursor).
- scheduler_integration_test.zig — 4 tests (events from Update
  drained before PostUpdate when lifetime=.phase, frame N events
  invisible in frame N+1, same-phase emit-then-poll within
  Update visible, world.event_bus smoke).

Standalone: 18/18 events tests pass. Full suite EC=0, all CI gates
green (build, test, fmt, lint).

Refs: briefs/M0.2-rtti-resources-events-bindgen.md sections
Journal + Notes (decisions techniques E3 + E4).
Lands the unified bindgen system per engine-c-bindings.md §1.
The two M0.2 adapters (vk_xml, wayland_xml) port the legacy
tools/vk_gen/ and tools/wayland_gen/ pipelines 1:1 byte-for-byte.

Layout:
- tools/bindgen/main.zig — CLI dispatcher (--target vulkan|wayland|all)
- tools/bindgen/adapters/vk_xml.zig — entry, was tools/vk_gen/main.zig
- tools/bindgen/adapters/vk_xml/{parser,emit}.zig — moved from
  tools/vk_gen/{parser,emit}.zig via git mv
- tools/bindgen/adapters/wayland_xml.zig + wayland_xml/* — same
  pattern for the Wayland port
- tools/bindgen/core/{api_description,validator,resolver,emitter}.zig
  — skeleton for Phase 1+ keepers (Opus, Assimp, KTX/Basis,
  libdatachannel, ACL, HarfBuzz, ONNX). Not exercised by M0.2
  adapters per decision technique E5 (i) tracked in
  briefs/M0.2-rtti-resources-events-bindgen.md § Notes.

Decision technique E5 (i): the M0.2 adapters bypass the
ApiDescription intermediate to preserve the 'diff vide'
critère mécanique. Routing 1497 lines of Vulkan emission +
697 of Wayland through a common typed format and a generic
emitter would introduce bit-level divergence risk (whitespace,
internal ordering, ID stability) incompatible with the
non-negotiable byte-for-byte port contract. The structure is
in place ; the first Phase 1+ keeper to ship via
bindings/manual/*.api.zig will be the first real exerciser of
the core emitter.

bindings/generated/{vulkan,wayland}.api.zig: placeholder
sidecar descriptions (name, version, source, link) — minimal
ApiDescription that documents the binding's identity without
participating in the round-trip pipeline in M0.2.

build.zig changes:
- bindgen-vk and bindgen-wayland point to the relocated
  adapters under tools/bindgen/adapters/.
- New 'bindgen' step aggregates both adapters.
- New 'bindgen-verify' step regenerates and runs
  'git diff --quiet bindings/generated/ src/core/platform/'.
  Exit code 0 if the regen is byte-identical to the committed
  output, non-zero on any drift.

Validation:
- diff -r /tmp/baseline_platform_E5 src/core/platform/ → EMPTY
  (snapshot taken pre-E5 at 7adb40c).
- zig build bindgen-verify → exit 0.
- Full test suite EC=0 (zig build / zig build test / zig fmt
  --check / zig build lint all green).

CI: .github/workflows/ci.yml gains a 'zig build bindgen-verify'
step in the matrix build-and-test job (Ubuntu + Windows ×
Debug + ReleaseSafe). Drift blocks the merge.

Tests: tests/bindgen/roundtrip_test.zig invokes
'zig build bindgen-verify' as a subprocess and asserts exit 0.
Mirrors the CI gate at the test layer for local fast feedback.

Smoke test macOS: pre-existing UnsupportedPlatform via the
stub backend (cf. validation/s6-go-nogo.md). Not a regression.
Hardware smoke on Linux + Windows deferred to E6 milestone
closure validation.

Refs: briefs/M0.2-rtti-resources-events-bindgen.md sections
Journal + Notes (decisions techniques E5 (i)).
Squelette du plugin loader Tier 0 — signatures finales de la table
WeldAPI + 7 sous-APIs (ECS 24 + Resource 8 + Event 6 + Service 2 +
Memory 8 + Editor 17 + Platform 14 callbacks). Toutes les
callbacks renvoient WELD_ERR_NOT_IMPLEMENTED — aucun câblage réel
vers le Tier 0 Zig (Phase 3).

Composants livrés :
- desc.zig : C ABI fondamentaux (WeldEntity, WeldComponentId,
  WeldResult, WeldVec*, WeldQuat, WeldMat*, WeldColor, WeldStr,
  WeldPluginDesc, WeldPluginCaps, WeldPluginCallbacks). Le ptr
  WeldAPI dans les callbacks est *const anyopaque pour briser la
  dépendance cyclique desc <-> api ; cast côté boundary.
- api.zig : 79 callbacks au total dans les 7 sous-APIs, table
  WeldAPI + singleton stub_api exposé.
- loader.zig : std.DynLib wrapper (platform.dynamic_loader prévu
  M0.3 — substitution drop-in sans changement de surface). load
  valide weld_plugin_entry présent + api_version_min <=
  WELD_API_VERSION_MAJOR. log.warn (pas .err) sur chemins erreur
  pour éviter false fails du test runner Zig 0.16 sur tests
  négatifs.
- root.zig : re-export public (desc, api, loader).

Tests :
- load_unload_test.zig (6 tests) : happy load, lecture
  WeldPluginDesc, unload sans leak, MissingEntryPoint,
  ApiVersionTooNew, LibraryLoadFailed. Path lib OS-spécifique via
  builtin.os.tag.
- api_stub_test.zig (12 tests) : énumère exhaustivement chaque
  callback des 7 sous-APIs, vérifie le retour stub. Fige les
  signatures pour C0.5.

Build :
- 3 stubs cross-platform (.so / .dylib / .dll) compilés en Zig pur
  via tests/core/plugin_loader/stub_plugin/{plugin,plugin_future_api,
  plugin_no_entry}.zig. Module weld_plugin_abi exposé aux
  sub-projets stub (décision Cas 3 i — import croisé sur ABI
  plutôt que duplication). Step aggregator stub-plugins.
- TestSpec.needs_stub_plugins flag : le test load_unload_test
  dépend des install steps des 3 stubs.

Cohérent engine-c-api.md §2-§11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pose les marqueurs `/// FROZEN — see engine-phase-0-criteria.md C0.5
(M0.2)` sur chaque type publique et chaque fonction publique de
surface dans les 3 subsystems Tier 0 gelés au tag M0.2 :

- src/core/rtti/ : 5 fichiers, 29 marqueurs au total (root +
  type_info + registry + hash + comptime_builder)
- src/core/resources/ : 3 fichiers, 10 marqueurs (root + registry +
  api)
- src/core/events/ : 5 fichiers, 8 marqueurs (root + lifetime +
  cursor + queue + bus)

Total : 13 fichiers, 47 occurrences (audit grep 100 % de couverture
surface-publique). Le marqueur file-level `//! FROZEN —` est posé
sur chaque module entry (root.zig). Critère C0.5 mécanique « ≥ 1
occurrence par fichier de surface publique » → atteint.

Toute modification de signature après ce point requiert un process
de versioning explicite (cf. engine-phase-0-criteria.md C0.5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rapports de rejeu de fin de milestone (clôture E6) :

- bench/reports/ecs_benchmark_S1_2026-05-22.md : S1 ReleaseSafe
  --workers=4 + ReleaseFast (7 runs), médiane ~74 µs vs gate 65 µs
  (dev-mode non-opposable per engine-phase-0-criteria.md §
  Méthodologie bench). Pas de régression structurelle — les
  sub-systèmes M0.2 ne touchent pas le hot path ECS. Re-bench
  cold-isolé planifié pré-tag.
- bench/reports/ecs_benchmark_C0.1_2026-05-22.md : C0.1 ReleaseFast
  --workers=8, médiane 3.21 ms / p99 8.92 ms / imbalance 9.84 %
  (gate 17.5 ms — GO confortable 5.5x).
- bench/reports/ipc_rtt_2026-05-22.md (overwrite) : IPC RTT
  ReleaseSafe, p50 5 µs / p99 14 µs / max 56 µs (baseline S6 6 µs
  / gate +5 % = 6.3 µs — GO, p50 structurellement sous baseline).
  Annexe : archive E2 post-swap 5-run préservée pour traçabilité.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clôture du milestone M0.2 :

- Journal d'exécution étendu (E6 ÉTAPES A-J détaillées : plugin
  loader, freeze C0.5, bindgen cleanup, smoke, benchs, audit
  FROZEN, Notes de fin).
- Section « Notes de fin » remplie (5 rubriques : ce qui a marché,
  déviations, à signaler en review, mesures finales, dette
  résiduelle).
- Status PLANNED → CLOSED, date de fermeture 2026-05-22.

Tous gates verts au commit de clôture : `zig build` EC=0, `zig
build test` EC=0, `zig build lint` EC=0, `zig fmt --check` EC=0,
`zig build bindgen-verify` EC=0.

Closes M0.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`std.DynLib` est `@compileError("unsupported platform")` sur Windows
dans la stdlib Zig 0.16 (cf. `lib/std/dynamic_library.zig` ligne 21).
Le squelette E6 initial consommait `std.DynLib` directement et
cassait le CI Windows Debug (3 erreurs de compilation).

Fix : hand-roll d'une mince abstraction `dlopen` / `LoadLibraryA`
locale au loader, exactement le pattern utilisé par
`src/core/platform/vk.zig` ligne 10866 (qui a déjà résolu le même
problème pour le loader Vulkan). Branche `builtin.os.tag` :

- Windows : `LoadLibraryA` / `GetProcAddress` / `FreeLibrary` via
  `extern "kernel32"`
- POSIX (Linux + macOS + *BSD) : `dlopen(RTLD_NOW=2)` / `dlsym` /
  `dlclose` via `extern "c"`

API publique du loader inchangée. `PluginHandle.dyn_lib:
?std.DynLib` devient `PluginHandle.dyn_handle: ?*anyopaque` pour
porter le handle opaque commun aux deux plateformes.
`lookupSymbol(handle, T, name) ?T` devient `lookupSymbol(handle,
name) ?*anyopaque` — le cast côté caller est cohérent avec le
pattern dlopen et permet de garder la signature
cross-platform-compatible (le wrapper type-safe arrive avec
`platform.dynamic_loader` en M0.3).

Quand `platform.dynamic_loader` sera extrait en M0.3, ce bloc local
sera remplacé par l'import du wrapper sans changement de la
surface publique du loader.

Tests : full suite verte (EC=0), aucun test modifié — la
sémantique cross-platform est strictement préservée.

Closes Windows CI Debug failure on PR #14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entrée 2026-05-22 16:15 — diagnostic + fix du Windows CI Debug
fail post-push PR #14. `std.DynLib` Windows compileError dans la
stdlib Zig 0.16 → hand-roll dlopen/LoadLibraryA en commit `c3e1284`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-bench S1 cold-isolé opposable au HEAD `f2f823d` de la branche
M0.2, en pré-requis du tag `v0.2.0-M0.2-rtti`. Protocole strict
respecté : session dev fermée (confirmation Guy), 60 s cool-down
post-build, 7 runs successifs avec pause 30 s entre chaque, aucun
retry ni cherry-pick.

Résultat : médiane des médianes = 74.7 µs > gate strict 65 µs.

Détail (médianes des 7 runs en ns, ordre d'exécution) :
60 042 / 60 667 / 72 750 / 78 166 / 75 667 / 76 041 / 74 709.

Médianes triées : 60 042 ; 60 667 ; 72 750 ; **74 709** ; 75 667 ;
76 041 ; 78 166. La position 4 sur 7 = 74 709 ns = 74.7 µs.

Pattern bimodal observé : runs 1-2 mesurent ~60 µs (régime
quiescent post-cool-down), runs 3-7 mesurent 73-78 µs (régime
« warm » avec pause 30 s insuffisante pour ramener au quiescent).
Imbalance dans le gate sur tous les runs (max 8.72 %, gate 15 %)
— pas de régression du work-stealing.

Verdict opposable : NO-GO (+15 % vs gate 65 µs ; +37 % vs baseline
S1 cold-isolé 54.5 µs au tag `v0.0.2-S1-mini-ecs`).

Aucune mitigation unilatérale proposée — pas de re-run
cherry-pick, pas de re-tune de paramètres, pas d'ajustement du
gate. Le verdict est archivé tel quel pour analyse Claude.ai.

**Blocage Cas 2 — retour Claude.ai requis avant tag M0.2.**

Rapport complet : `bench/reports/ecs_benchmark_S1_2026-05-22-coldisolated.md`.
Le rapport dev-mode `bench/reports/ecs_benchmark_S1_2026-05-22.md`
reste en place comme trace de la session E6 — non-modifié.

Note conventions : le titre directive Guy était `bench(ecs):` mais
ce TYPE n'est pas autorisé par CLAUDE.md (`feat|fix|perf|refactor|
test|docs|chore|breaking`) ni par le brief §Conventions (« Pas de
TYPE bench »). Substitué par `test(bench-ecs):` — déviation
mineure tracée ici, sémantique du message inchangée.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-bench S1 cold-isolé v2 spec-conforme au HEAD `4de00f6` de la
branche M0.2. Protocole strict respecté avec timestamps tracés :

- Cool-down initial 484 s (seuil 300 s) ✓
- 6 pauses inter-runs de 120 s chacune (seuil 120 s) ✓
- Machine pré-confirmée isolée par Guy (DND, apps non-système
  fermées, pas de Time Machine / Spotlight / sync iCloud)
- Aucun retry, aucun cherry-pick, aucune interruption

Résultat : médiane des médianes = 75.2 µs > gate strict 65 µs.

Détail médianes des 7 runs en ordre d'exécution (ns) :
75 166 / 72 125 / 74 583 / 77 000 / 79 958 / 76 708 / 60 000.

Médianes triées : 60 000 ; 72 125 ; 74 583 ; **75 166** ; 76 708 ;
77 000 ; 79 958. Position 4/7 = 75 166 ns = 75.2 µs.

Pattern observé : 6 runs/7 dans la zone 72-80 µs avec imbalance
6.76-8.40 %, 1 run outlier (run 7) à 60 µs avec imbalance 1.60 %.
Pas de motif temporel — le run 7 n'est ni le premier ni un cas
particulier. L'imbalance est corrélée à la médiane (faible
imbalance → faible médiane) mais reste dans le gate 15 % sur tous
les runs.

Verdict opposable : NO-GO (+15.7 % vs gate 65 µs, +21 % vs
baseline canonique 62 µs post-recalibration M0.1/E6 sur même
machine, +38 % vs baseline S1 v0.0.2 mini-ECS minimal 54.5 µs).

La v1 (protocole non spec-conforme, médiane 74.7 µs) avait
suggéré un effet thermique bimodal. La v2 (strict) confirme la
régression chiffrée mais avec un pattern différent — l'hypothèse
thermique est insuffisante pour expliquer la dégradation
structurelle.

Aucune mitigation unilatérale proposée. Aucune hypothèse de cause
avancée. Surface modifiée listée dans le rapport (candidats E3
Resources + E4 Events car seuls qui touchent scheduler / rescan
path), pas un diagnostic.

**Blocage Cas 2 — régression structurelle suspectée. Retour
Claude.ai requis avant tag M0.2.**

Note convention : title directive Guy était
`bench(ecs): cold-isolated S1 regression confirmed for M0.2 (strict protocol)`
mais TYPE `bench` non autorisé par CLAUDE.md, et titre > 72 char.
Substitué par `test(bench-ecs): cold-isolated S1 v2 strict NO-GO (M0.2)`
— sémantique du message inchangée.

Rapport complet :
`bench/reports/ecs_benchmark_S1_2026-05-22-coldisolated-v2.md`.

Le rapport v1 `ecs_benchmark_S1_2026-05-22-coldisolated.md` reste
archivé pour traçabilité méthodologique. Le rapport dev-mode
`ecs_benchmark_S1_2026-05-22.md` reste également intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-baseline opposable thermal-aware suite à l'investigation
post-bench v2 strict NO-GO. Le bisect a montré que le tag M0.1
produisait aussi 74-78 µs sous le protocole v2 strict (5 min + 2
min) — pas de "first bad commit" identifiable, hypothèse retenue :
thermal throttling cumulé sur MacBook M4 Pro inadapté au
protocole spec calibré pour desktop refroidissement actif.

Protocole thermal-aware appliqué (30 min idle initial + 15 min
inter-run + 3 runs / session) avec instrumentation `powermetrics
--samplers thermal,cpu_power` pour confirmer/réfuter l'hypothèse.

Résultats :
- HEAD M0.2 (`70c08c1`) : médiane des médianes = 60.17 µs
  (runs : 60.17 / 59.71 / 60.75 µs)
- M0.1 (`v0.1.0-M0.1-ecs-full`) : médiane des médianes = 59.21 µs
  (runs : 57.54 / 61.04 / 59.21 µs)
- Écart HEAD vs M0.1 : +1.62 % (bruit, non significatif)

Hypothèse thermal **confirmée instrumentalement** :
- 1500/1500 samples thermal restent en `Nominal` sur 6 runs
- Aucun passage Moderate/Heavy/Trapping/Sleeping
- Avec 30+15+3 protocole, M4 Pro ne déclenche pas le throttling
- Le v2 strict (5+2+7) accumulait suffisamment de charge thermal
  pour faire glisser les médianes 72-80 µs SANS signaler `pressure`
  (comportement SoC Apple Silicon : freq limit sustained avant
  signal `Moderate`)

**Pas de régression code détectable entre M0.1 et HEAD M0.2.**
Les sous-systèmes E1-E6 (RTTI, IPC swap, Resources, Events,
bindgen, Plugin loader) n'introduisent aucune dégradation
observable sur le hot-path scheduler S1.

Baseline candidate proposée :
- Médiane M4 Pro thermal-aware : 60 µs (rounded conservative)
- Gate associé compatible : 65 µs canonique (60 + 5 % = 63 µs
  intra-machine, ou 65 µs cross-machine compatible)
- Protocole opposable M-series : 30 min initial + 15 min
  inter-run + 3 runs / session

Décisions laissées à Claude.ai :
- Adoption baseline 60 µs comme nouvelle baseline M4 Pro
- Intégration du protocole 30+15+3 dans
  engine-phase-0-criteria.md § Méthodologie bench (variante
  machine-aware ; le 5+2+7 reste valide pour desktop refroid actif)
- Tag M0.2 ou re-confirmation post-merge

Annexes captures complètes archivées sous /tmp/bench_thermal/
(éphémère). Les rapports v1 / v2 strict restent en place comme
trace méthodologique du parcours.

Note parser : `temp_avg=NA` sur tous les runs — Apple Silicon
n'expose pas la die temperature via `powermetrics` (limitation
macOS/M-series). Le signal canonique sur M-series est `Current
pressure level`, capté correctement (1500/1500 Nominal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@guysenpai guysenpai merged commit 7db5c86 into main May 23, 2026
6 checks passed
@guysenpai guysenpai deleted the phase-0/core/rtti-resources-events-bindgen branch May 23, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant