Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
868a497
Record the SCM demo CLI rebuild plan and its API inventory
Sep 1, 2026
9f2c851
Host reader and pure outcome map for CliWireResponse
Sep 1, 2026
6a71d4c
Merge remote-tracking branch 'origin/main' into scm-cli
Sep 1, 2026
b4dee9f
Wire CliWireResponse into run_verb: the SCM answer reaches an operator
Sep 1, 2026
684c0dd
Record the working read surface and state its evidence boundary
Sep 1, 2026
6afa087
Make gunbc.scm.cli's own decisions witnessable, and enroll them
Sep 1, 2026
ead172b
Merge remote-tracking branch 'origin/main' into scm-cli
Sep 1, 2026
a345dca
scm init: the first write verb, and a real round trip
Sep 1, 2026
093348b
Record what add and commit need, including the one design step
Sep 1, 2026
1efaf10
init refuses an occupied path: naming a destructive default did not m…
Sep 1, 2026
0259205
Make scm init proceed only from an established absence, and receipt t…
Sep 1, 2026
4399b15
Integrate main
Sep 1, 2026
0230151
Merge remote-tracking branch 'origin/main' into scm-cli
Sep 2, 2026
76f0551
Regenerate the Rust source-type bindings from their authority
Sep 2, 2026
1d87dc4
The relocation note was prose in a String row, which §4c quarantines
Sep 2, 2026
3096ca1
Admit the entry name, and make the init decision witnessable
Sep 2, 2026
cda2be3
A malformed wire response is not "some other type"
Sep 2, 2026
da79cc1
The seed-growth receipt undercounted the module I had just added
Sep 2, 2026
d778e87
Cut scm.init from this PR, and refuse a printable whose exit is not one
Sep 2, 2026
82f8312
Regenerate the dispatch-surface projection from its corrected authority
Sep 2, 2026
7974566
Take init's private artifacts out with it, and derive the receipt count
Sep 2, 2026
a440b4e
Restore the sentence the relocation dropped, and stop the doc claimin…
Sep 2, 2026
594d6db
Merge remote-tracking branch 'origin/main' into scm-cli
Sep 2, 2026
bb096de
Drop the empty_repository import that outlived its only consumer
Sep 2, 2026
01ebacb
Stop the plan and the source narrating init as landed behaviour
Sep 2, 2026
f2ff7ae
Merge remote-tracking branch 'origin/main' into scm-cli
Sep 2, 2026
dfedbfe
Merge remote-tracking branch 'origin/main' into scm-cli
Sep 2, 2026
4f7c779
Scope the plan's time claims, and drop a write-renderer bullet with n…
Sep 2, 2026
91550f7
Merge remote-tracking branch 'origin/main' into scm-cli
Sep 2, 2026
af506ef
Merge main into scm-cli: #10026 landed first and reverted four of thi…
Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dag/gunbc/cli_dispatch_surface.dag
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ fn gunbc_cli_subcommands() -> List<CliSubcommandRow> {
}
],
realization: CliRefusesUnwired {
successor: "unwired: gunbc.cli_wire CliWireResponse has no host renderer bound. MEASURED, not assumed -- `gunbc run --entry dag/gunbc/scm/read_command.dag --function scm_read_log --arg path=<p>` binds its argument and READS THE FILE, then the host refuses with `returned ScmReadResult, not ProcessExit`, so the answer is computed and discarded. There is therefore NO route a user can invoke today that yields this answer, and naming one would be a dead end dressed as a redirect."
successor: "unwired AS A SUBCOMMAND -- `gunbc scm log` does not exist -- but the answer is now REACHABLE, and this row said it was not. The successor is `gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/scm/cli.dag --function scm_log --arg path=<p>`, which prints the rendered answer and exits on the exit the response itself carries. What changed: gunbc.cli_wire CliWireResponse gained a host binding in run_verb (cli_run classify_cli_wire + cli_wire_outcome), so a wire-returning entry is no longer refused with `returned ScmReadResult, not ProcessExit` and its answer is no longer computed and discarded. The earlier text -- `there is NO route a user can invoke today` -- was true when written and this row is the authority that has to stop saying it, because a surface that keeps asserting an absence the host has since filled is a §3 fork between the model and the thing it models. scm_status is reachable by the same route. scm_init is NOT part of this change: its safe form needs an atomic create-only write, because observing an absence and then writing unconditionally can still truncate bytes it claims to refuse, and that primitive does not exist yet -- so init lands with it rather than being described here as reachable."
},
emission: AbsentFromEmitMainRs
},
Expand Down
181 changes: 181 additions & 0 deletions dag/gunbc/cli_wire_host_admission.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
module gunbc.cli_wire_host_admission

import gunbc.roadmap_model { RoadmapNodeId }
import gunbc.seed_growth { SeedGrowthJustification }
import std.decl_ref { DeclarationRef, WholeDeclaration }
import std.disposition { Disposition, Terminal }
import std.types { String }
import v2.std.collection { List }

data cli_wire_host_receipt_disposition: Disposition = Terminal {
reason: "Single authority for the SEED-GROWTH RECEIPT owed by the host renderer that carries a modeled CliWireResponse to a process's stdout and exit status. It declares WHICH hand-authored Rust declarations that change adds to the seed, why they exist, and what retires them. It does NOT admit them: the realization disposition is an operator judgment and fails closed in its absence, because a receipt that also granted its own admission would be the self-authorized dissolution failure DESIGN names. Terminal because the RECEIPT is permanent -- it outlives the code it describes, recording what was owed and on what trigger it was discharged."
}

// WHAT THIS GROWTH BOUGHT, STATED AS THE DEFECT IT REMOVED RATHER THAN AS A FEATURE.
//
// gunbc.cli_dispatch_surface already carried a row saying the scm family REFUSES UNWIRED: the
// modeled command surface computed an answer and no route existed to deliver it. So every scm
// entry was a value the host discarded -- specification without execution, DESIGN section 5's
// deepest trap, and flagged by the compiler's OWN surface model rather than discovered by review.
// Five production declarations sit below. THREE ARE THE ROUTE -- classify a returned value as a
// CliWireResponse or refuse to call it one, decide what the host must DO about it, and exit
// accordingly. The other two are the carriers that keep the route's states apart, and they are
// counted in the census below exactly like the functions are.
//
// WHY THE CLASSIFIER CANNOT BE MODELED AWAY TODAY. It reads an interpreter Value -- a host runtime
// representation with no .dag denotation -- and asks whether it inhabits a declared coproduct. The
// answer is a fact about the modeled type, but the SUBJECT is a seed data structure, so the fold
// has to live where that structure lives. That is the same boundary every other value-projecting
// item in this roster sits on, not new debt of its own kind.

// exit_status_for_class IS NOT NEW BEHAVIOUR, and why the roster counts it anyway. Its body was
// MOVED out of main.rs's exit_status_for, which now delegates to it. It is enumerated below because
// it is a hand-authored declaration that did not exist before at that name and in that module, and
// netting a relocation against an addition is exactly the arithmetic gunbc.seed_growth_admission
// refuses -- but a reader counting BEHAVIOUR rather than DECLARATIONS should know that the
// process-exit path gained nothing here and was only made callable from the wire path beside it.
//
// This was a `data ... : String` row until review 58455 named it: a String declaration whose only
// purpose is commentary is the state DESIGN section 4c quarantines, because nothing distinguishes it
// from program data. It is authored rationale about why the roster has the shape it has, so it is an
// annotation. The fact it carried -- that one of the five is a relocation -- is stated inside the
// justification's own `reason`, where a consumer reading the receipt can see it.
data cli_wire_host_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification {
hand_authored_declarations: [
DeclarationRef {
module_path: "v1_compiler.cli_run",
decl_name: "CliWireClass",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run",
decl_name: "classify_cli_wire",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run",
decl_name: "CliWireOutcome",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run",
decl_name: "cli_wire_outcome",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run",
decl_name: "exit_status_for_class",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run",
decl_name: "cli_wire_outcome_tests",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_outcome_tests",
decl_name: "a_printable_response_writes_its_bytes_and_honors_its_own_exit",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_outcome_tests",
decl_name: "a_printable_response_can_carry_a_failing_exit",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_outcome_tests",
decl_name: "an_unprintable_response_refuses_rather_than_printing_nothing",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_outcome_tests",
decl_name: "a_non_wire_value_yields_no_outcome_so_the_process_exit_path_is_untouched",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_outcome_tests",
decl_name: "an_exit_failure_with_code_zero_is_refused_on_the_wire_path_too",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run",
decl_name: "cli_wire_classify_tests",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "ctx",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "variant",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "exit_success",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "is_malformed",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "is_not_wire",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "a_well_formed_printable_response_classifies_as_printable",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "a_process_exit_is_not_a_wire_response_and_falls_through",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "a_printable_response_missing_its_bytes_is_malformed_not_foreign",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "a_printable_response_whose_bytes_are_not_a_string_is_malformed",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "a_printable_response_missing_its_exit_is_malformed",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "a_printable_response_whose_exit_is_not_a_process_exit_is_malformed_and_prints_nothing",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "an_unprintable_response_with_an_empty_or_absent_cause_is_malformed",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "an_unknown_wire_variant_is_malformed",
field: WholeDeclaration
},
DeclarationRef {
module_path: "v1_compiler.cli_run.cli_wire_classify_tests",
decl_name: "a_malformed_wire_response_refuses_rather_than_falling_through",
field: WholeDeclaration
}
],

reason: "WHY RUST IS STILL NEEDED HERE: the subject is the HOST PROCESS -- stdout bytes and an exit status -- and it is reached from a returned interpreter Value. Neither end has a .dag denotation today: there is no modeled carrier for a running process's standard output, and no modeled projection of the interpreter's own Value representation, so the fold between them cannot be written in the substrate it is bridging into. This is a boundary item, not seed-shrink debt.\n\nWHAT IT REMOVED: gunbc.cli_dispatch_surface's scm row carried CliRefusesUnwired -- the compiler's own dispatch model recording that the scm command family computed answers no user could invoke. Every modeled scm entry was therefore specification without execution. With this route bound, the scm log and status answers reach an operator, which is the only reason the .dag side of this change is worth anything. scm_init is deliberately NOT in this change: observing an absence and then writing unconditionally can still truncate the bytes it claims to refuse, and the atomic create-only write that closes that does not exist yet, so init lands with the primitive rather than being claimed reachable here.\n\nHAND-ITEM DELTA: +26 FROM THE MERGE BASE, ENUMERATED ABOVE.\n\nTHE COUNT IS STATED AS A DERIVATION RATHER THAN A NUMBER, because typing the number is how this row was wrong twice. It said +11 while the classifier test module added thirteen items nobody had listed, and then +25 while a test added after the row was written -- a_printable_response_whose_exit_is_not_a_process_exit_is_malformed_and_prints_nothing, the one that closed review 58518 -- sat outside it. Both times the receipt was edited as a step SEPARATE from the code it admits, so anything added afterwards fell silently outside the subject. Re-derive instead of trusting: for each of the two test modules, the entries are the module itself, every `fn` in it that carries #[test], and every `fn` in it that does not. 5 production + (cli_wire_outcome_tests: 1 module + 0 helpers + 5 tests) + (cli_wire_classify_tests: 1 module + 5 helpers + 9 tests) = 26.\n\nCHILD ITEMS CARRY THEIR NESTED MODULE PATH -- v1_compiler.cli_run.cli_wire_classify_tests, not v1_compiler.cli_run -- because that is where the declaration actually lives, and a roster whose identities do not resolve to the code cannot be checked against it. The outcome module's five were flattened onto the parent path when first written; they are corrected here rather than left as a second convention beside the new one.\n\nReproducible from the merge base, never the moving tip: git diff $(git merge-base origin/main HEAD)...HEAD -- src/v1/stage0/src/cli_run.rs src/v1/stage0/src/main.rs. Two further .rs files differ and are GENERATED projections rather than hand-authored items -- gunbc_cli_dispatch_surface.rs and gunbc_rust_source_type_bindings.rs, both regenerated from their .dag authority -- so they are outside this roster's subject by construction, not by exemption. One of the 26, exit_status_for_class, is a RELOCATION rather than new behaviour: its body was moved out of main.rs's exit_status_for, which now delegates to it, so the process-exit path gained no behaviour and was only made callable from the wire path beside it. It is counted because the roster counts hand-authored DECLARATIONS, and netting a relocation against an addition is the arithmetic gunbc.seed_growth_admission refuses.\n\nWHY THE CLASSIFIER AND THE OUTCOME ARE TWO ITEMS: the classification is a question about a value and the outcome is a decision about a process, and fusing them would have put the write-or-refuse decision inside a match arm in main.rs where no test could reach it. Splitting them is what made the five-case matrix authorable, including the two cases that carry the actual safety content -- an unprintable response REFUSES rather than printing nothing, and an exit_failure carrying status zero is refused on the wire path exactly as it already was on the process-exit path.",
owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId,
trigger: "Delete CliWireClass, classify_cli_wire and their tests when the interpreter publishes a modeled projection of a returned Value against a declared type -- an inhabitance decision the substrate answers, rather than a hand-written field-by-field match on the host Value representation. Delete CliWireOutcome, cli_wire_outcome, exit_status_for_class and their tests when std models a process's standard output and exit status as a carrier the .dag side can produce directly, so a command entry returns the whole host effect and the seed only executes it. Both triggers name a CAPABILITY the substrate does not have today; neither is discharged by any artifact short of it, and in particular neither is discharged by moving this Rust to another file.",
current_boundary: "gunbc.cli_wire_host_admission -> v1_compiler.cli_run cli_wire_outcome -> v1_compiler.main run_verb -> the host process stdout and exit status"
}
74 changes: 74 additions & 0 deletions dag/gunbc/scm/cli.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
module gunbc.scm.cli

// THE ONE PLACE A READ VERB BECOMES BYTES AN OPERATOR CAN SEE.
//
// `gunbc.scm.read_command` composes load-then-ask and names no destination; `gunbc.scm.render`
// turns that answer into a `CliWireResponse`. Both existed before this module and neither had a
// consumer outside a witness test, so every SCM read answer was computed and discarded --
// the unwired-renderer state `gunbc.cli_dispatch_surface` already records. This module is the
// entry the host actually invokes: `gunbc run --entry dag/gunbc/scm/cli.dag --function scm_log
// --arg path=<repo>`.
//
// WHY A `path` PARAMETER AND NOT A DISCOVERED ROOT. `load_repository` takes a path, and
// inventing a default here (cwd, an env var, a conventional `.gunbc` directory) would make this
// module decide a question its authority left open -- the same reason `scm_read_status` takes
// `pending` as a parameter rather than reading it from somewhere.
//
// THE CAPABILITY IS DECLARED, NOT DETECTED. `plain_terminal_capability` claims no colour, ASCII
// tier and no cursor addressing, which is the floor every terminal meets. Detecting the real
// terminal's capability is a host observation this module cannot make and must not fabricate;
// when a host learns to report one, it passes it in and this row becomes the fallback rather
// than the answer.
import std.render { RenderCapability }
import std.symbols { Ascii }
import extdeps.render.surface { RenderTarget, PlainTerminal }
import gunbc.cli_wire { CliWireResponse }
import gunbc.scm.authoring { empty_proposal }
import gunbc.scm.log { CommitLog }
import gunbc.scm.status { RepositoryStatus }
import gunbc.scm.read_command { ScmReadResult, scm_read_log, scm_read_status }
import gunbc.scm.render { scm_log_cli_response, scm_status_cli_response }

data plain_terminal_capability: RenderCapability = RenderCapability {
color: false,
tier: Ascii,
cursor_addressable: false
}

data plain_terminal_target: RenderTarget = PlainTerminal

// THE COMPOSITION IS SPLIT FROM THE READ so the composition is WITNESSABLE. `scm_log` reads a
// file, and the SCM witnesses are SubstrateInputsOnly -- they refuse at the hermetic boundary --
// so a claim can never call it. `scm_log_response` takes the read's RESULT and is pure, which
// makes this module's actual decisions (which target, which capability) reachable by a witness
// that constructs a result in substrate. Without the split, changing `plain_terminal_capability`
// to `color: true` would turn nothing red.
fn scm_log_response(result: ScmReadResult<CommitLog>) -> CliWireResponse {
scm_log_cli_response(
result: result,
target: plain_terminal_target,
capability: plain_terminal_capability
)
}

fn scm_log(path: String) -> CliWireResponse {
scm_log_response(result: scm_read_log(path: path))
}

// STATUS OF A REPOSITORY WITH NOTHING STAGED. `repository_status` needs a pending `Proposal`,
// and a CLI reading a repository document has no staging area to read one from -- the document
// does not carry what is staged. `empty_proposal()` is therefore the honest argument for this
// entry, and it is a REAL limitation rather than a placeholder: a verb that reports staged roles
// needs a staging authority to exist first, and none does.
fn scm_status_response(result: ScmReadResult<RepositoryStatus>) -> CliWireResponse {
scm_status_cli_response(
result: result,
target: plain_terminal_target,
capability: plain_terminal_capability
)
}

fn scm_status(path: String) -> CliWireResponse {
scm_status_response(result: scm_read_status(path: path, pending: empty_proposal()))
}

2 changes: 2 additions & 0 deletions dag/gunbc/seed_growth_admission.dag
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import gunbc.capability_origin {
origin_refusals
}
import gunbc.rust_item_identity { RustItemIdentity, rust_item_identity_key }
import gunbc.cli_wire_host_admission { cli_wire_host_seed_growth_justification }
import gunbc.seed_growth { SeedGrowthJustification }
import gunbc.stage0_rust_host_observation { stage0_rust_observation_seed_growth_justification }
import gunbc.floor_non_verdict_enrollment { floor_non_verdict_seed_growth_justification }
Expand Down Expand Up @@ -217,6 +218,7 @@ fn seed_growth_justification_roster() -> List<SeedGrowthJustification> {
generated_artifact_boundary_seed_growth_justification,
regen_convergence_seed_growth_justification,
decl_facts_shared_memo_seed_growth_justification,
cli_wire_host_seed_growth_justification,
required_lane_judgment_seed_growth_justification
]
}
Expand Down
Loading
Loading