Skip to content

refactor(registry): exhaustive platformExecution discriminator (ADR 0019 §6) - #1740

Merged
thymikee merged 6 commits into
mainfrom
refactor/adr19-platform-execution-discriminator
Aug 12, 2026
Merged

refactor(registry): exhaustive platformExecution discriminator (ADR 0019 §6)#1740
thymikee merged 6 commits into
mainfrom
refactor/adr19-platform-execution-discriminator

Conversation

@thymikee

@thymikee thymikee commented Aug 11, 2026

Copy link
Copy Markdown
Member

What changed

ADR 0019 §6 (as amended in #1738) requires every command descriptor to declare its
platform-execution mode explicitly, because a registry-entry default cannot distinguish
a command with no platform execution from an unmigrated one. This makes the #1739
migration denominator machine-readable.

  • CommandPlatformExecution gains { kind: 'none' }; assertCommandPlatformExecution
    accepts it in exactly-one-shape form (and still rejects { kind: 'none', ... } widened).
  • readDeclaredPlatformExecution (new src/core/command-descriptor/platform-execution-entry.ts)
    replaces the silent { kind: 'legacy' } default at registry entry. An undeclared
    discriminator is a registry-load error; none on a descriptor that keeps a capability
    bucket is also rejected (capability buckets are legacy platform admission).
  • RawCommandDescriptorShape no longer makes platformExecution optional, so an
    undeclared descriptor is a compile error as well as a load error.
  • All 76 descriptors annotated. Final classification:
mode count
none 17
legacy 55
inventory 1 (devices)
device-runtime 3 (logs, network, record)

none (17): lease_allocate, lease_heartbeat, lease_release, artifacts,
session_list, session_save_script, release_materialized_paths, device, metro,
session, cdp, auth, connect, connection, disconnect, mcp, proxy.

Each was verified against its handler/owner file rather than its catalog group: lease and
publication handlers touch only the store/registry; release_materialized_paths is
filesystem-only cleanup; session_list reads SessionStore and formats platform strings
as opaque identifiers; the client-backed CLI commands reach remote/provider/daemon
plumbing only.

Classifications that were judgment calls

Three commands the tracker text groups with "local-CLI/daemon-management" are declared
legacy, fail-closed, because they reach platform behavior:

  • daemondaemon stop --clean dynamically imports
    src/platforms/apple/core/runner/runner-{lease,disposal}.ts and runs Apple runner-lease
    cleanup (src/cli/commands/daemon.ts:24-31).

  • debugdebug symbols resolves to symbolicateCrashArtifact from
    src/platforms/apple/core/debug-symbols.ts (src/agent-device-client.ts:440-446).

  • react-devtools — raised in review (P1). Its CLI route injects
    configureDirectPortReverse (src/cli.ts:333-362); on a Limrun Android instance
    react-devtools start dispatches internal runtime port-reverse, and
    session-runtime-command.ts runs configureProviderPortReverse against a provider
    device runtime. Delegated platform execution is platform execution.

None of the three binds a device or owns a capability bucket, but all three consume
platform behavior, so none would be a false negative in the denominator. Reclassifying either to none is a
reviewed decision, not a silent one. capabilities, doctor, and web are legacy as
specified.

Coherence gate for delegated platform execution

The entry gate inspects one descriptor at a time, so it structurally cannot see the
react-devtools shape: a descriptor whose own module is platform-free while its CLI route
injects a callback that dispatches a platform-executing command. That is a silent
denominator undercount.

This is a construction seam, not a scanner. The first two revisions parsed src/cli.ts
to recover dispatches from syntax, and each review found another shape it had not been
taught — occurrence-vs-name, unresolvable targets, then variable envelopes and computed
callees. The scanner enumerated what counts as a dispatch, an open-ended set where
anything unrecognized passed silently; that failure direction was the real defect.

src/cli/injected-daemon-dispatch.ts now owns the concern: one typed table of route/command
pairs and sendInjectedDaemonRequest, the single construction point. command is a typed
parameter constrained to the declared pairs, so an undeclared dispatch is a compile
error
and the dispatched command is known by type rather than recovered from an object
literal.

The gate follows from that:

  • Mode dominance reads the table directly — no AST. If a CLI route dispatches command D,
    the route's command may declare none only when D is none.
  • Seam singularity is the only syntax check left, and it is positional rather than
    value-resolving: the transport identifier may appear in imports, type queries, property
    signatures, property values, member access, and call arguments — nowhere else. Calls,
    aliases and unanticipated forms fail by default. The enumeration is now of where the
    transport may appear
    (closed) rather than what a dispatch looks like (open).

Planted reds: direct call, member call, computed-member call, variable envelope, and
alias — each rejected outside the seam; react-devtools pinned back to none fails
dominance against the declared pair; and a positive test keeps the legitimate client
transport wiring (transport: deps.sendToDaemon, createClientDaemonTransport(...))
passing. Net −141 lines: a 448-line test becomes ~124 plus a 46-line seam.

Scope note: the structural half lives in the vitest gate rather than as a numbered
layering rule, because #1745 and #1750 are both moving the layering rule namespace right
now. Moving it into scripts/layering/ in the #1744 shape is a clean follow-up once those
land.

Why this is behavior-neutral

platformExecution is an internal cutover discriminant that is stripped from daemon and
public projections (deriveDaemonCommandDescriptors, existing test in
platform-execution.test.ts). The only production consumer is
listCapabilityCommands, which keys on kind === 'device-runtime' — unchanged by this
diff, since no descriptor's mode changed value: every annotated descriptor previously
resolved to { kind: 'legacy' } via the default, and none is a new label applied only
where the default was legacy-by-omission. No admission, execution, cleanup, or hint path
reads none yet.

Validation

  • pnpm check:layering — OK (136/136; R2/R3/R7/R9/R10/R13 ratchets unchanged, no
    SessionState fields touched).
  • pnpm check:affected --run — 468 files / 3930 tests passed.
  • Planted red (compile gate): deleting platformExecution from the lease_allocate
    descriptor produces
    registry.ts(351,3): error TS2322: ... Property 'platformExecution' is missing in type ... but required in type 'Omit<CommandDescriptorBase & ...>' (plus two TS2345 at the
    .map call sites).
  • Planted red (load gate): with the same plant, platform-execution-entry.test.ts
    fails at import with
    TypeError: Command descriptor "lease_allocate" must declare platformExecution (none, legacy, inventory, or device-runtime); there is no registry-entry default.
  • New tests: src/core/command-descriptor/__tests__/platform-execution-entry.test.ts
    (undeclared discriminator rejected; none + capability bucket rejected; registry-wide
    exhaustiveness and no-contradiction assertions).

Docs: none needed — ADR 0019 §6 already defines the none mode; no CLI/user-facing
surface changes.

Runtime readers audited

Every production reader of the platformExecution value (excluding gates and tests):

  • listCapabilityCommands() (src/core/capabilities.ts:163), consumed by
    session-inventory.ts for capabilities.availableCommands — keys on
    kind === 'device-runtime' only. No descriptor entered or left device-runtime in this
    diff, and every none descriptor is gate-proven to have no capability bucket (so it was
    already excluded as legacy-without-capability).
  • assertRecordRuntimeExecution at registry entry — applies to record only; unchanged.
  • deriveDaemonCommandDescriptors — strips the field from the daemon/public projection
    (existing test asserts the absence).

No production code branches on legacy, so none is not observable at runtime by design;
it is metadata for the derived denominator. Verified empirically rather than by argument:
listCapabilityCommands() returns a byte-identical 43-command list on origin/main and on
this branch. Runtime readers audited: listCapabilityCommands, assertRecordRuntimeExecution,
deriveDaemonCommandDescriptors — decisions unchanged.

Size

The size bot reports +1.9 kB raw / +169 B gzip / +221 B tarball (chunk
dist/src/sdk-batch-runner.js), startup unchanged. Per ADR 0019 §8 this growth is
itemized rather than absorbed: it is entirely the ~76 explicit platformExecution
annotations that replace the silent { kind: 'legacy' } default at registry entry. Two
shared frozen literals (NO_PLATFORM_EXECUTION, LEGACY_PLATFORM_EXECUTION) keep it to
one property reference per descriptor. This is the metadata that makes the derived
migration denominator machine-readable — the whole point of the wave-0 item — and it
shrinks again as descriptors leave legacy for a declared use.

Part of #1739 (wave 0)

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.18 MB 2.18 MB +2.1 kB
JS gzip 713.0 kB 713.2 kB +211 B
npm tarball 839.1 kB 839.3 kB +268 B
npm unpacked 2.92 MB 2.92 MB +2.1 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 18.1 ms 17.8 ms -0.4 ms
CLI --help 45.6 ms 43.6 ms -2.0 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/sdk-batch-runner.js +1.9 kB +158 B
dist/src/cli.js +187 B +53 B

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed exact head 9428496.

P1: react-devtools is incorrectly classified as platformExecution: none. Its production CLI route injects configureDirectPortReverse in src/cli.ts; for Limrun Android react-devtools start, that callback sends internal runtime port-reverse, and session-runtime-command.ts executes configureProviderPortReverse. This is delegated provider/device platform behavior, while ADR 0019 §6 defines none as executing no platform behavior.

Reclassify react-devtools as legacy until that operation migrates. Also add derived coherence coverage, with a planted-red proof, for injected/delegated platform execution. The current entry gate rejects only missing or malformed declarations and none plus a capability bucket, so it cannot catch this real route and can silently undercount the migration denominator.

The other none classifications, runtime readers/public projection behavior, exhaustiveness checks, and itemized +1.9 kB raw / +169 B gzip size tradeoff otherwise look clean. All completed checks are green; iOS Smoke and Layering Guard were still pending at review time.

@thymikee

Copy link
Copy Markdown
Member Author

You're right, and my reach probe had exactly the blind spot you named: it followed imports, and react-devtools's platform reach is injected from src/cli.ts rather than imported, so the module read clean. react-devtools is now legacy (counts: none 17 / legacy 55 / inventory 1 / device-runtime 3), with the delegation recorded at the declaration site. For the coherence coverage I added mode dominance across composition in platform-execution-cli-route.test.ts: it parses the composition root, attributes each command: dispatch literal to its if (command === '<name>') route by following calls into same-module functions, resolves both ends through the command catalog and descriptor registry, and fails when a none route dispatches a non-none command. It is total within that scope — a dispatch no route can claim fails rather than passing quietly, which is the undercount you were pointing at. The planted red runs the real src/cli.ts with react-devtools pinned back to none and fails with react-devtools declares platformExecution none but its CLI route dispatches runtime (legacy); that proof is kept permanently by injecting the mode lookup instead of reverting the registry. Sweeping the same join over the whole composition root, react-devtools -> runtime is the only such delegation, so no other none classification changes.

🤖 Addressed by Claude Code

@thymikee
thymikee force-pushed the refactor/adr19-platform-execution-discriminator branch from 902be1f to 8ad1292 Compare August 11, 2026 15:46
@thymikee

Copy link
Copy Markdown
Member Author

Reviewed current exact head 8ad1292b. The prior react-devtools classification P1 is fixed: delegated runtime port-reverse makes it correctly legacy, and the current planted-none case detects that edge. One actionable gate issue remains:

[P2] Track dispatch occurrences, not only command names. platform-execution-cli-route.test.ts stores both attributed and all dispatches as Set<string> targets, then subtracts by target name. Once the routed react-devtools edge attributes runtime, any second runtime dispatch outside a recognized route is silently treated as attributed too. I reproduced this with a planted source containing one routed and one stray INTERNAL_COMMANDS.runtime: expected unattributed: ["runtime"], received []. That contradicts the gate’s stated totality and lets future injected/delegated platform behavior evade the migration denominator merely by sharing an existing target. Preserve AST occurrence/source identity through extraction, subtract attributed occurrences rather than names, and keep the one-routed/one-stray duplicate-target witness as a regression.

The current classification/production route otherwise looks coherent. The branch was freshly rebased during review, so the new exact-head CI run is still pending; no ready label yet.

@thymikee

Copy link
Copy Markdown
Member Author

Confirmed and fixed — your reproduction is exact. I ran your planted source against the old scanner first and got expected [] to deeply equal [ 'runtime' ]: attributed and the full dispatch set were both Set<string>, so the routed react-devtools -> runtime edge consumed the name and the stray occurrence fell out of the subtraction. The totality claim was therefore false in exactly the way you describe — shared target names were an escape hatch from the denominator.

Dispatch extraction now carries source identity: every command: site becomes a DispatchSite { command, offset } taken from the AST node's start position, route attribution collects sites rather than names, and the unclaimed set is computed by subtracting attributed offsets. Route edges are still deduplicated by (route, command) so the dominance check is unchanged, and a site with no source position falls back to -1, which can never match an attributed offset and so stays unclaimed (fail-closed). Your one-routed-plus-one-stray case is now a permanent regression test asserting both halves — the routed occurrence is claimed (dispatches is exactly the react-devtools edge) and the stray one is reported (unattributed is ['runtime']) — so a future fix that over-reports everything fails too.

🤖 Addressed by Claude Code

@thymikee
thymikee force-pushed the refactor/adr19-platform-execution-discriminator branch from 4dbbbb7 to 4c1c429 Compare August 11, 2026 16:22
@thymikee

Copy link
Copy Markdown
Member Author

Self-correction on the commit I just pushed: the edge key I introduced in the occurrence-identity fix was written with a raw NUL byte as its separator instead of a space. The logic was correct and every test passed, but git classified the file as binary — so that commit had no reviewable diff, and depgraph:test's no tracked TypeScript source contains a raw NUL byte gate failed the Layering Guard job on 4dbbbb74c (a real failure, not the smoke-lane flake class, so I did not rerun it).

Amended and force-pushed as 4c1c42979 with a plain space separator. The commit now diffs as text (57 insertions / 22 deletions), pnpm depgraph:test is 22/22 locally including that NUL gate, and the six route-gate tests still pass. Worth noting the gate that caught it already existed — nothing new was needed.

🤖 Addressed by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 4c1c429. The prior occurrence-vs-name issue is fixed: attribution now tracks AST dispatch sites by source offset, and the one-routed/one-stray same-target planted regression proves the stray occurrence stays visible. Production react-devtoolsruntime classification is coherent.

[P2] The scan still fails open for unknown command targets. dispatchTargetOf() passes literal strings through knownCommand() and returns undefined for an unregistered/typo literal; unsupported dynamic/computed targets are likewise returned as undefined. Those properties are then absent from both allSites and unattributed, contradicting the gate’s totality claim. Please represent unresolved command properties explicitly (or fail the scan immediately) and add planted-red coverage for at least command: 'unknown-target' plus a computed/dynamic target, so every daemon dispatch syntax outside the supported catalog forms is rejected rather than silently omitted. All authoritative CI is green; no readiness label while this remains.

@thymikee
thymikee marked this pull request as ready for review August 11, 2026 17:12
@thymikee

Copy link
Copy Markdown
Member Author

Coordinator check: this PR is currently marked ready while the exact-head unknown/computed-target scanner P2 remains unresolved and no corrected head has been pushed since 4c1c429. Please convert it back to draft immediately, address the finding with planted coverage, rerun pnpm check:affected --run, and only restore readiness after exact-head re-review. Five downstream ADR 0019 units are blocked on this correction.

@thymikee
thymikee marked this pull request as draft August 11, 2026 18:10
@thymikee
thymikee force-pushed the refactor/adr19-platform-execution-discriminator branch from 4c1c429 to 5b4f1e5 Compare August 11, 2026 18:21
@thymikee

Copy link
Copy Markdown
Member Author

Confirmed — and I reproduced the drop before fixing it. Running your shape (one routed-known dispatch plus one stray SOME_OTHER_CATALOG.hidden) through the pre-fix resolver, the extraction yields exactly ['runtime']: the stray never enters the scan, so no downstream attribution arithmetic could ever surface it. Unknown was silently equivalent to absent.

Design. command: on its own cannot mark a dispatch in the composition root — withDiagnosticsScope({ command: argv[0] }), resolveRunContextOrExit({ command, positionals }) and the client-config builders all carry that key — so treating every unresolved command: as a failure would fire on six honest sites. Instead the request envelope is identified by the call it is passed to: the first object argument of a sendToDaemon(...) call (identifier or member callee). For each envelope the gate resolves the command property, and anything it cannot resolve — computed target, variable, unknown literal, unknown catalog key, or no command at all — is reported in a new unresolvedTargets list keyed by source line, rather than skipped. Known-target detection stays broad, so a resolvable dispatch anywhere in the file still attributes as before; only the failure case is scoped to send calls. Restricting to the first argument matters: sendToDaemon(request, { authToken }) would otherwise report the options object as a command-less envelope.

Your planted red is permanent, asserting both halves (the routed edge still resolves, the stray surfaces as 7: daemon send target is not a registered command), plus a four-case table covering computed target, unknown literal, variable target, and missing target. The composition root asserts unresolvedTargets is empty alongside the existing dominance and attribution assertions.

Head for re-review: 5b4f1e599, rebased onto 52402aec4. check:affected --run clean; depgraph:test 22/22 including the NUL-byte gate. Left in draft per your coordinator-check note — not requesting readiness.

🤖 Addressed by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 5b4f1e5. The prior unknown/computed inline-envelope finding is fixed, but one actionable totality gap remains.

[P2] daemonSendEnvelopes records a send only when the first argument is directly an ObjectExpression. A request passed by identifier is silently ignored. Concrete planted witness:

const request = { command: "not-a-command", positionals: [] };
await deps.sendToDaemon(request);

The standalone object property does not become an allSites dispatch because dispatchTargetOf rejects the unknown literal, and daemonSendEnvelopes drops the identifier argument, so unresolvedTargets and unattributed are both empty. This is the same denominator escape through one level of request construction. Resolve local request identifiers (or fail closed on every non-object first argument), and keep planted coverage for unknown/computed targets behind sendToDaemon(request). The inline target tests and production react-devtools edge otherwise look coherent. Keep draft pending the corrected head and affected gate.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 5b4f1e5. The prior unknown literal/computed command target inside an inline object envelope is fixed, and the new planted regressions are meaningful.

[P2] The claimed total scan still fails open one level earlier. daemonSendEnvelopes() records only a directly recognized sendToDaemon call whose first argument is an ObjectExpression. A normal refactor such as const request = { command: 'not-a-command' }; await deps.sendToDaemon(request) is silently omitted from envelopes, dispatches, unattributed, and unresolvedTargets; a computed send callee is likewise skipped. Please make every recognized daemon-send call produce either a resolved envelope or an explicit unresolved failure, and plant the variable-envelope case red. Given this is another syntax-form omission, prefer a typed construction/source of truth over continuing to extend a syntax mini-compiler. Production react-devtoolsruntime remains coherent, and all authoritative CI is green/CLEAN, but no readiness label while this remains.

@thymikee
thymikee force-pushed the refactor/adr19-platform-execution-discriminator branch from 5b4f1e5 to 0980853 Compare August 11, 2026 19:17
@thymikee

Copy link
Copy Markdown
Member Author

I took the inversion. The scanner is deleted, not extended.

Your holes are real and I confirmed both against the old code before removing it: daemonSendEnvelopes accepted only a direct/member sendToDaemon call whose first argument was an inline ObjectExpression, so const request = {...}; deps.sendToDaemon(request) and deps['sendToDaemon'](...) each produced zero envelopes — invisible to dispatches, unattributed and unresolvedTargets alike.

Why I stopped extending it. This was the third omission patch in three reviews: occurrence-vs-name, unknown targets, now variable envelopes and computed callees. AGENTS.md is explicit that a guard needing a second omission patch should be redesigned around the source of truth rather than given another exception, and each patch made the scanner enumerate what counts as a dispatch — an open-ended set where anything unrecognized passes silently. That failure direction is the actual defect; no amount of syntax coverage fixes it.

The seam. src/cli/injected-daemon-dispatch.ts holds one typed table of route/command pairs and sendInjectedDaemonRequest, the single construction point. command is a typed parameter constrained to the declared pairs, so an undeclared dispatch is a compile error, and the dispatched command is known by type at the call site rather than recovered from an object literal. runReactDevtoolsCli now goes through it.

The gate follows: mode dominance reads the table directly, with no AST at all. The only remaining syntax check is seam singularity, and it is positional rather than value-resolving — the transport identifier may appear in imports, type queries, property signatures, property values, member access and call arguments, and nowhere else. Calls, aliases, and any form I have not imagined fail by default, so the enumeration is now of where the transport may appear (closed) instead of what a dispatch looks like (open). That inversion is what closes the surface; I would not claim the old scanner's surface was closed.

Both of your cases are planted reds, alongside direct call, member call, and alias, plus a positive test that the legitimate client-transport wiring (transport: deps.sendToDaemon, createClientDaemonTransport(deps.sendToDaemon)) still passes. Net −141 lines: a 448-line test becomes ~124 plus a 46-line seam.

One judgement to flag: I kept the structural half in the vitest gate rather than adding a layering rule number, because #1745 and #1750 are both moving the layering rule namespace right now and a fourth concurrent number would collide. If you'd rather it live in scripts/layering/ as an R-rule in the #1744 shape, that is a clean follow-up once those land.

Head for re-review: 09808532b, rebased onto 057ab1c82 (#1746). check:affected --run clean, check:layering green, check:production-exports unchanged at 22/22 pre-existing. Still draft.

🤖 Addressed by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 0980853. Moving to a declared pair table plus a construction seam is the right direction, but two actionable enforcement gaps remain.

[P1] The table types only route keys, not dispatched command values. CLI_INJECTED_DAEMON_DISPATCHES satisfies Partial<Record<DescriptorCliCommandName, readonly string[]>>, so a new legacy route can declare ["not-a-command"] and compile. dominanceFailures also ignores it when the route is legacy, because the filter only evaluates none routes. Type values as registered descriptor/catalog command names and add a planted unknown-value compile/runtime witness.

[P2] Seam singularity still allows arbitrary transport forwarding. isAllowedTransportPosition accepts every CallExpression argument, so forward(deps.sendToDaemon) is clean even when forward later invokes the transport outside sendInjectedDaemonRequest. The existing planted alias catches const send = deps.sendToDaemon, but not transport-as-argument or assignment/destructuring aliases. Restrict wiring to exact approved construction calls/positions, or change the API so raw sendToDaemon is never available to arbitrary CLI modules. Plant forward(deps.sendToDaemon) and a renamed destructuring/assignment alias.

The production react-devtools -> runtime pair and the new request construction itself are coherent. Keep draft until these are closed and check:affected is rerun.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 0980853. Moving the live react-devtoolsruntime injection behind a typed construction seam is a strong improvement; the prior inline/variable-envelope and computed-callee cases are meaningfully covered, and the current production pair is coherent.

[P2] Two holes remain in the claimed singular source of truth. First, CLI_INJECTED_DAEMON_DISPATCHES constrains route keys to descriptor CLI commands but leaves target values as readonly string[]; an unknown target such as not-a-command typechecks and passes dominance while the route is legacy. Constrain values to the descriptor-derived daemon-command union and plant an unknown-table-target red. Second, isAllowedTransportPosition() blanket-allows a sendToDaemon reference whenever its parent is any CallExpression. runInjected(deps.sendToDaemon) therefore passes; that helper can rename the parameter to transport and dispatch directly, invisible to both the scan and table. Restrict raw-transport escape to the exact reviewed client-factory callsites/capabilities, or make bypass impossible with a narrower construction boundary, and plant this arbitrary-helper case red. All checks except the still-running iOS smoke are green; no readiness label yet.

ADR 0019 §6 (amended): every command descriptor declares its platform-execution
mode explicitly. Adds the `none` mode to `CommandPlatformExecution`, removes the
silent `{ kind: 'legacy' }` default at registry entry, and annotates all 76
descriptors so the migration denominator is machine-readable.

Part of #1739 (wave 0)
`react-devtools start` on a Limrun Android instance dispatches internal
`runtime port-reverse`, which reaches a provider device runtime, so ADR 0019 §6
`none` is false for it. Reclassify as `legacy` and add the derived coherence gate
that catches delegated platform execution: if a CLI route for command R
dispatches command D, R may declare `none` only when D is `none`.

Part of #1739 (wave 0)
Subtracting attributed command NAMES let a stray dispatch hide behind a routed
one that names the same command, so the gate's totality claim did not hold.
Dispatch sites now carry their source offset and attribution subtracts
occurrences.

Part of #1739 (wave 0)
An unknown literal or computed command target resolved to undefined and never
entered the scan, so a dispatch could evade attribution by naming a target the
gate could not read. Daemon-send envelopes are now located by their send call and
an unresolvable target is reported instead of skipped.

Part of #1739 (wave 0)
… seam

The syntactic scan recognized only a direct sendToDaemon call whose first
argument was an inline object literal, so a variable envelope or a computed
callee was omitted from every result. Rather than teach the scanner more shapes,
the CLI's injected dispatches now flow through one typed construction point whose
route/command pairs are declared, and the gate reads that declaration instead of
recovering it from syntax.

Part of #1739 (wave 0)
@thymikee
thymikee force-pushed the refactor/adr19-platform-execution-discriminator branch from 0980853 to a63a99f Compare August 12, 2026 06:22
@thymikee

Copy link
Copy Markdown
Member Author

Both holes closed. Head for re-review: a63a99f5c, rebased onto 74eab2a55 (#1744).

1. Table values. The values are now DescriptorDaemonRouteCommandName, a new derived union in the registry alongside DescriptorSessionRouteCommandName — the names of descriptors that carry a daemon facet. A command with no daemon route cannot be the target of a request, so that is the honest constraint, and it comes from the same declaration site rather than a second list.

Planted both ways, because the type alone is not the whole gate:

  • Compile: adding 'not-a-command' to the table gives TS2322: Type '"not-a-command"' is not assignable to type '"alert" | "app-switcher" | ... | "wait"'.
  • Runtime cast-around: unresolvedPairs now checks that both ends of every declared pair resolve to a registered command. The test asserts the hole explicitly first — dominanceFailures returns [] for {'react-devtools': ['not-a-command']}, because an unknown command has no mode to compare — and then that unresolvedPairs reports it. An unresolvable end is a gate error, not an absence.

2. Transport position. You're right that the blanket CallExpression allowance re-opened the bypass; I verified it against the old predicate first, where both runInjected(deps.sendToDaemon, request) and runInjected({ send: deps.sendToDaemon }) returned zero misuse. The allowlist is now over named positions, expressed as data:

parent reviewed when
ImportSpecifier / ImportDeclaration / TSTypeQuery always — naming the type or importing it dispatches nothing
MemberExpression the deps.sendToDaemon access itself; its own parent is checked in turn
Property / TSPropertySignature key is sendToDaemon (the deps record) or transport (the reviewed slot)
CallExpression callee is createClientDaemonTransport — the one client-factory callsite

Every other position fails, so the enumeration stays over reviewed positions rather than over call syntax. Three new plants: handoff to an arbitrary helper, handoff under an unreviewed key, and a bare re-export; the existing five (direct/member/computed call, variable envelope, alias) and the positive wiring test still hold.

While tightening this, fallow's complexity gate flagged the switch and the visitor, so the allowlist became a lookup table and the visitor split into a single transportDefect — which reads better anyway: the reviewed positions are now literally a table you can review.

Validation: check:affected --run clean (fallow included), check:layering green, tsc clean. Still draft.

🤖 Addressed by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed exact head a63a99f5. The typed dispatch table and sendInjectedDaemonRequest are a strong improvement, and the prior unknown/variable-envelope recovery problems are gone. One P2 seam escape remains: REVIEWED_TRANSPORT_POSITIONS.Property globally allows any property named transport or sendToDaemon, without checking the enclosing callee or reviewed factory callsite. Consequently runInjected({ transport: deps.sendToDaemon }) passes transportMisuse; the helper can extract/rename the transport and dispatch arbitrary targets. The planted test covers the unreviewed key send, while the positive test globally blesses transport. Constrain property occurrences to exact reviewed client-factory construction/callsites (or expose a narrower capability) and keep this object-handoff witness as a planted red. All authoritative checks are green and the branch is CLEAN/MERGEABLE, but no readiness label while this remains.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed the inherited transport-handoff blocker at exact head fac5294b7.

  • Raw transport / sendToDaemon properties are no longer globally trusted by name.
  • The structural gate now permits only the reviewed constructions in src/cli.ts.
  • Planted regressions prove runInjected({ transport: deps.sendToDaemon }) and member-spoofed factory calls are rejected, while the legitimate seams remain accepted.
  • pnpm check:affected --run && git push passed: 481 files / 3,970 tests.

Keeping the PR draft while exact-head GitHub checks run and the stack-level R18 catalog dependency is resolved.

@thymikee

Copy link
Copy Markdown
Member Author

The confirmed transport-handoff gap is fixed at exact head fac5294b7.

The coherence gate no longer trusts a property merely because it is named transport or sendToDaemon. Transport references are accepted only at the exact reviewed src/cli.ts construction sites: the two injected-dispatch factories, the client transport factory, and DEFAULT_CLI_DEPS. Member-call spoofing and generic object handoffs are rejected.

Regression evidence:

  • planted old behavior: 2/17 failed (generic { transport: deps.sendToDaemon } handoff and member-callee spoof were falsely accepted)
  • corrected focused suite: 18/18 passed
  • pnpm check:affected --run: 481 files / 3,970 tests passed, plus format, lint, typecheck, layering, Fallow, and build

No production behavior or documentation changed; this tightens the structural ownership proof only. GitHub exact-head checks are now running.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 12, 2026
@thymikee
thymikee marked this pull request as ready for review August 12, 2026 08:40
@thymikee
thymikee merged commit 97c87ee into main Aug 12, 2026
31 checks passed
@thymikee
thymikee deleted the refactor/adr19-platform-execution-discriminator branch August 12, 2026 08:40
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-12 08:41 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant