Skip to content

architecture: touch behavior comes from shared internal modules; Core and Lab reach them through ./internal/* (INV13) - #8464

Merged
cixzhang merged 3 commits into
mainfrom
spec/touch-runtime-injection
Oct 26, 2026
Merged

cixzhang merged 3 commits into
mainfrom
spec/touch-runtime-injection

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Decided (Cindy, 2026-10-26): touch behavior comes from a set of reusable internal modules that components import. There's no install step. This supersedes the 2026-10-25 install-entry and dependency-check designs. Core and Lab components may use Core's internals (2026-10-26).

Summary

Every touch component shares one copy of the gesture and touch code instead of each carrying its own. An app does nothing to get touch behavior, and a page loads the shared code once however many components use it. People using a mouse get no touch listeners.

  • architecture:react-component-runtime INV13: one always-present set of internal gesture and touch modules (gesture core, recognizers, arena, animator, touch press recognizer).
    • Components import them statically, and the bundler keeps one copy per page.
    • There is no install call, no public entry, and no public installed check; tests reset through an internal hook.
    • Under a fine primary pointer, nothing attaches (spec:AST-094 FR19, FR24).
  • Internal subpaths: Core and Lab reach the modules through @astryxdesign/core/internal/touch. Core's ./internal/* subpaths are reserved for Astryx's own packages: undocumented, outside the compatibility promise, versioned in lockstep with Core, and lint-limited to Core and Lab. The rule is general, so other internals Lab needs (for example a reorder module) use the same text.
    • spec:AST-017 FR21: behavior modules stay unexported except through these subpaths.
    • architecture:public-component-api INV12: these subpaths are the one reachable-but-reserved exception.
  • spec:AST-080 DEC-5 (cixzhang 2026-10-26): touch primitives over the budget come from the shared internal modules, or the cost is justified. This supersedes DEC-4's per-component lazy-load remedy; DEC-4's reporting and budget stand. architecture:component-style-authoring INV6 says the same.
  • spec:AST-094 is unchanged: touch defaults stay plain, with no install condition.

Writing rubric results

  • R1 present tense, no history in the record: pass.
  • R2 research: none in the records.
  • R3 verification: INV13's row checks one copy per page, no bundled primitive copies, no import(), no public export or install API, and no touch listener under a fine pointer.
  • R4 rejected alternatives: one line in AST-080 DEC-5.
  • R5 change references: none in the records.
  • R6 open questions: none.

@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 26, 2026 5:53pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 26, 2026, 8:58 PM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 25, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 25, 2026
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded cancelled. Current analysis is unavailable.

View Storybook for this PR · View Sandbox for this PR


Generated by PR Enrichment workflow | View current CI run

@cixzhang

cixzhang commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Posted by Cindy's agent on her behalf.

Cindy's direction on touch code: there should be one global touch and gesture module that components can leverage when it exists.

  • One build per component, with no lazy import() inside components.
  • Components detect the global module and use it when it's present, and keep their plain behavior when it isn't.
  • Desktop loads nothing extra.

The install shape (how an app or the server installs the module, for example a module-level install vs a provider) is left to vjeux's dependency-injection proposal. This record holds that open until then.

Evidence to work from:

  • Size audit: Switch adds ~28 KB gz today on desktop and phone alike. ~25 KB of it is the shared gesture core and animator, ~4 KB is Switch's own, and the shared runtime drops to ~17–19 KB gz after the routed reductions.
  • First tap, measured at 4× CPU on Fast 3G: with a startup or server preload, the module is ready ~0.9 s before the app is interactive. Without one, it's ready 0.72 s after the first touch, while that tap still clicks natively at once.

Where component touch logic lives (measured, gzip, upper bounds per component):

  • Component-side touch code today: ContextMenu ~11.4 KB, SegmentedControl ~7.4, TreeList ~2.5, Item swipe ~1.8, Switch ~1.7, NumberInput ~0.4.
  • Folding every controller into the global module: about 42–47 KB on every phone page after reductions, whatever the page uses.
  • Glue in each component over the global primitives: the global module is about 18–20 KB after reductions, but desktop also pays each component's glue, because a runtime check can't be tree-shaken.
  • A tiny adapter in each component (~0.1–0.3 KB) whose controller the global module loads on first use: desktop pays only the adapters, and a phone pays the primitives once plus what the page uses.

@cixzhang cixzhang changed the title architecture: proposed: touch behavior comes from one injected runtime (INV13) architecture: proposed: one global touch and gesture module (INV13) Oct 25, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval of this proposal waits on an engineering owner's decision. INV13 adds a new architecture invariant and the INV6 rewrite changes a current one, so an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. That decision should also settle whether a touch device whose app never installs the module may fall back to fine-pointer behavior. Whatever is decided, these fixes are needed first, since a new commit resets approval:

  • INV13 conflicts with spec:AST-094 DEC-1 ("by default, without an opt-in"): either require the module on every coarse-pointer page or amend DEC-1 in this pull request.
  • The INV6 rewrite drops the lazy-loading remedy that spec:AST-080 DEC-4 still grants: keep lazy loading for touch-only code outside the module, or amend DEC-4 to match.
  • INV13's verification row should fail on a second registry or a second installation.

[Automated review]

Comment on lines +163 to +168
gesture core and the animator, held in one module-level registry and
installed once for the page by the app or its server. Each Core component
ships one build, detects the module, and uses it when it is installed;
without it the component keeps its plain behavior, the one it has under a
fine pointer, and stays fully usable, and a tap acts as a click and is never
lost. No component loads the module itself or holds an `import()` of it, so

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, a phone page whose app or server never installs the module gets each component's fine-pointer behavior. spec:AST-094 DEC-1 says "Every touch device gets one native-feeling touch behavior by default, without an opt-in", and this pull request leaves it unchanged. Either require the module on every coarse-pointer page (OQ2 then decides only how), or amend AST-094's Intent and DEC-1 here.

Comment on lines +104 to +107
to one component is flagged. Its pull request either takes that JavaScript
from the global touch and gesture module (`architecture:react-component-runtime`
INV13, proposed), so the component's import does not carry it, or justifies
the cost. For flagged CSS, its pull request

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV6 still names spec:AST-080/DEC-4 as its deciding spec, and DEC-4 says "JavaScript can load lazily, only when a coarse pointer needs it, or be justified". This rewrite drops that remedy for every touch-only feature, including layer chunks the description keeps outside INV13. Keep lazy loading for touch-only code outside the global module, or amend DEC-4 to match.

| INV9 | Provider/external-store identity and render-count tests | Unchanged semantic payload changes identity, or snapshot identity creates a render loop |
| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change |
| INV13 (proposed) | Source boundary check plus real-browser tests on a throttled phone profile, with and without the module installed, and on a hybrid device | An `import()` of the module in a component, touch code in a component's own build beyond OQ3's answer, a lost first tap, a component unusable without the runtime, or a listener under a fine pointer fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV13's core claim is one registry, installed once for the page, and react-component-runtime INV5 lets a document-owned singleton persist only when "tests prove one shared installation". Consider adding "a second registry or a second installation" to this failure signal.

@cixzhang

Copy link
Copy Markdown
Contributor Author

Posted by Cindy's agent on her behalf.

Cindy's decision on where component touch code lives: a dependency check. Each component checks whether the global touch and gesture module is loaded. If it is, the component runs its touch behavior using that module's primitives; if not, it behaves as it does today.

  • The global module carries the shared primitives and loads only when the app installs it.
  • Each component keeps a small piece of glue that uses those primitives when they're present. Measured glue cost on desktop: Switch about 1.7 KB gzipped, SegmentedControl about 7.4 KB, ContextMenu about 11.4 KB, TreeList about 2.5 KB, Item swipe about 1.8 KB, NumberInput about 0.4 KB. Trimming the ContextMenu and SegmentedControl glue is a follow-up.
  • How the module gets installed is still open, pending vjeux's dependency-injection proposal.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval of this proposal waits on an engineering owner's decision: INV13 adds a new architecture invariant and the INV6 rewrite changes a current one, so an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. That decision should also settle whether a phone whose app never installs the module may keep fine-pointer behavior, as "loads only when the app installs it" in the latest comment implies. Because a new commit resets approval, these fixes belong first:

  • INV13 conflicts with spec:AST-094 DEC-1 and its Intent ("by default, without an opt-in"): amend them in this pull request, or require the module on every coarse-pointer page.
  • The INV6 rewrite drops the lazy-loading remedy that spec:AST-080 DEC-4 still grants: keep lazy loading for touch-only code outside the module, or amend DEC-4.
  • INV13's verification row should fail on a second registry or installation (react-component-runtime INV5); its stray trailing "fails" and the "(decided by …)" note should go.

[Automated review]

Comment on lines +164 to +167
installed once for the page by the app or its server. Each Core component
ships one build, detects the module, and uses it when it is installed;
without it the component keeps its plain behavior, the one it has under a
fine pointer, and stays fully usable, and a tap acts as a click and is never

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, a phone page whose app or server never installs the module keeps each component's fine-pointer behavior. spec:AST-094 DEC-1 says "Every touch device gets one native-feeling touch behavior by default, without an opt-in", and its Intent says the same; this pull request leaves both unchanged. Either amend AST-094's Intent and DEC-1 here, or require the module on every coarse-pointer page and leave OQ2 to decide only how.

Comment on lines +104 to +107
to one component is flagged. Its pull request either takes that JavaScript
from the global touch and gesture module (`architecture:react-component-runtime`
INV13, proposed), so the component's import does not carry it, or justifies
the cost. For flagged CSS, its pull request

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV6 still names spec:AST-080/DEC-4 as its deciding spec, and DEC-4 says "JavaScript can load lazily, only when a coarse pointer needs it, or be justified". This rewrite drops that remedy for every touch-only feature, including lazy layer chunks the description keeps outside INV13. Keep lazy loading for touch-only code outside the global module, or amend DEC-4 to match.

| INV9 | Provider/external-store identity and render-count tests | Unchanged semantic payload changes identity, or snapshot identity creates a render loop |
| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change |
| INV13 (proposed) | Source boundary check plus real-browser tests on a throttled phone profile, with and without the module installed, and on a hybrid device | An `import()` of the module in a component, touch code in a component's build other than glue over the module's primitives, a lost first tap, a component unusable without the runtime, or a listener under a fine pointer fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV13's core claim is one registry installed once for the page, and react-component-runtime INV5 lets a document-owned singleton persist only when "tests prove one shared installation". Add "a second registry or a second installation" to this failure signal, and drop the trailing "fails".

glue in that component over the module's primitives: the component checks
whether the module is present and, if it is, runs its touch behavior with
the module's primitives, so each component carries its glue in its own
build (decided by `cixzhang`, 2026-10-25).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: "(decided by cixzhang, 2026-10-25)" is decision history. architecture:knowledge-contracts INV10 keeps it out of a record ("a record states the fact a change established, not the change"), and the rubric's R1 flags it; the decision can stay in this pull request's conversation.

@cixzhang

cixzhang commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Posted by Cindy's agent on her behalf.

Cindy's decision on how the touch module is installed: TouchSupport.

import TouchSupport from '@astryxdesign/core/touch-support';
TouchSupport.install(); // once, in the client entry, before render or hydrate
  • install() can be called more than once safely, and it does nothing on the server, so server HTML doesn't change. An optional TouchSupport.isInstalled() reports whether it has run. Nothing else is exposed on the object.
  • Components find the module through one internal hook, the dependency check. Without it, they keep today's plain behavior.
  • Recognizers, the arena, the animator and press tracking stay off the public exports and docs. Core and Lab components can still use them through a package-private path.
  • The doc comment will say that motion such as useAnimatedLayout works without it, that mouse-only users pay nothing, that swipe-only actions still need a non-touch path, and that Reduce Motion is respected.

vjeux, please build your install proposal on this name and shape.

What this means for the "by default" wording: touch presentations come by default on pages that install TouchSupport. Module-free behavior, such as press feedback, works without it. The earlier "no opt-in" wording in spec:AST-094 is superseded by Cindy's decisions on 2026-10-25 (components use the global touch module when it's present, and the app installs it with TouchSupport.install()). This PR updates AST-094's intent and the matching public-component-api sentence to match.

@cixzhang
cixzhang force-pushed the spec/touch-runtime-injection branch from 3a58015 to 0b00fc8 Compare October 26, 2026 04:34
@cixzhang cixzhang changed the title architecture: proposed: one global touch and gesture module (INV13) architecture: one global touch and gesture module, installed by TouchSupport (INV13) Oct 26, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval of this proposal waits on an engineering owner's decision: INV13 adds a new architecture invariant with a public TouchSupport entry and the INV6 rewrite changes a current one, so an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Because a new commit resets approval, these fixes belong first:

  • Amend spec:AST-094 Intent and DEC-1 ("by default, without an opt-in"), and architecture:public-component-api INV16's default sentence, for the app-installed module.
  • Scope INV13's fallback ("the one it has under a fine pointer") and its failure signal to behavior that runs on the module, so released touch behavior such as press feedback, swipes and sheets keeps working without the call.
  • Reconcile the INV6 rewrite with spec:AST-080 DEC-4, which still grants lazy loading.
  • Give Lab a path to the internals that resolves outside Core's exports map, say whether install() skips the module under a fine pointer, and add the spec:AST-002 FR5 admission argument to the description.

[Automated review]

Comment on lines +171 to +173
touch logic is small glue in its own build. Without it the component keeps
its plain behavior, the one it has under a fine pointer, and stays fully
usable, and a tap acts as a click and is never lost. No component loads the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, a phone whose app never calls TouchSupport.install() keeps every component's fine-pointer behavior. spec:AST-094 DEC-1 says "Every touch device gets one native-feeling touch behavior by default, without an opt-in", and this pull request leaves it and the Intent unchanged. The sentence also reaches released touch behavior that needs no module, such as the usePressFeedback press paint, Item swipes and the menu and Selector sheets. Consider "Without it, behavior that needs the module's primitives keeps its fine-pointer form, and the component's other behavior is unchanged", with an AST-094 amendment for the opt-in.

Comment on lines +179 to +181
and Lab components reach them through a package-private path that is not in
the exports map and that lint limits to Core and Lab, where Lab's access
follows Lab's own module-sharing rule. `TouchSupport`'s documentation says

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astryxdesign/lab imports Core only through Core's exports map, so a path outside it does not resolve from Lab's published files (#8076 hits "Package path ./gestures is not exported"). Either limit this path to Core, or name an exported internal channel, which architecture:public-component-api INV12 would count as public.

Comment on lines +182 to +185
that motion such as `useAnimatedLayout` works without it, that it is called
once in the client entry before render or hydrate, that people using only a
mouse pay nothing, that a swipe-only action still needs a non-touch path,
and that Reduce Motion is respected.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in INV13 makes "people using only a mouse pay nothing" true for an app that calls install() in its client entry, which runs for every visitor. Say that install() loads the gesture core and animator only where the primary pointer is coarse, or state what a fine-pointer visitor downloads.

Comment on lines +104 to +107
to one component is flagged. Its pull request either takes that JavaScript
from the global touch and gesture module (`architecture:react-component-runtime`
INV13), so the component's import does not carry it, or justifies
the cost. For flagged CSS, its pull request

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV6 still names spec:AST-080/DEC-4 as its deciding spec, and DEC-4 says "JavaScript can load lazily, only when a coarse pointer needs it, or be justified". This drops that remedy for every touch-only feature, including code outside the module. Keep lazy loading for touch-only JavaScript outside the module, or amend DEC-4 here.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval of this proposal waits on an engineering owner's decision: INV13 adds a new architecture invariant with a public TouchSupport entry, and the INV6, spec:AST-094 DEC-12 and INV16 edits change current records, so an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. That decision should also settle how Lab reaches the touch internals. The latest revision fixes the AST-094 opt-in contradiction, the fallback's scope and the admission argument; because a new commit resets approval, these fixes belong first:

  • Supersede spec:AST-080 DEC-4's lazy-load remedy in AST-080 itself and list that decision as INV6's deciding spec, instead of retiring it from INV13.
  • Aim the INV13 row's "touch code in a component's build other than glue" signal at the module's primitives, so released press-feedback and useMenuPress code passes it.
  • Replace "people using only a mouse pay nothing", which install() loading the module under every pointer makes untrue, or skip the load under a fine pointer.
  • Give Lab a path that resolves from its published build, say what "press tracking" covers, and carry the install condition into AST-094 FR31 and FR33.

[Automated review]

mouse pay nothing, that a swipe-only action still needs a non-touch path,
and that Reduce Motion is respected. Touch JavaScript comes from this module
rather than a lazy load (`architecture:component-style-authoring` INV6),
which replaces the lazy-load allowance `spec:AST-080` DEC-4 recorded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV13 says it replaces spec:AST-080 DEC-4's lazy-load allowance, but DEC-4 stays current and still says "JavaScript can load lazily, only when a coarse pointer needs it, or be justified", and INV6 still names spec:AST-080/DEC-4 as its deciding spec. Supersede that remedy in AST-080 itself, as DEC-12 does for AST-094, and list the new decision as INV6's deciding spec; or keep lazy loading for touch-only code outside the module.

| INV9 | Provider/external-store identity and render-count tests | Unchanged semantic payload changes identity, or snapshot identity creates a render loop |
| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change |
| INV13 | Source boundary check plus real-browser tests on a throttled phone profile, with and without the module installed, and on a hybrid device | An `import()` of the module in a component, touch code in a component's build other than glue over the module's primitives, a lost first tap, a component unusable without the module, a module-free touch behavior such as press feedback that stops without it, or a listener under a fine pointer fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row fails "touch code in a component's build other than glue over the module's primitives" and also "a module-free touch behavior such as press feedback that stops without it". Press feedback is touch code in every pressable's build that does not run on the module (utils/pressFeedback.ts, installed by usePressFeedback()), and so is useMenuPress's finger handling, so no implementation passes both. Consider "a copy of a recognizer, the arena or the animator in a component's build".

Comment on lines +185 to +186
once in the client entry before render or hydrate, that people using only a
mouse pay nothing, that a swipe-only action still needs a non-touch path,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"People using only a mouse pay nothing" contradicts "install() loads the module under every pointer" above: the install runs in the client entry for every visitor, so an installing app's mouse users download the module, and every import carries its component's glue. Say what a fine-pointer visitor pays, or have install() skip loading where the primary pointer is fine.

Comment on lines +181 to +182
and Lab components reach them through a package-private path that resolves
without Core's exports map and that lint limits to Core and Lab, where Lab's access

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astryxdesign/lab is a separate package published to npm's canary tag as Babel output, so its imports of Core resolve through Core's exports map; a path that "resolves without Core's exports map" is out of its reach, and no record defines "Lab's own module-sharing rule". Limit the path to Core, as INV10 and INV11 do, or name an exported internal subpath, which architecture:public-component-api INV12 counts as public.

it, such as a desktop page, loads nothing extra. `install()` loads the module
under every pointer, and where the primary pointer is fine it registers
nothing (`spec:AST-094` FR24). The recognizers, the arena, the animator, and press
tracking are not public exports and are not documented for consumers; Core

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Press tracking" names no module, and Core's press-tracking exports are the released, documented useMenuPress and usePressFeedback hooks, which the INV13 install row's "an exported or documented internal" signal would then fail. Name the internal piece, or say those hooks stay public.

default on pages that install TouchSupport, and module-free behavior such as
press feedback works without it, following the owner's 2026-10-25 decisions
that components use the module when it is present and that an app installs it
with `TouchSupport.install()`. This supersedes DEC-1's "without an opt-in".

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEC-12 qualifies DEC-1, but FR31 ("Under a finger, on every touch device, an anchored menu or a Popover MUST open by growing out of its trigger") and FR33 still carry no install condition. State the condition once in the Requirements, or in FR31 and FR33.

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Oct 26, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction in this pull request has engineering-owner approval for this head, but two record defects need fixing first:

  • INV13 says Core and Lab reach the touch internals through a package-private entry "that Lab's published build resolves, that is not in Core's exports map", yet Lab's published package resolves Core only through that map and spec:AST-017 FR21 keeps gesture modules out of it, so limit the entry to Core (Lab keeping its own touch handling, as INV10 and INV11 have it) or name an exported internal subpath and amend FR21 and architecture:public-component-api INV12.
  • spec:AST-094 FR33's lift sentence still says "On every touch device"; give it the install condition FR31 and FR33's first sentence now carry, or state that the lift runs without the module.

Also consider replacing the useAnimatedLayout example, which does not exist on main yet, and adding INV13 to the record's react-runtime routing. A new commit resets the approval, so an engineering owner re-approves the fixed head by commenting /approve-spec <full head SHA>.

[Automated review]

Comment on lines +183 to +186
and Lab components reach them through a package-private entry that Lab's
published build resolves, that is not in Core's exports map, and that lint
limits to Core and Lab, where Lab's access
follows Lab's own module-sharing rule. `TouchSupport`'s documentation says

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astryxdesign/lab is published to npm's canary tag as Babel output, so its Core imports resolve only through Core's exports map; an entry "that is not in Core's exports map" does not resolve from Lab's published build, and adding it to the map conflicts with spec:AST-017 FR21. No record defines "Lab's own module-sharing rule" either. Limit the entry to Core (Lab keeps its own touch handling, as INV10 and INV11 have it), or name an exported internal subpath and amend FR21 and architecture:public-component-api INV12.

published build resolves, that is not in Core's exports map, and that lint
limits to Core and Lab, where Lab's access
follows Lab's own module-sharing rule. `TouchSupport`'s documentation says
that motion such as `useAnimatedLayout` works without it, that it is called

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useAnimatedLayout does not exist on main yet; it is only proposed elsewhere. Name motion that exists, or describe it without a name.

Comment on lines +341 to +342
- **FR33 — A ContextMenu under a finger.** On a page that installs
TouchSupport (DEC-12), with a finger or pen, the long

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence now carries the install condition, but the lift sentence a few lines below still reads "On every touch device, a ContextMenu that a held finger opens on a block trigger MUST lift the trigger". ContextMenu's long press runs without the module, so give the lift the same condition, or state that it runs without the module.

@cixzhang
cixzhang force-pushed the spec/touch-runtime-injection branch from 40df6fb to da0b25c Compare October 26, 2026 06:38
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 26, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

Posted by Cindy's agent on her behalf.

Cindy's decision: Lab components may use Core's internals. Core exports undocumented internal subpaths (@astryxdesign/core/internal/touch, @astryxdesign/core/internal/reorder, and others as needed). They aren't public API: they're absent from the docs, outside the compatibility promise, versioned with Core, and a lint rule limits them to Core and Lab. spec:AST-017 FR21 and architecture:public-component-api INV12 will say that internal subpaths aren't public.

What this means here:

@cixzhang

Copy link
Copy Markdown
Contributor Author

Posted by Cindy's agent on her behalf.

Thanks, vjeux. Cindy has simplified the touch model: there's no install step. The gesture and touch code is a set of reusable internal modules that components import directly, and the bundler keeps one copy per page. That replaces TouchSupport.install().

  • No public touch-support entry, and no install() or isInstalled(). An internal test reset is still fine.
  • Under a mouse or another fine pointer, nothing attaches listeners (spec:AST-094 FR19 and FR24 are unchanged).
  • Lab components reach the same modules through Core's internal subpaths (@astryxdesign/core/internal/touch and similar).

What this means per PR:

If these stay idle, we'll make the changes after about 30 minutes, keeping your authorship on the commits.

@cixzhang cixzhang changed the title architecture: one global touch and gesture module, installed by TouchSupport (INV13) architecture: touch behavior comes from shared internal modules; Core and Lab reach them through ./internal/* (INV13) Oct 26, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval of this proposal waits on an engineering owner's decision. INV13 and spec:AST-080 DEC-5 are new, and the head rewrites architecture:public-component-api INV12, spec:AST-017 FR21 and the INV6 remedy, so an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author, who is an engineering owner, may do so. A new commit resets that approval, so these fixes belong first:

  • Carve ./internal/* out of spec:AST-017's FR21–FR24 verification row, its Current-state impact line and FR25 ("An internal label ... does not weaken this rule"), which still forbid the new exports.
  • Do the same for public-component-api's INV12/INV13 verification row ("A reachable supporting type or behavior is called internal"), and give INV12 the Core-and-Lab limit that FR21 and INV13 state.
  • Add a failure signal for importing @astryxdesign/core/internal/* outside Core and Lab to INV13's verification row, and list DEC-5 in component-style-authoring's Deciding specs section.

[Automated review]

Comment on lines +376 to +379
never exported, as experimental or otherwise, except through Core's
`./internal/*` subpaths, which are reserved for Astryx's own packages,
undocumented, outside the compatibility promise, versioned in lockstep with
Core, and limited by lint to Core and Lab (`architecture:react-component-runtime` INV13). In Core, an experimental public

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception still fails this record's own checks. The FR21–FR24 verification row fails when "a behavior, utility, or internal module without a public contract is exported" (line 916), Current-state impact says Core's behavior modules "stay unexported (FR21)" (878-879), and FR25 says "An internal label ... does not weaken this rule" for export changes (430-439). Carve ./internal/* out of each, or state the exception where FR25 and Platform support set the promise's scope.

Comment on lines +139 to +141
behind a non-exported boundary. Core's `./internal/*` subpaths are the one
exception: reachable, but reserved for Astryx's own packages, undocumented,
and outside the compatibility promise (`spec:AST-017` FR21).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The INV12/INV13 verification row (line 276) still fails "A reachable supporting type or behavior is called internal", which is exactly what ./internal/* is, so it needs the same carve-out. This sentence also says "reserved for Astryx's own packages" without the "limited by lint to Core and Lab" bound that AST-017 FR21 and INV13 state.

| INV10 | Shared test over every adopter of the announcement module | An adopter holding its own Effect, observer, timer, or listener for its own-content announcement |
| INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change |
| INV12 | Source boundary check plus real-browser tests on a throttled phone profile over a 10-level by 50-sibling tree and two changes in one frame | A per-frame height or layout, a walk past the area or into a subtree other than `display: contents`, an element moved twice for one frame, an added element, or an exported reporting function fails |
| INV13 | Source boundary check plus bundle analysis of a page with several touch components, and real-browser tests on a throttled phone profile, under a fine pointer, and on a hybrid device | A second copy of a touch module on one page, a component bundling its own copy of a module's primitive, an `import()` of a module in a component, a public export or doc entry for the modules, an install call or installed check in the public API, or a touch listener under a fine primary pointer fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV13 says the subpath is "limited by lint to Core and Lab" (line 191), but this row has no failure signal for it and no such lint rule exists yet. Suggest adding "an import of @astryxdesign/core/internal/* outside Core and Lab" to the failure signal.

deciding_specs:
[
spec:AST-080/DEC-4,
spec:AST-080/DEC-5,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ## Deciding specs section (line 279) still credits INV6 to DEC-4 alone. A line for DEC-5 there would tell its readers that the lazy-loading remedy was replaced.

…upport; Core and Lab reach internals through ./internal/* (INV13, AST-017 FR21, public-component-api INV12)
…l modules; no install (INV13; AST-080 DEC-5; AST-094 back to plain defaults)
@cixzhang
cixzhang force-pushed the spec/touch-runtime-injection branch from ad6db58 to b1d6f9a Compare October 26, 2026 17:43
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec b1d6f9a

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ready to merge: an engineering owner approved this exact head, and the records now carve Core's reserved ./internal/* subpaths out of the spec:AST-017 and architecture:public-component-api checks that forbade them, with INV13's verification row covering the Core-and-Lab lint limit. No further changes are needed in this pull request.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — b1d6f9ad Deployed Oct 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants