Skip to content

feat(#461): manifest-v2 PR11 — open config for 3rd-party backends + capability gate - #472

Merged
apotema merged 3 commits into
mainfrom
feat/453-manifest-v2-pr11-open-config
Jul 1, 2026
Merged

feat(#461): manifest-v2 PR11 — open config for 3rd-party backends + capability gate#472
apotema merged 3 commits into
mainfrom
feat/453-manifest-v2-pr11-open-config

Conversation

@apotema

@apotema apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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 Backend enum tag required — plus capability validation fails fast before wiring.

1. @tagName(cfg.backend) coupling removed

externalUsesEnumPath (build_files.zig) was the one live site using @tagName(cfg.backend) as an external backend's identity. Now keys off ProjectConfig.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.backend at its meaningless .raylib default) returns false → never reaches switch (cfg.backend), routes entirely through backend_registry.resolveBackendPackage (string-keyed) + its v2 manifest. Fixed the stale manifest_splice.zig doc comment too.

2. Capability validation before wiring (v2 path)

generateBuildZig now runs validateProviderIdentity(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-time validateProviderContracts read only the v1 manifest, so a v2-only backend bypassed both. Missing capability → readable error.UnsupportedCapability; a 3rd party claiming labelle.* 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.
  • Tests: (a) name-only backend generates valid build.zig via name+package+v2 (asserts b.dependency("acme_foo"), no labelle_raylib, generic walk, hookless); (b) AST-valid; (c) .requires=&.{.screenshots}error.UnsupportedCapability; (d) requiring the declared .headless generates 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 build exit 0.

Ref #461, #453.

https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw

Summary by CodeRabbit

  • New Features

    • Added support for third-party backends via the v2 manifest “open-config” flow, enabling integration without relying on built-in backend selection.
    • Build generation now performs upfront capability validation based on what the manifest declares.
  • Bug Fixes

    • Corrected generated build.zig.zon backend dependency keys to match the manifest’s declared dependency name.
    • Adjusted routing so open-config backends are resolved through the backend registry rather than enum-based logic.
  • Tests

    • Added an end-to-end test suite and fixtures covering hookless behavior, capability gating, and the updated zon keying.

… 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
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e863b66-5987-4238-99bd-18dbb7c49031

📥 Commits

Reviewing files that changed from the base of the PR and between d3532a3 and f6d61f3.

📒 Files selected for processing (1)
  • src/build_files.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/build_files.zig

📝 Walkthrough

Walkthrough

Adds a v2 backend manifest for acme_foo, updates v2 build generation to validate capabilities and provider identity, re-keys backend ZON dependencies from manifest data, and expands third-party open-config fixtures and tests.

Changes

Third-party v2 manifest routing and testing

Layer / File(s) Summary
acme_foo v2 backend manifest
backends/acme_foo/backend.manifest.v2.zon
New declarative v2 manifest defines acme_foo metadata, headless capability, module sources, desktop-only packaging, and no build hook.
Enum-tag-backed backend helper
src/config.zig, src/codegen/manifest_splice.zig
Adds ProjectConfig.isEnumTagBacked() and updates documentation for registry-based backend package resolution.
V2 manifest validation and enum-path routing
src/build_files.zig
Imports capability validation, switches enum-path detection to isEnumTagBacked(), and validates v2 provider identity and capabilities before emitting build.zig.
V2 backend dependency keying
src/build_files.zig
Loads v2 manifest dependency names and re-keys backend entries in build.zig.zon and the fallback path from the derived labelle name to dep_name.
Open-config fixture and coverage
test/helpers.zig, test/build_zig_tests.zig
Adds the acme_foo fixture and helpers plus tests for v2 build generation, AST validity, capability gating, ZON keying, and the raylib-wasm ZON comparison update.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Poem

I boing through manifests, soft and neat,
A headless acme_foo finds its beat.
ZON keys shift with a tiny hop,
And tests go thunk without a stop. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: v2 manifest support for third-party backends and capability gating.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/453-manifest-v2-pr11-open-config

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/codegen/manifest_splice.zig (1)

34-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale 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 backendPackageDir docstring a few lines below still describes this as a future "next step" and claims backendName() "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

📥 Commits

Reviewing files that changed from the base of the PR and between a67b3f9 and fcb473d.

📒 Files selected for processing (6)
  • backends/acme_foo/backend.manifest.v2.zon
  • src/build_files.zig
  • src/codegen/manifest_splice.zig
  • src/config.zig
  • test/build_zig_tests.zig
  • test/helpers.zig

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread test/helpers.zig
cfg.backend_package = acme_foo_fixture_package;
var opts = opts_in;
opts.project_dir = ".";
opts.backend_manifest_name = "backend.manifest.v2.zon";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@apotema

apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Two findings triaged:

P1 (ZON dep name) — fixing now. Correct bug: the v2 build.zig calls b.dependency(m.dep_name, …) but the ZON generator / deps-linker still derives labelle_<name> instead of reading m.dep_name, so a v2 backend with an arbitrary dep name (e.g. acme_foo) produces a build.zig.zon key that build.zig can't resolve. Pushing a fix so the v2 ZON entry uses m.dep_name.

P2 (production root.generate doesn't pass backend_manifest_name) — acknowledged, deferred by design. This is the production cutover, intentionally out of scope here. Per the design's migration plan (§6), the v2 path is gated-dark / opt-in throughout PRs 3–11 — production generate deliberately stays on the v1/enum path so every conversion lands byte-identical/golden-verified without changing shipped output. The cutover (production generate auto-detecting/using v2) also requires the external backend repos to actually ship v2 backend.manifest.v2.zon files — today those live only as in-tree test fixtures. So making the third-party path work in production is a distinct phase (PR 12 + an external-repo v2 adoption sweep), not PR 11. PR 11's scope is opening config + the capability gate + proving the mechanism end-to-end in tests.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fcb473d and d3532a3.

📒 Files selected for processing (3)
  • src/build_files.zig
  • test/build_zig_tests.zig
  • test/helpers.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/helpers.zig

Comment thread src/build_files.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
@apotema
apotema merged commit 61c1430 into main Jul 1, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/build_files.zig
(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});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

apotema added a commit that referenced this pull request Jul 1, 2026
…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
apotema added a commit that referenced this pull request Jul 1, 2026
…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
apotema added a commit that referenced this pull request Jul 1, 2026
…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
apotema added a commit that referenced this pull request Jul 2, 2026
…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
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