Skip to content

refactor(contracts): name façade exports explicitly and retire the pin table - #1614

Merged
thymikee merged 3 commits into
mainfrom
claude/explicit-facade-exports
Aug 5, 2026
Merged

refactor(contracts): name façade exports explicitly and retire the pin table#1614
thymikee merged 3 commits into
mainfrom
claude/explicit-facade-exports

Conversation

@thymikee

@thymikee thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member

Thirteen of the fourteen @agent-device/contracts façades were bare
export * barrels. facades/snapshot.ts, added by #1582, was the one
exception — explicit named re-exports — and that is now the rule.

Everything #1574 built to cope with export * goes with them:

scripts/layering/facade-symbols.ts -980 (816 pinned names)
scripts/layering/facade-exports.ts -192 (readFacadeExports)
scripts/layering/facade-exports.test.ts -234 (star semantics)
scripts/layering/package-boundaries.test.ts -55

readFacadeExports re-implemented ESM GetExportedNames/ResolveExport
— star-chain resolution, ambiguity rejection, diamond binding identity,
cycle guards, spec-accurate default filtering at the star rather than
the source. All of it existed to enumerate what export * hides. 523 of
the 816 pinned names belonged to contracts, i.e. to those thirteen files.
Once a façade names its exports, the façade file IS the pin, and it is
visible in the diff of the file that widened rather than in a separate
table a reviewer has to cross-check.

readNamedExports (20 lines) stays and is enough: it already throws on
bare export * and on export default. The pin is replaced by one
structural gate — no façade may contain a bare star — which reuses that
rejection rather than adding a regex.

Surface equivalence verified independently, not asserted: main's own
readFacadeExports run over the new façades, compared against main's own
FACADE_SYMBOLS table — 31 subpaths, 0 added, 0 removed.

Red evidence for the new gate: planting export * from '../request-progress.ts'
back into facades/progress.ts fails it with the file named and the reason
quoted; 12 pass / 0 fail once reverted.

Not included: the lowerAndroidTouchPlan tuple-assertion drive-by. It
needs sampleGestureOffsets to carry a min-arity tuple through .map(),
which TypeScript will not infer without a typed helper — a real change to
the gesture-plan contract rather than a drive-by, so it stays out.

Gates: typecheck / lint / format / check:layering / check:production-exports green.


Local gate note. pnpm check:affected --run is flaky on a loaded dev host. Across six runs on three branches — plus a reproduction on unmodified origin/main by a parallel agent — failures came only from the pool android-lifecycle / android-recording / doctor / input-actions / daemon-client, always as timeouts, with a different subset each run. All pass in isolation (verified for this branch). Please treat the CI Integration Tests job on this head as authoritative for provider-integration, per AGENTS.md ("GitHub remains authoritative for reported device/toolchain lanes").

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.97 MB 1.97 MB 0 B
JS gzip 631.7 kB 631.7 kB 0 B
npm tarball 760.3 kB 760.3 kB 0 B
npm unpacked 2.67 MB 2.67 MB 0 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.9 ms 28.8 ms +0.9 ms
CLI --help 68.7 ms 66.1 ms -2.7 ms

Top changed chunks: no changes in the largest emitted chunks.

@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

P1 — rebase this conversion onto current main and regenerate the explicit façade lists from that public surface before deleting facade-symbols.ts.

#1567 landed after this branch forked and added 13 exports that the existing export * façades forwarded: DragOptions in contracts/client; the drag gesture values/types/helpers in contracts/interaction (COORDINATE_GESTURE_KINDS, CoordinateGesturePayload, the three DEFAULT_DRAG_* constants, DragGestureInput, DragGesturePayload, GestureCommandInput, buildDragGesturePlan, dragGesturePayloadFromPositionals, and normalizeGestureCommandInput); and MultiTargetAnnotationV1 in contracts/replay. None appears in this head’s explicit lists.

The #1567 update to facade-symbols.ts is now a modify/delete conflict with this PR. Resolving it by deleting the table while retaining these lists removes those public symbols; it also breaks current in-repo imports, including MultiTargetAnnotationV1 from @agent-device/contracts/replay. The replacement structural gate only rejects bare stars, so it will not catch this narrowed surface. Please carry all 13 exports into the rebase resolution and validate the resulting surface before retiring the table.

Separately, the iOS Smoke job is red on RunnerTests.testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden ("h" vs "hardware-keyboard"). This refactor does not touch that runner path, so it is unrelated to the P1 above, but it remains an owner-action CI failure that needs a rerun or diagnosis before merge.

@thymikee
thymikee force-pushed the claude/explicit-facade-exports branch from cdbfed0 to 328d1de Compare August 5, 2026 11:10
thymikee added a commit that referenced this pull request Aug 5, 2026
Review on #1614 caught this conversion silently narrowing the public
surface. The explicit lists were generated against the surface at fork
time; #1567 landed 13 exports meanwhile — `DragOptions`, the drag-gesture
vocabulary (`COORDINATE_GESTURE_KINDS`, `CoordinateGesturePayload`, the
three `DEFAULT_DRAG_*` constants, `DragGestureInput`, `DragGesturePayload`,
`GestureCommandInput`, `buildDragGesturePlan`,
`dragGesturePayloadFromPositionals`, `normalizeGestureCommandInput`) and
`MultiTargetAnnotationV1`. The `export *` barrels had been forwarding all
13 automatically; the rebase dropped every one, and only a human diff
caught it.

The star-rejection gate could not: it only proves a façade does not WIDEN
invisibly. Narrowing is the failure an explicit list newly makes possible,
because `export *` could not narrow by construction. So the property the
stars gave for free is now asserted directly — every name a re-exported
source declares must appear in the façade.

Scoped to `packages/*/src/facades/`, the barrels this PR converted. A
hand-curated package `index.ts` is a different thing: `ad-replay`
deliberately publishes two values out of a much larger `internal/`, and
forcing exhaustiveness there would widen a surface its owner narrowed on
purpose (#1555). A source that itself carries a bare `export *` is skipped
— unknowable from that file alone, and reachable because the façade
re-exports the starred module directly too, which IS checked.

Red evidence: dropping `MultiTargetAnnotationV1` from facades/replay.ts —
one of the 13 the old gate was blind to — fails with the file, the source
and the symbol named. 13 pass / 0 fail once restored.
@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Follow-up on the earlier P1: the rebase correctly restores the 13 exports added by #1567. I independently compared every contracts subpath against current main’s resolved star surfaces: all 15 match exactly, and the branch is now mergeable.

Two blockers remain:

  • P1 — Fallow is a real owner-action failure, not a flake. Fallow Code Quality reports the three new public re-exports at packages/contracts/src/facades/interaction.ts:41-43 as unused: DEFAULT_DRAG_DESTINATION_HOLD_MS, DEFAULT_DRAG_MOVE_MS, and DEFAULT_DRAG_SOURCE_HOLD_MS. The existing public-façade suppression preserves the other intentional contract exports but omits these three. Add them there (or establish production consumers) and rerun the gate.

  • P2 — the new exhaustiveness regression gate is incomplete for gesture-plan.ts. When readNamedExports hits that source’s bare export *, the test catches the error and continues, skipping that module’s direct exports too. The façade separately names gesture-plan-types, so the transitive type star is covered, but a future direct gesture-plan.ts export — for example removing buildDragGesturePlan alone from the façade — would silently narrow the public surface and still pass this test. Make that source enumerable or have the gate check its direct exports while resolving/handling the star separately.

…n table

Thirteen of the fourteen `@agent-device/contracts` façades were bare
`export *` barrels. `facades/snapshot.ts`, added by #1582, was the one
exception — explicit named re-exports — and that is now the rule.

Everything #1574 built to cope with `export *` goes with them:

  scripts/layering/facade-symbols.ts          -980   (816 pinned names)
  scripts/layering/facade-exports.ts          -192   (readFacadeExports)
  scripts/layering/facade-exports.test.ts     -234   (star semantics)
  scripts/layering/package-boundaries.test.ts  -55

`readFacadeExports` re-implemented ESM `GetExportedNames`/`ResolveExport`
— star-chain resolution, ambiguity rejection, diamond binding identity,
cycle guards, spec-accurate `default` filtering at the star rather than
the source. All of it existed to enumerate what `export *` hides. 523 of
the 816 pinned names belonged to contracts, i.e. to those thirteen files.
Once a façade names its exports, the façade file IS the pin, and it is
visible in the diff of the file that widened rather than in a separate
table a reviewer has to cross-check.

`readNamedExports` (20 lines) stays and is enough: it already throws on
bare `export *` and on `export default`. The pin is replaced by one
structural gate — no façade may contain a bare star — which reuses that
rejection rather than adding a regex.

Surface equivalence verified independently, not asserted: main's own
`readFacadeExports` run over the new façades, compared against main's own
`FACADE_SYMBOLS` table — 31 subpaths, 0 added, 0 removed.

Red evidence for the new gate: planting `export * from '../request-progress.ts'`
back into facades/progress.ts fails it with the file named and the reason
quoted; 12 pass / 0 fail once reverted.

Not included: the `lowerAndroidTouchPlan` tuple-assertion drive-by. It
needs `sampleGestureOffsets` to carry a min-arity tuple through `.map()`,
which TypeScript will not infer without a typed helper — a real change to
the gesture-plan contract rather than a drive-by, so it stays out.
Review on #1614 caught this conversion silently narrowing the public
surface. The explicit lists were generated against the surface at fork
time; #1567 landed 13 exports meanwhile — `DragOptions`, the drag-gesture
vocabulary (`COORDINATE_GESTURE_KINDS`, `CoordinateGesturePayload`, the
three `DEFAULT_DRAG_*` constants, `DragGestureInput`, `DragGesturePayload`,
`GestureCommandInput`, `buildDragGesturePlan`,
`dragGesturePayloadFromPositionals`, `normalizeGestureCommandInput`) and
`MultiTargetAnnotationV1`. The `export *` barrels had been forwarding all
13 automatically; the rebase dropped every one, and only a human diff
caught it.

The star-rejection gate could not: it only proves a façade does not WIDEN
invisibly. Narrowing is the failure an explicit list newly makes possible,
because `export *` could not narrow by construction. So the property the
stars gave for free is now asserted directly — every name a re-exported
source declares must appear in the façade.

Scoped to `packages/*/src/facades/`, the barrels this PR converted. A
hand-curated package `index.ts` is a different thing: `ad-replay`
deliberately publishes two values out of a much larger `internal/`, and
forcing exhaustiveness there would widen a surface its owner narrowed on
purpose (#1555). A source that itself carries a bare `export *` is skipped
— unknowable from that file alone, and reachable because the façade
re-exports the starred module directly too, which IS checked.

Red evidence: dropping `MultiTargetAnnotationV1` from facades/replay.ts —
one of the 13 the old gate was blind to — fails with the file, the source
and the symbol named. 13 pass / 0 fail once restored.
Two review findings, plus a third the gate caught on itself.

P1 — the three `DEFAULT_DRAG_*` constants join the existing public-façade
suppression, alongside `COORDINATE_GESTURE_KINDS` and
`normalizePublicGesture` which the same conversion surfaced. All five are
#1567's drag vocabulary, made individually visible to `--production`
analysis for the first time because a bare star used to hide them from
that exact check. Kept rather than narrowed, for the reason the existing
entry already states: the façade's surface stays byte-identical to what
the retired pin table asserted, and narrowing is a follow-up with its own
review.

P2 — the exhaustiveness gate skipped any source carrying a bare
`export *`, which dropped that module's DIRECT exports from the check too.
`gesture-plan.ts` stars `gesture-plan-types.ts`, so removing
`buildDragGesturePlan` from the façade narrowed the public surface and
still passed. `readDirectNamedExports` now reads exactly the names a module
declares or re-exports BY NAME and ignores the star, so direct exports are
checked while the starred set stays covered by the façade's own direct
re-export of that module.

Red evidence: removing `buildDragGesturePlan` from facades/interaction.ts
now fails naming file, source and symbol; 13 pass / 0 fail restored.

Third, and the reason the gate is worth having: rebasing onto main after
#1612 merged silently dropped `TEXT_ENTRY_ROUTES`, `TextEntryRoute` and
`TypeTextBackendResult` from the interaction façade — the same narrowing
class as the #1567 one review caught by hand, one merge later. The gate
failed on it before CI did. Restored.
@thymikee
thymikee force-pushed the claude/explicit-facade-exports branch from 328d1de to 5196cf7 Compare August 5, 2026 12:50
@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Both blockers addressed on 5196cf7, plus a third the new gate caught on itself.

P1 — suppression extended. The three DEFAULT_DRAG_* constants join the existing public-façade entry, together with COORDINATE_GESTURE_KINDS and normalizePublicGesture which the same conversion surfaced for the same reason. check:production-exports is green. The rationale comment now names them as #1567's drag vocabulary explicitly.

P2 — real hole, fixed. You were right that continue dropped the starred module's direct exports from the check. readDirectNamedExports now reads exactly what a module declares or re-exports by name and ignores the star; the starred set stays covered because the façade re-exports the starred module directly too, and that path is checked on its own turn.

Red evidence: removing buildDragGesturePlan from facades/interaction.ts now fails with file, source and symbol named — it passed silently before. 13 pass / 0 fail restored. (My first attempt to plant this used a sed that didn't match the multi-line list and reported a false green; worth knowing if you reproduce it.)

Third — the gate paid for itself during this fix. Rebasing onto main after #1612 merged silently dropped TEXT_ENTRY_ROUTES, TextEntryRoute and TypeTextBackendResult from the interaction façade. Same narrowing class as the #1567 one you caught by hand, one merge later, and the gate failed on it before CI did. Restored.

That is twice now that this conversion has narrowed the surface on a rebase, which I think settles whether the gate earns its keep.

Gates: typecheck / lint / format / check:layering / check:production-exports / check:fallow all green.

🤖 Addressed by Claude Code

@thymikee
thymikee marked this pull request as ready for review August 5, 2026 12:50
@thymikee

thymikee commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 5196cf7cb11756bc03c6e4a614e23eeddd081076: clean and ready for human review.

The prior findings are resolved: all three DEFAULT_DRAG_* exports have the scoped Fallow treatment, and the façade-exhaustiveness gate now retains direct named exports from sources that also carry export *, covering buildDragGesturePlan. Public contract-façade surface parity remains exact, including the restored #1612 text-entry exports (TEXT_ENTRY_ROUTES, TextEntryRoute, and TypeTextBackendResult).

The structural gates and all GitHub checks are green. This is a surface-preserving structural refactor, so runtime/device evidence is not required. No blockers found.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 5, 2026
@thymikee
thymikee merged commit d8b309c into main Aug 5, 2026
28 checks passed
@thymikee
thymikee deleted the claude/explicit-facade-exports branch August 5, 2026 13:58
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-05 13:58 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