feat(#461): manifest-v2 PR11 — open config for 3rd-party backends + capability gate - #472
Conversation
… capability gate Cut the residual `@tagName(cfg.backend)` coupling so a THIRD-PARTY backend selected purely by name+package (no enum tag) resolves + generates entirely through backend_registry + its v2 manifest: - config: add documented `ProjectConfig.isEnumTagBacked()` — the one remaining enum-as-identity read (name == tag), covering enum-shorthand built-ins and their local dev overrides; a name-only third-party backend returns false and never reaches the enum `switch (cfg.backend)`. - build_files: `externalUsesEnumPath` now keys off `isEnumTagBacked()` instead of the raw `@tagName` compare (pure relocation, no behavior change). - manifest_splice: fix the stale doc comment claiming `backendPackageDir` still uses `@tagName` — it routes through `backend_registry.resolveBackendPackage` by string name. Capability validation before wiring (RFC step 1): on the v2 generation path, run `validateProviderIdentity` + `capabilities.validate(requiredCapabilities, manifest.capabilities)` right after the v2 manifest loads and BEFORE any build-graph text is emitted. A v2-only backend previously bypassed both checks (the resolve-time path reads only the v1 backend.manifest.zon). A missing required capability now fails with the readable project-level `error.UnsupportedCapability`, and a third party claiming `labelle.*` is still rejected. No-op in production (v2 manifest is null unless opted in), so the enum/v1 path stays byte-identical. Third-party fixture + tests: `backends/acme_foo` — a hookless declarative v2 backend with a non-`labelle.` id (`acme.foo`), name matching no enum tag. New tests prove: (1) it generates a valid build.zig via name+package + v2 manifest with no enum tag; (2) a project `.requires`ing a capability it does not declare errors with `error.UnsupportedCapability` before wiring; (3) the complement (requiring the declared `.headless`) generates fine. sokol-desktop byte anchor 0-diff and all prior goldens unchanged; `zig build` + `zig build test` exit 0. Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a v2 backend manifest for ChangesThird-party v2 manifest routing and testing
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request implements support for third-party backends using the v2 manifest schema, allowing backends to be selected purely by name and package without a matching Backend enum tag. Key changes include introducing the isEnumTagBacked helper in ProjectConfig to route name-only backends through the registry, adding capability negotiation and identity validation on the v2 path, and introducing a mock third-party backend fixture (acme_foo) with corresponding unit tests. There are no review comments to address, so I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/codegen/manifest_splice.zig (1)
34-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStale docstring now contradicts the updated file-header doc.
The new header doc (lines 34-42) states the open-config step (arbitrary name+package, no enum entry) is done — proven by the acme_foo fixture. But the unchanged
backendPackageDirdocstring a few lines below still describes this as a future "next step" and claimsbackendName()"only yields built-in names today," which is no longer true.✏️ Suggested docstring update
/// Locate the backend package so the splice can read its manifest (chicken-and- /// egg: the dir name lives in the manifest we haven't read yet, so we resolve by -/// the backend's *name*). Now routed through the `backend_registry` — keyed by -/// `cfg.backendName()`, a string, NOT `@tagName` directly. The only residual -/// enum coupling is config *parsing* (`.backend` is still the closed enum, so -/// `backendName()` only yields built-in names today); opening config to an -/// arbitrary name+package is the next step, after which a third-party backend -/// flows through this same registry lookup with no enum entry. +/// the backend's *name*). Routed through the `backend_registry` — keyed by +/// `cfg.backendName()`, a string, NOT `@tagName` directly. A third-party backend +/// named only via `.backend_package` (no matching `Backend` enum tag) flows +/// through this same registry lookup with no enum entry (open-config, `#453` PR 11).Also applies to: 196-207
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/codegen/manifest_splice.zig` around lines 34 - 42, Update the stale docstring in manifest_splice.zig so it matches the new header doc and current behavior of backendPackageDir/backend_registry.resolveBackendPackage. The comment should no longer describe arbitrary name+package backends as a future “next step” or say cfg.backendName() only returns built-in names; instead, explain that name-only backends already resolve through the registry without relying on `@tagName`(cfg.backend). Keep the wording aligned with the existing symbols backendPackageDir, cfg.backendName(), and cfg.isEnumTagBacked().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/codegen/manifest_splice.zig`:
- Around line 34-42: Update the stale docstring in manifest_splice.zig so it
matches the new header doc and current behavior of
backendPackageDir/backend_registry.resolveBackendPackage. The comment should no
longer describe arbitrary name+package backends as a future “next step” or say
cfg.backendName() only returns built-in names; instead, explain that name-only
backends already resolve through the registry without relying on
`@tagName`(cfg.backend). Keep the wording aligned with the existing symbols
backendPackageDir, cfg.backendName(), and cfg.isEnumTagBacked().
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c40cc784-8882-4f9f-8638-4982e57dd893
📒 Files selected for processing (6)
backends/acme_foo/backend.manifest.v2.zonsrc/build_files.zigsrc/codegen/manifest_splice.zigsrc/config.zigtest/build_zig_tests.zigtest/helpers.zig
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fcb473dc93
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| cfg.backend_package = acme_foo_fixture_package; | ||
| var opts = opts_in; | ||
| opts.project_dir = "."; | ||
| opts.backend_manifest_name = "backend.manifest.v2.zon"; |
There was a problem hiding this comment.
Wire v2 manifest selection through the real generator
This helper is the only place that sets backend_manifest_name for the new name-only provider, but the production root.generate path still preflights externals with the legacy manifest name and calls generateBuildZig without this option. A real project using an acme_foo-style v2-only backend_package therefore errors as manifest-less before the v2 manifest can be loaded, so the advertised third-party backend path works only in these direct build.zig unit tests unless the production generator also detects or passes the v2 manifest name.
Useful? React with 👍 / 👎.
| .dir_name = "acme_foo", | ||
| // A third party names its own dependency however it likes — NOT the | ||
| // `labelle_*` convention. Emitted verbatim into `b.dependency("acme_foo", ..)`. | ||
| .dep_name = "acme_foo", |
There was a problem hiding this comment.
Emit ZON entries using the manifest dep name
For a third-party manifest whose dependency key is intentionally acme_foo, the generated build.zig calls b.dependency("acme_foo", ...), but the ZON generator/deps linker still derives backend entries as labelle_<backendName()> rather than reading m.dep_name. Once this v2 manifest is used outside the unit helper, build.zig.zon will contain .labelle_acme_foo while build.zig looks up .acme_foo, so Zig cannot resolve the backend dependency; either the ZON entry needs to use the manifest dep name or v2 manifests must not advertise arbitrary dep names.
Useful? React with 👍 / 👎.
|
Two findings triaged: P1 (ZON dep name) — fixing now. Correct bug: the v2 P2 (production |
A v2 backend's generated build.zig resolves its provider modules via
`b.dependency(m.dep_name, ..)`, but the build.zig.zon generator derived the
backend dependency key as `labelle_<name>` from the package/backend name. For a
third-party backend whose package name is not `labelle_*` (acme_foo → dep_name
`acme_foo`) the two files disagreed (`.labelle_acme_foo` in the zon vs
`b.dependency("acme_foo")` in build.zig), so Zig could not resolve the backend
dependency once a v2 manifest drove generation.
Both zon emission paths (the deps-linker loop and the relative-path fallback)
now re-key the backend dependency entry to `m.dep_name` on the v2 path, computed
once via the new `v2BackendDepName` helper (loads the named manifest, returns
`dep_name` for a v2 manifest, null for v1/enum). For a built-in v2 backend
`m.dep_name` already equals the `labelle_<name>` derivation (sokol →
`labelle_sokol`), so built-in zon output is byte-unchanged; the v1/enum path is
untouched.
Test: MANIFEST_V2_THIRD_PARTY_OPEN_CONFIG asserts the acme_foo build.zig.zon
keys the backend dep `.acme_foo` (matching `b.dependency("acme_foo")`) and never
`labelle_acme_foo`. The raylib_v2-wasm byte-identical zon test — whose fixture
has a package-name/dep_name mismatch (raylib_v2 vs labelle_raylib) — was
comparing two both-wrong zons; it now compares the emsdk section (its actual
subject) and locks in the correct `labelle_raylib` key.
Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/build_files.zig`:
- Around line 1092-1093: `v2BackendDepName` can fail before the later
`resolved_deps` cleanup defer is installed, so any already-owned entries leak.
In the code around `v2BackendDepName` in `build_files.zig`, add cleanup for
`resolved_deps` immediately before or around that lookup so it runs on error
too, and make sure the existing `resolved_deps` ownership handling remains
consistent with the later `if (resolved_deps)` block.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 43510154-8743-4083-b5f1-9bc7e33a06f3
📒 Files selected for processing (3)
src/build_files.zigtest/build_zig_tests.zigtest/helpers.zig
🚧 Files skipped from review as they are similar to previous changes (1)
- test/helpers.zig
…me call (PR #472) Move the `deps_linker.freeDepEntries` defer for `resolved_deps` to immediately after it's assigned, so the entries are freed on every subsequent error path — including the fallible `v2BackendDepName` call added by the P1 fix, which previously leaked on error since the free defer was installed only inside the later `if (resolved_deps)` block. Removed the now-duplicate defer from that block (no double-free). Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f6d61f32d8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| (if (std.mem.eql(u8, dep.zon_name, derived_backend_zon)) dn else dep.zon_name) | ||
| else | ||
| dep.zon_name; | ||
| try w.print(" .{s} = .{{\n", .{zon_name}); |
There was a problem hiding this comment.
Quote v2 backend dep names in build.zig.zon
When a v2 manifest uses a dependency name that is valid for b.dependency("...") but not a bare ZON identifier, such as the common raylib-zig/acme-foo form, this raw field emission produces invalid ZON like .acme-foo = .{ ... }. The repo already has to spell these keys as .@"raylib-zig" in package ZON files, so a third-party backend with a hyphenated dep_name will generate a build.zig.zon that fails to parse before Zig can resolve the backend; please escape/quote the field name or reject unsupported dep_name values in both this linked-deps path and the fallback emitter.
Useful? React with 👍 / 👎.
…anifest.v2.zon (#473) * feat(#453): production `generate` v2 manifest cutover (closes #472 P2) Make the real `generate` entry auto-detect a backend's `backend.manifest.v2.zon` in the resolved backend package and drive the manifest-v2 codegen path — without the caller passing `backend_manifest_name`. Closes the #472 P2 finding. - Add `manifest_v2.V2_MANIFEST_NAME` (canonical `backend.manifest.v2.zon`). - `root.zig`: probe ONCE via `detectV2ManifestName` (resolveBackendPackage + access), thread the result through the 4 sites: `requireManifestIfExternal` (so a v2-only external isn't rejected as manifest-less), `generateBuildZigZon`, `generateBuildZig`, and `stageBackendBuildHook` (stages the hook next to build.zig when the v2 manifest declares one). The tests-target path (#83) inherits this since the probe lives inside `generate`. - Graceful degradation: any probe I/O error (resolution/access/OOM) falls back to null (v1/enum), never crashes. Production NO-OP today: no external backend repo ships a v2 manifest yet, so every real `generate` returns null and output is byte-identical. Tests (drive the REAL `generate`, not the `generateBuildZig` unit helper): - acme_foo (v2-only) → generate auto-detects v2 and emits v2 build.zig (`b.dependency("acme_foo")` + generic `unifyCoreDiamond` walk), no opt-in. - new `backends/sokol_v1only` fixture → generate stays on v1/enum (no v2 markers, no hook import). - sokol (dual manifest) → auto-detected v2 output == v1/enum baseline (production-no-op guarantee). `zig build` + `zig build test` exit 0; existing goldens/byte-anchors unchanged. Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw * fix(#453): thread auto-detected v2 manifest into template + contract seams (PR #473) PR #473 review: `generate` auto-detects a `backend.manifest.v2.zon` and threads `backend_manifest_name` through build-file emission, but two downstream sites still ignored it. A v2-ONLY backend (no legacy `backend.manifest.zon`) therefore passed build.zig emission and then either failed main.zig template loading or had its identity/capability contract silently skipped. Finding 1 (main-template loading, `loadBackendTemplate`): now takes `backend_manifest_name`; when a v2 manifest is detected it resolves the entry-point template from `.platforms[<platform>].entry` and keys `requireManifestIfExternal` off that same name (so a v2-only external is not rejected as manifest-less). The per-platform run-loop style is likewise read from `.platforms[<platform>].loop_style`. v1/enum path unchanged (name null → falls through to the existing v1 splice / enum mappings). Finding 2 (provider-contract validation, `validateProviderContracts`): now takes `backend_manifest_name`; when a v2 manifest is detected it runs the identity + capability-gate checks against the v2 `.id`/`.capabilities` instead of the (absent) legacy provider manifest. Shared body factored into `validateProviderContractsInner` so v1 and v2 run the same negotiation. Both seams load the detected v2 manifest in place (matching the existing v1 style, which loads per-site) and degrade gracefully (probe/parse failure or a file that parses as v1 → the v1/enum path). Tests: strengthened the real-`generate` cutover coverage with a capability mismatch caught through the production entry point, plus direct seam tests proving `loadBackendTemplate` + `validateProviderContracts` honor a passed `backend_manifest_name` (v2-only sokol variant for the template, acme_foo for the contract) — each with a null-name counter-test proving the detected name is load-bearing. Added `backends/sokol_v2only/templates/desktop.txt` so the v2-only fixture can be driven through template loading. Production byte-unchanged (no external repo ships v2 → probe null → legacy behavior). Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw * fix(#453): run legacy loop_style path for a v1-by-name manifest (PR #473 Major) The loop_style resolution chained the legacy `manifestPathEnabled` branch as an `else if (backend_manifest_name == null)`. When a detected named manifest (`backend.manifest.v2.zon`) parsed as v1 (`manifest_version <= 1`), the v2 arm no-oped AND the legacy `else if` was skipped because the name was non-null — silently leaving `loop_style_override` unset and dropping that backend's loop_style. Extract the resolution into `resolveLoopStyleOverride` and restructure so the legacy path is a SECOND guarded pass (`!v2_resolved`), not an else-if: it runs whenever a REAL v2 manifest (union tag .v2) did NOT handle it — name null, a named file that parsed as v1, or a swallowed v2 load error. Mirrors the .v2-returns / .v1-falls-through shape loadBackendTemplate + validateProviderContracts already use correctly (verified; neither had the bug, since their v2 arms return early so the legacy code after runs unconditionally). Production byte-unchanged (name is null in production today, so the legacy pass runs exactly as before). Tests: add three resolveLoopStyleOverride tests to MANIFEST_V2_CUTOVER_SEAMS — (1) the regression lock: a v1-by-name file (sokol_v1only) still resolves its legacy .callback loop_style instead of null; (2) a v2-only backend resolves from its per-platform matrix despite shipping no legacy manifest; (3) the same bgfx v2 manifest yields desktop=.loop, android=.callback. Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw * fix(#453): preserve a v2-named/v1-content manifest during fallback (PR #473 Major) `detectV2ManifestName` keys off file EXISTENCE, so a backend shipping ONLY `backend.manifest.v2.zon` whose CONTENT is v1 (manifest_version 1/omitted) is threaded as the detected name. Both fallback seams then retried the legacy pass with the canonical (null) name, probing the ABSENT `backend.manifest.zon` — the loop_style override dropped to null and the template fell to the enum path (reading the closed `cfg.backend` for a backend with no tag). Fix both sites to resolve from the file actually found: - resolveLoopStyleOverride (~505): the `.v1` arm now resolves loop_style straight from the parsed v1 manifest and marks it handled, so the canonical-name pass is suppressed. Renamed `v2_resolved` -> `handled`. - loadBackendTemplate (~1258): the `.v1` arm now resolves `main_loop_template` from the parsed v1 manifest and reads that template, instead of freeing and falling through to the canonical-name legacy pass. Order is now (1) `.v2` per-platform matrix; (2) detected name parsed as `.v1` -> resolve from THAT file; (3) name null / load error -> canonical `backend.manifest.zon`. Production is byte-unchanged (name is null -> straight to canonical, as before). Fixture: `backends/sokol_v1inv2` ships ONLY `backend.manifest.v2.zon` with v1 content (loop_style = .loop, no canonical sibling). Two seam tests assert the loop_style + template resolve from that file (not dropped to null, not erroring on a missing canonical). Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
…tests target The prior fix (be826cd) skipped only the ROOT-level `validateProviderContracts` capability requirement for the tests target. But `build_files.generateBuildZig` runs its OWN v2 capability validation (added in the #472 open-config PR), and the tests target calls `generateBuildZig` directly via `generateTestsTarget`. So the forced-null (`.headless`-only null-v2) tests harness still hard-failed `UnsupportedCapability` for GUI/gamepad projects — the #474 examples-integration gamepad example's tests-target generate. Guard `generateBuildZig`'s v2 capability REQUIREMENT check with `if (!opts.is_tests_target)`, consistent with the root-level skip. The provider IDENTITY check stays ON for the tests target (cheap + still valid); only the capability requirement is skipped. The real exe target (`is_tests_target = false`) is unchanged. `is_tests_target` was already threaded from the tests-target generate call site (root.zig:1006-1007). Adds two DIRECT `generateBuildZig` tests (via `h.genNullV2BuildZig`): a raw_backend (imgui) GUI project generating build.zig against forced-null (null-v2) does NOT error with `is_tests_target = true`, and STILL errors `UnsupportedCapability` with `is_tests_target = false`. This mirrors the CI scenario the root-level `validateProviderContracts` tests could not reach. Also re-points the "#473 finding 2" test to drive the REAL exe target (`is_tests_target = false`): it was passing only because `generateBuildZig`'s second gate caught the mismatch on the tests-target path (`generateAndReadBuildZig` forces `is_tests_target = true`), which this fix now correctly skips. The finding-2 catch is a resolve-time `validateProviderContracts` concern, so the exe path errors before any engine-template work. Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
…2.0) (#474) * feat(#461): bump null pin to 0.2.0 — flip null to v2 in production labelle-null 0.2.0 ships backend.manifest.v2.zon. With the generate cutover (#473) live, bumping builtinProvider(.null) means production `generate` now fetches null 0.2.0, auto-detects its v2 manifest, and builds the null backend via the declarative v2 build graph. First real production flip of the epic. null is the safest first backend (headless, desktop-only, pure-declarative, hookless). Validated by the null headless examples-integration CI (build+run on v2). Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw * fix(#83): skip capability gate for the tests-target forced-null The tests target (#83) force-substitutes `cfg.backend = .null` as a headless test harness while keeping the rest of the project config (e.g. `resolved_gui = imgui`), so `requiredCapabilities(cfg)` still derives the REAL backend's needs (`.raw_gui_adapter`, …). Now that null ships a v2 manifest declaring only `.headless`, the opted-in capability gate hard-failed `zig build test` (`UnsupportedCapability`) for every GUI/gamepad project — surfaced by the null→v2 flip in #474 CI. Thread `is_tests_target` into `validateProviderContracts` / `validateProviderContractsInner` and skip ONLY the capability requirement check for the forced-null tests harness. Identity + id-collision checks still run (cheap + valid). The real exe target is unaffected: a GUI project whose CHOSEN backend lacks `.raw_gui_adapter` still fails. Adds both-directions test: same imgui (raw_backend) project against the null-v2 fixture passes with `is_tests_target = true` and still errors `UnsupportedCapability` with `is_tests_target = false`. Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw * fix(#83): skip the SECOND capability gate (generateBuildZig) for the tests target The prior fix (be826cd) skipped only the ROOT-level `validateProviderContracts` capability requirement for the tests target. But `build_files.generateBuildZig` runs its OWN v2 capability validation (added in the #472 open-config PR), and the tests target calls `generateBuildZig` directly via `generateTestsTarget`. So the forced-null (`.headless`-only null-v2) tests harness still hard-failed `UnsupportedCapability` for GUI/gamepad projects — the #474 examples-integration gamepad example's tests-target generate. Guard `generateBuildZig`'s v2 capability REQUIREMENT check with `if (!opts.is_tests_target)`, consistent with the root-level skip. The provider IDENTITY check stays ON for the tests target (cheap + still valid); only the capability requirement is skipped. The real exe target (`is_tests_target = false`) is unchanged. `is_tests_target` was already threaded from the tests-target generate call site (root.zig:1006-1007). Adds two DIRECT `generateBuildZig` tests (via `h.genNullV2BuildZig`): a raw_backend (imgui) GUI project generating build.zig against forced-null (null-v2) does NOT error with `is_tests_target = true`, and STILL errors `UnsupportedCapability` with `is_tests_target = false`. This mirrors the CI scenario the root-level `validateProviderContracts` tests could not reach. Also re-points the "#473 finding 2" test to drive the REAL exe target (`is_tests_target = false`): it was passing only because `generateBuildZig`'s second gate caught the mismatch on the tests-target path (`generateAndReadBuildZig` forces `is_tests_target = true`), which this fix now correctly skips. The finding-2 catch is a resolve-time `validateProviderContracts` concern, so the exe path errors before any engine-template work. Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
…ion, audio/font wiring IS called) (#506) Doc/comment-only follow-up from the #386 audit. Corrects comments that now describe the opposite of what the code does — no behavior change, no golden/generated output moves, `zig build test` passes unchanged. - root.zig `detectV2ManifestName`: drop "PRODUCTION NO-OP TODAY" — the #472 P2 cutover shipped; detection is live and driven solely by the resolved backend package shipping `backend.manifest.v2.zon`. - root.zig `generate` call-site: replace the "production no-op today" sentence with the live-detection description. - codegen/blocks/asset_wiring.zig audio + font: replace the "SCAFFOLDING … NOT yet called" blocks with WIRED docs — both are called from buildSetupCode (loop.zig:107/108) and buildCallbackInitCode (callback.zig:95/96), gated on `.sound`/`.font` resources. - deps_linker.zig `stagesSdlGamepad`/`stagesAndroidGamepad`: document the enum arms as defensive dead code (every Backend tag is now external post-#386), citing the pinning tests by name. Refs #503 Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
PR 11 of the manifest-v2 epic (#461, #453 item 3). The ecosystem payoff: a third-party backend now works purely by name+package+v2 manifest — no
Backendenum tag required — plus capability validation fails fast before wiring.1.
@tagName(cfg.backend)coupling removedexternalUsesEnumPath(build_files.zig) was the one live site using@tagName(cfg.backend)as an external backend's identity. Now keys offProjectConfig.isEnumTagBacked()(config.zig) — true only when the resolved backend NAME equals the enum tag spelling (enum-shorthand built-ins + their local-dev overrides). Pure relocation, byte-identical. A name-only third-party backend (cfg.backendat its meaningless.raylibdefault) returns false → never reachesswitch (cfg.backend), routes entirely throughbackend_registry.resolveBackendPackage(string-keyed) + its v2 manifest. Fixed the stalemanifest_splice.zigdoc comment too.2. Capability validation before wiring (v2 path)
generateBuildZignow runsvalidateProviderIdentity(cfg, m.id)+capabilities.validate(requiredCapabilities(cfg), m.capabilities, provider_id)right after the v2 manifest loads and BEFORE any build-graph text is emitted. Previously the resolve-timevalidateProviderContractsread only the v1 manifest, so a v2-only backend bypassed both. Missing capability → readableerror.UnsupportedCapability; a 3rd party claiminglabelle.*still rejected. No-op in production (v2 manifest null unless opted in).3. Third-party fixture + tests
backends/acme_foo/backend.manifest.v2.zon— hookless declarative desktop backend, non-labelle.id (acme.foo), name matching no enum tag, advertises.headless.b.dependency("acme_foo"), nolabelle_raylib, generic walk, hookless); (b) AST-valid; (c).requires=&.{.screenshots}→error.UnsupportedCapability; (d) requiring the declared.headlessgenerates fine.All 6 built-ins unchanged (enum shorthand intact); sokol byte anchor 0-diff; all prior goldens unchanged; v1/enum byte-identical; #457 identity/collision/reserved-namespace checks not weakened (v2 path now actively invokes
validateProviderIdentity).zig build test+zig buildexit 0.Ref #461, #453.
https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
Summary by CodeRabbit
New Features
Bug Fixes
build.zig.zonbackend dependency keys to match the manifest’s declared dependency name.Tests