Repository navigation
feat(#584): .language policy — one script language per project - #589
Conversation
The one-language-per-project policy from RFC-LANGUAGE-PLUGINS revs 8-9
(labelle-engine#237), enforced at generate time before any target
write:
- PluginDep gains singular .language (mixing unrepresentable); vocab =
{lua, typescript, ruby, rust, crystal, go, csharp}, each name = its
convention dir; at most ONE plugin may declare it.
- validateLanguagePolicy gate beside validatePackGraph: scans language
convention dirs across the game root AND all packs (game-local +
plugin-bundled, riding P2's PackEntry/resolveSrcDir), hard-errors
with a 10-file listing on out-of-language scripts, errors on
language dirs with no scripting plugin, warns on empty dirs.
- requires_language on plugin AND pack manifests (P2 split both
schemas): vocab-checked at manifest load, project-match checked in
the gate, mismatch names both sides.
- Errors follow the P2 house style with actionable fixes; violations
reject before target-dir creation (no stale output).
- Tests: +65 (18 unit in-module, 7 integration over real pack loading,
zspec e2e through real generate() incl. no-target-dir-on-failure and
the clean-project BYTE-IDENTITY guard pinned to the committed
golden). Clean-HOME suite: 48/48 steps, 1512/1516 (4 skipped).
Discovered (out of scope, follow-up to be filed): a light pack shipping
only pack.labelle breaks writePackModuleRoots at generate — predates
this change.
Epic: labelle-engine#237
Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Warning Review limit reached
Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Design pivot before merge (RFC rev 13): plugin options must never
become bespoke PluginDep fields. The declaration is now
.params = .{ .language = "lua" } — PluginDep.params is the v1 slice
of the generic plugin-params mechanism (schema-declared params =
follow-up #591). Policy semantics unchanged (vocabulary, one
scripting plugin, dir scan, requires_language); diagnostics spell
.params.language.
Unknown params are hard parse errors by construction (std.zon
ignore_unknown_fields=false), pinned by a test asserting the
'unexpected field' diagnostic — which surfaced a std quirk: a null
Diagnostics pointer leaks the parser's owned note; the test passes a
real Diagnostics (+deinit).
Clean-HOME: 48/48 steps, 1518/1522 (4 skipped), +6 tests;
byte-identity guard green.
Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
|
Design pivot applied before merge (RFC rev 13 + labelle-toolkit/labelle-engine#237 decision): the declaration is now |
# Conflicts: # src/root.zig
…, drain touchpoint (#596) Makes .params.language projects RUN their scripts (epic labelle-engine#237; contract labelle-engine#749; plugin labelle-scripting#1): - scripting_splice.detect(): the #589-parsed language + manifest name 'scripting' gate; EMBED_LANGUAGES table (lua -> .lua; native languages deliberately absent -> warn, no splice). - Copy <lang>/ via the prefabs/scenes scanner machinery (sorted stems, /-joined subdirs); generated main registers each script (scripting.registerScript(stem, @embedfile(...))) BEFORE PluginControllers.setup boots the VM — both lifecycle shapes. - module_vars emit the scripting import alias + scripting_enabled flag; build.zig passes .language enum to the plugin dependency. - drainEvents rides tick_code between the plugin ticks and g.dispatchEvents (engine-gated, outside the scaled_dt gate) — NOT the backend template: dispatch is emitted inside the tick_code hole, so a template drain would tap an already-swapped buffer; riding tick_code also lands the drain on every backend at once. Templates carry only the bind touchpoint (bgfx PR). - Tests: 6 zspec (registrations sorted/pre-setup, drain placement, whole-main expectAstGenOk, empty-set, no-splice zero-markers, build.zig .language) + 7 unit; all byte-identity goldens unchanged (script-less projects byte-identical). Clean-HOME: 50/50 steps, 1576/1580 (4 skipped). Closes #593 Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
…597) * feat(#593): scripting codegen splice — embed scripts, wire the plugin, drain touchpoint Makes .params.language projects RUN their scripts (epic labelle-engine#237; contract labelle-engine#749; plugin labelle-scripting#1): - scripting_splice.detect(): the #589-parsed language + manifest name 'scripting' gate; EMBED_LANGUAGES table (lua -> .lua; native languages deliberately absent -> warn, no splice). - Copy <lang>/ via the prefabs/scenes scanner machinery (sorted stems, /-joined subdirs); generated main registers each script (scripting.registerScript(stem, @embedfile(...))) BEFORE PluginControllers.setup boots the VM — both lifecycle shapes. - module_vars emit the scripting import alias + scripting_enabled flag; build.zig passes .language enum to the plugin dependency. - drainEvents rides tick_code between the plugin ticks and g.dispatchEvents (engine-gated, outside the scaled_dt gate) — NOT the backend template: dispatch is emitted inside the tick_code hole, so a template drain would tap an already-swapped buffer; riding tick_code also lands the drain on every backend at once. Templates carry only the bind touchpoint (bgfx PR). - Tests: 6 zspec (registrations sorted/pre-setup, drain placement, whole-main expectAstGenOk, empty-set, no-splice zero-markers, build.zig .language) + 7 unit; all byte-identity goldens unchanged (script-less projects byte-identical). Clean-HOME: 50/50 steps, 1576/1580 (4 skipped). Closes #593 Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j * feat(#739): scripting-smoke example — Lua vs the real engine, in CI The Phase-1 proof (labelle-engine#739, epic #237): a null-backend example whose behavior runs in Lua against the real engine through the real generated main — LUA_INIT id=1 -> ticks 1..5 -> LUA_EMIT_OK (tick 2) -> LUA_ENGINE_TICK_SEEN + LUA_EVENT_SEEN_N_2 (tick 3, real bus) -> LUA_MOVED_X_30 (Position via the contract) - examples/scripting-smoke: null-example shape, backend_package local:../../../labelle-null (branch-fallback sibling clone in CI), scripting plugin via the REAL consumer path (github v0.1.0 + .params.language = lua), custom lua_ping event, unified-format scene. - CI: 6 codegen greps + build + LABELLE_NULL_FRAMES=5 run + exact ordered 10-token transcript diff. Two chain bugs the demo flushed out, fixed here: - plugin_registries: generated PluginControllers.deinit now arity- dispatches (v0.1.0 ships zero-arg deinit()). - render lifecycle (SILENT FREEZE): nothing dispatched the scripting Controller's per-frame tick (no Systems -> PluginSystems skips it; PluginControllers wires only setup/deinit) — lua update() would never run. The splice now emits scripting.Controller.tick(&g, scaled_dt) inside the scaled_dt gate, before the drain. Tests extended (ordering, empty-set, no-splice absence, arity emission). Clean-HOME suite: all steps green (4 skipped). plugin-controllers example regression-checked (canonical 13-event sequence intact). Closes labelle-engine#739 Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
…livered comptime (#606) * Schema-declared plugin params: validated at generate, delivered comptime plugin.labelle gains .params_schema (name, str/i64/f64/bool/enum types, enum vocab, defaults, required); the project's .params bag is validated against it at generate (unknown/mistyped/out-of-vocabulary/ missing-required -> pointed errors naming plugin + param) and delivered as a generated plugin_<name>_params.zig injected via overrideImport under the fixed import name plugin_params. Heterogeneous .params parsing: std.zon has no dynamic Value, so a Zoir side-walk extracts each plugin's .params into tagged-scalar bags, blanks the value spans (newline-preserving — every other diagnostic keeps exact byte offsets AND line numbers) and re-runs the unchanged strict typed parse. No-.params sources take a single-parse fast path (byte-identical). Supersede semantics: schema-less + exactly {language} keeps the #589 native fast-path (published scripting pins keep working — the rival draft removed it and broke Examples CI); schema-less + any other key errors (nothing silently drops); a present schema is the sole authority, language included. Manifest key is .params_schema, strict-parsed even though the manifest stays tolerant overall; .params stays free for future presets. Additive beside #586's .build block. +70 tests (31 unit + 8 e2e + threading), #589 policy tests green UNMODIFIED, params-less plugins byte-identical (golden). Negative- verified x3: unconditional emitter -> 3 byte-identity pins; fast-path removed -> 11+ failures across 4 suites; corrupted renderer -> unit + both delivery goldens. Closes #591. Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j * OOM-path errdefers in plugin_params (review findings) Two toArrayList->toOwnedSlice moves gain errdefer arr.deinit (safe: the writer's deinit no-ops on the emptied buffer) and the resolved- param append gains errdefer freeParamValue on the duped value — an OutOfMemory mid-resolution no longer leaks the owned strings. Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j * Fix params review findings: enum-literal language, enum vocab/type collisions - declaredLanguage() resolves .enum_tag bags too: the policy gate and scripting_splice.detect read this accessor BEFORE param resolution, so the natural enum-literal spelling .params = .{ .language = .lua } against an enum schema silently skipped the one-language checks and the entire splice (generate succeeded WITHOUT the scripting wiring). Audit: both pre-resolution consumers route through the accessor; the schema-less fast path stays string-only deliberately — it errors loudly at resolution with the pointed fix (documented). - Schema load rejects duplicate enum vocabulary (each entry becomes a Zig enum tag) and PascalCase type-name collisions across enum params (mode/mode_ both render pub const Mode) — manifest errors at load instead of uncompilable generated projects. Param-decl vs type-decl collisions impossible by construction (documented). +8 tests incl. an e2e where the enum-spelled language drives the real splice (negative control: str-only accessor -> generate succeeds silently without the splice arg — the exact reported gap). 1794/1798 clean-HOME. Claude-Session: https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j
One script language per project — the
.languagepolicy fromRFC-LANGUAGE-PLUGINSrevs 8–9. Closes #584 (Phase 1 of the language-plugins epic labelle-toolkit/labelle-engine#237).What it enforces
.languageon plugin declarations (mixing is unrepresentable in config); vocabulary{lua, typescript, ruby, rust, crystal, go, csharp}; at most one scripting plugin per project.validateLanguagePolicygate (besidevalidatePackGraph, before any target write — violations reject with no stale output): scans language convention dirs across the game root and all packs (game-local + plugin-bundled, riding P2'sPackEntry/resolveSrcDir), hard-errors with a capped file listing on out-of-language scripts, errors on script dirs with no scripting plugin, warns on empty dirs.requires_languageon both plugin and pack manifests (P2 split the schemas — landed in both): vocab-checked at manifest load, project-match checked in the gate, mismatches name both sides.Verification
Clean-HOME (CI-equivalent): 48/48 steps, 1512/1516 passed (4 skipped, 0 failed) — suite grew +65. Includes the load-bearing regression guard: a clean project generates byte-identically (real
generate()output pinned against the committed pre-#584 golden). Real-HOME shows 2 failures that reproduce identically on clean origin/main (flow_catalogsidecar test resolving a populated~/.labelleengine cache) — pre-existing and environment-dependent, not from this change.Discovered along the way (follow-up to be filed, out of scope here)
A light pack shipping ONLY
pack.labelle(no convention dirs) breaks fullgenerate()atwritePackModuleRoots(scanPacknever creates the target pack dir → FileNotFound) — predates this PR.Epic: labelle-toolkit/labelle-engine#237 · contract PR: labelle-toolkit/labelle-engine#749
https://claude.ai/code/session_01P7YLw4hXFCCaY2LAUt4G1j