From c84e0e1485d516aa564502f3b56d01ebba8ff55f Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 09:44:41 -0700 Subject: [PATCH 1/3] architecture: one global touch and gesture module installed by TouchSupport; Core and Lab reach internals through ./internal/* (INV13, AST-017 FR21, public-component-api INV12) --- .../architecture/component-style-authoring.md | 15 +++-- docs/architecture/public-component-api.md | 10 +++- docs/architecture/react-component-runtime.md | 58 +++++++++++++++---- docs/specs/AST-017/spec.md | 5 +- .../specs/AST-080-package-stylesheets/spec.md | 13 +++++ .../specs/AST-094-touch-presentations/spec.md | 25 ++++++-- 6 files changed, 102 insertions(+), 24 deletions(-) diff --git a/docs/architecture/component-style-authoring.md b/docs/architecture/component-style-authoring.md index ba98105e5d519..ec0f63514c4e4 100644 --- a/docs/architecture/component-style-authoring.md +++ b/docs/architecture/component-style-authoring.md @@ -17,7 +17,13 @@ verified_by: internal/eslint-plugin-astryx/no-stylex-null-override.test.mjs, internal/eslint-plugin-astryx/no-style-only-wrapper.test.mjs, ] -deciding_specs: [spec:AST-080/DEC-4, spec:AST-017/DEC-26, spec:AST-017/DEC-29] +deciding_specs: + [ + spec:AST-080/DEC-4, + spec:AST-080/DEC-5, + spec:AST-017/DEC-26, + spec:AST-017/DEC-29, + ] --- # Component style authoring architecture @@ -101,9 +107,10 @@ not evidence of conformance or violation. in, including its transitive modules, and that component's gzip CSS delta in its package stylesheet (`spec:AST-080` FR8). A feature that runs only on a coarse pointer and adds more than 2 KB gzip of JavaScript or 1 KB gzip of CSS - to one component is flagged. Its pull request either loads that JavaScript - lazily, only when a coarse pointer needs it, so the component's import does - not carry it, or justifies the cost. For flagged CSS, its pull request + 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 justifies the cost, because that CSS stays in the package stylesheet (`spec:AST-080` FR1, FR8). Sizes are gzip at the default compression level, and 1 KB is 1,024 bytes; measured as `spec:AST-080` FR8 measures CSS, a diff --git a/docs/architecture/public-component-api.md b/docs/architecture/public-component-api.md index c84890ad9f231..f9fe56030ebd5 100644 --- a/docs/architecture/public-component-api.md +++ b/docs/architecture/public-component-api.md @@ -136,7 +136,9 @@ guidance owns the process used to propose and test APIs. return, utility signature, default, or operation reachable from a supported package path is public even when its source comment, filename, or current use calls it internal. Privacy requires removing the export or moving the capability - behind a non-exported boundary. + 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). - **INV13 — Public deltas route to their authority owner.** Every public API update and every public behavior change follows the result and disposition defined by `architecture:knowledge-contracts`. Tests, a Changeset, or a generated manifest @@ -155,8 +157,10 @@ guidance owns the process used to propose and test APIs. such as touch tracking in an open list, takes one `presentation` prop, and a value of it, such as `presentation="popover"`, keeps the surface and input mode the component uses with a fine pointer on every device. Touch behavior - is the component's default where it serves touch users, and a caller's - explicit choice always wins over that default. Every internal touch mode of + is the component's default where it serves touch users, on pages that + install TouchSupport (`architecture:react-component-runtime` INV13), with + module-free behavior such as press feedback working without it, and a + caller's explicit choice always wins over that default. Every internal touch mode of that kind is reachable by callers through a public prop or value; none is hard-coded beyond a caller's reach. Touch styling, such as press paint or a text-size floor, and touch-only behavior that its own record defines, such as diff --git a/docs/architecture/react-component-runtime.md b/docs/architecture/react-component-runtime.md index 55aa43644fe40..30179ab916613 100644 --- a/docs/architecture/react-component-runtime.md +++ b/docs/architecture/react-component-runtime.md @@ -36,7 +36,8 @@ deciding_specs: [spec:AST-086/DEC-2, spec:AST-099/DEC-1, spec:AST-105/DEC-1] "INV8", "INV10", "INV11", - "INV12" + "INV12", + "INV13" ] } } @@ -174,6 +175,37 @@ those contracts. each moving element is one the page already renders (`architecture:component-style-authoring` INV7). The module follows INV3 and INV5. +- **INV13 — One global touch and gesture module.** Core has one global touch + and gesture module, the gesture core and the animator, held in one + module-level registry. An app installs it once in its client entry, before + it renders or hydrates, with `TouchSupport.install()` from + `@astryxdesign/core/touch-support`. `install()` is idempotent and does + nothing on the server, so server HTML is unchanged; `TouchSupport.isInstalled()` + reports whether it ran; nothing else is reachable from the object. Each + Core component ships one build and reaches the module only through one + internal hook that checks whether it is installed; when it is, the + component runs its touch behavior with the module's primitives, and its own + touch logic is small glue in its own build. Without it, behavior that runs on the module + takes its plain form, the one it has under a fine pointer, and the component + stays fully usable; a tap acts as a click and is never lost; and touch + behavior that does not run on the module, such as press feedback, keeps + working. No component loads the + module itself or holds an `import()` of it, so a page that does not install + it, such as a desktop page, loads nothing extra. `install()` loads the module + only where some input is a coarse pointer (`any-pointer: coarse`), and loads it + when a coarse pointer appears later, such as a tablet attached to a laptop, + so a page used only with a mouse loads nothing, and where the primary pointer is fine + the module registers nothing (`spec:AST-094` FR24). The recognizers, the arena, the animator, and the module's + touch press recognizer are not public exports and are not documented for consumers; Core + and Lab components reach them through `@astryxdesign/core/internal/touch`, + a subpath reserved for Astryx's own packages: undocumented, outside the + compatibility promise, versioned in lockstep with Core, and limited by lint + to Core and Lab (`spec:AST-017` + FR21). `TouchSupport`'s documentation says + that layout and presence motion 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. ## Authoring guidance @@ -310,14 +342,16 @@ observable-behavior predicates require review or runtime evidence. Likewise, a focused test proves only its named behavior and mutation expectation; a green component suite is not blanket conformance with every invariant. -| Invariant | Evidence | Failure signal | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| INV1–INV2 | Initial-render and interaction tests | Output flashes a known-wrong state, or one action is delayed or repeated through an Effect | -| INV3, INV5 | StrictMode setup/cleanup and dependency-change tests | Duplicate callback, leaked work, stale subscription, or missing resynchronization | -| INV1, INV4, INV6 | Ref attach/null/replacement, DOM-in-state rejection, and old-node cleanup-before-new-attach mutations | A DOM node or collection enters state, old node remains active, cleanup misses replacement, or ref mutation changes semantics silently | -| INV7 | Real-browser geometry plus N-consumer observer-instance/subscription mutations | Geometry is invalid, observer instances grow with repeated consumers, one unsubscribe silences peers, or cleanup removes another subscriber | -| INV8 | Owner-observable behavior across documented composition replacement; optional focused internal mechanism tests | Behavior disappears or changes owner when a documented subpart is replaced | -| 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 | -| 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 | +| Invariant | Evidence | Failure signal | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| INV1–INV2 | Initial-render and interaction tests | Output flashes a known-wrong state, or one action is delayed or repeated through an Effect | +| INV3, INV5 | StrictMode setup/cleanup and dependency-change tests | Duplicate callback, leaked work, stale subscription, or missing resynchronization | +| INV1, INV4, INV6 | Ref attach/null/replacement, DOM-in-state rejection, and old-node cleanup-before-new-attach mutations | A DOM node or collection enters state, old node remains active, cleanup misses replacement, or ref mutation changes semantics silently | +| INV7 | Real-browser geometry plus N-consumer observer-instance/subscription mutations | Geometry is invalid, observer instances grow with repeated consumers, one unsubscribe silences peers, or cleanup removes another subscriber | +| INV8 | Owner-observable behavior across documented composition replacement; optional focused internal mechanism tests | Behavior disappears or changes owner when a documented subpart is replaced | +| 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 | +| 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 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, a component bundling its own copy of 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 | +| INV13 install | Unit and server-render tests of `TouchSupport`: `install()` called twice; `install()` in a server render; a page without it; `isInstalled()` before and after; a fine-pointer page and one that gains a coarse pointer later; a public export or doc entry for the internals | A second install that registers again, server HTML that changes, a component that breaks without it, a wrong `isInstalled()`, a load on a fine-pointer-only page or none after a coarse pointer appears, or an exported or documented internal fails | diff --git a/docs/specs/AST-017/spec.md b/docs/specs/AST-017/spec.md index 0014be414a4a0..c405c3e5635d6 100644 --- a/docs/specs/AST-017/spec.md +++ b/docs/specs/AST-017/spec.md @@ -373,7 +373,10 @@ one removal merged ahead of the decision to allow it. live inside one optional `experimental` prop object; omitting that object preserves the component's stable defaults and behavior. A behavior or utility module without a public contract, such as motion, gesture, or keyboard handling, stays internal: it is - never exported, as experimental or otherwise. In Core, an experimental public + 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 API's status is visible at the call site where it is used, carried by the name the caller writes, not only in docs, declarations, or a file path; a component's diff --git a/docs/specs/AST-080-package-stylesheets/spec.md b/docs/specs/AST-080-package-stylesheets/spec.md index 6779891b3d3cb..488e075adb18a 100644 --- a/docs/specs/AST-080-package-stylesheets/spec.md +++ b/docs/specs/AST-080-package-stylesheets/spec.md @@ -276,6 +276,19 @@ uses, so one component's tuning does not ride along with every other import. Rejected: splitting touch-only CSS into a separate stylesheet, because it breaks the one-import setup DEC-3 keeps. +### DEC-5 — Touch JavaScript comes from the global touch module + +**Reference:** `spec:AST-080/DEC-5` +**Decider:** `cixzhang`, `2026-10-25` + +Touch-only JavaScript over the budget comes from the one global touch and +gesture module an app installs (`architecture:react-component-runtime` +INV13), not from a lazy load inside a component, so a component ships one +build and a page without the module loads none of it. This supersedes DEC-4's +lazy-load remedy; its reporting and budget stand. + +Rejected: lazy loading inside each component. + ## Open questions None. diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index bd002ad760891..ddf678eb0234c 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -29,7 +29,9 @@ A person using an Astryx product on a phone or tablet gets controls, sheets, menus, lists and navigation with native-feeling touch behavior: measured physics, timings and gesture thresholds instead of desktop motion stretched to a finger. This behavior is the default on every touch device, iOS and Android -alike, with no opt-in, and its motion keeps running at display rate while the +alike, on pages that install TouchSupport +(`architecture:react-component-runtime` INV13); behavior that does not run on +that module, such as press feedback, works without it (DEC-12). Its motion keeps running at display rate while the page's JavaScript is busy. The behavior comes from one measured table. Its values are measured from UIKit @@ -337,7 +339,8 @@ FR33 apply to menus only. A mouse keeps today's behavior. The scrolling stops when the finger leaves the edge, the menu reaches its end, or the gesture ends. - **FR31 — Menus and popovers grow from their trigger and shrink back.** - Under a finger, on every touch device, an anchored menu or a Popover MUST + Under a finger, on every touch device whose page installs TouchSupport + (DEC-12), an anchored menu or a Popover MUST open by growing out of its trigger and close by shrinking back into it, on its motion roles (FR4; a menu on `springs.menuOpen` and `springs.menuClose`). The shrink plays in full after the surface has @@ -352,12 +355,13 @@ FR33 apply to menus only. A mouse keeps today's behavior. WCAG 2.2 SC 1.4.4). Listening for that outside touch costs nothing once it ends (FR24). A menu whose caller passes `presentation="popover"` keeps its released outside dismissal (FR25). -- **FR33 — A ContextMenu under a finger.** With a finger or pen, the long +- **FR33 — A ContextMenu under a finger.** On a page that installs + TouchSupport (DEC-12), with a finger or pen, the long press that opens a ContextMenu MUST NOT close it at the lift, and a press outside it follows FR32. After a held finger opens it, nothing is lit and a lift acts on nothing until that finger has travelled the handoff distance (FR2) from the first position the open - menu sees; from then on FR26–FR28 apply. On every touch device, a ContextMenu that a held + menu sees; from then on FR26–FR28 apply. On every touch device whose page installs TouchSupport, a ContextMenu that a held finger opens on a block trigger MUST lift the trigger above the page and show the menu beside it, with no row lit. A tap outside closes the menu and reaches nothing beneath it, and closing returns the trigger to its place. @@ -665,6 +669,19 @@ This defers DEC-1's and DEC-3's theme retuning until that set exists. Rejected: publishing the per-component roles as theme tokens. +### DEC-12 — Module-driven touch behavior needs the app's one install + +**Reference:** `spec:AST-094/DEC-12` +**Decider:** `cixzhang`, `2026-10-25` + +Touch behavior that runs on the global touch and gesture module is on by +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". + +Rejected: keeping every touch behavior on without an install. + ## Open questions - **OQ1 — Scroll view bounce-back curve.** What curve brings a scroll view From 2e9720f45fd782e2bdc6d2603ed85b34a9014309 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 10:05:50 -0700 Subject: [PATCH 2/3] architecture: touch behavior comes from always-present shared internal modules; no install (INV13; AST-080 DEC-5; AST-094 back to plain defaults) --- .../architecture/component-style-authoring.md | 6 +- docs/architecture/public-component-api.md | 6 +- docs/architecture/react-component-runtime.md | 71 +++++++------------ .../specs/AST-080-package-stylesheets/spec.md | 16 ++--- .../specs/AST-094-touch-presentations/spec.md | 25 ++----- 5 files changed, 44 insertions(+), 80 deletions(-) diff --git a/docs/architecture/component-style-authoring.md b/docs/architecture/component-style-authoring.md index ec0f63514c4e4..72ace1df77f83 100644 --- a/docs/architecture/component-style-authoring.md +++ b/docs/architecture/component-style-authoring.md @@ -108,9 +108,9 @@ not evidence of conformance or violation. its package stylesheet (`spec:AST-080` FR8). A feature that runs only on a coarse pointer and adds more than 2 KB gzip of JavaScript or 1 KB gzip of CSS 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 + from the shared internal touch modules (`architecture:react-component-runtime` + INV13), which a page carries once however many components use them, or + justifies the cost. For flagged CSS, its pull request justifies the cost, because that CSS stays in the package stylesheet (`spec:AST-080` FR1, FR8). Sizes are gzip at the default compression level, and 1 KB is 1,024 bytes; measured as `spec:AST-080` FR8 measures CSS, a diff --git a/docs/architecture/public-component-api.md b/docs/architecture/public-component-api.md index f9fe56030ebd5..e2e30064b29b7 100644 --- a/docs/architecture/public-component-api.md +++ b/docs/architecture/public-component-api.md @@ -157,10 +157,8 @@ guidance owns the process used to propose and test APIs. such as touch tracking in an open list, takes one `presentation` prop, and a value of it, such as `presentation="popover"`, keeps the surface and input mode the component uses with a fine pointer on every device. Touch behavior - is the component's default where it serves touch users, on pages that - install TouchSupport (`architecture:react-component-runtime` INV13), with - module-free behavior such as press feedback working without it, and a - caller's explicit choice always wins over that default. Every internal touch mode of + is the component's default where it serves touch users, and a caller's + explicit choice always wins over that default. Every internal touch mode of that kind is reachable by callers through a public prop or value; none is hard-coded beyond a caller's reach. Touch styling, such as press paint or a text-size floor, and touch-only behavior that its own record defines, such as diff --git a/docs/architecture/react-component-runtime.md b/docs/architecture/react-component-runtime.md index 30179ab916613..1fc809a5d79ed 100644 --- a/docs/architecture/react-component-runtime.md +++ b/docs/architecture/react-component-runtime.md @@ -175,37 +175,21 @@ those contracts. each moving element is one the page already renders (`architecture:component-style-authoring` INV7). The module follows INV3 and INV5. -- **INV13 — One global touch and gesture module.** Core has one global touch - and gesture module, the gesture core and the animator, held in one - module-level registry. An app installs it once in its client entry, before - it renders or hydrates, with `TouchSupport.install()` from - `@astryxdesign/core/touch-support`. `install()` is idempotent and does - nothing on the server, so server HTML is unchanged; `TouchSupport.isInstalled()` - reports whether it ran; nothing else is reachable from the object. Each - Core component ships one build and reaches the module only through one - internal hook that checks whether it is installed; when it is, the - component runs its touch behavior with the module's primitives, and its own - touch logic is small glue in its own build. Without it, behavior that runs on the module - takes its plain form, the one it has under a fine pointer, and the component - stays fully usable; a tap acts as a click and is never lost; and touch - behavior that does not run on the module, such as press feedback, keeps - working. No component loads the - module itself or holds an `import()` of it, so a page that does not install - it, such as a desktop page, loads nothing extra. `install()` loads the module - only where some input is a coarse pointer (`any-pointer: coarse`), and loads it - when a coarse pointer appears later, such as a tablet attached to a laptop, - so a page used only with a mouse loads nothing, and where the primary pointer is fine - the module registers nothing (`spec:AST-094` FR24). The recognizers, the arena, the animator, and the module's - touch press recognizer are not public exports and are not documented for consumers; Core - and Lab components reach them through `@astryxdesign/core/internal/touch`, - a subpath reserved for Astryx's own packages: undocumented, outside the - compatibility promise, versioned in lockstep with Core, and limited by lint - to Core and Lab (`spec:AST-017` - FR21). `TouchSupport`'s documentation says - that layout and presence motion 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. +- **INV13 — One shared set of internal touch modules.** Core's touch + behavior comes from one always-present set of internal gesture and touch + modules: the gesture core, the recognizers, the arena, the animator, and the + touch press recognizer. Components import them statically, and the bundler + keeps one copy per page however many components use them. There is no + install call, no public entry, and no public installed check; tests reset + the modules through an internal hook only. A component's own touch logic is + small glue in its own build over these modules. Where the primary pointer + is fine, no recognizer attaches and no touch listener is registered + (`spec:AST-094` FR19, FR24). The modules are not public exports and are not + documented for consumers; Core and Lab components reach them through + `@astryxdesign/core/internal/touch`, a subpath reserved for Astryx's own + packages: undocumented, outside the compatibility promise, versioned in + lockstep with Core, and limited by lint to Core and Lab (`spec:AST-017` + FR21). ## Authoring guidance @@ -342,16 +326,15 @@ observable-behavior predicates require review or runtime evidence. Likewise, a focused test proves only its named behavior and mutation expectation; a green component suite is not blanket conformance with every invariant. -| Invariant | Evidence | Failure signal | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| INV1–INV2 | Initial-render and interaction tests | Output flashes a known-wrong state, or one action is delayed or repeated through an Effect | -| INV3, INV5 | StrictMode setup/cleanup and dependency-change tests | Duplicate callback, leaked work, stale subscription, or missing resynchronization | -| INV1, INV4, INV6 | Ref attach/null/replacement, DOM-in-state rejection, and old-node cleanup-before-new-attach mutations | A DOM node or collection enters state, old node remains active, cleanup misses replacement, or ref mutation changes semantics silently | -| INV7 | Real-browser geometry plus N-consumer observer-instance/subscription mutations | Geometry is invalid, observer instances grow with repeated consumers, one unsubscribe silences peers, or cleanup removes another subscriber | -| INV8 | Owner-observable behavior across documented composition replacement; optional focused internal mechanism tests | Behavior disappears or changes owner when a documented subpart is replaced | -| 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 | -| 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 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, a component bundling its own copy of 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 | -| INV13 install | Unit and server-render tests of `TouchSupport`: `install()` called twice; `install()` in a server render; a page without it; `isInstalled()` before and after; a fine-pointer page and one that gains a coarse pointer later; a public export or doc entry for the internals | A second install that registers again, server HTML that changes, a component that breaks without it, a wrong `isInstalled()`, a load on a fine-pointer-only page or none after a coarse pointer appears, or an exported or documented internal fails | +| Invariant | Evidence | Failure signal | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| INV1–INV2 | Initial-render and interaction tests | Output flashes a known-wrong state, or one action is delayed or repeated through an Effect | +| INV3, INV5 | StrictMode setup/cleanup and dependency-change tests | Duplicate callback, leaked work, stale subscription, or missing resynchronization | +| INV1, INV4, INV6 | Ref attach/null/replacement, DOM-in-state rejection, and old-node cleanup-before-new-attach mutations | A DOM node or collection enters state, old node remains active, cleanup misses replacement, or ref mutation changes semantics silently | +| INV7 | Real-browser geometry plus N-consumer observer-instance/subscription mutations | Geometry is invalid, observer instances grow with repeated consumers, one unsubscribe silences peers, or cleanup removes another subscriber | +| INV8 | Owner-observable behavior across documented composition replacement; optional focused internal mechanism tests | Behavior disappears or changes owner when a documented subpart is replaced | +| 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 | +| 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 | diff --git a/docs/specs/AST-080-package-stylesheets/spec.md b/docs/specs/AST-080-package-stylesheets/spec.md index 488e075adb18a..0f0e5d7c1a8e6 100644 --- a/docs/specs/AST-080-package-stylesheets/spec.md +++ b/docs/specs/AST-080-package-stylesheets/spec.md @@ -276,18 +276,18 @@ uses, so one component's tuning does not ride along with every other import. Rejected: splitting touch-only CSS into a separate stylesheet, because it breaks the one-import setup DEC-3 keeps. -### DEC-5 — Touch JavaScript comes from the global touch module +### DEC-5 — Touch JavaScript comes from the shared internal touch modules **Reference:** `spec:AST-080/DEC-5` -**Decider:** `cixzhang`, `2026-10-25` +**Decider:** `cixzhang`, `2026-10-26` -Touch-only JavaScript over the budget comes from the one global touch and -gesture module an app installs (`architecture:react-component-runtime` -INV13), not from a lazy load inside a component, so a component ships one -build and a page without the module loads none of it. This supersedes DEC-4's -lazy-load remedy; its reporting and budget stand. +Touch primitives over the budget come from the shared internal touch modules +components import (`architecture:react-component-runtime` INV13), which a +page carries once, or the cost is justified; a component does not lazy-load +its own. There is no install step: the modules are always present. This +supersedes DEC-4's lazy-load remedy; its reporting and budget stand. -Rejected: lazy loading inside each component. +Rejected: lazy loading inside each component, and an app-installed module. ## Open questions diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index ddf678eb0234c..bd002ad760891 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -29,9 +29,7 @@ A person using an Astryx product on a phone or tablet gets controls, sheets, menus, lists and navigation with native-feeling touch behavior: measured physics, timings and gesture thresholds instead of desktop motion stretched to a finger. This behavior is the default on every touch device, iOS and Android -alike, on pages that install TouchSupport -(`architecture:react-component-runtime` INV13); behavior that does not run on -that module, such as press feedback, works without it (DEC-12). Its motion keeps running at display rate while the +alike, with no opt-in, and its motion keeps running at display rate while the page's JavaScript is busy. The behavior comes from one measured table. Its values are measured from UIKit @@ -339,8 +337,7 @@ FR33 apply to menus only. A mouse keeps today's behavior. The scrolling stops when the finger leaves the edge, the menu reaches its end, or the gesture ends. - **FR31 — Menus and popovers grow from their trigger and shrink back.** - Under a finger, on every touch device whose page installs TouchSupport - (DEC-12), an anchored menu or a Popover MUST + Under a finger, on every touch device, an anchored menu or a Popover MUST open by growing out of its trigger and close by shrinking back into it, on its motion roles (FR4; a menu on `springs.menuOpen` and `springs.menuClose`). The shrink plays in full after the surface has @@ -355,13 +352,12 @@ FR33 apply to menus only. A mouse keeps today's behavior. WCAG 2.2 SC 1.4.4). Listening for that outside touch costs nothing once it ends (FR24). A menu whose caller passes `presentation="popover"` keeps its released outside dismissal (FR25). -- **FR33 — A ContextMenu under a finger.** On a page that installs - TouchSupport (DEC-12), with a finger or pen, the long +- **FR33 — A ContextMenu under a finger.** With a finger or pen, the long press that opens a ContextMenu MUST NOT close it at the lift, and a press outside it follows FR32. After a held finger opens it, nothing is lit and a lift acts on nothing until that finger has travelled the handoff distance (FR2) from the first position the open - menu sees; from then on FR26–FR28 apply. On every touch device whose page installs TouchSupport, a ContextMenu that a held + menu sees; from then on FR26–FR28 apply. On every touch device, a ContextMenu that a held finger opens on a block trigger MUST lift the trigger above the page and show the menu beside it, with no row lit. A tap outside closes the menu and reaches nothing beneath it, and closing returns the trigger to its place. @@ -669,19 +665,6 @@ This defers DEC-1's and DEC-3's theme retuning until that set exists. Rejected: publishing the per-component roles as theme tokens. -### DEC-12 — Module-driven touch behavior needs the app's one install - -**Reference:** `spec:AST-094/DEC-12` -**Decider:** `cixzhang`, `2026-10-25` - -Touch behavior that runs on the global touch and gesture module is on by -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". - -Rejected: keeping every touch behavior on without an install. - ## Open questions - **OQ1 — Scroll view bounce-back curve.** What curve brings a scroll view From b1d6f9ad615fd256b73c81562a63ffa071e432a4 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 26 Oct 2026 10:43:21 -0700 Subject: [PATCH 3/3] =?UTF-8?q?spec(AST-017),=20architecture:=20carve=20./?= =?UTF-8?q?internal/*=20out=20of=20FR21=E2=80=93FR25=20and=20INV12=20rows;?= =?UTF-8?q?=20Core-and-Lab=20limit;=20DEC-5=20listed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../architecture/component-style-authoring.md | 2 + docs/architecture/public-component-api.md | 7 ++- docs/architecture/react-component-runtime.md | 2 +- docs/specs/AST-017/spec.md | 61 ++++++++++--------- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/docs/architecture/component-style-authoring.md b/docs/architecture/component-style-authoring.md index 72ace1df77f83..a267e49c07413 100644 --- a/docs/architecture/component-style-authoring.md +++ b/docs/architecture/component-style-authoring.md @@ -281,6 +281,8 @@ before assigning a verdict. - `spec:AST-080/DEC-4` — every pull request reports component JavaScript and CSS size; touch-only JavaScript over 2 KB or CSS over 1 KB is flagged, and shared value tables ship only the rows a component uses (INV6). +- `spec:AST-080/DEC-5` — touch primitives come from the shared internal touch + modules, or the cost is justified; no component lazy-loads its own (INV6). - `spec:AST-017/DEC-26` — components are built from minimal DOM, with two budgets and an owner-recorded exception path; reducing DOM is compatible (INV7). diff --git a/docs/architecture/public-component-api.md b/docs/architecture/public-component-api.md index e2e30064b29b7..15068b81b1469 100644 --- a/docs/architecture/public-component-api.md +++ b/docs/architecture/public-component-api.md @@ -137,8 +137,9 @@ guidance owns the process used to propose and test APIs. package path is public even when its source comment, filename, or current use calls it internal. Privacy requires removing the export or moving the capability 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). + exception: reachable, but reserved for Core and Lab, undocumented, versioned + in lockstep with Core, limited by lint to those two packages, and outside + the compatibility promise (`spec:AST-017` FR21). - **INV13 — Public deltas route to their authority owner.** Every public API update and every public behavior change follows the result and disposition defined by `architecture:knowledge-contracts`. Tests, a Changeset, or a generated manifest @@ -273,7 +274,7 @@ copy the component matrix. | INV5, INV6, INV7 | BaseProps/passthrough lint and representative runtime tests | Consumer ARIA/data/style/events are dropped, clobber component semantics, or fail to compose | | INV9 | Published-surface, Changeset, migration, and public-type checks | A released API changes without explicit compatibility evidence | | INV11 | Public-API admission review plus owning theming/component tests | A public semantic custom property exposes derivable or unsupported implementation detail | -| INV12, INV13 | Generated declaration/export/behavior inventory plus canonical-owner mapping | A reachable supporting type or behavior is called internal, or mechanical evidence is treated as permission to accept it | +| INV12, INV13 | Generated declaration/export/behavior inventory plus canonical-owner mapping | A reachable supporting type or behavior outside `./internal/*` is called internal, or mechanical evidence is treated as permission to accept it | | INV14 | `AppShell.test.tsx` | A component hardcodes a divergent point or treats equality as below | | INV15 | Public-API review of the generated declaration inventory | A part or StyleX-typed styling prop ships without class-name and inline-style counterparts on the same element | | INV16 | Public-API review of touch presentations plus coarse- and fine-pointer runtime tests | A stable component that presents a different surface or switches an open surface's input mode under a coarse pointer and has no `presentation` value that keeps its fine-pointer surface and mode; such a touch mode callers cannot reach; a default that overrides an explicit caller choice | diff --git a/docs/architecture/react-component-runtime.md b/docs/architecture/react-component-runtime.md index 1fc809a5d79ed..d3f62df21a126 100644 --- a/docs/architecture/react-component-runtime.md +++ b/docs/architecture/react-component-runtime.md @@ -337,4 +337,4 @@ component suite is not blanket conformance with every invariant. | 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 | +| 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, a touch listener under a fine primary pointer, or an import of `@astryxdesign/core/internal/*` outside Core and Lab fails | diff --git a/docs/specs/AST-017/spec.md b/docs/specs/AST-017/spec.md index c405c3e5635d6..ff2e07d06d84a 100644 --- a/docs/specs/AST-017/spec.md +++ b/docs/specs/AST-017/spec.md @@ -436,7 +436,7 @@ one removal merged ahead of the decision to allow it. behavior fix under FR53; a breaking change lands only in a declared minor (FR31). Documentation catalog names and routes follow FR45. An internal label, low adoption, or lack of a source-level API change does - not weaken this rule. + not weaken this rule; Core's `./internal/*` subpaths are outside it (FR21). - **FR26 — Public lifecycle state is explicit.** Each public surface is `experimental`, `stable`, `deprecated`, or `removed`; private and unreleased surfaces remain outside the published lifecycle. Stable cannot become experimental. Deprecated @@ -876,7 +876,8 @@ Public lifecycle classification remains in force with these operational effects: alongside touch work, such as a new modal backdrop dim, becomes its own design decision (FR59); - no file path or subpath marks a surface experimental and Core's behavior and - utility modules without a public contract stay unexported (FR21). + utility modules without a public contract stay unexported, except through + the reserved `./internal/*` subpaths (FR21). Main declares the next planned version, and Changeset admission uses that declaration to refuse `[breaking]` work on a patch plan. Pull-request lint does not compare the @@ -902,34 +903,34 @@ lands, maintainers apply its requirement in review and release approval. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1–FR3 | PR compatibility statement plus latest stable package inspection | released export, narrowed type, changed interaction default; unreleased and private surface; visual-only spacing, typography, layout styling, or default appearance; a new touch presentation or open-surface touch mode as a default | A change is labeled from diff size or possibility alone, a functional contract loss is hidden as visual-only, or a pixel-only change is tagged `[breaking]` even though callers need no source or configuration change, or such a touch default names no `presentation` value that restores the previous surface and mode | -| FR4–FR6 | Old-usage type/runtime/CLI regression test | alias retained, deprecation warning, broad rewrite, low-adoption caller | Contractual old usage fails despite a nonbreaking label, or risk is substituted for compatibility | -| FR7–FR8 | `pnpm check:changesets` (tag, bump, contributors, declared tier) plus FR8 migration review of `[breaking]` text | existing `[fix]`, `[feat]`, `[breaking]`, and `[experimental]` tags paired with compatible, deprecation, planned-removal, or incompatible-fix metadata | A new lifecycle replaces an established tag, tag/metadata/bump diverge, an incompatible correction uses bare `[fix]`, or migration is unusable | -| FR9–FR13 | CLI contract tests, response-schema/type snapshots, text projections, generated consumer docs, and template catalog/output tests | slug rename, metadata edit, source rebuild, optional field addition, command or schema change | Catalog data is frozen as API, a command/schema incompatibility is mislabeled as catalog-only, or a response field lacks a complete projection | -| FR12 | Minimum and representative supported-version tests plus manifest and release-note review | retained range, narrowed range, adapter, coordinated upgrade | An in-range combination breaks under a nonbreaking label, or release coordination hides the affected package or migration | -| FR14 | Help/manifest snapshots, public API and consumer docs, and focused contract tests | command, option, API/config, private rollout/test hook | Supported behavior is hidden, an environment variable changes behavior, or automation lacks a documented API | -| FR15 | Full manifest-derived command matrix, supported-consumer evidence, and scope-specific contract tests | global invariant, scoped command group, single command, programmatic API | A global control is a no-op for any command, has different meanings, or replaces a narrower owning surface | -| FR16–FR18 | Boundary inventory, hostile side-effect probes, response snapshots, and concurrent API tests | known and new extension, partial result, text/JSON/API parity, independent concurrent calls | A route bypasses the guarantee, omitted work looks complete, or one invocation changes another | -| FR19–FR20 | Proposal evidence with a regression fixture for the detection failure, plus composition and provenance tests | convention covers the case, detection fails, app plus two integrations, refusal, failing contribution, repeated load | A key ships without a reproduced detection failure, a contribution displaces the app or applies twice, a part of the effective value has no inspectable source, or a failure silently weakens protection | -| FR21–FR24 | Published-surface comparison, declaration/doc metadata checks, import-boundary checks, and Changeset classification review | experimental prop object, experimental subpath, patch evolution, promotion, unpublished adjacent field, stable warning behavior, integration theme; a component or module `/experimental` subpath; an `_experimental` export; a behavior module | A prose-only marker excludes a stable API, adjacency falsely publishes a new field, an experimental export leaks through a stable path, a patch changes stable behavior, or a promoted API remains unprotected, a file path or subpath alone marks a surface experimental, a behavior, utility, or internal module without a public contract is exported, or an experimental public API in Core does not show its status at the call site in the name the caller writes | -| FR25–FR27 | Stable-surface inventory and experimental-boundary matrix | package/API, CSS/token, CLI/config, persistence, build tooling, docs, whole Lab component | An unmarked experiment changes stable defaults or a stable surface bypasses compatibility review | -| FR28–FR31 | Declared-tier check: on a minor, the `[breaking]` Changeset describes the change (old usage, replacement, migration); on a patch, a breaking Changeset is refused and the deprecation ships instead; declaration/doc/build/CLI warning projections; old/new fixtures; dry-run/idempotence/migration tests; maintained-source and latest-stable upgrade evidence | whole component, prop/value, theme field/helper/token/target/source, CLI command, CLI option; old-only, new-only, both, static/dynamic/uncertain migration, text/JSON, development/production | A warning has no usable modernization route, production runtime changes for deprecation, maintained guidance teaches the old surface, a migration guesses, old behavior degrades early, cadence invents a wait, or one cleanup id covers multiple incompatible deltas | -| FR32–FR35 | Latest-stable victim fixture, pre-existing authority named in the Changeset or pull request, owner review, declared-tier check, and emergency attestation | victim-free `[fix]`, `[fix]` plus incompatible-fix metadata, coexistence, cleanup transition, critical-harm emergency | A released victim uses bare `[fix]`, a new Changeset tag replaces metadata, a correction record disagrees with its public-surface state, or urgency bypasses migration | -| FR36–FR40 | Owner-declared target, locked final-patch receipt, three-way delta classification against the merged `[breaking]` Changesets, and separated release notes | ordinary cadence, immediate pair, cleanup, release metadata, pre/post-lock compatible fix | Cadence becomes eligibility, the final patch is recut for bookkeeping, a feature or incompatible fix enters the incidental-fix lane, or release notes merge cleanup with fixes | -| FR41–FR44 | Schema validation, PR declaration, semantic stable/base/head comparisons, exact-main gate, and immutable publish/rollback receipt | duplicate ids, missing evidence, route/schema removal, old-client metadata, partial publish, safe rollback | A label passes without semantics, a delta maps zero or multiple times, fixed-group membership drifts, or a release rebuilds under one identity | -| FR45 | Docs route inventory, repository-reference checks, and Changeset review | catalog entry rename, former-route miss, stable command and JSON schema | Catalog routing is frozen as API, or a rename silently changes the contractual command or response schema | -| FR46–FR47 | Declared-tier Changeset admission, canary publication ordering, and release-versioning fixtures | owner increase/decrease, above/equal/below stable, split group, `[breaking]` on patch and minor plans | Pending Changesets derive the plan, `[breaking]` passes on a patch or fails on a minor, equality publishes a canary, or a lower/split declaration publishes instead of failing | -| FR48 | Category and classification admission under each declared tier | patch release with `[breaking]`, patch release with a deprecation, minor release with `[breaking]` | Incompatible work passes in a patch release, or a deprecation is refused in one | -| FR49 | Malformed and inconsistent declaration fixtures | disagreeing group versions, canary identifier, release version unlike the cut manifest, already-released version, skipped version, no stable tag | An untrustworthy or mismatched declaration is released | -| FR50 | Three-way squash merge-back fixtures bound to the immutable tag, branch head, and plan digest | exact branch outputs, frozen deletion, branch-only marker exclusion, post-cut Changeset/manifest edit, newer main declaration, merge conflict, regenerated output | Merge-back differs from release bytes, lands lifecycle files, consumes post-cut work, overwrites a newer main declaration or unrelated field, or chooses a version | -| FR51 | Cut-time successor-plan preparation plus manifest/pin/lockfile/compatibility-asset and lifecycle fixtures | default patch successor, explicitly named minor, abort before any tag or publication, immutable tag before merge-back, pre-advanced main plan, unrelated main work during equality | The plan merges before the cut version is non-reusable, survives a terminal cut with neither an immutable tag nor stable publication, skips the default patch, derives a minor from pending Changesets, splits pins/assets, or the prepared plan or merge-back lowers or duplicates an existing newer plan | -| FR52 | Promotion fixtures plus every checked-in source consumer run before and after codemod promotion | staged transform, promoted transform, missing source, ambiguous candidates, changed entry identity | Evidence hardcodes only `next/`, keeps a duplicate source, silently chooses among candidates, or passes when the promoted transform no longer exposes the same identity and entries | -| FR53 | Changeset classification review against the governing component record's accessibility evidence | a fix that replaces documented keyboard or focus behavior to meet an adopted APG pattern's required parts; a fix that changes a role, state, or accessible name to meet WCAG 2.2 A/AA; a fix that adds an optional prop | An accessibility fix is held for a minor, given a coexistence path, or classified `[breaking]`; or a change that needs an app-layer code edit, or that removes, renames, or narrows public surface, ships as an accessibility `[fix]` | -| FR54 | Changeset classification review against the surface's current record, then the same tier admission every Changeset receives | a CLI correction the CLI record classifies as `[fix]`; a CLI change the CLI record leaves undecided; a CLI change the CLI record classifies as `[breaking]` on a patch plan; a component change | A non-component change is classified by a rule this record restates instead of its surface record, a surface rule turns on risk or adoption, a surface-classified `[breaking]` change ships in a patch or a surface rule makes it patch-eligible, or a component change is classified by a surface record | -| FR59 | Pull request review of fine-pointer before-and-after evidence | a touch change that also fixes a defect fine-pointer users hit; a deliberate cross-device consistency change; a touch-only change; a new fine-pointer look proposed with touch work | A touch change alters fine-pointer behavior or appearance with neither reason, shows no fine-pointer before-and-after evidence, or ships a new fine-pointer look without its own owner decision | -| FR60 | Element counts per story at rest and open, fine and coarse pointer, against architecture INV7 | a surface component at rest; an open layer; a DOM reduction; a recorded exception; an open layer under touch with extra elements | An element beyond INV7's budgets with no recorded owner exception, a hidden twin or second presentation, or a DOM reduction that removes a theme target or documented anatomy, or that is classified as breaking; an open layer more than 5 elements over its fine-pointer count, an added element that is themeable, a new anatomy part, or without a job in its record, or a closed layer, trigger, or repeated component above its fine-pointer count | +| Contract | Verification | Representative states | Mutation or failure expectation | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1–FR3 | PR compatibility statement plus latest stable package inspection | released export, narrowed type, changed interaction default; unreleased and private surface; visual-only spacing, typography, layout styling, or default appearance; a new touch presentation or open-surface touch mode as a default | A change is labeled from diff size or possibility alone, a functional contract loss is hidden as visual-only, or a pixel-only change is tagged `[breaking]` even though callers need no source or configuration change, or such a touch default names no `presentation` value that restores the previous surface and mode | +| FR4–FR6 | Old-usage type/runtime/CLI regression test | alias retained, deprecation warning, broad rewrite, low-adoption caller | Contractual old usage fails despite a nonbreaking label, or risk is substituted for compatibility | +| FR7–FR8 | `pnpm check:changesets` (tag, bump, contributors, declared tier) plus FR8 migration review of `[breaking]` text | existing `[fix]`, `[feat]`, `[breaking]`, and `[experimental]` tags paired with compatible, deprecation, planned-removal, or incompatible-fix metadata | A new lifecycle replaces an established tag, tag/metadata/bump diverge, an incompatible correction uses bare `[fix]`, or migration is unusable | +| FR9–FR13 | CLI contract tests, response-schema/type snapshots, text projections, generated consumer docs, and template catalog/output tests | slug rename, metadata edit, source rebuild, optional field addition, command or schema change | Catalog data is frozen as API, a command/schema incompatibility is mislabeled as catalog-only, or a response field lacks a complete projection | +| FR12 | Minimum and representative supported-version tests plus manifest and release-note review | retained range, narrowed range, adapter, coordinated upgrade | An in-range combination breaks under a nonbreaking label, or release coordination hides the affected package or migration | +| FR14 | Help/manifest snapshots, public API and consumer docs, and focused contract tests | command, option, API/config, private rollout/test hook | Supported behavior is hidden, an environment variable changes behavior, or automation lacks a documented API | +| FR15 | Full manifest-derived command matrix, supported-consumer evidence, and scope-specific contract tests | global invariant, scoped command group, single command, programmatic API | A global control is a no-op for any command, has different meanings, or replaces a narrower owning surface | +| FR16–FR18 | Boundary inventory, hostile side-effect probes, response snapshots, and concurrent API tests | known and new extension, partial result, text/JSON/API parity, independent concurrent calls | A route bypasses the guarantee, omitted work looks complete, or one invocation changes another | +| FR19–FR20 | Proposal evidence with a regression fixture for the detection failure, plus composition and provenance tests | convention covers the case, detection fails, app plus two integrations, refusal, failing contribution, repeated load | A key ships without a reproduced detection failure, a contribution displaces the app or applies twice, a part of the effective value has no inspectable source, or a failure silently weakens protection | +| FR21–FR24 | Published-surface comparison, declaration/doc metadata checks, import-boundary checks, and Changeset classification review | experimental prop object, experimental subpath, patch evolution, promotion, unpublished adjacent field, stable warning behavior, integration theme; a component or module `/experimental` subpath; an `_experimental` export; a behavior module; an `./internal/*` subpath imported from Core, from Lab, and from another package | A prose-only marker excludes a stable API, adjacency falsely publishes a new field, an experimental export leaks through a stable path, a patch changes stable behavior, or a promoted API remains unprotected, a file path or subpath alone marks a surface experimental, a behavior, utility, or internal module without a public contract is exported other than through `./internal/*`, an `./internal/*` subpath is documented or imported outside Core and Lab, or an experimental public API in Core does not show its status at the call site in the name the caller writes | +| FR25–FR27 | Stable-surface inventory and experimental-boundary matrix | package/API, CSS/token, CLI/config, persistence, build tooling, docs, whole Lab component | An unmarked experiment changes stable defaults or a stable surface bypasses compatibility review | +| FR28–FR31 | Declared-tier check: on a minor, the `[breaking]` Changeset describes the change (old usage, replacement, migration); on a patch, a breaking Changeset is refused and the deprecation ships instead; declaration/doc/build/CLI warning projections; old/new fixtures; dry-run/idempotence/migration tests; maintained-source and latest-stable upgrade evidence | whole component, prop/value, theme field/helper/token/target/source, CLI command, CLI option; old-only, new-only, both, static/dynamic/uncertain migration, text/JSON, development/production | A warning has no usable modernization route, production runtime changes for deprecation, maintained guidance teaches the old surface, a migration guesses, old behavior degrades early, cadence invents a wait, or one cleanup id covers multiple incompatible deltas | +| FR32–FR35 | Latest-stable victim fixture, pre-existing authority named in the Changeset or pull request, owner review, declared-tier check, and emergency attestation | victim-free `[fix]`, `[fix]` plus incompatible-fix metadata, coexistence, cleanup transition, critical-harm emergency | A released victim uses bare `[fix]`, a new Changeset tag replaces metadata, a correction record disagrees with its public-surface state, or urgency bypasses migration | +| FR36–FR40 | Owner-declared target, locked final-patch receipt, three-way delta classification against the merged `[breaking]` Changesets, and separated release notes | ordinary cadence, immediate pair, cleanup, release metadata, pre/post-lock compatible fix | Cadence becomes eligibility, the final patch is recut for bookkeeping, a feature or incompatible fix enters the incidental-fix lane, or release notes merge cleanup with fixes | +| FR41–FR44 | Schema validation, PR declaration, semantic stable/base/head comparisons, exact-main gate, and immutable publish/rollback receipt | duplicate ids, missing evidence, route/schema removal, old-client metadata, partial publish, safe rollback | A label passes without semantics, a delta maps zero or multiple times, fixed-group membership drifts, or a release rebuilds under one identity | +| FR45 | Docs route inventory, repository-reference checks, and Changeset review | catalog entry rename, former-route miss, stable command and JSON schema | Catalog routing is frozen as API, or a rename silently changes the contractual command or response schema | +| FR46–FR47 | Declared-tier Changeset admission, canary publication ordering, and release-versioning fixtures | owner increase/decrease, above/equal/below stable, split group, `[breaking]` on patch and minor plans | Pending Changesets derive the plan, `[breaking]` passes on a patch or fails on a minor, equality publishes a canary, or a lower/split declaration publishes instead of failing | +| FR48 | Category and classification admission under each declared tier | patch release with `[breaking]`, patch release with a deprecation, minor release with `[breaking]` | Incompatible work passes in a patch release, or a deprecation is refused in one | +| FR49 | Malformed and inconsistent declaration fixtures | disagreeing group versions, canary identifier, release version unlike the cut manifest, already-released version, skipped version, no stable tag | An untrustworthy or mismatched declaration is released | +| FR50 | Three-way squash merge-back fixtures bound to the immutable tag, branch head, and plan digest | exact branch outputs, frozen deletion, branch-only marker exclusion, post-cut Changeset/manifest edit, newer main declaration, merge conflict, regenerated output | Merge-back differs from release bytes, lands lifecycle files, consumes post-cut work, overwrites a newer main declaration or unrelated field, or chooses a version | +| FR51 | Cut-time successor-plan preparation plus manifest/pin/lockfile/compatibility-asset and lifecycle fixtures | default patch successor, explicitly named minor, abort before any tag or publication, immutable tag before merge-back, pre-advanced main plan, unrelated main work during equality | The plan merges before the cut version is non-reusable, survives a terminal cut with neither an immutable tag nor stable publication, skips the default patch, derives a minor from pending Changesets, splits pins/assets, or the prepared plan or merge-back lowers or duplicates an existing newer plan | +| FR52 | Promotion fixtures plus every checked-in source consumer run before and after codemod promotion | staged transform, promoted transform, missing source, ambiguous candidates, changed entry identity | Evidence hardcodes only `next/`, keeps a duplicate source, silently chooses among candidates, or passes when the promoted transform no longer exposes the same identity and entries | +| FR53 | Changeset classification review against the governing component record's accessibility evidence | a fix that replaces documented keyboard or focus behavior to meet an adopted APG pattern's required parts; a fix that changes a role, state, or accessible name to meet WCAG 2.2 A/AA; a fix that adds an optional prop | An accessibility fix is held for a minor, given a coexistence path, or classified `[breaking]`; or a change that needs an app-layer code edit, or that removes, renames, or narrows public surface, ships as an accessibility `[fix]` | +| FR54 | Changeset classification review against the surface's current record, then the same tier admission every Changeset receives | a CLI correction the CLI record classifies as `[fix]`; a CLI change the CLI record leaves undecided; a CLI change the CLI record classifies as `[breaking]` on a patch plan; a component change | A non-component change is classified by a rule this record restates instead of its surface record, a surface rule turns on risk or adoption, a surface-classified `[breaking]` change ships in a patch or a surface rule makes it patch-eligible, or a component change is classified by a surface record | +| FR59 | Pull request review of fine-pointer before-and-after evidence | a touch change that also fixes a defect fine-pointer users hit; a deliberate cross-device consistency change; a touch-only change; a new fine-pointer look proposed with touch work | A touch change alters fine-pointer behavior or appearance with neither reason, shows no fine-pointer before-and-after evidence, or ships a new fine-pointer look without its own owner decision | +| FR60 | Element counts per story at rest and open, fine and coarse pointer, against architecture INV7 | a surface component at rest; an open layer; a DOM reduction; a recorded exception; an open layer under touch with extra elements | An element beyond INV7's budgets with no recorded owner exception, a hidden twin or second presentation, or a DOM reduction that removes a theme target or documented anatomy, or that is classified as breaking; an open layer more than 5 elements over its fine-pointer count, an added element that is themeable, a new anatomy part, or without a job in its record, or a closed layer, trigger, or repeated component above its fine-pointer count | ## Decision log