Skip to content

fix(§12.1): a branch-declared function is routed BEFORE it is collected — closing the client half alone leaked a server fn body into client.js - #818

Merged
bryanmaclee merged 2 commits into
mainfrom
fix/s395-if-chain-server-boundary-land
Sep 2, 2026
Merged

fix(§12.1): a branch-declared function is routed BEFORE it is collected — closing the client half alone leaked a server fn body into client.js#818
bryanmaclee merged 2 commits into
mainfrom
fix/s395-if-chain-server-boundary-land

Conversation

@bryanmaclee

Copy link
Copy Markdown
Owner

Closes g-collect-functions-branch-decl-vs-server-boundary-routing (HIGH, SECURITY-GATED).

The trap, and why the order is the fix

A function declared inside an if=/else branch was never defined while its call sites still emitted unmangled — a ReferenceError at exit 0 (PA-reproduced on merged main: 0 definitions, 2 bare calls). The obvious fix — teach collect.ts's collectFunctions to descend the if-chain — is wrong alone: it emits a server fn body into client.js with no server.js at all, trading a loud ReferenceError for a silent server-code-in-client leak.

Root, and it explains the trap: the server and client emitters read one collector. The client emitter omits a server fn body only because route-inference already claimed it. So RI must see the declaration first.

  1. route-inference.tscollectFileFunctions + collectWorkerBodyFunctionIds descend branch bodies. The file had zero occurrences of if-chain before this. collectWorkerBodyFunctionIds was not in the brief: left blind it goes out of lockstep and false-fires E-ROUTE-001 — blast radius the fix itself would have created.
  2. collect.tscollectFunctions, second.

Phase 1 — three-way security control

variant body in client.js server.js
base 0 none
codegen half ALONE 1 none ← positive control reproduced
RI half alone 0 none
both halves 0 present
lone-if= oracle 0 present

PA-reproduced independently on the merged baseline: base produces no server.js at all; the fix puts the body in server.js and zero of it in client.js.

Phases 3–4 — measured, and the zero is explained

7408 artifacts byte-identical, 0 differing, 0 server-bundle deltas. Not assumed: an AST census over the same 1914 sources finds 17 containing a collapsed if-chain and zero declaring a function inside a branch. ⛑ The agent's first differential said 1021 differed and it threw that out itself — it had built the base tree by deleting .git, a project-root marker, so every chunk token hashed an absolute path.

R26 across r25/r27/r28 (r26 does not exist): 104 artifacts byte-identical; a source-anchored placement checker over 1928 sources / 237 server-fn declarations reports 0 violations. ⛑ The agent tested its own sweep, found it did not bite on this bug — the leak shape is body in client AND no server.js, so there is nothing to overlap — and wrote a second checker rather than accept a clean result from an unproven instrument.

The leak guard is hardened — four defects, not three

Whitespace-normalized body check; a structural check that every client-side zzload definition must be a fetch stub; real server-only markers replacing the vacuous not.toContain("server fn") (emitted JS never contains a scrml source keyword, so it could not fail for any input); and a positive limb the agent found itself — the guard was absence-only, so the base column satisfied it by making the function vanish. Bite-proved: codegen-half-alone 2 fail, pristine base 3 fail, both halves 6 pass.

PA fix round on the S239 findings (1 MED + 4 LOW, none blocking)

  • The route-inference.ts comment named endpointClientSkipIds as the mechanism keeping a server body out of the client. It isn't — that set is built solely from <endpoint> private-arm reachability; the real mechanism is route.boundary === "server". A future engineer chasing this leak class would have followed it to a dead end.
  • The ROUTING PARITY test could silently become a tautology: String.replace no-ops if the template is reflowed, so loneSrc === SERVER_FN_SRC, the test compiles the same source twice, and every assertion passes with the discriminator gone. Added expect(loneSrc).not.toBe(SERVER_FN_SRC).
  • Dropped a byte-length equality assertion between two different sources; it passes today only because no node id reaches server.js.

Comment- and test-only, inert to the runtime surface → re-review judged nil-blast-radius. Guard 6 pass, conformance 894 pass / 0 fail after.

Not fixed, filed — both verified pre-existing

  • Same-named declarations in sibling branches bind to the last definition, no diagnostic. PA-verified pre-existing language-wide: two lone if= divs on base mis-bind identically. The fix brings the chain to parity with that oracle, which is its goal. No server body leaked in any duplicate-name shape tested.
  • types:check grows 9 → 10 TS7016 for ast-if-chain.js (baseline never refreshed when that module landed). Relayed, not PA-measuredtsc isn't installed in this checkout and the gate correctly refuses to pass without running, so I did not land an unverified .d.ts on someone else's number.

⛑ Landing note — base-drift filtered

The agent's branch was cut before the maps refresh merged, so the range diff showed .claude/maps/* at −1020 lines. Stale views, not edits (absent from its files-touched list). A wholesale file-delta pull would have silently reverted the wrap-6c refresh landed earlier this session — the lost-update the OCC discipline exists to prevent. Excluded; verified 0 map paths staged.

provenance: spec:§12.1"The compiler SHALL decide where each function executes" (unqualified by declaration site), reinforced by §12.4 "No function SHALL be split across client and server without the analysis completing" — precisely what the landing order enforces. Conformance restoration, not a widening.

🤖 Generated with Claude Code

https://claude.ai/code/session_019eHiNS6uvnGPFz55fzinNJ

…ed — closing the client half alone leaked a server fn body into client.js

Closes g-collect-functions-branch-decl-vs-server-boundary-routing (HIGH, SECURITY-GATED).

THE TRAP, AND WHY THE ORDER IS THE FIX. A `function` declared inside an `if=`/`else`
branch was never defined while its call sites still emitted unmangled — a
`ReferenceError` at exit 0 (PA-reproduced on merged main: 0 definitions, 2 bare calls).
The obvious fix — teach `codegen/collect.ts`'s `collectFunctions` to descend the
if-chain — is WRONG ALONE: it emits a `server fn` BODY into `client.js` with no
`server.js` at all, trading a loud ReferenceError for a silent server-code-in-client
leak. A prior dispatch walked into exactly that, ran a control, reverted, and filed the
gap; this arc closes it in the mandated order.

ROOT, and it explains the trap: `emit-server.ts` and the client emitter read ONE
collector. The client emitter omits a `server fn` body ONLY because route-inference
already claimed it. So RI must see the declaration first.

  1. compiler/src/route-inference.ts — `collectFileFunctions` + `collectWorkerBodyFunctionIds`
     descend if-chain branch bodies via `ifChainChildNodes`. The file had ZERO
     occurrences of `if-chain` before this. `collectWorkerBodyFunctionIds` was NOT in
     the brief: left blind it goes out of lockstep with the fixed collector and
     false-fires E-ROUTE-001 on a branch-declared fn in a worker body — blast radius the
     fix itself would have created.
  2. compiler/src/codegen/collect.ts — `collectFunctions`, second.

PHASE 1 — THREE-WAY SECURITY CONTROL (file copy only; never `git stash`, whose refs are
shared across worktrees):

    variant                body-in-client   server.js
    base                         0             none
    codegen half ALONE           1             none      <- positive control reproduced
    RI half alone                0             none
    BOTH halves                  0             present
    lone-if= oracle              0             present

PA-REPRODUCED INDEPENDENTLY on the merged baseline: base produces no server.js at all;
the fix puts the body in server.js and zero of it in client.js.

PHASE 3 — measured differential: 7408 artifacts byte-identical, 0 differing, 0
server-bundle deltas. The zero is EXPLAINED, not assumed: an AST census over the same
1914 sources finds 17 containing a collapsed if-chain and ZERO declaring a function
inside a branch. The agent's FIRST differential run reported 1021 differing and it threw
it out itself — it had built the base tree by deleting `.git`, a project-root marker, so
every chunk token hashed an absolute path; the tool flagged it INCOMPARABLE.

PHASE 4 — R26 across r25/r27/r28 (r26 does not exist): 104 artifacts byte-identical, and
a source-anchored placement checker over 1928 sources / 237 server-fn declarations
reports 0 violations. ⚑ The agent tested its OWN sweep and found it did NOT bite on this
bug — the leak shape is "body in client AND no server.js", so there is nothing to
overlap — and wrote a second checker rather than accept a clean result from an unproven
instrument.

THE LEAK GUARD IS HARDENED, four defects not three. Whitespace-normalized body check; a
structural check that every client-side `zzload` definition must be a fetch stub; real
server-only markers replacing the VACUOUS `not.toContain("server fn")` (emitted JS never
contains a scrml source keyword, so it could not fail for any input); and a POSITIVE limb
the agent found itself — the guard was ABSENCE-ONLY, so the base column satisfied it by
making the function vanish. Bite-proved: codegen-half-alone 2 fail, pristine base 3 fail,
both halves 6 pass.

PA FIX ROUND on the S239 findings (1 MED + 4 LOW, none blocking, core sound):
  - route-inference.ts comment named `endpointClientSkipIds` as the mechanism keeping a
    server body out of the client. It is not — that set is built solely from <endpoint>
    private-arm reachability; the real mechanism is `route.boundary === "server"`.
    A future engineer chasing this leak class would have followed it to a dead end.
  - the ROUTING PARITY test could silently become a TAUTOLOGY: `String.replace` no-ops if
    the template is reflowed, making `loneSrc === SERVER_FN_SRC` so the test compiles the
    same source twice and every assertion passes with the discriminator gone. Added
    `expect(loneSrc).not.toBe(SERVER_FN_SRC)`.
  - dropped a byte-length equality assertion between two DIFFERENT sources; it passes
    today only because no node id reaches server.js. The marker-count loop carries the
    real signal.
These are comment- and test-only, inert to the runtime surface, so re-review was judged
nil-blast-radius rather than re-run. Guard 6 pass, conformance 894 pass / 0 fail after.

NOT FIXED, FILED (both verified pre-existing, neither introduced here):
  - same-named declarations in sibling branches bind to the LAST definition with no
    diagnostic. PA-verified this is pre-existing LANGUAGE-WIDE: two LONE if= divs on base
    mis-bind identically. The fix brings the chain to parity with that oracle, which is
    its goal. No server body leaked in any duplicate-name shape tested.
  - `bun scripts/types:check` grows 9 -> 10 TS7016 for `ast-if-chain.js` (its baseline was
    never refreshed when that module landed). RELAYED, NOT PA-MEASURED — `tsc` is not
    installed in this checkout and the gate correctly refuses to pass without running, so
    I did not land an unverified .d.ts on someone else's number.

⚑ LANDING NOTE — base-drift filtered. The agent's branch was cut before the maps refresh
merged, so `git diff origin/main..<branch>` showed .claude/maps/* at -1020 lines. Those
are STALE VIEWS, not edits (absent from its files-touched list). A wholesale file-delta
pull would have silently REVERTED the wrap-6c refresh landed earlier this session — the
lost-update the OCC discipline exists to prevent. Excluded; verified 0 map paths staged.

provenance: spec:§12.1 — "The compiler SHALL decide where each function executes"
(unqualified by declaration site), reinforced by §12.4 "No function SHALL be split across
client and server without the analysis completing" — which is precisely what the landing
order enforces. Conformance restoration, not a widening.

Agent branch: worktree-agent-a493f8f4a01973d11 @ e2e2ecb9
@bryanmaclee
bryanmaclee merged commit c4c55c5 into main Sep 2, 2026
2 of 3 checks passed
@bryanmaclee
bryanmaclee deleted the fix/s395-if-chain-server-boundary-land branch September 2, 2026 14:33
bryanmaclee added a commit that referenced this pull request Sep 2, 2026
…and two instruments that failed their own tests (#819)

GAPS. g-collect-functions-branch-decl-vs-server-boundary-routing -> RESOLVED via #818
(c4c55c5), PA-verified two-sided on MERGED main: body-in-client 0 with server.js
produced and the body inside it; the plain-fn symptom goes 0 definitions / 2 bare calls
-> 1 definition / 0 bare calls.

THREE NEW ENTRIES. Two were verified PRE-EXISTING before filing, which is the point —
neither is introduced by #818, and saying so is what keeps the next reader from
"fixing" a regression that is not one:

  - g-same-named-branch-declarations-bind-to-the-last-definition (MED). Sibling branches
    declaring one name emit BOTH definitions and bind EVERY call to the last, silently.
    PA-verified pre-existing on the lone-if= oracle, so #818 brings the chain to parity
    with an already-defective oracle. Direction AT THIS SHAPE is loud->silent (base threw
    ReferenceError), which is why it is filed rather than shrugged at. No server body
    leaked in any duplicate-name shape tested. The scoping question underneath is the
    SAME one g-if-chain-all-arms-run-at-module-init asks — rule once, not twice.
  - g-call-expression-interpolation-in-if-chain-branch-renders-empty (MED). PA-verified
    on merged main with a TOP-LEVEL fn, i.e. outside the fix's surface. _scrml_boot does
    querySelector + _scrml_render_value BEFORE _scrml_nav_rewire(document) inserts the
    branch, so el is null and the render is a silent no-op. A CELL interpolation in the
    same position works — which is why the #811/#818 class sweeps missed it. It is an
    ORDERING defect, not a collection one.
  - g-types-check-baseline-never-refreshed-for-ast-if-chain (LOW). RELAYED, NOT
    PA-MEASURED, and the entry says so. Two independent agents concur on 9->10 TS7016,
    but tsc is absent from this checkout and types-gate.ts correctly refuses to fall back
    or skip — "a gate that passes without running is worse than no gate", its own words.
    So no unverified .d.ts was landed on someone else's number. Re-measure before acting.

REVIEW FLOOR: markers for #817 and #818.

⛑ THE ARC'S DURABLE FINDING — three instruments were tested rather than trusted, and two
FAILED. (1) The dev agent's first corpus differential reported 1021 files differing and
it invalidated its own run: the base tree had been built by deleting `.git`, one of
chunk-namespace.ts's PROJECT_ROOT_MARKERS, so every chunk token hashed an absolute path;
re-run gives 0 of 7408. (2) Its security overlap sweep did NOT bite on this bug — the
leak shape is body-in-client AND no server.js, so there is nothing to overlap — found by
pointing it at a deliberately-leaking build, getting "clean", and writing a second
source-anchored checker. (3) The leak guard was ABSENCE-ONLY: the base column satisfied
it by making the function vanish, so "absent from the client" meant nothing until a
POSITIVE limb required the body present on the SERVER. All three found by adversarial
construction, none by inspection.

master-list.md carries only the @generated recent-sessions regen. Minor drift noted, not
chased: that block's own header says it is derived from the `wrap(s…)` commits, and it
has just indexed a `maps(...)` one.
bryanmaclee added a commit that referenced this pull request Sep 3, 2026
…reen while wrong (#824)

Ran concurrently with S396-peter throughout. Five rulings given, four code arcs landed, the
review floor drained twice to zero, maps refreshed. ONE ARC HELD on a ruling, worktree retained.

⚑ THE SESSION'S REAL OUTPUT IS NOT THE LANDINGS. Nine separate checks read green or
authoritative while being wrong, and every one was caught by RUNNING something rather than
by remembering. Three were my own claims. TWO WERE CAUGHT BY GATES I DID NOT WRITE. Three
were agents' own instruments, found by those agents.

RULINGS
  · the trailing-`else` question — direction stands, and the reported "inverted ruling" was
    MY OWN mis-description: §17.6.4 has always said a missing `else` is valid. I relayed a
    hand-off's framing into the boot report without opening the section.
  · the `~` arm-body question — limb (a) ratified, limb (c) BANKED as dpa-040, not closed.
  · 2a / 2b / 2c — the runAnchored fix (sequenced), the dev-root auth gate (limb b), and the
    tracking-job sequence with its first step folded in.
  · SPLIT the dev-auth arc when its §52.13 half turned out to require a second decider.

LANDED
  #815 the §17.6.2 value-form sugar binds its result at a binding site — it bound null forever.
  #818 a branch-declared function is ROUTED before it is COLLECTED; closing the client half
       alone put a `server fn` BODY into client.js.
  #822 18 conformance assertions read as coverage and checked nothing. ⚑ MY BRIEF WAS WRONG —
       a naive `continue` deletion would have reddened 62 count:0 ABSENCE assertions — and the
       agent deviated with a stated reason. Only 2 of the 18 were covered elsewhere; 16 were
       entirely unguarded.
  #823 `scrml dev` served a protected document unauthenticated at `/`. The fix is a DELETION:
       two serving paths become one.
  #816 #819 #820 continuity + filings; #817 the overdue wrap-6c maps refresh, which corrected
       three premises in my own concurrently-dispatched brief.

⚑ HELD, NOT LANDED — the `~` build, worktree RETAINED at agent-ac264a1015c0da19d @ c2ad6f49.
Four fix rounds, four adversarial passes, 7 SPEC loci reconciled. Blocked on bryan's
revert-vs-press-on for the READ half, whose widening PA-verifiably DELETES A DOM LIFT (base
emits _scrml_lift/createTextNode, the branch pushes into a dead array, exit 0). Resume the
agent; do not re-dispatch.

FINDINGS WORTH CARRYING
  · SUPPRESSING A SIGNAL SUPPRESSES WHATEVER RODE ON IT — three instances, one of them my own
    instruction. The `tracking` job's routine red concealed a REAL §52.13 assertion failing on
    Linux (compiler/tests/commands/ runs in NO blocking job on any platform). A knowingly-red
    assertion placed FIRST in a test turned the four after it into dead code that still read as
    coverage. And I instructed `test.failing` on that test — which passes when the body fails
    for ANY reason, so it would have MASKED A LEAK. The agent's formulation is the keeper:
    a mechanism that makes a red acceptable also makes a red invisible.
  · A FIX THAT NEEDS A SECOND DECIDER IS THE FORK RULE SAYING NO. Three rounds found three
    distinct divergences between the dev pre-gate and the resolution loop; the split then
    CLOSED a dev/prod divergence rather than opening one.
  · A REVIEW FINDING WAS FALSIFIED RATHER THAN FILED — the first time this session verification
    prevented a FABRICATED finding instead of catching a missed one. The corpus IS gated;
    corpus-bridge.test.js reaches it from inside the gated root.
  · TWO SHARED-DOC CONCURRENCY HAZARDS, both caught by machinery: conflicting @generated gap
    counts (resolved by regenerating from the merged population — the union, not a pick), and
    duplicate delta-log sequence numbers with peter (caught by the cloud gate's step 14; a
    duplicate makes the flogence bridge DROP the second entry from the digest).
  · BASE-DRIFT nearly reverted landed work FOUR times.
  · EVERY ONE of the five agent deviations from my instructions this session was correct.

WRAP STEPS: hand-off rewritten (S396-peter's rotated to handOffs/hand-off-282.md, his live items
carried) · changelog block · delta-log [2040]-[2045], sequence gate PASS · review floor 6 OWED
-> 0 · inbox: TWO items deliberately left unarchived because both await bryan (the E-ROUTE-004
fork and the ~11-day-old FSP Initialize deliberation), both summarised in the hand-off · suite
30945 pass / 54 fail — ONE FEWER than base, the §52.13 assertion · worktrees: four landed swept,
one retained · maps refreshed to 2d8dd8c, watermark verified via state.ts not by reading the
file · @generated + facts gates PASS.
bryanmaclee added a commit that referenced this pull request Sep 3, 2026
… them mine (#826)

The wrap-6c refresh completed after #824/#825 landed partial writes. This is the settled
output, plus the `master-list.md` @generated:recent-sessions regen the wrap commit staled
(state.ts --check was FAILING on it; now PASS).

⚑ FOUR PREMISES FAILED ON MEASUREMENT, AND THE FIRST WAS IN MY OWN BRIEF.

1. I briefed "compiler/tests/commands/ runs in NO blocking job on any platform." WRONG on
   the pre-push clause: `.git/hooks/pre-push:96` DOES name it, in a suite its own comment
   calls blocking. The CONCLUSION survives by a different mechanism — `RUN_SUITE` is 1 only
   on a release-tag push or a ref-diff failure, so a normal code push skips it entirely, and
   the source-controlled hook (`scripts/git-hooks/pre-push:87`) never names it at all. The
   map states the MECHANISM now, not my slogan. I had repeated the slogan in three PR bodies
   and a hand-off; it was right about the outcome and wrong about why.

2. `test.map.md` carried FOUR count cells wrong at their own watermark — Unit 909→925,
   Integration 213→216, Browser 98→102, Commands 14→17 — identical at `ad7b65dc`, with zero
   test files added in the window. The prior pass applied DELTAS to a base it never
   re-measured, so a correct total sat on four wrong components. Its Integration gate column
   was also wrong: integration blocks a COMMIT, not a merge.

3. `auth.map.md`'s anchor was DELETED, not moved — `:1046-1048` exists in no form. A
   line-drift repair would have produced a confidently wrong map.

4. The `gate` "14 steps" figure was ambiguous (12 `- name:` + 2 `- uses:`); stated both ways.

⚑ ALL THREE FACTUAL ERRORS FOUND THIS PASS WERE IN THE MAP SET, NOT IN THE DOCS IT AUDITS.

MAPPED: #818 — invariant 82's `collect.ts` prohibition RETIRED (that arc closed both halves
together), and the ORDER it protected promoted to invariant 86, including the third
`collectWorkerBodyFunctionIds` walk whose omission would have false-fired E-ROUTE-004's
sibling. #822 (domain §62.2) and #823 (domain §52.13, auth row 3, dev.js structure entry).
The two routing facts I asked for are invariants 87 and 88.

STAMP: `8e278c73`, deliberately NOT the branch tip. Reaching a literal `maps: current` would
require stamping an unpushed branch SHA — the S326/S328/S331 orphaned-stamp hazard, since
branch tips squash-merge under a different SHA. The mapper stamped the furthest commit
satisfying all three MAP-STAMP checks (== origin/main, == merge-base, source diff EMPTY,
is-ancestor exit 0). Headers now state the two-SHA split explicitly: source delta walked is
`ad7b65dc..2d8dd8c`; watermark is `8e278c73`.

NON-COMPLIANCE, standing items re-verified and both STILL LIVE — with a wording correction
worth more than the finding: N12's "never in ANY map" is no longer true (`structure.map.md:196`
names `docs/audits/`), so the accurate claim is "NAMED, never SCANNED". Its "two self-declare
superseded" figure holds, but a `grep -li superseded` returns 9 and over-counts 4.5×. N14
confirmed again: `collapseIfChains` is at `:18871`, `:18885` is `let i = 0;`, and the same
entry's OTHER citation is correct — which is what makes the wrong one read as trustworthy.

NEW, needs a human: `docs/articles/` sits outside EVERY compile gate. `snippet-gate.js`'s
SNIPPET_CORPUS covers only tutorial/readme/website and gates `.scrml` FILES, not fenced
blocks — so the 6 `scrml` blocks in the enum article at publish state have nothing telling
anyone if they stop compiling.
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