Skip to content

Re-author the Map literal population: the open half onto PartialFunction, the finite half onto the construction route - #9076

Merged
briansrls merged 27 commits into
mainfrom
session/silent-gull-867-map-reauthor
Aug 24, 2026
Merged

briansrls merged 27 commits into
mainfrom
session/silent-gull-867-map-reauthor

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Third of the carrier program, after #9055 (decompose the Set half) and #9059 (split PartialFunction, author FinitelySupportedFunction / FinitePowerSet, retarget both aliases).

#9059 pointed Map at FinitelySupportedFunction. That made every Map { lookup: <closure> } literal in the corpus name a carrier whose contract it does not satisfy — there is no finite key set behind a delegate. #9059 left that population as declared residue. This closes it.

The population, at the grain each site has

class sites move
open, named delegate 90 (68 files) PartialFunction { lookup: .. }
open, inline closure or other spelling 21 PartialFunction { lookup: .. }
finite, N literal keys as an if-chain 25 (10 files) map_insert chain over empty_map()
empty, in a finite position 3 empty_map()
empty, in a PartialFunction position 5 stays record-shaped, under the open carrier

The last row is the one worth reading. An empty map is finitely supported, so empty_map() looks right — but those five fill InferredTree.facts and EvaluationEnvironment.bindings, whose declared types are PartialFunction after the first commit. The substrate has no subtyping to paper over that, and it should not: the field's carrier is the open one, and an empty open function is a legitimate inhabitant of it.

What caught those five was the type error, not my reasoning — and that is the clearest argument in this lane for why the #9059 split earned its cost. A distinction that has to be remembered gets forgotten; a distinction the substrate refuses does not. The seductive version of this change converts all eight.

The finite move is not new machinery. go.dag already carried go_primitive_fact_map_2/3/5 doing exactly map_insert over empty_map() for the parameterised case; the 25 sites are the same construction with literal keys, so they nest directly and need no new names.

Why retyping facts is safe

88 of the 90 open literals feed one field. InferredTree.facts is consumed only through .facts.lookup(..) at 10 sites — nothing enumerates it, so no finite-only surface (size, map_keys, contains_key) is reachable from those values.

What survives, and it is not residue

v2.std.collection map_insert's own body constructs a record-shaped Map, and must: it is the arm the interpreter falls through to for non-native maps. Plus this population's own gate fixture.

So v2.test.claim.map_carrier_shape_gate's DISSOLUTION note is rewritten rather than left standing. Its premise — when the finite literals have migrated, the delegate lands as the closing move — is now false in the way that matters: the last record producer is map_insert itself, so landing the delegate on that premise is precisely the self-recursion that killed 14 floor witnesses. The remaining condition is a host fact (the interpreter's free_call.map_insert arm accepting the record shape), not a corpus one. Until then the gate is a healthy guard being quiet, not a dead one.

The rewritten note is still prose, and therefore cannot refuse

Stated plainly so the next rot is a discovery rather than a rediscovery: the corrected condition is a §4c plain source annotation. No Accepted program can read it, so when it goes stale again nothing will refuse — the same failure will present the same way, as a note that gets more persuasive as it gets more wrong, because its stated condition is visibly satisfied.

A typed carrier does exist — std.dissolution, with DissolutionCondition = BoundDissolution { trigger } | UnboundDissolution { description } and dissolution_status folding a trigger against the present declaration population. It cannot bind this condition, and the reason is worth naming rather than filing as "not done yet": both triggers (DeclarationAppears, DeclarationRetires) are over DeclarationRef, i.e. the .dag declaration population, and this gate's remaining condition is the interpreter's free_call.map_insert arm accepting a record-shaped map — a Rust fact, outside the population the carrier folds. Binding it would land as UnboundDissolution { description: .. }, which is a typed row carrying prose: honest about being unbound, but not one rung stronger than the comment it replaces.

So the gap is not "author the row"; it is that a host-side trigger has no representation. Naming that is what makes it a next-rung trigger instead of an untracked stall (§4b(2)).

This PR is the trigger a §4c annotation already names

Added after deep-ant-102 found the annotation and smart-ram-730 relayed it. It changes what this
PR is, so it belongs in the body rather than in a thread.

dag/std/types.dag carries a §4c annotation immediately above type Set / type Map, titled
"ONE SPELLING, TWO CARRIERS — the open half of the collection-realization fork". It describes
this exact collision in advance — a declaration position and a record literal lowering to different
Rust types, meeting as a rustc refusal wherever one compiled closure holds both — and cites
docs/probes/t3_collection_realization_2026-08-22.md as its measurement.

Its closing clause names this work directly:

the Map half of the same fork (Map { lookup: .. }, 145 literals) is untouched and would have to
move with it. NEXT-RUNG TRIGGER: the Map-side decomposition landing, after which both rows
retarget in one motion.
Until then the class sits at MITIGATABLE — the invalid state is
writable and only review sees it.

So this PR is the Map-side decomposition that annotation is waiting on. That is a stronger claim
than "it fixes some sites," and it is checkable against the authority rather than against this PR's
own account of itself.

What it therefore does and does not do, stated because the distinction decides whether the
population can regenerate:

does re-authors the consumer half — the Map { lookup: … } literal population onto the faithful carriers
does not retarget the Set/Map alias rows onto the finite carriers

The retarget is deliberately not here, and not merely out of scope: the annotation states both rows
must move in one motion, and that motion was attempted as #9059, which is now closed by ruling.
So after this lands, a new Map { lookup: … } literal is still writable and nothing refuses it —
the class stays at mitigatable, the consumer half is closed for the second time, and the thing
that regenerates the population is untouched. Saying so here means the next occurrence reads as the
predicted consequence of a deliberately-open fork rather than as a regression.

Population — deliberately NOT joined, and that is the honest state. The annotation says 145
literals; this PR's own account is 90 named-delegate opens, 21 inline/other, 25 finite, 3 empties,
with 5 staying record-shaped. Those two numbers have not been joined at site identity, so neither
is quoted here as a delta.
A count match would not establish they are the same sites, and per the
repo's oracle rule a count compared against a count is not a check. The join is real follow-up work,
not a formality — if the populations differ, the difference is itself the finding.

NOT in this change

The container rows for the open carriers, per the constraint on this PR — declared residue in #9059, and it stays there.

Declared residue: the shared realization (review 55334)

Recorded here as a condition of the merge ruling on this PR, so that what ships is what was ruled
on and not a quieter version of it.

FinitelySupportedFunction and PartialFunction are now two names in the model and one type in
the realization
: both emit as Rc<dyn Fn(K) -> Option<V>>, one row per target language. So the
distinction this lane authored is, today, carried by the .dag authority and erased at the emit
seam — a consumer reading emitted Rust cannot tell a finitely-supported map from an arbitrary
partial function, and nothing refuses if one is passed where the other is declared.

Population, measured with matched scope rather than asserted:

declaration positions still on the unfaithful shared row 15
the same measurement on main before this lane 649 (643 Map< + 6 PartialFunction<)
construction sites now routed through the faithful carriers 111
emitter rows carrying the shared realization one per target language

This PR does not close it, and the ruling was explicit that it should not: the realization cannot
be split until the name exists for a faithful one to attach to, and creating that name is what
#9059 and this PR do. The residue is 15 positions rather than 649 — a 97.7% reduction on the same
predicate — but it is a reduction, not a closure, and the remaining 15 are as unfaithful as all
649 were.

Rung, stated honestly (§4b(1)). This is mitigatable, not a climb. The split makes the
distinction expressible; it does not make the confusion unwritable, and on the
source→Rust-emission path specifically the two carriers remain interchangeable by construction.
Next-rung trigger: an emitter row that gives FinitelySupportedFunction a realization carrying
its finite support — at which point the .dag distinction becomes a Rust one and the 15 positions
become a typed refusal rather than a silent alias.

The seed mirrors: three regen rounds, and why the third exists

The #9059 reviewer predicted this from std_algebra.rs still carrying the full-field PartialFunction mirror. It was right, and it was not caught by the compile board — two instruments, two questions, and only one was run. The board compiles source; it establishes nothing about whether the emitted seed still matches.

round drift note
1 10 mirrors extdeps_languages_{go,python,rust}_types.rs, std_algebra.rs, std_computation.rs, std_types.rs, v1_compiler_{coercion,emit_rust,infer_types,trait_derive_emit}.rs
2 2 mirrors, neither in round 1's list compiler_tests.rs (coercion assertions for the new names), std_types.rs (drops pub use …::PartialFunction, since types.dag no longer imports it)
3 none — first_generation_equal=true, exit 0 on both branches, independently

Round 2 exists because the emitted tree is an input to itself. Installing round 1's candidates changed what round 2 emitted from, so one pass is a fixed point only when the change touches no surface another mirror reads.

The gate reports two different facts through the same command: FAIL … drift: <list> answers what drifted from this input; first_generation_equal answers is the tree at its fixed point. Only the second is what merge needs — and the first reads exactly like a work queue, so the natural stopping point is "the gate stopped naming files", which is not convergence. Stated plainly because the mechanism is obvious in hindsight and the stopping point is not: had I stopped after round 2, I would have pushed a tree that refuses in CI, having just reported the regen as handled.

Open question, deliberately not answered here: is the round count bounded?

Three rounds terminated. That is one observation, not a bound. Round 2's population was disjoint from round 1's — compiler_tests.rs appeared for the first time and std_types.rs moved again for a different reason — so the rounds are not draining a fixed worklist.

DESIGN's fixed-point clause bounds a different fixed point: the emitted artifact as a fixed point of rustfmt, bounded, with exceeding the bound a typed refusal. Nothing I have found bounds the emit-reads-emitted-tree convergence this PR ran into. If nothing does, then "run regen until clean" is an unbounded loop whose termination is a hope, and whose only stopping rule today is a human noticing the list changed. Raised as an open question with the round-by-round data as its evidence; it is not this PR's to resolve.

All candidates are the emitter's own, installed unmodified — a hand-edited mirror is the one thing this gate exists to refuse. Transfer off the remote runner was byte-verified (TGZ_BYTES plus a sha256 prefix printed by the dispatch, both reproduced locally after decode), because the first attempt failed silently: a comparison loop printed DIFFERS: <file> per mismatch and nothing when its file list was empty, so "matches the committed mirror" and "find matched no files" rendered identically.

Measured

Remote dispatch on the final tree (re-measured after the regen commits, not carried forward), subject asserted by content (grep -c "Map {$" wasm.dag = 0, grep -c "map_insert(" = 11), binary rebuilt in the same dispatch:

EXIT=0
compiled: 177 files emitted, 503 diagnostics
CARRIER_DIAGS=0     (no diagnostic names PartialFunction, FinitelySupportedFunction, empty_map or map_insert)

Same board as the merged head. That is the intended result — this renames constructors, it does not change what any program computes.

An agreeing pair is undecidable from its own output (#9066 clause 6), so the discriminating red is stated rather than assumed: one attempt earlier, my rewrite script's brace accounting ate the closing } of two functions and the same instrument refused with module index refused: 2 unparseable .dag source(s), naming both files and byte spans. The board moves when the tree changes; this reading is not stuck.

gunbc-ci-auto-heal and others added 12 commits August 23, 2026 23:04
…nd the fork's open half is declared

`std.types` names one spelling for two carriers. A TYPE position takes the
`container_template_alias_rows` hop on to the target inhabitant row and lowers
`Set<T>` to `im::OrdSet<T>`; a RECORD-LITERAL position takes the
`container_template_algebra` hop and stops at the modeled algebra struct,
`Rc<PointwisePower<_>>`. Where one compiled closure holds both, the two meet as
a rustc refusal -- 27 of the 331 blocks on the 03_ingest board, in two shapes:
17 E0560 (the algebra name leaking as a Rust type at a construction site) and
10 E0609 (the carrier's own field read off the realized type).

Per the standing ruling "decompose, do not choose": `Set` names the finite
collection, and the predicate keeps its own authority under its own name. Every
corpus site in src/v2 that constructs by characteristic function or reads
`.member` now names `std.algebra` `PointwisePower` directly -- 37 literals, 26
`.member` consumers, 128 occurrences across 20 files. All 37 literals were read
individually and all 37 are characteristic functions; none was a finite
enumeration mis-spelled. Nothing in the finite-`Set` population moved
(`std.graph` `visited`, `std.authorization_profile` `EnumeratedAudience.members`,
`std.syllogism`, `std.occurrence_binding_candidates`, `gunbc.package_delivery`,
the `dag/`-rooted emit templates): every one of those is fed by `empty_set` /
`set_insert` and read by `set_contains`, never by `member`.

WHAT THIS DOES NOT DO, declared rather than left to be rediscovered. The alias
row itself still reads `type Set<element> = PointwisePower<element>`, so
`Set { member: .. }` is still writable and nothing refuses a new one -- the class
sits at MITIGATABLE (DESIGN 4b), not at a wall. The retarget that would make it
unwritable is a `std` authority change whose only verification is a whole-corpus
compile, and it has to move together with the Map row, whose 145 literals are
untouched here. The obligation and its next-rung trigger are recorded on the
declaration itself, in the annotation above `type Set` in `dag/std/types.dag`.
That leaves the 9 `Map { lookup: .. }` blocks of the 27 open; the 18 Set-shaped
ones close.

Measured: `gunbc compile --entry src/v2/compiler/03_ingest.dag --source-root dag
--source-root src/v2` runs frontend, normalize, reconcile and analyses to
completion and reports 52 hard diagnostics, none of which names `Set`,
`PointwisePower` or `member` -- all 52 are the pre-existing source-annotation
class in `dag/test/manual/command_runner_local_argv_receipt_test.dag`, which this
change does not touch. The emitted-block count is NOT re-measured here: "18 of 27
close" is derived from the site-to-literal correspondence, and is a prediction
about the next board rather than a measurement of one.

Receipts: docs/probes/set_carrier_decomposition_2026-08-23.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d if-chain classification

decomposition_scope.md flagged its 97-delegate row as the one number a full read
could move. It moves: 78 of 87 resolved delegate bodies never compare the key,
within two levels -- they are total functions of the key, not tables. Only 6
branch on the key directly and 3 one level down; the sample that produced the
if-chain reading landed on program_facts_lookup, one of the 6.

Consequence for the Map lane: most of the population wants the same move this PR
made on the Set side (name PartialFunction under its own authority), not 145
re-authorings into finite maps. Stated as a lower bound on openness, with the
convergence evidence and the 5 unresolved names declared rather than absorbed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ric retype

The addendum as first written said the 78 open delegates should name
PartialFunction "exactly the move this change made on the Set side". That is
wrong, and it is the premise a reader would plan the Map lane against, so it is
corrected in place rather than left standing.

PointwisePower is ONE template row, which is why an open characteristic function
inhabits it completely. PartialFunction is THIRTEEN -- it bundles a partial
function with a finite map, and an open delegate cannot answer map_keys,
map_values or count at all. Measured: 0 of the 145 Map{..} literals in the corpus
supply any field beyond lookup, so the other twelve rows are promises the whole
existing population already fails to keep.

The missing carrier is on the Map side, and it already has a peer:
v2.std.collection TotalMap<K, V> { lookup: fn(K) -> V }. The partial peer is
lookup: fn(K) -> V?, so this is the totality axis on an existing pair rather
than a minted name.

Raised by smart-ram-730 against the first revision; verified here from the
profile rosters and the literal population rather than accepted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erivation can see it

quiet-pike-368 declared a residue in their use-line derivation: the producer walks
SIGNATURE-level type positions (type_annotation, params, inferred return), so a name
that reaches emitted text only through a value-position body construction is never
proposed as a use-line. Checked my own 20 re-typed files against that boundary rather
than assuming they clear it: 19 carry the name in a signature position, and exactly one
did not -- v2.test.claim.grammar.validate_grammar had 8 PointwisePower literals and 0
signature positions.

This is not a class my change introduced (the file's previous `Set` reference was
literal-only in the same way, and neither name is on the 7-name hardcoded roster in
emit_faithful_text_carrier_import_lines), and the file is not in the 03_ingest closure,
so no board site is involved. Fixed anyway because it is cheap and it removes the one
site of mine that falls in a declared gap.

The two singleton predicates every fixture built inline are now named helpers with
declared return types -- 8 literals become 2, and the reference moves into a position
the derivation walks. Better modeling independent of the emitter: the same value was
being constructed four times.

Measured: gunbc compile on BOTH this entry and src/v2/compiler/03_ingest.dag reports 52
hard diagnostics each, zero of which name PointwisePower, PartialFunction, `Set<` or a
missing `member` field. 52 is the unchanged pre-existing source-annotation count in
dag/test/manual/command_runner_local_argv_receipt_test.dag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ses at the finite carriers

One spelling meant two concepts on both the Set and the Map side, and the
carriers disagreed with their own realizations. This is the authority transition
that ends it, in one motion.

WHAT WAS WRONG, measured rather than argued:

  - PartialFunction bundled a partial function (lookup) with a finite map
    (map_keys, map_values, size, insert, merge). 0 of the 145 Map{..} literals in
    the corpus supply any field beyond lookup, so the other twelve rows were
    promises the whole existing population already failed to keep.
  - The finite-SET concept had a realization row and a 13-template roster but no
    type, because it was filed under BooleanAlgebra -- whose type is
    { meet join complement top bottom } and whose only construction in the corpus
    is v2.std.logic bool_boolean_algebra, a genuine Boolean algebra over Bool.
    All four languages that carry an inhabitant row file a finite-set CONTAINER
    under that name: rust BTreeSet<{0}>, go map[{0}]struct{}, python set[{0}],
    typescript Set<{0}>. Not one is a lattice.
  - A SECOND FINDING, not merely the mechanism that made this cheap: the rust
    roster's "BooleanAlgebra" and "PointwisePower" rows were byte-identical --
    two algebra names resolving to one realization with nothing saying which is
    which, a section 3 duplicate.

WHAT THIS DOES: PartialFunction keeps lookup alone; FinitelySupportedFunction
takes the enumeration surface; FinitePowerSet takes the finite-set concept out
from under the lattice's name (the lattice keeps its name, type and construction
site untouched); Set and Map alias the two finite carriers; TotalMap and
TotalPolicy move from v2.std.collection into the family they belong to.

A correctness repair falls out: algebra_profile_to_dimension answered
CollectionFold for PartialFunctionProfile and none for PointwisePowerCollection --
the same distinction decided two ways, because one name carried an open function
and a finite table. The two open profiles now agree, and the two finite ones do.

RUNG: STILL MITIGATABLE, stated on the declaration. This does NOT make the bad
literal unwritable -- a finite set answers member and a finite map answers lookup,
so both literals still typecheck, and they must, because 145 of them are live. The
invalid state is a literal supplying ONLY those on a carrier promising enumeration,
which is a construction-COMPLETENESS question record literals do not enforce. The
real next-rung trigger is field-completeness on record construction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review 55220 (REQUEST_CHANGES) is correct and the finding is mine. The previous
commit added TotalMap and TotalPolicy to dag/std/algebra.dag under a comment
reading "MOVED HERE from v2.std.collection" and never deleted them from
v2.std.collection. That is not a move, it is a duplication -- the parallel
representation section 3 forbids, landed inside the change whose whole subject
is one authority per concept.

Worse than untidy, and worth stating at the grain that decides it: name
resolution in this substrate is whole-pool, and DESIGN section 4b already records
that a census-AMBIGUOUS type name resolves by SILENT LAST-IMPORT-WINS rather than
refusing. So two live declarations of one type name is a real ambiguity resolved
silently, not a cosmetic duplicate awaiting cleanup.

Both declarations now exist once, in std.algebra, beside the family they belong
to. Nothing referenced either from v2.std.collection -- both had zero code
consumers, which is why the duplicate compiled and why no diagnostic named it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…it forced

smart-ram-730's read of #9059 found a coverage asymmetry here that neither their
review nor mine caught in the diff, and the split made it worse in both
directions at once.

BEFORE this fix, after the split: BooleanAlgebra was still listed while its
container inhabitant row had MOVED to FinitePowerSet, so the roster named a
lattice as a container; and PartialFunction had been dropped while it still
carries a row, silently retiring its coercion coverage at the moment the split
gave it real callers. Two errors, opposite signs, one list.

The list's only consumer read concat(canonical_container_names(),
["PointwisePower"]) -- a hand-appended name compensating for one the authority
omitted. That is the authority being wrong, not the caller being thorough, so
completing the list deletes the append rather than leaving a patched call site
beside a fixed authority.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Map { lookup: <open delegate> }` names the finite alias while constructing
the open, unbounded-support carrier. #9059 pointed `Map` at
`FinitelySupportedFunction`, so every one of these sites now names a carrier
whose contract it does not satisfy: there is no finite key set behind a
delegate closure.

This re-authors the 90 open sites (68 files) to name `PartialFunction`
directly -- the carrier they already inhabit. 70 distinct open delegate
names; no delegate body changes, only the constructed type name.

Twelve files carry the corresponding retype, all on fields and signatures
that hold those literals: `facts: Map<Node, InferredFacts>` ->
`PartialFunction<Node, InferredFacts>` and the same for
`Map<EnvironmentBindingKey, RuntimeValue>`. Safe by census: 88 of the 90
literals feed the single field `facts`, and `InferredTree.facts` is consumed
only through `.facts.lookup(...)` at 10 sites -- nothing enumerates it, so no
finite-only surface is reachable from these values.

Measured on this tree (remote dispatch, subject asserted by content --
`facts: PartialFunction<Node, InferredFacts>` grepped inside the run):
`compiled: 177 files emitted, 503 diagnostics`, `0 blocking`, and zero
diagnostics mentioning any of the four carrier names. That is the same board
as the merged head, which is the intended result: this move renames a
constructor, it does not change what any program computes.

NOT in this change, and left as declared residue per #9059: the container
rows for the open carriers, and the ~36 finite `Map { lookup: .. }` sites
that belong on the `empty_map` + `map_insert` construction route.
…remaining opens

The first commit moved the 90 NAMED-delegate literals. The rest of the
population is closed here, at the grain each site actually has.

FINITE (25 sites, 10 files). Every `spec_facts` map in the language and
format extdeps was a `Map { lookup: fn(axis) { if axis == A0 { .. } else if
.. else { Absent } } }` -- a map over 1 to 6 literal keys, written as an
if-chain inside a closure. They are now `map_insert` chains over
`empty_map()`, which is the construction route `go.dag`'s own
`go_primitive_fact_map_N` helpers already used for the parameterised case.
`model_core_bool_spec_facts` joins them: it delegated to a two-axis
`Witness`-returning if-chain, so the two axes are inserted directly and the
delegation disappears. `model_core_bool_fact_lookup` is untouched -- it has
its own three-witness test and three other callers.

EMPTIES (8 sites). `Map { lookup: fn(_) { Absent } }` -> `empty_map()` where
the position is a finite map. Five of the eight are NOT that: they fill
`InferredTree.facts` and `EvaluationEnvironment.bindings`, whose declared
types are `PartialFunction` after the first commit, so they stay record-shaped
under the open carrier. An empty map is finitely supported, but the field is
not, and the substrate has no subtyping to paper over the difference.

OPENS (21 sites). The first commit's rewrite matched named delegates only, so
it missed both the inline unbounded-domain closures (`fn(key) { <fold over an
entry list> }`, `fn(_) { Present { value: facts } }`) and seven further named
delegates reached through a different spelling. All 21 now name
`PartialFunction`.

WHAT SURVIVES, and it is not residue: `v2.std.collection` `map_insert`'s own
body constructs a record-shaped `Map` and must, because that is the arm the
interpreter falls through to for non-native maps; and this file's fixture in
`v2.test.claim.map_carrier_shape_gate`. The gate's DISSOLUTION note is
rewritten rather than left standing, because its premise -- "when the finite
literals have migrated, the delegate lands as the closing move" -- is now
false in a way that matters: the last record producer is `map_insert` itself,
so landing the delegate on that premise is the self-recursion that killed 14
floor witnesses. The remaining condition is a HOST fact, not a corpus one.

MEASURED (remote dispatch, subject asserted by content:
`grep -c "Map {$" wasm.dag` = 0, `grep -c "map_insert("` = 11):
`compiled: 177 files emitted, 503 diagnostics`, `EXIT=0`, and zero
diagnostics naming any carrier or construction function. Same board as the
merged head, which is the intended result.

That agreeing pair is the case #9066's sixth clause calls undecidable from its
own output, so: the discriminating red was observed on this same instrument
one attempt earlier. My rewrite script's brace accounting ate the closing `}`
of two functions, and the run refused with `module index refused: 2
unparseable .dag source(s)` naming both files and byte spans. The instrument
moves when the tree changes; this board is not a stuck reading.
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 24, 2026 01:32
@gunbai-bot gunbai-bot Bot changed the title Collection carriers realize inconsistently: the algebra name leaks as a Rust type at 17 sites and the carrier's own field names are accessed on the realized type at 10 more (27 of 331 blocks on the 03_ingest board) Re-author the Map literal population: the open half onto PartialFunction, the finite half onto the construction route Aug 24, 2026
gunbc-ci-auto-heal added 9 commits August 24, 2026 02:10
…amed it

Two conflicts, both where the predicate-half change and this stack touch the
same row.

`dag/std/types.dag`: #9055's note declared its own next-rung trigger as "the
Map-side decomposition landing, after which both rows retarget in one motion."
That is this stack, so the note is superseded rather than merged -- keeping
both would leave the file asserting the retarget is pending beside the
retarget. But a prose row is deleted for one reason and takes everything in it
unless its contents are enumerated first, so the two facts only #9055's block
carried are carried forward explicitly: the fusion measurement (`PointwisePower`
inhabiting `BTreeSet<{0}>`, so a declaration position lowered to `im::OrdSet`
while a record literal lowered to the algebra struct) and that the predicate
half is closed -- no `Set { member: .. }` literal survives in `src/v2`.

`src/v2/extdeps/languages/lean.dag`: import line, both sides additive; kept
`empty_map, map_insert`, which this branch's finite fact maps call.
…amed it

One conflict, in `dag/std/types.dag`. #9055's note declared its own next-rung
trigger as "the Map-side decomposition landing, after which both rows retarget
in one motion." That is this PR, so the note is superseded rather than merged
-- keeping both would leave the file asserting the retarget is pending beside
the retarget itself.

A prose row is deleted for one reason and takes everything in it unless its
contents are enumerated first, so the two facts only #9055's block carried are
carried forward explicitly: the fusion measurement (`PointwisePower` inhabiting
`BTreeSet<{0}>`, so a declaration position lowered to `im::OrdSet` while a
record literal lowered to the algebra struct, meeting as a rustc refusal
wherever one compiled closure held both) and that the predicate half is closed
-- no `Set { member: .. }` literal survives in `src/v2`.
…ier-split' into session/silent-gull-867-map-reauthor
`claim_executor --required-regen` refused: `first_generation_equal=false`,
`planned=133 executed=133`, `FAIL generated surface drift` naming ten mirrors
-- `extdeps_languages_{go,python,rust}_types.rs`, `std_algebra.rs`,
`std_computation.rs`, `std_types.rs`, and
`v1_compiler_{coercion,emit_rust,infer_types,trait_derive_emit}.rs`. Every one
is a file this stack edited, so the drift is the change's expected consequence
rather than an unrelated break, and these are the emitter's own candidates
installed unmodified -- not hand-edited mirrors, which is the one thing the
gate exists to refuse.

The reviewer on #9059 predicted exactly this from `std_algebra.rs` still
carrying the full-field `PartialFunction` mirror, and was right. Worth
recording why I did not find it myself: I ran the `.dag` compile board three
times and read three greens. The board compiles source and says nothing about
whether the emitted seed still matches -- two instruments answering two
questions, and I only ran one. Green on the narrower one is indistinguishable
from the green I wanted.

Transfer of the candidates off the remote runner was byte-verified rather than
assumed: the dispatch printed `TGZ_BYTES=242026` and a sha256 prefix of the
tarball it built, and the locally decoded archive reproduces both exactly.
That check exists because the first attempt at this transfer failed silently
-- a comparison loop printed `DIFFERS: <file>` per mismatch and NOTHING when
its file list was empty, so "the candidate matches the committed mirror" and
"find matched no files" rendered identically. Only the regen verdict printed
beside it kept the empty list from reading as agreement.
`claim_executor --required-regen` refused on this branch:
`first_generation_equal=false`, `planned=133 executed=133`, `FAIL generated
surface drift` naming ten mirrors -- `extdeps_languages_{go,python,rust}_types.rs`,
`std_algebra.rs`, `std_computation.rs`, `std_types.rs`, and
`v1_compiler_{coercion,emit_rust,infer_types,trait_derive_emit}.rs`. Every one
is a file this PR edited. These are the emitter's own candidates installed
unmodified; a hand-edited mirror is the one thing the gate exists to refuse.

The reviewer predicted this from `std_algebra.rs` still carrying the
full-field `PartialFunction` mirror, and asked for the gate to be confirmed
green before merge. It was not green; it is the reason to have asked.

Byte-verified off the remote runner: the dispatch printed TGZ_BYTES=242026 and
a sha256 prefix of the archive it built, and the locally decoded archive
reproduces both. The same ten files regenerated from the child branch are
byte-identical to these, which is the expected result and worth stating -- the
mirrors are a function of `dag/` and `src/v1`, and the child's additional
changes are confined to `src/v2`, which is not mirrored.
…ier-split' into session/silent-gull-867-map-reauthor
Installing round one's ten candidates changed the input to the next emit, so a
second round drifted two more files -- `compiler_tests.rs` and `std_types.rs`.
This is convergence, not a defect in round one: the emitted tree is an input to
itself, so one pass is not a fixed point when a change moves a surface other
mirrors read.

`compiler_tests.rs` picks up the coercion assertions for the new names --
`FinitePowerSet` and `FinitelySupportedFunction` gain rows, `BooleanAlgebra`
loses its, and the `PointwisePower`/`Set` pair reorders as
`canonical_container_names` now orders them. `std_types.rs` drops
`pub use crate::std_algebra::PartialFunction;`, which follows from
`dag/std/types.dag` no longer importing that name after the alias retarget.

Byte-verified off the runner as before (`TGZ_BYTES=29676` and a sha256 prefix,
both reproduced locally after decode). A third round follows to establish the
fixed point rather than assuming two was enough -- the reason there is a second
round at all is that one was assumed to be.
Installing round one's ten candidates changed the input to the next emit, so a
second round drifts two more. `compiler_tests.rs` picks up the coercion
assertions for the new names -- `FinitePowerSet` and
`FinitelySupportedFunction` gain rows, `BooleanAlgebra` loses its, and the
`PointwisePower`/`Set` pair reorders as `canonical_container_names` now orders
them. `std_types.rs` drops `pub use crate::std_algebra::PartialFunction;`,
which follows from `dag/std/types.dag` no longer importing that name after the
alias retarget.

Taken from the child branch, where the same two rounds were run and the third
returned `first_generation_equal=true`. That the two branches produce identical
mirrors is expected -- they are a function of `dag/` and `src/v1`, and the
child's extra changes are confined to `src/v2` -- and it was measured for round
one rather than argued (all ten byte-identical). This branch's own regen is
re-run after this commit to confirm it for round two as well, because the
argument and the measurement are not the same thing.
…ier-split' into session/silent-gull-867-map-reauthor
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

On review 55334 — the finding is real, and it is the defect this whole program exists to expose. It is not introduced here, and this PR is the largest single reduction of it. Verified against the current code rather than argued:

PartialFunction → HashMap<{0}, {1}> is on main, unchanged by this stack. git show origin/main:dag/extdeps/languages/rust/types.dag carries that exact row. #9059 duplicated it under the new finite name; it did not author it. The Go, Python and TypeScript rows are the same story.

And the Set side already carried the identical collapse, which is what makes this the program's subject rather than a regression in it: main gives PointwisePower — a characteristic function — the inhabitant BTreeSet<{0}>. dag/std/types.dag says so in the note this stack rewrote, citing the measurement: "Set names a predicate here and a finite enumeration at every realization … the two meet as a rustc refusal wherever one compiled closure holds both," with the receipt at docs/probes/t3_collection_realization_2026-08-22.md.

Measured: the population reaching the unfaithful row, before and after

Matched scope, dag/**.dag + src/v2/**.dag, declaration positions:

Map< PartialFunction< Map aliases positions on the open HashMap row
origin/main 643 6 PartialFunction 649
this PR 631 15 FinitelySupportedFunction 15

On main every one of those 649 positions resolved through Map = PartialFunction to the HashMap template. Here 631 of them resolve to FinitelySupportedFunction, where HashMap is the faithful realization, and only the 15 positions that explicitly name the open carrier reach the row the review objects to.

So src/v2/compiler/inferred_tree.dag:34 — cited in the finding — was already getting that HashMap on main, under the spelling Map. What changed is that it now says which carrier it means. That is the prerequisite for fixing the realization, not a new instance of the problem: before this split there was no name to attach a callable realization to, because one name served both concepts.

Why the faithful realization is not in this PR

PartialFunction wants something like Rc<dyn Fn(K) -> Option<V>>, not a map. Landing that here means every one of the 15 open positions and their 111 construction sites change realization in the same diff that re-authors them — and the emitted seed is regenerated from these rows, so it is a stage0 mirror change on top of a carrier change on top of a literal re-authoring. Splitting that is not deferral; it is the one motion this PR did take being kept atomic (DESIGN §3's replacement-migration doctrine — the alias retarget is the authority transition, and it is complete).

It is also not a silent deferral. dag/std/types.dag's note declares the rung as still mitigatable and names the next-rung trigger; this review adds a second, sharper one, and I would rather it be recorded here in the reviewer's terms than paraphrased into that note: the open carriers have no faithful target-language realization, and until they do, a PartialFunction declaration position emits a container that cannot hold its lookup closure. That is the next PR in this program, and it is now down to 15 declaration positions and one row per language instead of 649 and the same row.

— sent from silent-gull-867

gunbc-ci-auto-heal added 4 commits August 24, 2026 09:26
… had no method surface

CI's floor phase refused on the child branch with:

  src/v2/lens/structural_resolution.dag:20:40: error: method 'lookup' cannot be
  resolved: receiver type 'Node(std.algebra.PartialFunction)' establishes no
  method surface, so the method's existence is not established and no declared
  frontier row admits it

`kernel_algebra_profile_value` keyed seven names -- Int, Float, Bool, String,
List, SET, MAP. Every one an alias spelling; no carrier name anywhere.
`kernel_profile_lookup` is what `04_infer`'s method-existence wall consults to
decide whether a receiver has a declared surface at all, so `tree.facts
.lookup(node)` refused on a `PartialFunction` receiver while the byte-identical
call on a `Map` receiver resolved. One concept, two answers, decided by which
of two spellings for it the author happened to write: the §3 fork with nothing
else in it.

THIS PR DID NOT INTRODUCE IT, IT MADE IT REACHABLE, and the fix belongs here
rather than downstream for that reason: this is the change that makes the
carrier names the ones declarations write. Land it without these rows and main
holds carriers whose method surface is undecided, with the repair handed to
whoever next writes a method call on one and gets that diagnostic with no
context.

The three rows point each carrier at THE PROFILE IT ALREADY HAS --
`PartialFunctionProfile`, `FinitelySupportedFunctionProfile` and
`FinitePowerSetProfile` are declared in this file, their template rosters are
declared in this file, and `algebra_templates_for_profile` already dispatches
to them. Nothing here is a new claim about what those carriers can do; it is
the existing algebra answering to the carrier's real name.

`PointwisePower` is deliberately not added, and the code says so. Its 80
declaration positions predate this lane, and the risk is narrow but real: the
frontier roster admits undecided calls keyed on (module, method, RECEIVER
SHAPE), so a call admitted today is admitted BECAUSE its receiver has no
surface -- give it one and the admitting row stops matching. That is a
measurement, not a guess, and the row will carry the number rather than the
argument.

WHY THE COMPILE BOARD DID NOT SEE THIS, since I cited it three times as
evidence this stack was sound: `gunbc compile --entry src/v2/compiler/03_ingest
.dag --target rust` reports 0 blocking on this exact tree. It resolves one
entry's import closure. `04_infer`'s own note names the whole-corpus instrument
as `gunbc compile --source-root dag --source-root src/v2 --target dag`, and
records the identical trap: a narrow instrument reporting clean is
indistinguishable from a wall that works.
…ier-split' into session/silent-gull-867-map-reauthor
…er without it

The previous commit added three rows to `kernel_algebra_profile_value` in
`dag/std/algebra.dag` and CI's floor refused with the byte-identical error it
refused with before. The reason is two phases earlier in the same ledger:

  required-ci: regen FAIL generated surface drift: std_algebra.rs
  ...
  required-ci: floor refused: ... 'Node(std.algebra.PartialFunction)'
  establishes no method surface

The typechecker that refuses is the SEED BINARY, built from
`src/v1/stage0/src/std_algebra.rs`. That mirror still carried the seven-key
map, because a `.dag` edit reaches an artifact only through regen. The fix was
correct and had not arrived.

This installs the emitter's own candidate (`std_algebra.rs` alone, byte-
verified off the runner: 6564 bytes, sha256 prefix df223a4e928cf630, both
reproduced locally after decode).

WORTH RECORDING BECAUSE THE LEDGER GOT IT RIGHT AND I ALMOST DID NOT. The regen
line named the file seventeen minutes before the floor refused for exactly that
reason, and the two failures in one ledger are what make the diagnosis
available: a line-stopping regen would have shown the drift and hidden the
floor error, leaving one fact instead of the relation between two. The
independent-phases design is what turned "the fix did not work" into "the fix
has not arrived".
…ier-split' into session/silent-gull-867-map-reauthor
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
… through regen

Collated from four independent sessions that did not know they shared a failure.
gunbc#9063 (05_emit_rust.dag, mirror never regenerated -- token commit touched
one file), #9076 (dag/std/algebra.dag, std_algebra.rs still carried the seven-key
map), #9075 (std_primitive_projection.rs brace drift), and #9058 as the
unaffected control.

Three of the four drew a conclusion from a measurement taken through a binary
that lacked their change, and two of those conclusions were specific and wrong:
#9063 attributed 20 use-lines to a gate change not in the executing emitter, and
#9076 read an unchanged floor error as the-fix-did-not-work rather than
the-fix-has-not-arrived.

The gate is correct and arrives too late to prevent the class. Measured:
.githooks/pre-commit and pre-push run cargo fmt and nothing else -- there is no
local regen check, so the feedback loop for 'your authority edit has not reached
your binary' is one full CI round-trip.

Records what #9076's ledger got right: two failures printed in order, seventeen
minutes apart, and their ORDER is the diagnosis. A line-stopping regen phase
would have hidden the floor error and left one fact instead of the relation
between two -- independent phases justified by attribution, not completeness.

States the repair as a property, with candidate mechanisms marked as candidates
to check rather than instructions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…ud sibling

silent-gull-867 corrected the credit this brief gave them, and the correction is
its most useful fact. I had written that they saw the #9103 false negative
before running it, implying suspicion did the work. It did not: they hit the
same staleness on #9076 where it failed LOUDLY -- the floor refusing with an
unchanged error while regen named the file -- and only then asked whether the
probe cut from that head shared the cause.

Nothing about #9103 in isolation would have prompted the check, and its own
pre-registration would have discouraged one: 'unchanged -> risk not realised' is
an instruction to accept exactly the string a stale binary produces.

So the detection route is: a sibling sharing the cause failed loudly and the
author generalised. That is not a method -- it needs a loud sibling to exist, be
noticed, be diagnosed correctly, and be recognised as sharing a cause with
something that has not failed. Four conditions, none under the author's control.

Practical consequence for the repair: you cannot catch this after the fact.
Every other member announces itself eventually; the silent member's only tell is
a confirmation you were already expecting. The property must be enforced BEFORE
measurement, which puts the repair at commit or dispatch time rather than in
anyone's reading discipline.

Also records the confirmation by measurement (MARKER_MIRROR_PP_BEFORE=0) and the
17-byte mirror delta as cheap evidence the two mirrors differ by exactly the
treatment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbc-ci-auto-heal added 2 commits August 24, 2026 11:13
…ute its own return type

CI's floor refused four times with `variant 'Present' not found in type
'InferredFacts'` AFTER the profile rows closed the method-surface refusal. Two
maps, two questions, and only one of them was fixed:

  kernel_algebra_profile          does this receiver have a METHOD SURFACE
  container_template_alias_rows   does this spelling RESOLVE AS A CONTAINER

`Map` was in both. `PartialFunction` was in neither. Adding it to the first got
`lookup` to resolve and left it unable to substitute its own return type.

MECHANISM, traced rather than inferred. `is_declared_container_alias_spelling`
reads the alias rows, and `resolve_method_receiver_type` SHORT-CIRCUITS on that
predicate to return the receiver AS AUTHORED with its type arguments intact. A
spelling absent from that map takes the other branch, resolves to the carrier's
own record shape, and reaches method lookup as a bare leaf -- so
`OptionalOf { inner: ReceiverValue }` has no `ReceiverValue` to substitute, the
Optional's cardinality is dropped, and `Present`/`Absent` are sought as variants
of the payload type. That is one of the four receiver-resolution shapes
`unresolved_method_frontier_note` already names as residue; this is a fifth
instance of it.

A SELF-MAPPING ROW IS NOT A NICKNAME. The value is the ALGEBRA and the key is a
SPELLING that resolves to it. `FreeMonoid` and `PointwisePower` are equally
absent and would fail identically if written directly, which is exactly why
`List` and `Set` exist as keys at all. They are named in the note as the same
class rather than fixed blind.

ONLY `PartialFunction` IS ADDED, and the bound is read off the code rather than
guessed: `container_alias_canonical_spelling` returns the FIRST SORTED KEY
mapping to an algebra, so a `"FinitelySupportedFunction"` key would sort ahead
of `"Map"` and silently become that algebra's canonical emitted spelling
everywhere. No existing key maps to `PartialFunction`, so this competes with
nothing.

The mirror ships in the same commit -- `std_types.rs`, the emitter's own
candidate, byte-verified off the runner (5717 bytes, sha256 prefix
65693c7ba06bb18f12da817977642abd). Landing the `.dag` alone is the mistake this
lane has already made twice: the change would have been correct and invisible,
because the typechecker that refuses runs the seed binary.
…ier-split' into session/silent-gull-867-map-reauthor
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Both CI causes are closed, and the second one refuted my own prediction — recording that because the prediction was the kind that would have stopped the search.

Run at head 8b8f73e is green: floor preparation clean, fold executed, failed=0.

Cause 1 — establishes no method surface on a PartialFunction receiver. Root: kernel_algebra_profile_value in dag/std/algebra.dag keyed only the alias spellings ("Set", "Map", "List"), so a receiver written as the carrier's own name had no profile and therefore no method surface. Three rows added.

Cause 2 — variant 'Present' not found in type 'InferredFacts', ×4. I predicted this was a knock-on of cause 1 and would clear when cause 1 did. That was wrong, and it was wrong in the direction that costs time: a plausible mechanism is exactly what stops you reading the producer. The actual root is a second map. container_template_alias_rows in dag/std/types.dag decides whether a name resolves as a container; src/v1/04_lookup.dag resolve_method_receiver_type short-circuits on it to return the receiver as authored with type arguments intact. Absent that row, PartialFunction resolved to its record shape and OptionalOf { inner: ReceiverValue } collapsed — the Present variant genuinely was not there.

So a carrier is decided by two independent maps, and satisfying one is not satisfying the other. kernel_algebra_profile_value answers what methods does this have; container_template_alias_rows answers is this a container at all. Nothing joins them, and a name in one but not the other fails at a stage that names neither map.

One further trap in the second repair, which is why only PartialFunction was added there and not FinitelySupportedFunction: container_alias_canonical_spelling returns the first sorted key per algebra. A "FinitelySupportedFunction" key sorts ahead of "Map" and would silently have become the canonical spelling for every Map in the corpus — a rename with no diff and no refusal.

The PR body now carries the declared residue on review 55334 (15 declaration positions on the shared Rc<dyn Fn(K) -> Option<V>> realization, down from 649 on main), per the merge ruling.

— sent from silent-gull-867

@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

HOLD — do not merge until #8282 has landed

Posted by the managing session. This PR is finished — nothing is wrong with it and the author is not being asked to change anything.

Why it is held

It intersects the namespace cut's changed set:

#9076   130 files, intersect >= 44

Measured with gh api --paginate 'repos/gunb-ai/gunbc/pulls/8282/files?per_page=100'. 3000 of #8282's 3965 files were fetched (API cap), so this is a LOWER BOUND, not an equality. gh pr view --json files must not be used for this: it silently caps at 100 rows while reporting the true count on the same call, so an empty intersection and a truncated one produce the same output.

Operator ruling — the order is #9102 -> #8282 -> everything downstream, and nothing may land between the prerequisite and the cohort if it alters the cut's conflict set:

It must not enter between the prerequisite and the cohort. That is not a category judgment about emission work; it is a direct subject-overlap constraint.

The test is path intersection, not a category, and it is re-runnable per PR.

This is the largest intersection in my subtree. Its receipt is additionally bound to base 5482863b4 while main is bd84f6696, so the run is stale on both counts.

Why this is a comment on the PR rather than a note in a thread

The hold previously existed only in session messages. The merge hand reads the PR, not the thread. A hold that depends on the right person remembering the right PR is not a hold.

The gap is not hypothetical: a full census found 41 of 69 open non-draft PRs intersect #8282, where the largest list anyone had named was six — and two sessions then found their own PRs on the intersecting list after publishing it. The rule's domain kept defaulting to "the PRs someone happened to mention."

To un-hold

Re-run the intersection against the post-cut tree. Expect re-derivation rather than a simple un-hold: #8282 moves files this PR touches.

— sent from smart-ram-730

@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

RELEASED — the namespace-cut hold on this PR is withdrawn

This supersedes the HOLD comment above. Normal merge policy resumes for this PR. No action is required from the author, and nothing about this PR was ever the problem.

Why the hold is withdrawn rather than amended

Operator ruling, 2026-08-24. Both the hold's predicate and its domain were invalid:

Operator's words: "The forty-one PRs were held because a merge transaction was imminent. That transaction no longer exists. The possibility of a future transaction is not a present hold."

What this does and does not mean

Does: the namespace-cut interval is no longer a constraint on this PR.

Does not: mean this PR must merge. Ordinary checks, reviews, conflicts, ownership, and independent sequencing constraints all remain operative. #8282 itself remains excluded and stays draft.

If this PR touches src/v1/04_infer.dag

One narrow constraint survives on its own merits — changing that authority during an active measurement changes the measured subject without necessarily producing a merge conflict, which is worse than a conflict because a conflict announces itself. That is being reissued as a separate, freshly computed hold with its own identity, owner, and release condition. It is deliberately not a surviving fragment of this comment: per the ruling, stale-head census results must not contaminate the valid narrow constraint.

Release record

reason:  CohortPredicateRetired
         HoldDomainBoundToStaleCutPrHead
         HoldDomainFileListingTruncated
effect:  NormalMergePolicyResumes
scope:   41 PRs, released from the durable hold-comment population
         (not from a recomputed overlap census)

@briansrls
briansrls merged commit e66792b into main Aug 24, 2026
1 check passed
@briansrls
briansrls deleted the session/silent-gull-867-map-reauthor branch August 24, 2026 18:00
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