Skip to content

feat(#439): pack dir-scan — register pack components/events/prefabs into unified registries - #478

Merged
apotema merged 3 commits into
mainfrom
packs/dir-scan
Jul 1, 2026
Merged

apotema merged 3 commits into
mainfrom
packs/dir-scan

Conversation

@apotema

@apotema apotema commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

What

Finishes the pack directory-scan (convention_dirs = copy_and_scan), the phase-4 crux of the Packs initiative (RFC Flying-Platform/flying-platform-labelle docs/RFC-packs.md §4 + §6).

A local pack is the light, directory-scanned form of a plugin: instead of contributing components/events/prefabs through decl-modules (pub const Components), it drops game-convention files into components/ events/ prefabs/ and ships a thin pack.labelle. Before this PR, a pack's non-script convention dirs were scanned and then discarded — the names never reached the generated registries (root.zig copy_and_scan loop: scanner.freeNames(...) with a "computed but not exposed" comment). This wires them into the same registries the game root feeds (the unified set, RFC §6-1b), so pack components are stored/serialized identically to game-root ones.

Part of #651, implements #439.

Implemented

  • pack.labelle manifest (plugin_manifest.zig): the scalar .convention_dirs = .copy_and_scan shorthand (RFC §10 Q3), kept as a separate typed schema from plugin.labelle's per-dir array so neither parser has to disambiguate scalar-vs-array in one field. exposes / depends_on are accepted but ignored (ignore_unknown_fields) — the DAG/isolation work is later.
  • root.scanPack: copies <pack>/{components,events,prefabs}/ into <target>/packs/<name>/… and records the scanned stems as a PackScan (import_prefix = packs/<name>). Missing subdirs tolerated. Runs for every plugin that carries a pack.labelle; decl-module plugins (no pack.labelle) are untouched → fully back-compat.
  • Codegen registration — pack items land in the unified registries the game root feeds:
    • Components → ComponentRegistryWithPlugins(.{ … }) field list (registries.zig)
    • Events → GameEvents union + AllHookPayloads merge + event imports (events.zig, hooks.zig, imports.zig)
    • Prefabs → embedded via @embedFile + JsoncBridge.addEmbeddedPrefab in both loop and callback lifecycles (loop.zig, callback.zig; JsoncBridge gate widened so a pack can ship prefabs even when the game root has no scenes)
  • Threading: main_template.pack_scans module-level var (same pattern as the existing loop_style_override), set by root.generate and cleared after — so the ~19-arg generateMainZigFromTemplate signature (100+ test call sites) stays untouched.

Deferred (clean seams left, not silently dropped)

Tests

test/pack_scan_tests.zig:

  • scanPack copy/scan (files land under packs/<name>/…; stems returned; partial-dir tolerance; hooks/ ignored)
  • emission assertions: pack component in ComponentRegistryWithPlugins, pack event widens GameEvents + imported through the prefix, pack prefab embedded from the prefix, and empty pack_scans = byte-unchanged no-op
  • pack.labelle parser tests (scalar shorthand, default, ignores exposes/depends_on, name/version validation) in plugin_manifest.zig

zig build + zig build test green (22 test steps, exit 0).

Summary by CodeRabbit

  • New Features
    • Added pack directory scanning to discover and copy pack components, events, and prefabs into generated output.
    • Extended code generation to merge pack events into game event unions, import pack events, register pack components, and embed pack prefabs in the runtime prefab loader.
    • Added pack manifest support with parsing/validation and convention directory rules (including treating packs as reserved).
  • Tests
    • Added end-to-end tests covering pack scanning and pack-driven main.zig emission, including correct behavior when pack data is absent and validation for invalid pack manifests.

…nto unified registries

Finish the pack directory-scan (Packs RFC §4, convention_dirs = copy_and_scan)
so a light pack's components/ events/ prefabs/ are scanned like the game root
and wired into the generated registries, instead of the scanned names being
computed and discarded.

- New pack.labelle manifest (plugin_manifest.zig): scalar
  .convention_dirs = .copy_and_scan shorthand; exposes/depends_on accepted
  but ignored for now.
- root.scanPack copies <pack>/{components,events,prefabs}/ into
  <target>/packs/<name>/ and records stems as a PackScan.
- Codegen block-writers register pack items into the SAME registries the
  game root feeds (the unified set): Components (ComponentRegistryWithPlugins
  field list), GameEvents union + event imports, embedded prefabs.
- Threaded via main_template.pack_scans module-level var (mirrors
  loop_style_override) to avoid churning the 100+ call sites of the ~19-arg
  generateMainZigFromTemplate.

Deferred with clean seams: hooks/ scanning; the invisible <pack>__ prefix +
.jsonc ref rewrite (#440); the per-pack global++own registry partition /
PackView (#652-remainder); exposes/depends_on DAG. Registered names are BARE
today.

Part of #651, implements #439.

Tests: pack_scan_tests.zig (scanPack copy/scan + emission assertions) and
pack.labelle parser tests in plugin_manifest.zig.
@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: daad2fba-ea5d-4d3c-b7f4-64fa4acb04fe

📥 Commits

Reviewing files that changed from the base of the PR and between 01b6035 and e6644ea.

📒 Files selected for processing (4)
  • src/codegen/lifecycle/callback.zig
  • src/codegen/lifecycle/loop.zig
  • src/plugin_manifest.zig
  • test/pack_scan_tests.zig
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/codegen/lifecycle/loop.zig
  • src/codegen/lifecycle/callback.zig
  • src/plugin_manifest.zig
  • test/pack_scan_tests.zig

📝 Walkthrough

Walkthrough

This PR adds pack support end to end: it parses pack.labelle, scans pack components/, events/, and prefabs/ directories, threads PackScan results into codegen, and updates generated registries, imports, events, hooks, and prefab embedding. New tests cover scanning and generated output.

Changes

Pack Support Implementation

Layer / File(s) Summary
Pack manifest parsing
src/plugin_manifest.zig
Adds PackConventionMode, PackManifest, loadPackOptional, and loadPackFromDir to read, validate, and parse pack.labelle, with reserved-dir handling and tests for missing files, shorthand syntax, ignored fields, name mismatch, and version validation.
PackScan data type and re-exports
src/codegen/scan.zig, src/main_zig.zig, src/root.zig
Introduces the PackScan struct (name, import_prefix, component/event/prefab name slices) with deinit, re-exported through main_zig.zig and root.zig, plus the main_template re-export.
scanPack directory scanning and generate() wiring
src/root.zig
Adds scanPackSubdir/scanPack to copy and scan pack convention subdirectories into packs/<name>/..., and wires a pack dir-scan phase into generate that loads manifests per plugin and collects PackScan results.
Codegen context and template plumbing
src/codegen/context.zig, src/codegen/main_template.zig
Adds pack_scans field plus hasPackEvents/hasPackPrefabs helpers to Codegen, and a pack_scans threadlocal in main_template.zig set during generateMainZigFromTemplate.
Pack-aware codegen block emission
src/codegen/blocks/events.zig, src/codegen/blocks/hooks.zig, src/codegen/blocks/imports.zig, src/codegen/blocks/registries.zig, src/codegen/lifecycle/callback.zig, src/codegen/lifecycle/loop.zig
Updates emission logic for game event unions, hook payloads, event/scene imports, component registries, and prefab embedding to include pack-sourced content alongside root-level names.
Pack scanning and emission tests
build.zig, test/pack_scan_tests.zig
Registers a new test file and adds tests validating scanPack directory scanning and generated main.zig output for pack components, events, and prefabs.

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

Possibly related issues

Possibly related PRs

Poem

A pack arrived with little feet,
Its files lined up so neat, so neat. 🐇
I scanned the roots, I wove the thread,
And tucked new prefabs into bed.
Hop on—now codegen hums and sings,
With packs entwined in all the things.

🚥 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 clearly matches the PR’s main change: pack dir-scanning and wiring pack components, events, and prefabs into unified registries.
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 packs/dir-scan

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 pack directory scanning (Packs RFC §4, #439), allowing light, directory-scanned plugins (packs) to contribute components, events, and prefabs directly into the unified game-root registries. It introduces the PackScan and PackManifest structures, updates the codegen templates and block-writers to integrate pack items, and adds comprehensive tests in test/pack_scan_tests.zig. The review feedback recommends replacing debug-specific print functions (std.debug.print) with standard logging facilities (such as std.log.warn) for user-facing warnings in src/plugin_manifest.zig.

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.

Comment thread src/plugin_manifest.zig Outdated
Comment on lines +186 to +189
std.debug.print(
"labelle: failed to parse pack.labelle for pack '{s}' at {s}\n parser error: {any}\n see docs/RFC-packs.md for the pack manifest schema\n",
.{ expected_name, manifest_path, err },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In CLI tools, use standard logging facilities (such as std.log.warn) or write directly to stderr for user-facing warnings/errors, rather than using debug-specific print functions like std.debug.print.

        std.log.warn(
            "labelle: failed to parse pack.labelle for pack '{s}' at {s}\n  parser error: {any}\n  see docs/RFC-packs.md for the pack manifest schema\n",
            .{ expected_name, manifest_path, err },
        );
References
  1. In CLI tools, use standard logging facilities (e.g., std.log.warn) or write to stderr for user-facing warnings, rather than using debug-specific print functions (e.g., std.debug.print).

Comment thread src/plugin_manifest.zig Outdated
Comment on lines +195 to +198
std.debug.print(
"labelle: pack.labelle name mismatch\n project.labelle declares '{s}'\n but its pack.labelle has name = '{s}'\n at {s}\n",
.{ expected_name, parsed.name, manifest_path },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In CLI tools, use standard logging facilities (such as std.log.warn) or write directly to stderr for user-facing warnings/errors, rather than using debug-specific print functions like std.debug.print.

        std.log.warn(
            "labelle: pack.labelle name mismatch\n  project.labelle declares '{s}'\n  but its pack.labelle has name = '{s}'\n  at {s}\n",
            .{ expected_name, parsed.name, manifest_path },
        );
References
  1. In CLI tools, use standard logging facilities (e.g., std.log.warn) or write to stderr for user-facing warnings, rather than using debug-specific print functions (e.g., std.debug.print).

Comment thread src/plugin_manifest.zig Outdated
Comment on lines +203 to +206
std.debug.print(
"labelle: pack '{s}' has manifest_version {d}\n but this labelle-cli release supports manifest_version 1..{d}\n",
.{ expected_name, parsed.manifest_version, SUPPORTED_MANIFEST_VERSION },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In CLI tools, use standard logging facilities (such as std.log.warn) or write directly to stderr for user-facing warnings/errors, rather than using debug-specific print functions like std.debug.print.

        std.log.warn(
            "labelle: pack '{s}' has manifest_version {d}\n  but this labelle-cli release supports manifest_version 1..{d}\n",
            .{ expected_name, parsed.manifest_version, SUPPORTED_MANIFEST_VERSION },
        );
References
  1. In CLI tools, use standard logging facilities (e.g., std.log.warn) or write to stderr for user-facing warnings, rather than using debug-specific print functions (e.g., std.debug.print).

… warnings

Address Gemini: the 3 new pack.labelle warning sites (parse failure, name
mismatch, unsupported manifest_version) are user-facing CLI warnings — route
them through std.log.warn. Left pre-existing debug.print calls untouched (out
of scope for this PR).
@apotema

apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed @gemini-code-assist — the 3 new pack.labelle warning sites now use std.log.warn instead of std.debug.print (parse failure / name mismatch / unsupported manifest_version). Left the pre-existing debug.print calls in this file untouched as out-of-scope. zig build + zig build test green.

@gemini-code-assist

Copy link
Copy Markdown

Thanks for the update, @apotema. Using std.log.warn for these diagnostic messages is a much better practice than std.debug.print, as it integrates properly with the project's logging infrastructure and keeps the console output cleaner for end-users. Leaving the existing debug.print calls as out-of-scope is also the right call here to maintain focus on the feature implementation. The zig build and zig build test status being green is great to hear.

@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: 12665e08df

ℹ️ 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/codegen/lifecycle/loop.zig Outdated
for (self.pack_scans) |pack| {
for (pack.prefab_names) |name| {
const display = std.fs.path.basename(name);
try w.print(" try JsoncBridge.addEmbeddedPrefab(&g, \"{s}\", @embedFile(\"{s}/prefabs/{s}.jsonc\"), \"prefabs\");\n", .{ display, pack.import_prefix, 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 Resolve pack prefab includes from the pack directory

When a pack prefab contains a JSONC include or other source-relative lookup, this emits the source from packs/<name>/prefabs/... but still registers the prefab with the root path literal "prefabs", so the bridge resolves relative paths against the game's prefab directory instead of the copied pack directory; the callback lifecycle mirrors the same literal. Pass the pack's prefixed prefab root (for example {pack.import_prefix}/prefabs) here so scanned pack prefabs can resolve their own local files.

Useful? React with 👍 / 👎.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/codegen/main_template.zig (1)

146-162: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Extend basename-collision checks to pack prefabs

checkBasenameCollisions only sees prefab_names, but pack prefabs are also registered by bare basename via JsoncBridge.addEmbeddedPrefab(...). A pack prefab that matches a root prefab—or another pack prefab—will overwrite the earlier entry at runtime. The same bare-name collision risk also exists for pack component/event registries, which currently fail later with a duplicate-field error.

🤖 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/main_template.zig` around lines 146 - 162, The basename-collision
check in the main template only covers root prefab names, so pack prefabs and
pack component/event registries can still collide and overwrite or fail later.
Extend the collision detection around `checkBasenameCollisions` / `prefab_names`
to include the bare basenames used by `JsoncBridge.addEmbeddedPrefab(...)` and
the pack registries, and surface the same early diagnostic when duplicates are
found. Update the generation path in `main_template.zig` so all registered bare
names are validated together before emission.
🧹 Nitpick comments (4)
src/plugin_manifest.zig (3)

202-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reusing SUPPORTED_MANIFEST_VERSION couples pack and plugin schema versioning.

The file's own comment (Lines 104-108) states pack.labelle is deliberately kept as a "SEPARATE file + schema from plugin.labelle... so neither parser has to disambiguate". However, loadPackFromDir validates against the same SUPPORTED_MANIFEST_VERSION constant used by loadFromDir for plugin.labelle (Line 13, Line 331). Since the two schemas are independent, a future bump to this constant driven purely by a plugin.labelle change would silently widen the accepted version range for pack.labelle too, even if the pack schema hasn't actually changed.

Consider introducing a distinct SUPPORTED_PACK_MANIFEST_VERSION constant so the two schemas can version independently.

🤖 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/plugin_manifest.zig` around lines 202 - 208, `loadPackFromDir` is reusing
the plugin schema version constant, which incorrectly ties `pack.labelle`
validation to `plugin.labelle` schema changes. Add a separate
`SUPPORTED_PACK_MANIFEST_VERSION` constant in `src/plugin_manifest.zig` and
update `loadPackFromDir` to validate against it instead of
`SUPPORTED_MANIFEST_VERSION`; keep `loadFromDir` using the existing plugin
constant so each parser can version independently.

158-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared file-read/parse boilerplate.

loadPackFromDir duplicates the read-file → dupeZ → zon.parse.fromSliceAlloc → error-log boilerplate already present in loadFromDir (Lines 272-310), differing only in the target type and error names. Consider extracting a small generic helper (e.g., readManifestFile(allocator, manifest_path) ![:0]u8 or a comptime-generic parse wrapper) to avoid maintaining two near-identical ~20-line blocks in sync.

♻️ Sketch of a shared helper
+fn readManifestBytes(allocator: std.mem.Allocator, manifest_path: []const u8) !?[]u8 {
+    const cwd = std.Io.Dir.cwd();
+    return cwd.readFileAlloc(config.globalIo(), manifest_path, allocator, .limited(64 * 1024)) catch |err| {
+        if (err == error.FileNotFound) return null;
+        return err;
+    };
+}

Both loadFromDir and loadPackFromDir could then call this and only diverge on the ZON type / validation.

🤖 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/plugin_manifest.zig` around lines 158 - 216, `loadPackFromDir` repeats
the same read-file, `dupeZ`, `zon.parse.fromSliceAlloc`, and error logging flow
already implemented in `loadFromDir`, so extract that shared boilerplate into a
small reusable helper. Add a generic or file-read helper that returns the parsed
ZON result, then have both `loadFromDir` and `loadPackFromDir` call it and keep
only their type-specific validation and error mapping (`PackManifestParseError`,
`PackManifestNameMismatch`, `PackManifestUnknownVersion`) in the individual
functions.

932-1048: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Good test coverage; one parity gap with malformed ZON.

The pack manifest tests mirror the plugin ones well (missing file, shorthand parse, default, ignored fields, name mismatch, version mismatch). One case present for loadFromDir but missing here: a malformed-ZON test asserting error.PackManifestParseError (parity with Line 757's "loadFromDir: errors on malformed ZON").

🤖 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/plugin_manifest.zig` around lines 932 - 1048, Add a malformed-ZON parity
test to loadPackFromDir alongside the existing pack manifest cases, similar to
the loadFromDir malformed ZON test. Use loadPackFromDir and
writePackManifestFile to feed invalid ZON, then assert it returns
error.PackManifestParseError so manifest parsing behavior matches the plugin
loader tests.
test/pack_scan_tests.zig (1)

139-164: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate long positional call — extend genWithPack instead of re-inlining it.

The "no packs" test (Lines 223-243) re-inlines the entire generateMainZigFromTemplate positional argument list that genWithPack already wraps, rather than calling genWithPack with an empty pack list. Since the PR explicitly notes this is "the existing large function signature" left unchanged, two independent call sites make it easy for them to silently diverge on a future signature change.

♻️ Suggested consolidation
-fn genWithPack(tmpl: []const u8, cfg: generate.ProjectConfig, pack: generate.PackScan) ![]const u8 {
-    generate.main_template.pack_scans = &.{pack};
+fn genWithPacks(tmpl: []const u8, cfg: generate.ProjectConfig, packs: []const generate.PackScan) ![]const u8 {
+    generate.main_template.pack_scans = packs;
     defer generate.main_template.pack_scans = &.{};

     return generate.generateMainZigFromTemplate(
         std.testing.allocator,
         tmpl,
         cfg,
         raylib_lifecycle,
         ...
     );
 }
+
+fn genWithPack(tmpl: []const u8, cfg: generate.ProjectConfig, pack: generate.PackScan) ![]const u8 {
+    return genWithPacks(tmpl, cfg, &.{pack});
+}

Then the "no packs" test becomes:

-        const main_zig = try generate.generateMainZigFromTemplate(
-            std.testing.allocator,
-            component_tmpl,
-            .{ .y_axis = .up, .name = "test-game", .backend = .raylib, .ecs = .mock },
-            raylib_lifecycle,
-            ...
-        );
+        const main_zig = try genWithPacks(
+            component_tmpl,
+            .{ .y_axis = .up, .name = "test-game", .backend = .raylib, .ecs = .mock },
+            &.{},
+        );

Also applies to: 218-250

🤖 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 `@test/pack_scan_tests.zig` around lines 139 - 164, The no-packs test is
duplicating the long generateMainZigFromTemplate positional argument list
instead of reusing genWithPack, which risks the two call sites drifting apart.
Update genWithPack to support an empty pack case (or a pack list parameter) and
have the no-packs test call that helper with no packs, using the existing
generate.main_template.pack_scans setup to keep all generateMainZigFromTemplate
arguments centralized.
🤖 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/root.zig`:
- Around line 980-1025: The pack scan logic now writes plugin content under a
top-level packs layout, so “packs” must be treated as a reserved convention-dir
name. Update RESERVED_DIR_NAMES in plugin_manifest.zig to include "packs" so
loadPackOptional rejects any plugin manifest that tries to declare
convention_dirs.name = "packs". Keep the check aligned with existing
reserved-name validation so scanPack and the new pack_scans flow can’t collide
with target_dir/packs/.

---

Outside diff comments:
In `@src/codegen/main_template.zig`:
- Around line 146-162: The basename-collision check in the main template only
covers root prefab names, so pack prefabs and pack component/event registries
can still collide and overwrite or fail later. Extend the collision detection
around `checkBasenameCollisions` / `prefab_names` to include the bare basenames
used by `JsoncBridge.addEmbeddedPrefab(...)` and the pack registries, and
surface the same early diagnostic when duplicates are found. Update the
generation path in `main_template.zig` so all registered bare names are
validated together before emission.

---

Nitpick comments:
In `@src/plugin_manifest.zig`:
- Around line 202-208: `loadPackFromDir` is reusing the plugin schema version
constant, which incorrectly ties `pack.labelle` validation to `plugin.labelle`
schema changes. Add a separate `SUPPORTED_PACK_MANIFEST_VERSION` constant in
`src/plugin_manifest.zig` and update `loadPackFromDir` to validate against it
instead of `SUPPORTED_MANIFEST_VERSION`; keep `loadFromDir` using the existing
plugin constant so each parser can version independently.
- Around line 158-216: `loadPackFromDir` repeats the same read-file, `dupeZ`,
`zon.parse.fromSliceAlloc`, and error logging flow already implemented in
`loadFromDir`, so extract that shared boilerplate into a small reusable helper.
Add a generic or file-read helper that returns the parsed ZON result, then have
both `loadFromDir` and `loadPackFromDir` call it and keep only their
type-specific validation and error mapping (`PackManifestParseError`,
`PackManifestNameMismatch`, `PackManifestUnknownVersion`) in the individual
functions.
- Around line 932-1048: Add a malformed-ZON parity test to loadPackFromDir
alongside the existing pack manifest cases, similar to the loadFromDir malformed
ZON test. Use loadPackFromDir and writePackManifestFile to feed invalid ZON,
then assert it returns error.PackManifestParseError so manifest parsing behavior
matches the plugin loader tests.

In `@test/pack_scan_tests.zig`:
- Around line 139-164: The no-packs test is duplicating the long
generateMainZigFromTemplate positional argument list instead of reusing
genWithPack, which risks the two call sites drifting apart. Update genWithPack
to support an empty pack case (or a pack list parameter) and have the no-packs
test call that helper with no packs, using the existing
generate.main_template.pack_scans setup to keep all generateMainZigFromTemplate
arguments centralized.
🪄 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: 6b87a5f2-24d2-478b-872f-bb014b565055

📥 Commits

Reviewing files that changed from the base of the PR and between 56c9d69 and 12665e0.

📒 Files selected for processing (14)
  • build.zig
  • src/codegen/blocks/events.zig
  • src/codegen/blocks/hooks.zig
  • src/codegen/blocks/imports.zig
  • src/codegen/blocks/registries.zig
  • src/codegen/context.zig
  • src/codegen/lifecycle/callback.zig
  • src/codegen/lifecycle/loop.zig
  • src/codegen/main_template.zig
  • src/codegen/scan.zig
  • src/main_zig.zig
  • src/plugin_manifest.zig
  • src/root.zig
  • test/pack_scan_tests.zig

Comment thread src/root.zig
…view)

Finding 1 (chatgpt-codex, functional): pack-scanned prefabs were
registered with the bare game-root prefab root `"prefabs"`, so a pack
prefab's JSONC `"include"` / source-relative lookups resolved against
the game's prefabs/ dir instead of the copied pack dir. Register them
with the pack's own `<import_prefix>/prefabs` root at both the loop-
lifecycle (loop.zig) and callback-lifecycle (callback.zig) emit sites.
Game-root prefabs keep using `"prefabs"`.

Finding 2 (CodeRabbit, minor): reserve `packs` as a convention-dir name
in RESERVED_DIR_NAMES so a plugin can't declare convention_dirs.name =
"packs" and write into target_dir/packs/, colliding with the pack-scan
layout.

Tests: extend pack_scan prefab-emission test to assert the pack prefab
root is `packs/citizens/prefabs` (not `"prefabs"`); add a plugin_manifest
reserved-name rejection test for `packs`; add `packs` to the
isReservedDirName inline list.
@apotema

apotema commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both review findings (commit e6644ea):

Finding 1 (chatgpt-codex, functional) — pack prefab includes resolved against the wrong dir. Confirmed real: JsoncBridge.addEmbeddedPrefab's 4th arg (prefab_dir) is stored on the engine's PrefabCache as the base directory that a prefab's JSONC "include" / source-relative lookups resolve against (labelle-engine/src/jsonc/prefab_cache.zig). Pack-scanned prefabs were registered with the bare game-root "prefabs", so a pack prefab's include would resolve against the game tree, not the copied packs/<name>/prefabs/ dir. Fixed at both emit sites to use the pack's own <import_prefix>/prefabs root:

  • src/codegen/lifecycle/loop.zig:148
  • src/codegen/lifecycle/callback.zig:136

Game-root prefabs are unchanged (still "prefabs").

Finding 2 (CodeRabbit, minor) — reserve packs. Added "packs" to RESERVED_DIR_NAMES in src/plugin_manifest.zig:26 so a plugin can't declare convention_dirs.name = "packs" and collide with the pack-scan layout.

Tests:

  • Extended test/pack_scan_tests.zig "pack prefab is embedded and registered from the pack prefix" to assert the registered root is packs/citizens/prefabs and NOT the bare "prefabs".
  • Added plugin_manifest.zig test "errors when plugin tries to declare the packs reserved name"; added packs to the isReservedDirName inline list test.

zig build and zig build test both pass.

@apotema
apotema merged commit 2118b04 into main Jul 1, 2026
4 checks passed
@apotema
apotema deleted the packs/dir-scan branch July 1, 2026 15:10
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