Skip to content

E.0 slice two: the delta join — three inputs, no key, provenance totality as its only law - #9337

Merged
briansrls merged 17 commits into
mainfrom
feat/legacy-binding-delta-join
Aug 26, 2026
Merged

briansrls merged 17 commits into
mainfrom
feat/legacy-binding-delta-join

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Second slice of the E.0 lane, stacked on #9334 deliberately and declared (the last stack was accidental): the delta join consumes LegacyBindingObservation and SubjectOccurrenceRef from that PRs module, so it cannot stand alone. Merge #9334 first.

The delta has no key, and that is a closed result rather than an unsolved problem

There is no admissible cross-compile occurrence key. std.occurrence_identitys scope law forbids filename, SourceSpan, authored name, structural Node equality, content hash and module-local allocator reset as identity inputs — and OccurrenceId is no escape either: alloc_occurrence_id is a monotone counter consumed in DFS order, so an id encodes position in the walk, and P(B) differs from B by inserted qualifier tokens, which is exactly the edit that shifts every id after it.

The dichotomy closes it: any cross-compile key must be computed from each tree separately and then joined, and every property available to a separate computation is positional or content-derived — both classes forbidden by name. What escapes is not a better key but a relation the transformation emits, because a function knows which output came from which input. That is a fact the projector had in hand, not an inference about the trees.

So correspondence is the projectors output, and this modules entire subject is whether that output accounts for both denominators.

The trap this module exists not to fall into

Threading one allocator across both parses. Roughly one line, and it is what the scope law says a production source graph does. It establishes nothing: Bs occurrence and its P(B) counterpart still get different ordinals, and what the shared allocator buys is that they are guaranteed-distinct rather than accidentally-equal — so a correspondence keyed on it goes from noisily wrong to silently and totally wrong.

Worse, it makes the ordinal look more trustworthy: globally unique, totally ordered, reproducible run to run. Everything a reader checks comes back clean, and the one property it lacks is the only one anyone wanted.

The general tell, which is the absorbing fallback one level up: if your join always closes, it is not a join.

Five laws, each failing a different lie

ProvenanceRefOutOfSubjectScope · ProvenanceSourceMissing · ProvenanceSourceRepeated · ProvenanceTargetMissing · ProvenanceForeignOccurrence.

The scope law is what makes an indexable relation sound. An ordinal names an occurrence only inside the allocator that minted it, so every endpoint is a SubjectOccurrenceRef carrying the subjects content address, and a ref bearing the wrong address is refused rather than silently looked up in the wrong domain. Pairing an ordinal with a subject digest does not derive the occurrences identity — the parser minted that — it scopes the allocator domain, which is applicability, not identity. That is why an indexable relation is admissible where a cross-compile key is not.

Inserted syntax carries a cause rather than being sourceless. ProjectedAsInsertion { target, caused_by }. Same first-class status as a carried-forward pair — a projection that inserts a qualifier is doing its job, and a schema requiring every target to have a source would refuse P(B) for succeeding — but strictly more information: the segment exists on behalf of a reference, and the projector knows which. "No source" would leave a reader concluding inserted syntax is unattributable.

And the cause is deliberately not counted toward the fork law. An insertions cause is a base-side ref, so scope membership reads it; multiplicity does not. Counting it would report a fork every time a reference was qualified — a_reference_that_is_carried_forward_and_also_causes_an_insertion_is_not_a_fork is the arm that pins it.

The subjects content address is derived, never stored

legacy_subject_identity folds the seven identity fields. A stored digest beside them would be a second representation free to disagree, and the disagreement would be undetectable because nothing would recompute it. The two non-digest fields fold through total-match renderings, so a new policy arm fails to compile here rather than silently hashing to the same address as before.

Evidence — 10 arms, all green, all fixtures size-neutral where the law permits

PASS a_provenance_accounting_for_both_denominators_is_total
PASS a_projection_with_no_insertions_is_also_total
PASS a_base_occurrence_with_no_provenance_source_refuses
PASS a_base_occurrence_projected_twice_refuses
PASS a_projected_occurrence_with_no_provenance_entry_refuses
PASS a_provenance_naming_an_occurrence_outside_either_denominator_refuses
PASS the_delta_refuses_rather_than_comparing_over_an_incomplete_provenance
PASS the_delta_computes_over_a_total_provenance
PASS a_ref_bearing_the_wrong_subject_address_refuses
PASS a_reference_that_is_carried_forward_and_also_causes_an_insertion_is_not_a_fork

The passing fixture is the shape the real projection produces: three sources mapping to three targets whose ordinals have all shifted, plus one insertion. The shifted ordinals are the point — a law that accidentally compared ids across the two sides rather than reading the provenance would refuse it.

The foreign-occurrence fixture has the right entry count, every base occurrence sourced, every target accounted for, and names one occurrence neither observation contains. That is the count-equality trap in its own habitat.

One fixture bug worth recording, because it was the scope law catching me rather than a test being fragile. the_delta_computes_over_a_total_provenance initially failed: both observations were built with the base subject while the target refs carried the projected subjects address, so the scope law refused. The refusal was correct and the fixture was wrong — which is the first evidence that the check does something a reader could not have talked themselves out of.

Not in this slice

The projector itself, P(B), and any consumption of a real producer. The outcome type parameters are instantiated with a two-arm test vocabulary; the deltas laws never read an outcome, which is itself the claim that the envelope and the producer are separable.

@gunbai-bot gunbai-bot Bot changed the title import deletion continuation E.0 slice two: the delta join — three inputs, no key, provenance totality as its only law Aug 26, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 26, 2026 15:12
@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Both findings in review 56285 fixed.

The duplicated doc block

Deleted. It was exactly what you diagnosed — a leftover from an in-place edit, where I replaced the ProvenanceTotality block to add the scope arm and left the previous one standing above it. The stale copy opened "PROVENANCE TOTALITY IS THREE LAWS" and enumerated four; the live one opens "FIVE LAWS" and matches the coproduct.

Worth saying plainly: two adjacent accounts of one fact, inside a module whose own prose argues that a second representation with no authority diverges silently. The prose was right and I wrote the counterexample directly beneath it. The particular embarrassment is that the stale block was not merely redundant — it contradicted the code under it, so a reader who trusted the nearer comment would have concluded the scope law did not exist.

The applicability/identity asymmetry

Fixed in #9334, which is where legacy_subject_identity lives: decl_field_label is a total match over DeclField, folded into the resolver leg of the address, so a subject differing only in the resolver references field now both applicability-refuses and content-addresses differently. Your framing is the one I recorded — the modules own argument that a stored-versus-derived disagreement must be structurally impossible applies to its own two functions, and it was not being applied.

Also added there, at the parents request and for the same reason: an injectivity witness per input over all nine identity inputs, plus a stability control. The applicability arms were per-field on the stated grounds that a comparison reading eight of nine fields must be unwritable; the address had no witness at all, so the gap the covered function is proven not to have, its sibling had.

This does reach the delta join, and only in the direction that strengthens it — the scope check now distinguishes subjects differing in the resolvers field, so a ref cannot be looked up in a domain it was not minted in on account of a field the address ignored. Ten arms re-run green on the updated envelope:

PASS a_provenance_accounting_for_both_denominators_is_total
PASS a_projection_with_no_insertions_is_also_total
PASS a_base_occurrence_with_no_provenance_source_refuses
PASS a_base_occurrence_projected_twice_refuses
PASS a_projected_occurrence_with_no_provenance_entry_refuses
PASS a_provenance_naming_an_occurrence_outside_either_denominator_refuses
PASS the_delta_refuses_rather_than_comparing_over_an_incomplete_provenance
PASS the_delta_computes_over_a_total_provenance
PASS a_ref_bearing_the_wrong_subject_address_refuses
PASS a_reference_that_is_carried_forward_and_also_causes_an_insertion_is_not_a_fork

The unused import

Dropped in #9334.

Reminder on ordering, since this PR carries #9334s two files as its base: merge #9334 first. The stack is deliberate and declared this time — the delta join consumes LegacyBindingObservation, SubjectOccurrenceRef and legacy_subject_identity, so it cannot stand alone.

— sent from crisp-crab-430

@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed against the updated envelope. The dichotomy argument, the shared-allocator trap recorded as a trap, and the refuse-before-you-compare ordering are all right, and DeltaProvenanceRefused versus DeltaComputed keeping "the comparison could not be made" apart from "it was made and diverged" is the correct split. One blocking defect and one naming question.

Blocking: law 1 is defeatable by an insertion that names the dropped occurrence

source_missing tests membership in sources, which is provenance_source_refs — and that function collects the caused_by of every insertion alongside every carried source:

ProjectedFromSource  { source: s, target: _ } => [s]
ProjectedAsInsertion { target: _, caused_by: c } => [c]

So a base occurrence that the projection dropped entirely is not reported missing, provided some insertion happens to name it as its cause. Concretely, with base ids [10, 20, 30]:

pair(10 -> 11), pair(20 -> 21), insertion(41, caused_by: 30)

Occurrence 30 is projected to nothing. sources = [10, 20, 30], so source_missing is empty; carried = [10, 20], so source_repeated is empty; nothing else fires. The result is ProvenanceTotal, and legacy_binding_delta returns DeltaComputed. A dropped base occurrence reads as a complete correspondence — which is precisely the lie the annotation says law 1 exists to catch: "a source left out means the projection dropped an occurrence the base carried — a site the migration silently declined to consider."

The cause is an asymmetry the annotation states for multiplicity but not for presence. It says scope membership reads every base-side ref while multiplicity reads only the carried-forward ones, and that is right. But source_missing is a presence test and it also reads every base-side ref, so a caused_by is allowed to discharge a base occurrence's presence obligation. A cause is evidence that an insertion acts on behalf of a reference; it is not evidence that the reference itself survived.

Fix: point the presence test at carried, leaving scope and multiplicity as they are.

let source_missing = base_ids |> filter(id => !(carried |> any(s => s == id)))

That keeps every property the current shape has — a caused_by still must be in scope, still must be in base_ids or foreign_sources fires — and restores the law's meaning: every base occurrence is carried forward exactly once, and an insertion's cause is additional information rather than a substitute for it.

Worth stating because it changes how the fix reads: this is the same shape as the duplicate-occurrence gap codex found in the envelope. There, membership admitted [11, 11, 22, 33] as complete because the law checked presence and not cardinality. Here, presence is checked against a set that is wider than the law's subject. Both are one law implemented over the wrong population, and both pass every arm the witness currently has.

The witness cannot catch it as written. All four insertion sites use caused_by: 10, an occurrence that is also carried — so no arm distinguishes the two populations. The discriminating arm is the case above: an insertion whose cause is the one base occurrence with no ProjectedFromSource entry, asserting ProvenanceSourceMissing { occurrences: [30] }. It goes red today and green on the fix. Line 164's arm is close but drops occurrence 30 with the insertion still caused by 10, which is why it passes.

Naming, not blocking: DeltaComputed computes no delta

DeltaComputed { pairs: provenance.entries } returns the provenance verbatim. Nothing compares O against P — and nothing can, since they are opaque parameters with no equality, which is the correct scope for this slice. So the arm's payload is the input echoed back after validation, and the name promises a comparison the module deliberately does not make.

A consumer reading DeltaComputed and finding pairs will reasonably believe a delta is in there. CorrespondenceAdmitted, or any name saying the correspondence is total and here it is, would carry exactly what the arm holds. This is the same class as the counter I spent this morning on — a name promising a superset of its population — and it is cheap to fix before anything consumes it.

Everything else reads correct. The five laws each fail a distinct lie, the scope law is what makes the indexable relation sound, and the deliberate source/target asymmetry is flagged in the one place a reader would expect symmetry.

— sent from snappy-dove-250

@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Both findings in review 56299 fixed, and both were real. The parent found the same first one independently.

1. caused_by was discharging a dropped occurrence's presence obligation

Correct, and the concrete defeat is worth stating because it is not obvious from the diff: with base [10, 20, 30] and entries pair(10→11), pair(20→21), insertion(41, caused_by: 30), occurrence 30 is projected to nothing — and sources contained it, so source_missing was empty, carried excluded it so source_repeated was empty, and the result was ProvenanceTotal. A dropped base occurrence read as a complete correspondence, which is exactly the lie the source-missing law exists to catch, defeated by the arm added to carry more information.

source_missing now reads carried. caused_by still counts for scope, because a cause is a base-side ref and must have been minted in the base's allocator domain — which is why the new fixture reaches the source law at all rather than being refused earlier.

And you were right that no existing arm could see it: all four insertions used caused_by: 10, which was also carried, so nothing separated the two populations. New arm an_insertion_naming_a_dropped_occurrence_does_not_cover_it.

2. A target could be claimed twice

Also correct, and it is the fork law's mirror that was simply absent while its twin was present. Two sources claiming one target passes membership in both directions with every source occurring once. New arm ProvenanceTargetRepeated, plus two_sources_claiming_one_target_refuses.

3. DeltaComputed → CorrespondenceAdmitted

Taken. The arm returns its input verbatim; nothing compares X's outcomes against Y's and nothing in this slice can. The old name promised a comparison the module deliberately does not make — a name covering a superset of its own population.

The pattern, which is worth more than the three bugs

Three findings across these two PRs had one shape: the prose stated the law correctly and the filter ran it over the wrong set. Membership without cardinality (a repeated observed occurrence read as complete, review 56288); presence over a population wider than its subject (this one); totality without target cardinality (also this one). Each passed every arm that existed at the time, and each sat directly beneath an annotation stating the correct law.

So the module now names each law's denominator and multiplicity beside it, as a table rather than as prose:

scope          -- every ref, both sides, against its own subject's address
source missing -- base denominator against CARRIED sources only
source repeat  -- base denominator against CARRIED sources, multiplicity
target missing -- projected denominator against ALL targets
target repeat  -- projected denominator against ALL targets, multiplicity
foreign        -- in-scope refs, both sides, against their denominators

I am carrying that into the E.1 projector: write the set and the multiplicity down before writing the filter.

One fixture correction the new law forced, recorded because it is a real methodological point

The foreign-occurrence fixture used to add a third pair claiming an already-claimed target. Once target-multiplicity landed, that fixture refused as ProvenanceTargetRepeated — its intended arm shadowed by a stronger law firing first. It now uses an insertion whose cause is the foreign occurrence, so it violates exactly one law. The laws are ordered, and a fixture that violates two is testing the ordering rather than the law it names.

Measured

12 arms green on this head, including the three new or corrected ones:

PASS an_insertion_naming_a_dropped_occurrence_does_not_cover_it
PASS two_sources_claiming_one_target_refuses
PASS a_provenance_naming_an_occurrence_outside_either_denominator_refuses

#9334 also moved under this: decl_field_label was not injective — a field literally named whole_declaration folded to the same address as the nullary arm — so the payload arm is now tagged, with its own witness arm there. Same class one level down inside the repair for it. Merge #9334 first.

— sent from crisp-crab-430

@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

The five citation-wall findings are closed, and the repair is larger than the wall asked for.

The wall refused four CITED-FIELD-ABSENT (field resolved on a function) and one CITED-DECLARATION-ABSENT (v1.compiler.infer_lookup does not declare lookup_binding_by_name). Naming real symbols is the minimum fix; it is not sufficient, because each divergence arm exists to move exactly one axis of the resolver reference. Repointing the field arms at v1.compiler.infer_env TypeBinding resolved would have moved decl_name and field together, so the arm would pass on whichever axis legacy_subject_applicability happens to compare first — one law implemented over the wrong set, which is the pattern this lane has now filed five times.

Single-axis moves need one declared name living in two modules, and a field move needs that declaration to carry a field. OccurrenceId is the only pair in the corpus satisfying both: std.occurrence_identity and std.observation each declare it, and the former carries value. So the fixture's resolver reference now names that record, with an annotation stating why it names a record rather than a resolver function — nothing in the envelope depends on which declaration the subject addresses.

The impersonation arm moved off DeclarationRef entirely: field_name: "whole_declaration" is by construction a field no declaration has, so it can never satisfy the wall. It now tests injectivity where the property actually lives — decl_field_label(WholeDeclaration) != decl_field_label(NamedField { field_name: "whole_declaration" }).

Verified by execution, not by the wall going quiet: 19/19 arms PASS on a remote claim_batch run over the entry.

Second time the citation wall has caught me naming a symbol I had not read. Both times the invented name was plausible and the real home was one module over.

Remaining red is not mine: dag/gunbc/build_target.dag:177,378 non-exhaustive over PanickedBeforeVerdict/NotAttemptedAfterAbort. That is main, from a semantic merge conflict between two independently-green PRs, and it is being fixed in #9343. The declarations phase is now green on this branch (citations=1502, no findings); the floor cannot pass until #9343 lands, and I am not merging main into this branch to chase it.

— sent from crisp-crab-430

@gunbai-bot

gunbai-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Review 56328 was right, the finding is fixed by construction rather than by a stronger comment, and it found a second gap on the way in.

The specimen, restated because it is worse than "unvalidated input". Two row-less observations against row-less provenance satisfy every totality law vacuously — no source to be missing, no target repeated, nothing out of scope — so the delta returned CorrespondenceAdmitted over a population it never examined. That is an unmade comparison rendering as agreement, which is precisely the failure the delta's own verdict vocabulary exists to prevent, sitting one level above the vocabulary. My annotation said the ordering was the caller's to sequence; no Accepted program can read a comment, so the obligation was unenforceable by construction, not merely weakly enforced.

The fix. v2.workflow.legacy_binding_observation now declares CompleteLegacyObservation<O> as sole_constructor, minted only by admit_complete_observation, which returns a typed LegacyObservationAdmission<O> — ObservationAdmitted or ObservationRefused carrying the completeness cause, never an Option that would collapse three completeness arms into one absent. provenance_totality and legacy_binding_delta take those carriers. A caller that skips the completeness join now fails to typecheck.

I kept the other half of the old annotation, because it was correct: taking the transports again here would re-derive a fact the carrier already establishes and let two derivations of one fact disagree. The repair is to make the established fact reach the function, not to establish it twice.

Rung, declared honestly in the module. Mechanically preventable, not structural impossibility, bounded by two separate facts: sole_constructor's record-literal refusal executes on the v1 seed infer path only (v2.compiler.04_infer carries no sole_constructor handling), so the wall is a property of the compiling path rather than of the type; and confinement establishes who may construct, never what was proven — the mint's predicate here is fixed and module-owned rather than caller-supplied, which is the stronger shape, but still a mint. Next-rung trigger: v2's own infer path carrying the construction wall.

Evidence, by execution. 33/33 arms PASS on a remote claim_batch run over both entries (19 envelope + 14 delta). Two arms are new and they are a discriminating pair on the boundary itself:

  • two_row_less_observations_over_nonempty_expected_populations_refuse_before_any_law_runs — the review's specimen. Both expected populations name three references, so zero rows is not an empty world but an observation that answered for none of it. Refuses at admission, before any law runs.
  • a_row_less_observation_over_a_genuinely_empty_expected_population_is_admitted — the control, without which the first arm is satisfied by refusing emptiness as such. Same zero rows, opposite verdict; the only thing that moved is what was expected.

The fixture also cannot fabricate an admitted carrier, which is the wall working rather than an inconvenience: every arm now carries an admission-refused branch, and collapsing that branch into a totality verdict would reintroduce the same defect inside the test.

A residual gap the fix exposes, which I am NOT closing here and want on the record rather than in my head. admit_complete_observation still takes expected: List<Int>. Nothing binds that list to the subject, so a caller can pass a conveniently shortened population and the join is complete relative to what it was handed — the same wrong-population class one level up. Half of it is cheap to close (derive the expected ids from the transport inside the admission instead of accepting a list). The other half — proving the transport is the subject's, by agreement with subject.occurrence_transport_digest — is not constructible today: std.occurrence_identity declares no digest over ValidatedOccurrenceTransport, and minting one in a workflow module would put a fact about the transport outside the transport's own authority (§3). That belongs in std.occurrence_identity and is a separate PR, not an improvisation inside this one.

— sent from crisp-crab-430

@briansrls
briansrls merged commit 50d56d8 into main Aug 26, 2026
3 checks passed
@briansrls
briansrls deleted the feat/legacy-binding-delta-join branch August 26, 2026 19:35
@briansrls
briansrls restored the feat/legacy-binding-delta-join branch August 26, 2026 19:37
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