Skip to content

feat(packs): exposes surface modules + depends_on wiring (#498 PR 4) - #548

Merged
apotema merged 1 commit into
mainfrom
feat/498-exposes
Jul 5, 2026
Merged

feat(packs): exposes surface modules + depends_on wiring (#498 PR 4)#548
apotema merged 1 commit into
mainfrom
feat/498-exposes

Conversation

@apotema

@apotema apotema commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

PR 4 of the #498 train — the exposes/depends_on half of the wall.

What lands

  • Verb-surface files: scanPack copies a pack's root-level queries.zig/commands.zig (single-file variant of the feat(packs): scan a light pack's scripts/ into the per-state dispatch (#487) #496 stale-prune discipline; a pack may ship only a verb surface — the dest-dir edge the new test caught). __pack_root.zig re-exports them raw for the pack's own code.
  • __surface.zig (new renderSurface): generated per pack, re-exporting exactly the manifest's exposes lists through the pack's "pack" self-import. A listed-but-missing verb fails compilation pointing at the generated surface; a null/empty exposes yields a header-only module — dependents can call nothing, the correct default.
  • Build graph: surface modules are declared on demand — only packs some sibling depends_on. (Uniform emission was the first cut; the smoke project caught Zig's unused local constant hard error in the generated build.zig for undepended surfaces.) depends_on entries naming sibling packs map the dep's plain name onto the dep's surface (overrideImport(pack__X_mod, "<dep>", pack_surface__<dep>_mod)) — dependents never see pack__<prefix>. contracts stays the implicit full-module import; plugin deps are already in every table.
  • Generate-time gates: exposes naming verbs from a file the pack doesn't ship → error.PackExposesMissingFile with the manifest named (pack_validate.checkExposesFiles, unit-tested); .exposes = .all → targeted diagnostic naming the explicit-list fix (the RFC shorthand is deliberately unsupported — an unbounded surface defeats the wall).

Proof on the real two-pack project

  • production (depends_on = .{"citizens"}) calling @import("citizens").queries.find_idle(game) — builds green, runs through the surface → pack-module chain.
  • ❌ non-exposed verb: error: struct '__surface.queries' has no member named 'internal_reset'
  • depends_on removed: error: no module named 'citizens' available within module 'pack0' (the pack0 is Zig's display-dedup of the shared "pack" self-import name — cosmetic)

Tests

Suite 46/46 steps, 1273/1277 (4 skipped, 0 failed) — new PACK_SURFACE struct: renderer shapes (exact re-export + header-only empty), checkExposesFiles positive/negative, scanPack verb-file copy + stale-prune round-trip, and the build-wiring asserts (surface createModule with sole-pack import, demand-driven declaration, dependent→surface direction with both negative directions pinned).

docs/packs.md gains the exposes/depends_on section with the authoring example and the error surfaces.

Next: PR 5 (lint demotion + stale-comment truth-up + root.zig/build_files.zig >1000-line splits), PR 6 (examples/packs-demo + CI e2e fixture).

Part of #498

https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j

Summary by CodeRabbit

  • New Features

    • Packs can now expose root-level queries and commands through generated surface modules.
    • Pack dependencies now resolve through declared surfaces, improving how pack-to-pack imports are wired.
  • Bug Fixes

    • Missing exposed files are now detected earlier with clear warnings.
    • Previously generated pack files are pruned when the source file is removed.
  • Documentation

    • Updated pack documentation to describe the new surface/import behavior and manifest requirements.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements a manifest-driven "verb surface" contract for packs: scanning root-level queries.zig/commands.zig, generating a __surface.zig module re-exporting declared exposes, validating exposed verbs against shipped files, wiring depends_on through build.zig module overrides, and updating documentation and tests accordingly.

Changes

Pack surface exposes/depends_on implementation

Layer / File(s) Summary
Scan pack root files
src/codegen/scan/pack_refs.zig, src/root/pack_scan.zig
PackScan gains has_queries/has_commands flags; scanPack copies queries.zig/commands.zig via a new copyPackRootFile helper, pruning stale destination files when sources are removed.
Pack root and surface codegen
src/codegen/pack_root.zig
PackModule gains depends_on; renderPackRoot emits queries/commands aliases; new SurfaceExposes/renderSurface generate __surface.zig re-exporting exactly the declared verbs.
Manifest and exposes validation
src/pack_validate.zig, src/plugin_manifest/pack.zig
Adds checkExposesFiles to gate exposes against shipped files and a targeted warning/helper detecting the unsupported .exposes = .all shorthand.
Generate wiring
src/root.zig
Generation writes __surface.zig per pack after validating exposes, and includes depends_on in pack module wiring records.
build.zig surface wiring
src/build_files/build_zig.zig
emitPackModules conditionally declares per-pack surface modules and overrides depender imports to route through dependency surface modules, excluding contracts.
Tests
test/pack_scan_tests.zig
Adds PACK_SURFACE tests covering renderSurface, checkExposesFiles failures, scan copy/prune behavior, and build.zig wiring correctness.
Documentation
docs/packs.md
Updates status notes and declared-deps row, and adds a section documenting the exposes/depends_on surface contract and error semantics.

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

Sequence Diagram(s)

sequenceDiagram
  participant Generate as root.zig generate()
  participant PackScan as scanPack
  participant Validate as pack_validate.checkExposesFiles
  participant Codegen as renderSurface
  participant BuildZig as emitPackModules

  Generate->>PackScan: scan pack directory
  PackScan-->>Generate: has_queries, has_commands
  Generate->>Validate: check exposes vs shipped files
  Validate-->>Generate: ok or error.PackExposesMissingFile
  Generate->>Codegen: renderSurface(pack_name, exposes)
  Codegen-->>Generate: __surface.zig source
  Generate->>BuildZig: pack module entry with depends_on
  BuildZig->>BuildZig: emit pack_surface__<dep>_mod
  BuildZig->>BuildZig: overrideImport depender -> surface module
Loading

Possibly related issues

Possibly related PRs

  • labelle-toolkit/labelle-assembler#478: Both PRs build on the existing scanPackpack_scans plumbing, with this PR extending that scan flow to detect/copy queries.zig/commands.zig and drive the new surface codegen.

Poem

Beneath the wall, a burrow deep,
I scan each pack while others sleep,
exposes named, no .all allowed,
surfaces bloom, imports re-vowed 🥕
Hop, hop — the wiring's clean and neat!

🚥 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 is concise and accurately describes the main change: pack surface modules and depends_on wiring.
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/498-exposes

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 the exposes and depends_on verb surfaces for the Packs system, narrowing down a pack's public API to explicitly declared queries and commands. It introduces the generation of __surface.zig modules, wires dependencies in the build configuration, and adds validation to prevent exposing missing files or using the unsupported .exposes = .all shorthand. Feedback focuses on escaping generated Zig identifiers in __surface.zig to handle reserved keywords or special characters safely, and refining the substring search in plugin_manifest.zig to avoid false-positive diagnostics.

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/codegen/pack_root.zig
Comment on lines +240 to +242
for (exposes.queries) |name| {
try w.print(" pub const {s} = pack.queries.{s};\n", .{ name, 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.

medium

The exposed query names are directly interpolated as Zig identifiers in the generated __surface.zig file. If a manifest defines an exposed name that is a reserved Zig keyword (like const or fn) or contains special characters, the generated code will fail to compile. Using Zig's @"" identifier escaping syntax ensures that any valid string can be safely re-exported without causing syntax errors.

        for (exposes.queries) |name| {
            try w.print("    pub const @\"{s}\" = pack.queries.@\"{s}\";\n", .{ name, name });
        }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 47e5c68 — see commit message for the specifics (escaped idents both sides / token-sequence scan / docs aligned with the header-only + targeted-diagnostic behavior).

Comment thread src/codegen/pack_root.zig
Comment on lines +248 to +250
for (exposes.commands) |name| {
try w.print(" pub const {s} = pack.commands.{s};\n", .{ name, 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.

medium

The exposed command names are directly interpolated as Zig identifiers in the generated __surface.zig file. If a manifest defines an exposed name that is a reserved Zig keyword (like const or fn) or contains special characters, the generated code will fail to compile. Using Zig's @"" identifier escaping syntax ensures that any valid string can be safely re-exported without causing syntax errors.

        for (exposes.commands) |name| {
            try w.print("    pub const @\"{s}\" = pack.commands.@\"{s}\";\n", .{ name, name });
        }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 47e5c68 — see commit message for the specifics (escaped idents both sides / token-sequence scan / docs aligned with the header-only + targeted-diagnostic behavior).

Comment thread src/plugin_manifest.zig Outdated
Comment on lines +230 to +231
if (std.mem.indexOf(u8, raw_bytes, ".exposes") != null and
std.mem.indexOf(u8, raw_bytes, ".all") != null)

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

Performing a simple substring search for ".exposes" and ".all" on the entire raw_bytes can lead to false positives if either string appears in comments or unrelated fields. If the ZON parser fails for an entirely different reason, this warning might still be printed, potentially confusing the developer. Consider a more robust check (such as verifying the relative order or proximity of the tokens, or checking if they are not part of comments) to ensure the diagnostic is only shown when .exposes = .all is actually present in the active configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 47e5c68 — see commit message for the specifics (escaped idents both sides / token-sequence scan / docs aligned with the header-only + targeted-diagnostic behavior).

@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

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

199-258: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider validating exposed names are valid Zig identifiers.

renderSurface emits pub const {name} = pack.queries.{name}; directly from manifest-supplied strings. An exposes entry containing invalid identifier characters (hyphens, spaces, leading digit) produces a syntax error in the generated __surface.zig rather than a clear manifest-validation message pointing at the offending entry in pack.labelle. Compilation still fails safely, so this is a diagnostics-quality gap rather than a functional bug.

♻️ Possible approach

Add an identifier-format check alongside checkExposesFiles (in pack_validate.zig) that rejects an exposes entry which isn't a valid Zig identifier, with an error naming the pack and the bad entry.

🤖 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/pack_root.zig` around lines 199 - 258, `renderSurface` currently
emits manifest-supplied expose names directly into `__surface.zig`, so invalid
Zig identifiers only fail later as generated-code syntax errors. Add validation
for `exposes` entries in the pack validation path (near `checkExposesFiles` in
`pack_validate.zig`) to reject any query/command name that is not a valid Zig
identifier, and report the pack plus the offending entry in the error message.
Keep `renderSurface`, `SurfaceExposes`, and the generated `pub const {name}`
exports unchanged except for relying on the new validation.
🤖 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 `@docs/packs.md`:
- Around line 81-89: Update the surface-semantics wording in the docs to match
the actual `__surface.zig` behavior: in the pack import description, distinguish
the populated-surface case from the empty/null `exposes` case, since empty/null
`exposes` should produce a header-only `__surface.zig` rather than importing the
pack module. Also revise the `.exposes = .all` wording in the `@import("<dep>")`
/ `contracts` section to say it triggers a targeted validation diagnostic from
the manifest/validation path, not a generic parse error, while keeping the
references to `@import("<dep>")`, `__surface.zig`, and `contracts` aligned with
the implementation.

---

Nitpick comments:
In `@src/codegen/pack_root.zig`:
- Around line 199-258: `renderSurface` currently emits manifest-supplied expose
names directly into `__surface.zig`, so invalid Zig identifiers only fail later
as generated-code syntax errors. Add validation for `exposes` entries in the
pack validation path (near `checkExposesFiles` in `pack_validate.zig`) to reject
any query/command name that is not a valid Zig identifier, and report the pack
plus the offending entry in the error message. Keep `renderSurface`,
`SurfaceExposes`, and the generated `pub const {name}` exports unchanged except
for relying on the new validation.
🪄 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: 804d4f7f-c5b2-4925-876b-0ee64072bd95

📥 Commits

Reviewing files that changed from the base of the PR and between cd062ec and a3fb319.

📒 Files selected for processing (8)
  • docs/packs.md
  • src/build_files.zig
  • src/codegen/pack_root.zig
  • src/codegen/scan/pack_refs.zig
  • src/pack_validate.zig
  • src/plugin_manifest.zig
  • src/root.zig
  • test/pack_scan_tests.zig

Comment thread docs/packs.md Outdated
Rebuilt onto the six parallel split-refactors (#539-#549): identical
semantics, new homes — scanPack verb-copy in root/pack_scan.zig,
surface+depends_on emission in build_files/build_zig.zig, the .all
diagnostic in plugin_manifest/pack.zig. pack_root/pack_validate/
pack_refs/tests/docs carried verbatim (untouched by the splits).

Includes the review fixes from the first head: @"…" escaping on
exposed verb idents both sides, token-sequence .exposes = .all
detection (exposesAllShorthand), docs aligned with header-only +
targeted-diagnostic behavior.

Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
@apotema
apotema force-pushed the feat/498-exposes branch from 47e5c68 to 66008b2 Compare July 5, 2026 15:26

@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/root.zig (1)

848-855: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the repeated "find this pack's manifest entry" lookup.

The same pack_entries linear-search-by-name pattern is duplicated here twice (once resolving via unreachable on a miss, once silently defaulting to &.{} on a miss). Both are currently safe by construction (pack_scans/pack_modules are always built from pack_entries in lockstep), but the inconsistent miss-handling means a future refactor that breaks that invariant would silently wire empty depends_on here instead of loudly failing like the other site. Consider zip-iterating pack_scans/pack_modules together with pack_entries (as already done at line 1144) or extracting a single helper that always uses unreachable on a miss, to avoid the O(n²) duplicate lookups and the inconsistent fallback behavior.

Also applies to: 991-996

🤖 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/root.zig` around lines 848 - 855, Consolidate the repeated pack manifest
lookup in root.zig so both call sites use the same lookup path and failure
behavior. The current `pack_entries` linear search is duplicated in the
`exposes` block and the related `depends_on` handling, with one branch using
`unreachable` and the other falling back silently; refactor this by either
zip-iterating `pack_scans`/`pack_modules` with `pack_entries` as done elsewhere,
or extracting a helper that resolves the manifest entry for a pack name and միշտ
fails loudly on a miss. Keep the `pack_validate.checkExposesFiles` and
`pack.name` flow unchanged, but eliminate the O(n²) duplicate search and the
inconsistent empty-default behavior.
🤖 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/root.zig`:
- Around line 848-855: Consolidate the repeated pack manifest lookup in root.zig
so both call sites use the same lookup path and failure behavior. The current
`pack_entries` linear search is duplicated in the `exposes` block and the
related `depends_on` handling, with one branch using `unreachable` and the other
falling back silently; refactor this by either zip-iterating
`pack_scans`/`pack_modules` with `pack_entries` as done elsewhere, or extracting
a helper that resolves the manifest entry for a pack name and միշտ fails loudly
on a miss. Keep the `pack_validate.checkExposesFiles` and `pack.name` flow
unchanged, but eliminate the O(n²) duplicate search and the inconsistent
empty-default behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 76a80bf1-cb54-4f11-a3fb-7964da6998ee

📥 Commits

Reviewing files that changed from the base of the PR and between 47e5c68 and 66008b2.

📒 Files selected for processing (9)
  • docs/packs.md
  • src/build_files/build_zig.zig
  • src/codegen/pack_root.zig
  • src/codegen/scan/pack_refs.zig
  • src/pack_validate.zig
  • src/plugin_manifest/pack.zig
  • src/root.zig
  • src/root/pack_scan.zig
  • test/pack_scan_tests.zig
✅ Files skipped from review due to trivial changes (1)
  • docs/packs.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/pack_validate.zig
  • src/codegen/pack_root.zig
  • src/codegen/scan/pack_refs.zig
  • test/pack_scan_tests.zig

@apotema
apotema merged commit 8da649f into main Jul 5, 2026
4 checks passed
@apotema
apotema deleted the feat/498-exposes branch July 5, 2026 15:38
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