refactor(registry): exhaustive platformExecution discriminator (ADR 0019 §6) - #1740
Conversation
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed exact head 9428496. P1: Reclassify The other |
|
You're right, and my reach probe had exactly the blind spot you named: it followed imports, and 🤖 Addressed by Claude Code |
902be1f to
8ad1292
Compare
|
Reviewed current exact head [P2] Track dispatch occurrences, not only command names. 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. |
|
Confirmed and fixed — your reproduction is exact. I ran your planted source against the old scanner first and got Dispatch extraction now carries source identity: every 🤖 Addressed by Claude Code |
4dbbbb7 to
4c1c429
Compare
|
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 Amended and force-pushed as 🤖 Addressed by Claude Code |
|
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 [P2] The scan still fails open for unknown command targets. |
|
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. |
4c1c429 to
5b4f1e5
Compare
|
Confirmed — and I reproduced the drop before fixing it. Running your shape (one routed-known dispatch plus one stray Design. Your planted red is permanent, asserting both halves (the routed edge still resolves, the stray surfaces as Head for re-review: 🤖 Addressed by Claude Code |
|
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: [] }; 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. |
|
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. |
5b4f1e5 to
0980853
Compare
|
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: 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. 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 ( 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 Head for re-review: 🤖 Addressed by Claude Code |
|
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. |
|
Re-reviewed exact head 0980853. Moving the live [P2] Two holes remain in the claimed singular source of truth. First, |
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)
0980853 to
a63a99f
Compare
|
Both holes closed. Head for re-review: 1. Table values. The values are now Planted both ways, because the type alone is not the whole gate:
2. Transport position. You're right that the blanket
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 Validation: 🤖 Addressed by Claude Code |
|
Reviewed exact head |
|
Addressed the inherited transport-handoff blocker at exact head
Keeping the PR draft while exact-head GitHub checks run and the stack-level R18 catalog dependency is resolved. |
|
The confirmed transport-handoff gap is fixed at exact head The coherence gate no longer trusts a property merely because it is named Regression evidence:
No production behavior or documentation changed; this tightens the structural ownership proof only. GitHub exact-head checks are now running. |
|
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.
CommandPlatformExecutiongains{ kind: 'none' };assertCommandPlatformExecutionaccepts it in exactly-one-shape form (and still rejects
{ kind: 'none', ... }widened).readDeclaredPlatformExecution(newsrc/core/command-descriptor/platform-execution-entry.ts)replaces the silent
{ kind: 'legacy' }default at registry entry. An undeclareddiscriminator is a registry-load error;
noneon a descriptor that keeps acapabilitybucket is also rejected (capability buckets are legacy platform admission).
RawCommandDescriptorShapeno longer makesplatformExecutionoptional, so anundeclared descriptor is a compile error as well as a load error.
nonelegacyinventorydevices)device-runtimelogs,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_pathsisfilesystem-only cleanup;
session_listreadsSessionStoreand formats platform stringsas 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:daemon—daemon stop --cleandynamically importssrc/platforms/apple/core/runner/runner-{lease,disposal}.tsand runs Apple runner-leasecleanup (
src/cli/commands/daemon.ts:24-31).debug—debug symbolsresolves tosymbolicateCrashArtifactfromsrc/platforms/apple/core/debug-symbols.ts(src/agent-device-client.ts:440-446).react-devtools— raised in review (P1). Its CLI route injectsconfigureDirectPortReverse(src/cli.ts:333-362); on a Limrun Android instancereact-devtools startdispatches internalruntime port-reverse, andsession-runtime-command.tsrunsconfigureProviderPortReverseagainst a providerdevice 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
nonewould be a false negative in the denominator. Reclassifying either tononeis areviewed decision, not a silent one.
capabilities,doctor, andwebarelegacyasspecified.
Coherence gate for delegated platform execution
The entry gate inspects one descriptor at a time, so it structurally cannot see the
react-devtoolsshape: a descriptor whose own module is platform-free while its CLI routeinjects 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.tsto 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.tsnow owns the concern: one typed table of route/commandpairs and
sendInjectedDaemonRequest, the single construction point.commandis a typedparameter 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:
the route's command may declare
noneonly when D isnone.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-devtoolspinned back tononefailsdominance 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 thoseland.
Why this is behavior-neutral
platformExecutionis an internal cutover discriminant that is stripped from daemon andpublic projections (
deriveDaemonCommandDescriptors, existing test inplatform-execution.test.ts). The only production consumer islistCapabilityCommands, which keys onkind === 'device-runtime'— unchanged by thisdiff, since no descriptor's mode changed value: every annotated descriptor previously
resolved to
{ kind: 'legacy' }via the default, andnoneis a new label applied onlywhere the default was legacy-by-omission. No admission, execution, cleanup, or hint path
reads
noneyet.Validation
pnpm check:layering— OK (136/136; R2/R3/R7/R9/R10/R13 ratchets unchanged, noSessionStatefields touched).pnpm check:affected --run— 468 files / 3930 tests passed.platformExecutionfrom thelease_allocatedescriptor produces
registry.ts(351,3): error TS2322: ... Property 'platformExecution' is missing in type ... but required in type 'Omit<CommandDescriptorBase & ...>'(plus two TS2345 at the.mapcall sites).platform-execution-entry.test.tsfails at import with
TypeError: Command descriptor "lease_allocate" must declare platformExecution (none, legacy, inventory, or device-runtime); there is no registry-entry default.src/core/command-descriptor/__tests__/platform-execution-entry.test.ts(undeclared discriminator rejected;
none+ capability bucket rejected; registry-wideexhaustiveness and no-contradiction assertions).
Docs: none needed — ADR 0019 §6 already defines the
nonemode; no CLI/user-facingsurface changes.
Runtime readers audited
Every production reader of the
platformExecutionvalue (excluding gates and tests):listCapabilityCommands()(src/core/capabilities.ts:163), consumed bysession-inventory.tsforcapabilities.availableCommands— keys onkind === 'device-runtime'only. No descriptor entered or leftdevice-runtimein thisdiff, and every
nonedescriptor is gate-proven to have no capability bucket (so it wasalready excluded as legacy-without-capability).
assertRecordRuntimeExecutionat registry entry — applies torecordonly; unchanged.deriveDaemonCommandDescriptors— strips the field from the daemon/public projection(existing test asserts the absence).
No production code branches on
legacy, sononeis 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 onorigin/mainand onthis 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 isitemized rather than absorbed: it is entirely the ~76 explicit
platformExecutionannotations that replace the silent
{ kind: 'legacy' }default at registry entry. Twoshared frozen literals (
NO_PLATFORM_EXECUTION,LEGACY_PLATFORM_EXECUTION) keep it toone 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
legacyfor a declared use.Part of #1739 (wave 0)