Skip to content

DESIGN: an emitted binary can read files; the CLI's walk stays in main for two narrower gaps - #11526

Closed
briansrls wants to merge 1 commit into
mainfrom
design-cli-file-io-correction
Closed

briansrls wants to merge 1 commit into
mainfrom
design-cli-file-io-correction

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

Corrects a false claim in the Two CLIs rule landed by #11474.

What was wrong

The rule said no .dag fold can read a file in an emitted binary: resource operations resolve only inside a workflow function and are realized by the seed interpreter.

  • True, and kept: a resource method on a plain fn refuses (Filesystem.Read → receiver type 'Primitive(Filesystem)' establishes no method surface).
  • False: the conclusion. The free primitive filesystem_read is typed by v1.compiler.infer_method builtin_function_registry, bridged by extdeps.languages.rust.emit rt_function_registry, and realized by v1.compiler.runtime_rust rt_filesystem with std::fs. Emitted binaries read files with no interpreter.

What actually keeps the walk in the rendered main

Two gaps, not a wall:

  1. No refusal arm — a missing file panics instead of returning a typed outcome (§5).
  2. No directory listing in the emitted surface.

Both close in emission only, never by growing the interpreter. Closing them moves the walk into the CLI's own fold, which is what the second generation (the built CLI emitting a closure, judged by admit_promotion) needs.

Evidence

  • DESIGN.md regenerated with gunbc run … --entry dag/gunbc/instruments/generated_artifact_gate.dag --function main_wet on binaries rebuilt from this tree. The only projection change is this paragraph.
  • claim_executor --required-regen → first_generation_equal=true. The first attempt drifted on compiler_tests.rs against binaries from the previous branch; rebuilding cleared it, so that was instrument vintage, not the tree.

🤖 Generated with Claude Code

…n for two narrower gaps

The "Two CLIs" rule landed in #11474 stated that no `.dag` fold can read a file in an emitted
binary, because resource operations resolve only inside a workflow function and are realized by the
seed interpreter. The first half is true of resource METHODS and is kept, with its refusal quoted.
The conclusion drawn from it is false: the free primitive `filesystem_read` is typed by
`v1.compiler.infer_method` `builtin_function_registry`, bridged by `extdeps.languages.rust.emit`
`rt_function_registry`, and realized by `v1.compiler.runtime_rust` `rt_filesystem` with the host's
standard library. An emitted binary reads files without the interpreter.

What actually keeps the source-root walk in the rendered main is two gaps, not a wall: that read has
no refusal arm (a missing file panics rather than returning a typed outcome, which section 5 forbids
as a compiler's source-reading route), and the emitted surface has no directory-listing primitive.
Both close in emission only, never by growing the interpreter, and closing them moves the walk into
the CLI's own fold -- the prerequisite for the second generation.

A rule stated as a law of the language where it was the current emitted surface would send the next
reader to the interpreter for something the emitter already does, which is the direction the
operator ruled out.

DESIGN.md regenerated by `gunbc run --entry dag/gunbc/instruments/generated_artifact_gate.dag
--function main_wet` on binaries rebuilt from this tree; the only projection change is this paragraph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-17T17:16:48.505890Z 1eee2f0 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 1eee2f0788

ℹ️ 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".

li(text: "one-time per clone: `git config core.hooksPath .githooks` — the only documented manual seed; generated pre-commit/pre-push hooks then idempotently converge `merge.generated-artifact.driver` and re-assert `core.hooksPath` via argv derived from `gunbc.repo_local_git_config` (clones that skip hooksPath degrade to vanilla text-merge for generated-artifact paths; drift gate still guards at CI). The driver REFUSES rather than answering `true`: git reaches a low-level merge driver only when both sides changed the path since the merge base — measured on a four-case matrix, one-sided and identical changes never reach it — and taking the ours side there dropped the other side's authority-derived bytes with no conflict, twice on #7836 against the stage0 seed. It now leaves the ours side in the worktree with no conflict markers, marks the path unmerged, and prints the regeneration recipe; the class is mechanically preventable, not structural, and its next-rung trigger is the commit-writer binding rows in `gunbc.commit_workflow`"),
li(text: "**`gunbc test <label>` is the dedicated CLI for invoking a v2 measurement, and a new one is a row rather than a flag** (operator ruling, 2026-09-16). The seam is `gunbc.target_binding` `TargetProducer` bound to a `gunbc.instrument_targets` label, and the host note says the extension shape outright: *adding one is a row in `instrument_registry` and an arm here; it is not a new route*. What that buys is not tidiness: the label carries its own refusal vocabulary, so an unknown or pattern label REFUSES with exit 2 rather than reporting a pass, and the three terminations stay distinct — 0 held, 1 an observation that did not hold, 2 no observation — which is the conflation §5 forbids, closed by construction for every instrument at once. The subject is the instrument's own fact and never a CLI option, so an invocation cannot quietly measure a different corpus while reporting the same target's standing. **What this rules out is the alternative that keeps suggesting itself:** a flag on `claim_executor`, whose ~20 flags are hand-parsed in a 2783-line `main` over 44,451 lines of `cli_run.rs` with NO `.dag` authority at all — v2 work that lands there is unmodeled by construction, and `--self-host` was withdrawn in the same change that added `//gunbc/instruments:self-host` rather than left standing beside it, because two routes to one fact is the §3 fork. `gunbc`'s own surface is modeled (`gunbc.cli_dispatch_surface` → `gunbc_cli_dispatch_generated.rs`); `claim_executor`'s is not, and the gap is the point."),
li(text: "**Two CLIs, and which one a session wants is decided by which compiler is running.** `gunbc` is the SEED's CLI — `std.compiler_entry` `RetainedHostCliKernel`, a clap surface dispatching into hand-authored host Rust (`v1_compiler.cli_run`) — so its verbs are unreachable from a self-emitted binary BY CONSTRUCTION, not by omission: an emitted crate does not contain that module. The v2-exclusive door is `v2.cli.compile_cli`, `.dag` in `src/v2/cli/`, declaring the fourth driver `std.compiler_entry` `NativeCliDriver`. It parses argv, decides the verb, decides every refusal, decides what to emit and decides the process exit (`std.process` `ProcessExit`); its rendered main performs only the four operations no `.dag` fold can — read argv, walk and read the source roots the PLAN named, write stdout, set the status — and hands each back as a VALUE. **That boundary is measured, not chosen for taste:** a resource method on an ordinary `fn` is a located compiler refusal (`Filesystem.Read` off a plain function refuses with *receiver type 'Primitive(Filesystem)' establishes no method surface*), because resource operations resolve only inside a workflow function and are realized by the seed INTERPRETER, which an emitted binary is not running under. The shim shape is deliberately the capability `gunbc.source_root_eval_driver_seed_growth` names as the trigger retiring ITS row — *the rendered main becomes one call into that fold* — so the new door adds no seed debt. **Why a second entry module rather than a mode on the first:** one emitted crate has one entry point and the emitter admits exactly one `compiler_pipeline_entry` declaration per closure (`compiler_pipeline_entry_is_ambiguous` refuses two by name), and the CLI's closure reaches no part of `v2.compiler.compile`, so exactly-one holds by construction rather than by a flag choosing between them. The two generations are two instrument rows: `gunbc test //gunbc/instruments:self-host` is the seed emitting and building `v2.compiler.compile`, and `gunbc test //gunbc/instruments:v2-native-cli` is the seed emitting and building the CLI closure — two labels because they are two compilations, so a regression in either is reported under a name that says which. Neither is the second generation; that is the BUILT CLI emitting a closure, and it needs this door to exist first."),
li(text: "**Two CLIs, and which one a session wants is decided by which compiler is running.** `gunbc` is the SEED's CLI — `std.compiler_entry` `RetainedHostCliKernel`, a clap surface dispatching into hand-authored host Rust (`v1_compiler.cli_run`) — so its verbs are unreachable from a self-emitted binary BY CONSTRUCTION, not by omission: an emitted crate does not contain that module. The v2-exclusive door is `v2.cli.compile_cli`, `.dag` in `src/v2/cli/`, declaring the fourth driver `std.compiler_entry` `NativeCliDriver`. It parses argv, decides the verb, decides every refusal, decides what to emit and decides the process exit (`std.process` `ProcessExit`); its rendered main performs only four host operations — read argv, walk and read the source roots the PLAN named, write stdout, set the status — and hands each back as a VALUE. **That boundary is the emitted surface as it stands, not a law of the language, and this rule's first revision stated it as one** — that no `.dag` fold can read a file in an emitted binary. Two surfaces answer that question differently. A resource METHOD on an ordinary `fn` is a located refusal (`Filesystem.Read` off a plain function refuses with *receiver type 'Primitive(Filesystem)' establishes no method surface*), because resource methods resolve only inside a workflow function. The free primitive `filesystem_read` is not: `v1.compiler.infer_method` `builtin_function_registry` types it, `extdeps.languages.rust.emit` `rt_function_registry` bridges it, and `v1.compiler.runtime_rust` `rt_filesystem` realizes it with the host's standard library, so an emitted binary reads files without the interpreter. What keeps the walk in the main is narrower and is two gaps, not one wall: that read has NO REFUSAL ARM — a missing file panics instead of returning a typed outcome, which §5 forbids as the route a compiler reads its sources by — and the emitted surface has NO DIRECTORY LISTING at all. Both close in emission only, never by growing the interpreter, and closing them is what moves the walk out of the main and into the CLI's own fold. The shim shape is deliberately the capability `gunbc.source_root_eval_driver_seed_growth` names as the trigger retiring ITS row — *the rendered main becomes one call into that fold* — so the new door adds no seed debt. **Why a second entry module rather than a mode on the first:** one emitted crate has one entry point and the emitter admits exactly one `compiler_pipeline_entry` declaration per closure (`compiler_pipeline_entry_is_ambiguous` refuses two by name), and the CLI's closure reaches no part of `v2.compiler.compile`, so exactly-one holds by construction rather than by a flag choosing between them. The two generations are two instrument rows: `gunbc test //gunbc/instruments:self-host` is the seed emitting and building `v2.compiler.compile`, and `gunbc test //gunbc/instruments:v2-native-cli` is the seed emitting and building the CLI closure — two labels because they are two compilations, so a regression in either is reported under a name that says which. Neither is the second generation; that is the BUILT CLI emitting a closure, and it needs this door to exist first."),

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 Reconcile the CLI's remaining boundary authorities

This correction leaves the same disproven boundary in the declarations it directs readers to: src/v2/cli/compile_cli.dag:47-64 still says a .dag fold cannot read a file and therefore requires the host's two-call protocol, dag/std/compiler_entry.dag:64-71 repeats that all four operations are impossible in .dag, and dag/gunbc/source_root_eval_driver_seed_growth.dag:24 classifies source reads as irreducibly host. When the next CLI/emission change follows those local authorities, it will derive the old architecture despite this new rule, so those statements and the affected dissolution reasoning need to be corrected in the same change.

Useful? React with 👍 / 👎.

gunbai-bot Bot pushed a commit that referenced this pull request Sep 17, 2026
#11526 retracts "a `.dag` fold cannot read a file" from the design document's Two CLIs rule. This
module's own annotation asserted the same thing, one file away from where a reader of the CLI would
look, so the retraction would have held in the design document while the code kept restating it --
DESIGN section 4b(1)'s rung-honesty failure in the direction that matters, since a superseded
sentence left standing gets read as fact.

What is measured stays: a resource METHOD on a plain `fn` refuses with "receiver type
'Primitive(Filesystem)' establishes no method surface". The false part was the conclusion. The free
primitive `filesystem_read` is typed by `v1.compiler.infer_method` `builtin_function_registry`,
bridged by `extdeps.languages.rust.emit` `rt_function_registry`, and realized by
`v1.compiler.runtime_rust` `rt_filesystem` as `std::fs::read_to_string` -- and that block emits
`unrealized_host_seam` and `filesystem_read` two declarations apart, so the paragraph denying the
reader was landed beside the reader.

What keeps the walk in the rendered main is therefore two gaps rather than a wall: `filesystem_read`
panics on a missing file instead of returning a typed outcome, and the emitted surface carries no
directory listing. Both close in emission, neither by growing the interpreter. The DECISION split
the paragraph exists to state is unaffected -- it was never the file-reading claim.

Annotation only. DESIGN section 4c: annotation capture is disjoint from semantic occurrence
allocation, so this cannot alter the semantic graph, any hash, or the emitted bytes, and the
instrument receipts on this head stand unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@briansrls briansrls closed this Sep 29, 2026
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