Skip to content

feat(declare): generic .languages declare reader — native rust declares (#619/#774) - #621

Merged
apotema merged 1 commit into
mainfrom
feat/languages-declare-reader
Jul 12, 2026
Merged

feat(declare): generic .languages declare reader — native rust declares (#619/#774)#621
apotema merged 1 commit into
mainfrom
feat/languages-declare-reader

Conversation

@apotema

@apotema apotema commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

Implements the RFC-LANGUAGE-PLUGINS rev 17 §7 declare invocation contract (landed in labelle-engine#777): a generic reader of the plugin manifest's .languages capability rows. A native language (rust #774) now declares components/events through the identical .declare = { .tool, .dir, .events } shape an embedded language uses — no new field, no per-mechanism discriminant, zero cargo knowledge in the assembler.

Assembler half of the rust declare lane; the labelle-declare-rs tool + the .languages rust row landed in labelle-scripting#31.

How

  • plugin_manifest/plugin.zig — parse the .languages table: LanguageRow + DeclareCapability + LanguageKind, a languageRow(name) lookup, deinit, barrel re-exports. Unknown row keys (a future .transpile) ride the manifest-wide ignore_unknown_fields (forward-compat unit test included).

  • scripting_declare.zigrunPhase gains a generic branch taken only for a language absent from the hardcoded DECLARE_RUNNERS table (rust): resolve package + read the .languages declare row, gate events by the self-describing .events flag (rev-17's replacement for events_min_pin), build via the same zig build <.tool> machinery every runner uses, run with a persistent --cache-dir <output>/declare-tool/<tool>-cache + the declaration files, and hash the inputs (SHA-256 of path+contents) to skip re-invoking when unchanged (rev-17 invariant 1; cached schema JSON reused). lua/ruby (+ ts/crystal) rows are untouched — they short-circuit the branch, table stays as fallback. Shared finalizeSchema tail; runDeclareTool splits into raw execDeclareTool (optional --cache-dir) + parse.

  • root.zig — collect components/*.<ext> + events/*.<ext> for the native family too (previously embed-only, so a native splice never reached declare), routed to runPhase while s.scripts stays empty — gameplay scripts are compiler-staged, never fed to the declare probe.

Design decisions (from existing conventions)

  • Persistent cache dir: under the survives-generate <output>/declare-tool/. Opaque to the assembler.
  • Hashing: std.crypto.hash.sha2.Sha256 over each declaration file's path+bytes; cached JSON + digest under <cache>/.assembler-skip/ (best-effort).

Verification

  • Compiles; zig build test baseline-relative clean — the touched suites' e2e failures are byte-identical to origin/main's pre-existing Windows env failures (AppDataDirUnavailable); the new .languages parse tests pass; the typescript unit-skip invariant stays green.
  • A new generic-path test drives the whole flow (staged manifest row → capability → tool override → hash-skip → shared render, asserting the cache files land). POSIX-sh fixture, Windows-skipped like its siblings — runs on CI.

Scope

The CI rust-example assertion flip stays in the migration PR (rust-example still uses .zig components → phase no-ops). Closed PR #620's assembler-hardcoded cargo stays closed: rev 17 puts cargo in the tool.

🤖 Generated with Claude Code

…ares (#619/#774)

Implement the RFC-LANGUAGE-PLUGINS rev 17 §7 declare invocation contract: a
GENERIC reader of the plugin manifest's `.languages` capability rows, so a
native language (rust) declares components/events through the IDENTICAL
`.declare = { .tool, .dir, .events }` shape an embedded language uses — no new
field, no per-mechanism discriminant, zero cargo knowledge in the assembler.

- plugin_manifest/plugin.zig: parse the `.languages` table (LanguageRow +
  DeclareCapability + LanguageKind), a `languageRow(name)` lookup, deinit, and
  the barrel re-exports. Unknown row keys (a future `.transpile`) ride the
  manifest-wide ignore_unknown_fields (forward-compat test included).

- scripting_declare.zig: `runPhase` gains a generic branch taken ONLY for a
  language absent from the hardcoded DECLARE_RUNNERS table (rust). It resolves
  the plugin package, reads the `.languages` declare row, gates events by the
  self-describing `.events` flag (no version table), builds the tool via the
  same `zig build <.tool>` machinery every runner uses, and runs it with a
  PERSISTENT `--cache-dir <output>/declare-tool/<tool>-cache` + the declaration
  files — HASHING the inputs (SHA-256 of path+contents) to skip re-invoking the
  tool when unchanged (rev-17 invariant 1; a cached schema JSON is reused). The
  hardcoded lua/ruby (and ts/crystal) rows are untouched — they short-circuit
  the branch and keep the table as fallback. The declare-phase tail
  (events-none gate, collisions, renders) is extracted to a shared
  `finalizeSchema` both paths call; `runDeclareTool` splits into a raw
  `execDeclareTool` (optional `--cache-dir`) + parse. A no-declare-capability
  language falls through cleanly even on a degenerate plugin setup (the
  typescript-skip invariant).

- root.zig: collect `components/*.<ext>` + `events/*.<ext>` for the NATIVE
  family too (previously embed-only, so a native splice never reached declare),
  routed to `runPhase` while `s.scripts` stays empty — gameplay scripts are
  compiler-staged, never fed to the declare probe.

Verified: assembler compiles + `zig build test` baseline-relative clean (the
touched suites' e2e failures are byte-identical to origin/main's pre-existing
Windows env failures; the new `.languages` parse tests pass; the typescript
unit-skip test stays green). The generic-path e2e (fake-tool, hash-skip cache
assertion) runs on CI (POSIX-sh fixture, Windows-skipped like its siblings).

The CI rust-example assertion flip stays in the migration PR (the rust-example
still uses .zig components — zero .rs decl files — so the phase no-ops there).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1zt5TCHYqUJBjswJKjnCo
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@apotema, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 000ee2e8-4955-4511-97fc-4bf688567ac9

📥 Commits

Reviewing files that changed from the base of the PR and between ca8c7e3 and 50d7b44.

📒 Files selected for processing (4)
  • src/plugin_manifest.zig
  • src/plugin_manifest/plugin.zig
  • src/root.zig
  • src/scripting_declare.zig
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/languages-declare-reader

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.

❤️ Share

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 generic language capability rows in the plugin manifest according to RFC-LANGUAGE-PLUGINS rev 17, enabling languages like Rust to declare capabilities generically. It introduces manifest parsing for .languages rows, integrates a generic declare phase with input hashing to skip redundant tool execution, and shares schema finalization logic. Feedback recommends safely unwrapping optional component_embeds and event_embeds slices in src/root.zig to avoid potential runtime panics, and catching parsing errors from the cached schema.json in src/scripting_declare.zig to gracefully fall back to executing the declare tool fresh if the cache is corrupted.

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/root.zig
Comment on lines +607 to +610
const nd = try allocator.alloc(scripting_splice.EmbedScript, component_embeds.?.len + event_embeds.?.len);
@memcpy(nd[0..component_embeds.?.len], component_embeds.?);
@memcpy(nd[component_embeds.?.len..], event_embeds.?);
native_decl_embeds = nd;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Using .? on component_embeds and event_embeds is unsafe because these optional slices can be null if no component or event files are found. To prevent potential runtime panics, use safe optional unwrapping with if or orelse to determine the lengths and copy the slices defensively.

            const comp_len = if (component_embeds) |ce| ce.len else 0;
            const event_len = if (event_embeds) |ee| ee.len else 0;
            const nd = try allocator.alloc(scripting_splice.EmbedScript, comp_len + event_len);
            if (component_embeds) |ce| @memcpy(nd[0..comp_len], ce);
            if (event_embeds) |ee| @memcpy(nd[comp_len..], ee);
            native_decl_embeds = nd;

Comment thread src/scripting_declare.zig
Comment on lines +1710 to +1713
if (cwd.readFileAlloc(io, json_path, allocator, .limited(MAX_SCHEMA_BYTES))) |json| {
defer allocator.free(json);
return parseSchema(allocator, json);
} else |_| {}

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

If the cached schema.json is corrupted or invalid, parseSchema will fail and propagate the error, causing the entire generation phase to fail. Since the cache is best-effort, it is more robust to catch any parsing errors from the cached file and fall back to executing the declare tool fresh.

            if (cwd.readFileAlloc(io, json_path, allocator, .limited(MAX_SCHEMA_BYTES))) |json| {
                defer allocator.free(json);
                if (parseSchema(allocator, json)) |schema| {
                    return schema;
                } else |_| {}
            } else |_| {}

@apotema
apotema merged commit b9653be into main Jul 12, 2026
4 checks passed
@apotema
apotema deleted the feat/languages-declare-reader branch July 12, 2026 17:43

@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: 50d7b44bd9

ℹ️ 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/scripting_declare.zig
const json_path = try std.fs.path.join(allocator, &.{ skip_dir, "schema.json" });
defer allocator.free(json_path);

const digest = try declInputsDigestHex(allocator, opts.target_dir, opts.script_files);

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 Include tool identity in the declare cache key

When a project upgrades or changes the pinned scripting plugin/labelle-declare-rs tool while the declaration files are unchanged, this digest still matches the previous run, so lines below read .assembler-skip/schema.json and never invoke the newly built tool. That can leave generated components/events stale, or keep reusing a cached invalid schema after a tool/parser fix; include the resolved tool/package identity (or otherwise invalidate the cache on tool changes) in the skip key.

Useful? React with 👍 / 👎.

apotema added a commit that referenced this pull request Jul 19, 2026
Open the assembler's language VOCABULARY so a language plugin can add a
brand-new language (the RFC's "python") through a `plugin.labelle`
`.languages` capability row alone — zero assembler changes. This closes
the last per-language hardcode the row-driven declare/transpile/embed/
native machinery (already on main via #621#624) sat on top of: the
closed `SUPPORTED_LANGUAGES`/`scriptExtensions` table that the policy
gate, the shared-dir scan, the plugin-event consumption scan, and
`requires_language` validation all consulted, and which rejected any
language it had never heard of before the row machinery ever ran.

What moved (RFC-LANGUAGE-PLUGINS rev 16 §7 "language-agnostic assembler"):

- `language_policy.Vocabulary` — the project's OPEN language set: the
  frozen `SUPPORTED_LANGUAGES` built-ins ∪ the declaring scripting
  plugin's manifest `.languages` rows. Rows are PRIMARY (a row naming a
  frozen language shadows its extension set); a row language the tables
  never heard of is admitted exactly like a built-in. `EMPTY` (no rows)
  reproduces the pre-#619 closed-vocabulary behavior bit for bit.
- `validateDeclaredLanguage` — the vocabulary gate, re-homed from
  `resolveProjectLanguage` into `generate_phases.validateLanguagePolicy`
  where the declaring plugin's manifest rows are in hand. Self-describing
  capabilities replace the closed table: a pointed error naming BOTH the
  built-ins and the manifest rows, no version compare.
- `resolveProjectLanguage` / manifest-load `requires_language` now gate
  on SHAPE only (`isLanguageIdentifier` — `[a-z][a-z0-9_]*`, the one
  constraint the generated `.language = .<name>` enum literal genuinely
  imposes); the vocabulary check runs at generate where rows are visible.
- `scanUnitLanguageDirs` / `checkRequiresLanguage` take the `Vocabulary`
  and police row languages by the row's extensions (frozen ∪ rows).
- The plugin-event consumption scan gains the active splice's script
  extensions (`filterConsumedEvents(extra_extensions)`) so a row
  language's sources (`.py`) are not invisible to the scan and its
  subscriptions silently elided — the comptime `scanned_extensions`
  covers only the frozen built-ins.

Migration map (hardcoded path → row-driven):
- closed `SUPPORTED_LANGUAGES` vocabulary check in `resolveProjectLanguage`
  → `Vocabulary.isKnown` (frozen ∪ manifest rows) in `validateDeclaredLanguage`
- `scriptExtensions`-only dir/event policing → `Vocabulary.extensionsOf`
  (row extensions primary, frozen fallback)
- manifest-load closed-table `requires_language` reject → shape-only reject
  + generate-time vocabulary check
- (already on main, unchanged) declare → `runGenericDeclarePhase` rows;
  embed/native/transpile → `spliceFromRow`; the frozen `DECLARE_RUNNERS`
  / `EMBED_LANGUAGES` / `NATIVE_LANGUAGES` / `TSC_PLATFORMS` tables stay
  FROZEN FALLBACKS for row-less (pre-migration) manifests.

Back-compat proof: `LUA_ROW_BACKCOMPAT` generates a lua project twice —
frozen fallback (no row) vs an equivalent `.languages` row — and asserts
the main.zig + build.zig are BYTE-IDENTICAL. Migrating lua/ruby onto rows
shifts nothing. Every existing golden/e2e suite is unchanged.

Litmus (`test/quokka_litmus_tests.zig`): a fake embedded-VM language
"quokka" (`.qk`) the assembler learns ENTIRELY from a manifest row
generates end-to-end (embed registration, `.language = .quokka` dep arg,
the four splice touchpoints), a foreign frozen-language file (`.rb`) in
the same project still errors (combined vocabulary polices both ways),
and a META test proves "quokka"/".qk" appear in NO `src/**/*.zig` — the
mechanical proof the generate rode rows, not a hidden branch.

Closes #619

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
apotema added a commit that referenced this pull request Jul 19, 2026
Open the assembler's language VOCABULARY so a language plugin can add a
brand-new language (the RFC's "python") through a `plugin.labelle`
`.languages` capability row alone — zero assembler changes. This closes
the last per-language hardcode the row-driven declare/transpile/embed/
native machinery (already on main via #621#624) sat on top of: the
closed `SUPPORTED_LANGUAGES`/`scriptExtensions` table that the policy
gate, the shared-dir scan, the plugin-event consumption scan, and
`requires_language` validation all consulted, and which rejected any
language it had never heard of before the row machinery ever ran.

What moved (RFC-LANGUAGE-PLUGINS rev 16 §7 "language-agnostic assembler"):

- `language_policy.Vocabulary` — the project's OPEN language set: the
  frozen `SUPPORTED_LANGUAGES` built-ins ∪ the declaring scripting
  plugin's manifest `.languages` rows. Rows are PRIMARY (a row naming a
  frozen language shadows its extension set); a row language the tables
  never heard of is admitted exactly like a built-in. `EMPTY` (no rows)
  reproduces the pre-#619 closed-vocabulary behavior bit for bit.
- `validateDeclaredLanguage` — the vocabulary gate, re-homed from
  `resolveProjectLanguage` into `generate_phases.validateLanguagePolicy`
  where the declaring plugin's manifest rows are in hand. Self-describing
  capabilities replace the closed table: a pointed error naming BOTH the
  built-ins and the manifest rows, no version compare.
- `resolveProjectLanguage` / manifest-load `requires_language` now gate
  on SHAPE only (`isLanguageIdentifier` — `[a-z][a-z0-9_]*`, the one
  constraint the generated `.language = .<name>` enum literal genuinely
  imposes); the vocabulary check runs at generate where rows are visible.
- `scanUnitLanguageDirs` / `checkRequiresLanguage` take the `Vocabulary`
  and police row languages by the row's extensions (frozen ∪ rows).
- The plugin-event consumption scan gains the active splice's script
  extensions (`filterConsumedEvents(extra_extensions)`) so a row
  language's sources (`.py`) are not invisible to the scan and its
  subscriptions silently elided — the comptime `scanned_extensions`
  covers only the frozen built-ins.

Migration map (hardcoded path → row-driven):
- closed `SUPPORTED_LANGUAGES` vocabulary check in `resolveProjectLanguage`
  → `Vocabulary.isKnown` (frozen ∪ manifest rows) in `validateDeclaredLanguage`
- `scriptExtensions`-only dir/event policing → `Vocabulary.extensionsOf`
  (row extensions primary, frozen fallback)
- manifest-load closed-table `requires_language` reject → shape-only reject
  + generate-time vocabulary check
- (already on main, unchanged) declare → `runGenericDeclarePhase` rows;
  embed/native/transpile → `spliceFromRow`; the frozen `DECLARE_RUNNERS`
  / `EMBED_LANGUAGES` / `NATIVE_LANGUAGES` / `TSC_PLATFORMS` tables stay
  FROZEN FALLBACKS for row-less (pre-migration) manifests.

Back-compat proof: `LUA_ROW_BACKCOMPAT` generates a lua project twice —
frozen fallback (no row) vs an equivalent `.languages` row — and asserts
the main.zig + build.zig are BYTE-IDENTICAL. Migrating lua/ruby onto rows
shifts nothing. Every existing golden/e2e suite is unchanged.

Litmus (`test/quokka_litmus_tests.zig`): a fake embedded-VM language
"quokka" (`.qk`) the assembler learns ENTIRELY from a manifest row
generates end-to-end (embed registration, `.language = .quokka` dep arg,
the four splice touchpoints), a foreign frozen-language file (`.rb`) in
the same project still errors (combined vocabulary polices both ways),
and a META test proves "quokka"/".qk" appear in NO `src/**/*.zig` — the
mechanical proof the generate rode rows, not a hidden branch.

Closes #619

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
apotema added a commit that referenced this pull request Jul 19, 2026
#643)

* feat: language capability rows — assembler language-agnosticism (#619)

Open the assembler's language VOCABULARY so a language plugin can add a
brand-new language (the RFC's "python") through a `plugin.labelle`
`.languages` capability row alone — zero assembler changes. This closes
the last per-language hardcode the row-driven declare/transpile/embed/
native machinery (already on main via #621#624) sat on top of: the
closed `SUPPORTED_LANGUAGES`/`scriptExtensions` table that the policy
gate, the shared-dir scan, the plugin-event consumption scan, and
`requires_language` validation all consulted, and which rejected any
language it had never heard of before the row machinery ever ran.

What moved (RFC-LANGUAGE-PLUGINS rev 16 §7 "language-agnostic assembler"):

- `language_policy.Vocabulary` — the project's OPEN language set: the
  frozen `SUPPORTED_LANGUAGES` built-ins ∪ the declaring scripting
  plugin's manifest `.languages` rows. Rows are PRIMARY (a row naming a
  frozen language shadows its extension set); a row language the tables
  never heard of is admitted exactly like a built-in. `EMPTY` (no rows)
  reproduces the pre-#619 closed-vocabulary behavior bit for bit.
- `validateDeclaredLanguage` — the vocabulary gate, re-homed from
  `resolveProjectLanguage` into `generate_phases.validateLanguagePolicy`
  where the declaring plugin's manifest rows are in hand. Self-describing
  capabilities replace the closed table: a pointed error naming BOTH the
  built-ins and the manifest rows, no version compare.
- `resolveProjectLanguage` / manifest-load `requires_language` now gate
  on SHAPE only (`isLanguageIdentifier` — `[a-z][a-z0-9_]*`, the one
  constraint the generated `.language = .<name>` enum literal genuinely
  imposes); the vocabulary check runs at generate where rows are visible.
- `scanUnitLanguageDirs` / `checkRequiresLanguage` take the `Vocabulary`
  and police row languages by the row's extensions (frozen ∪ rows).
- The plugin-event consumption scan gains the active splice's script
  extensions (`filterConsumedEvents(extra_extensions)`) so a row
  language's sources (`.py`) are not invisible to the scan and its
  subscriptions silently elided — the comptime `scanned_extensions`
  covers only the frozen built-ins.

Migration map (hardcoded path → row-driven):
- closed `SUPPORTED_LANGUAGES` vocabulary check in `resolveProjectLanguage`
  → `Vocabulary.isKnown` (frozen ∪ manifest rows) in `validateDeclaredLanguage`
- `scriptExtensions`-only dir/event policing → `Vocabulary.extensionsOf`
  (row extensions primary, frozen fallback)
- manifest-load closed-table `requires_language` reject → shape-only reject
  + generate-time vocabulary check
- (already on main, unchanged) declare → `runGenericDeclarePhase` rows;
  embed/native/transpile → `spliceFromRow`; the frozen `DECLARE_RUNNERS`
  / `EMBED_LANGUAGES` / `NATIVE_LANGUAGES` / `TSC_PLATFORMS` tables stay
  FROZEN FALLBACKS for row-less (pre-migration) manifests.

Back-compat proof: `LUA_ROW_BACKCOMPAT` generates a lua project twice —
frozen fallback (no row) vs an equivalent `.languages` row — and asserts
the main.zig + build.zig are BYTE-IDENTICAL. Migrating lua/ruby onto rows
shifts nothing. Every existing golden/e2e suite is unchanged.

Litmus (`test/quokka_litmus_tests.zig`): a fake embedded-VM language
"quokka" (`.qk`) the assembler learns ENTIRELY from a manifest row
generates end-to-end (embed registration, `.language = .quokka` dep arg,
the four splice touchpoints), a foreign frozen-language file (`.rb`) in
the same project still errors (combined vocabulary polices both ways),
and a META test proves "quokka"/".qk" appear in NO `src/**/*.zig` — the
mechanical proof the generate rode rows, not a hidden branch.

Closes #619

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM

* fix(#619): reject unsafe/extensionless language rows; preserve built-in metadata on shadow (codex #643 P2)

Three review fixes on the capability-row vocabulary:

1. Reject language names that break build.zig emission. isLanguageIdentifier
   now also rejects Zig keywords (`error`/`fn`/`test`/…): the generated
   `.language = .<name>` enum literal would be a syntax error otherwise.
   Enforced at manifest load (pointed PluginManifestInvalidLanguageRow) and
   in resolveProjectLanguage/requires_language. (Primitives like i32 stay
   admissible — `.i32` is a legal enum literal; only keywords break syntax.)

2. Preserve built-in metadata when a row shadows a built-in. Vocabulary.build
   now UNIONS the frozen scriptExtensions into a shadowing row's extensions
   (row order first, deduped), so a migrated typescript row declaring only
   authored `.ts` keeps the frozen emitted `.js`. scanUnitLanguageDirs keeps
   the built-in's legacy-dir mapping for shadowed languages instead of
   dropping it. (Splice/declare/transpile metadata — TSC_PLATFORMS, staging
   geometry — is read directly from the manifest LanguageRow, unaffected.)

3. Reject rows without source extensions. A `.languages` row with empty
   `.extensions` is rejected at manifest load with a pointed diagnostic —
   the shared-dir/consumption scans find sources by extension, so an
   extensionless row would silently elide every source it declares.

Tests: keyword-named row rejected, extensionless row rejected, well-formed
row loads, shadow-row unions frozen extensions, keyword identifiers rejected.
Baseline green (flow_catalog env lanes tolerated).

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM

* feat(#619): row-ify #644's csharp runtime-output + toolchain-probe decisions

Migrate the two `language == "csharp"` DECISIONS #644 added into
capability rows, per #619's design (the module doc #644 shipped explicitly
anticipated this):

- New LanguageRow capabilities `runtime_output` (the link-less build
  outputs ARE the runtime payload — stage beside the binary + set the run
  step's assembly-dir env) and `probe_tools` (opt into the generate-time
  PATH probe of the language's steps).
- scripting_csharp.stagesRuntimeOutputs now takes the resolved
  `runtime_output` capability instead of hardcoding `== "csharp"`; the
  ensureStepToolsOnPath call is gated on the resolved `probe_tools`.
- The csharp knowledge is DEMOTED to a frozen fallback
  (scripting_csharp.frozenRuntimeOutput/frozenProbeTools), the same
  #619 pattern as DECLARE_RUNNERS/EMBED_LANGUAGES/NATIVE_LANGUAGES — the
  shipped csharp manifest (which predates these capabilities) keeps working
  byte-for-byte; a NEW runtime-loaded language rides its row alone.
- ScriptingSplice.detect resolves both capabilities (row PRIMARY ∪ frozen)
  and threads them onto the splice; root.zig consumes them.

csharp's EMBED compile wiring itself needed NO migration — it was already
fully row-driven (csharp has no frozen-table fallback; it resolves entirely
via its manifest .languages native row + plugin-declared .language_builds).
Only the runtime-output-staging + toolchain-probe seams were csharp-named.

Residue (documented, RFC honest boundary): the runtime assembly-dir env var
NAME (LABELLE_CS_ASSEMBLY_DIR) stays an assembler-owned constant, and the
dev-.csproj IDE aid stays csharp-keyed MSBuild codegen — analogous to the
assembler-owned tsconfig codegen; neither is needed for the language-agnostic
litmus (which adds an EMBEDDED language).

Tests: runtime_output/probe_tools row parse (+ default-false), frozen
fallback (csharp opts in, others out), the capability-driven
stagesRuntimeOutputs. csharp_splice_tests (#644's e2e) stay green via the
frozen fallback — the back-compat proof. Baseline green (flow_catalog lanes).

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
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