diff --git a/.changeset/ai-docs-audit.md b/.changeset/ai-docs-audit.md
deleted file mode 100644
index 810b126..0000000
--- a/.changeset/ai-docs-audit.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-'@amritk/lynx-deep-linking': patch
-'@amritk/lynx-notifications': patch
-'@amritk/lynx-dialogs': patch
-'@amritk/lynx-location': patch
-'@amritk/mini-lynx': patch
-'@amritk/mini': patch
----
-
-Bring every package's shipped `AI.md` back in line with what that package
-actually publishes, and add `bun run check:ai-docs` so it cannot drift again.
-
-The files had gone stale in the way generated-and-committed docs always do —
-silently, and only for the audience that cannot file an issue about it.
-`@amritk/mini` never documented `watch`, `template`, the typed `matchRoute` /
-`buildPath` re-exports on `/router`, `Field` on `/forms`, or the `/vite` subpath
-at all; `@amritk/mini-lynx` was missing `computed` / `effectScope`,
-`fadeTransition`, `keepAboveKeyboard` and `HANDLER_PREFIX`;
-`@amritk/lynx-notifications` documented neither its `/testing` subpath nor the
-fake behind it. All four native packages exported `MODULE` and `EVENTS` with no
-mention of what they are for, and only `@amritk/lynx-dialogs` showed how to wire
-a fake into `installNativeBridge` — which is the one thing a consumer testing
-its own screens needs.
-
-Two accuracy fixes matter more than the additions. Every native package's
-*Status* section claimed the Objective-C compiles against the real Lynx pod; the
-macOS CI job was disabled on cost, so it now compiles only when somebody runs
-`pod lib lint` by hand, and the docs say that. And `@amritk/lynx-dialogs` never
-carried a *Status* section at all, so nothing in it told a reader that none of
-it has run on a device.
-
-`bun run check:ai-docs` reads each package's `exports` and fails on a runtime
-export, a published subpath, or (for a package shipping native sources) a
-*Status* section its `AI.md` never mentions. It runs early in CI, before the
-build. Exports no consumer ever writes — the tree operations the JSX transform
-calls, and the like — are listed in `INTERNAL_EXPORTS` with the reason.
diff --git a/.changeset/ios-ci-disabled.md b/.changeset/ios-ci-disabled.md
deleted file mode 100644
index 3459b27..0000000
--- a/.changeset/ios-ci-disabled.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
----
-
-Comment out the iOS native CI job. Four `pod lib lint` invocations ran
-sequentially on a macOS runner and took 81 minutes a run — against 2m17s for the
-Android equivalent — because each lint builds the Lynx engine from scratch in
-its own sandbox to check roughly 2,600 lines of Objective-C.
-
-Nothing compiles the Objective-C now. `native-contract.test.ts` still pins the
-native method surfaces against the TypeScript, so a renamed or dropped method is
-still caught, but a syntax error or a missing header will reach a tarball. Run
-`pod lib lint` by hand on a Mac before releasing a change under
-`packages/lynx-*/ios/`. The job is left commented out in the workflow with the
-two fixes worth making before turning it back on.
diff --git a/.changeset/lucky-donkeys-shave.md b/.changeset/lucky-donkeys-shave.md
deleted file mode 100644
index 0b67e4d..0000000
--- a/.changeset/lucky-donkeys-shave.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-'@amritk/lynx-location': minor
----
-
-Add `@amritk/lynx-location` — device location for Lynx.
-
-Lynx ships no location module, Sparkling's built-ins stop at navigation,
-storage and media, and the one published community package
-(`@sigx/lynx-location`) calls `NativeModules` directly from its JavaScript half,
-which is background-thread only — so a main-thread `@amritk/mini-lynx` component
-importing it gets `undefined`. This is an Android module, an iOS module, and a
-promise-shaped facade over `@amritk/mini-lynx-native`, built from the shape
-`@amritk/lynx-notifications` established.
-
-`getPermissionStatus`, `requestPermission`, `getCurrentPosition`,
-`getLastKnownPosition`, `isLocationEnabled`, `isLocationAvailable` and
-`watchPosition`. `CLLocationManager` on iOS, `LocationManager` on Android — not
-the fused provider, so no Play Services dependency and no non-GMS devices
-excluded. Foreground only, deliberately: no `ACCESS_BACKGROUND_LOCATION` and no
-`Always` authorisation.
-
-Failures are values rather than rejections — `{ ok: true, position }` or
-`{ ok: false, error, message }` — because Lynx has no error convention for
-bridge callbacks and a missing permission is an ordinary UI branch.
-
-`@amritk/lynx-location/testing` ships the native contract as an executable fake.
-Nothing here has run on a device; see the package's `AGENTS.md` for what the
-three CI checks do and do not cover.
diff --git a/.changeset/lynx-keyboard-avoidance.md b/.changeset/lynx-keyboard-avoidance.md
deleted file mode 100644
index 6c0026f..0000000
--- a/.changeset/lynx-keyboard-avoidance.md
+++ /dev/null
@@ -1,63 +0,0 @@
----
-'@amritk/mini-lynx': minor
----
-
-Add `@amritk/mini-lynx/keyboard` — the soft keyboard as a signal, and the
-layouts that move out of its way.
-
-Lynx does not avoid the keyboard for you. `` does not do it, the docs say
-so outright, and what the engine offers is a single global event —
-`keyboardstatuschanged`, carrying `'on' | 'off'` and a height. Every Lynx app
-with a form writes the layout on top of that event, and the three mistakes are
-always the same: lifting by the whole keyboard rather than by the overlap,
-adding the bottom safe-area inset on top of a keyboard that already covers it,
-and clearing on `blur` — which makes the screen flinch when focus moves between
-two adjacent fields.
-
-The wiring is two lines and no provider:
-
-```tsx
-trackKeyboard() // once, next to setEngine
-
-
-
-
-
-```
-
-`ref` is this runtime's element-extension seam, so a control reports its own
-focus and the container reads it — nothing is threaded between them.
-
-- **`trackKeyboard(options?)`** subscribes to the engine's `GlobalEventEmitter`
- and feeds `keyboardHeight()`. The emitter is an option because Lynx's own
- compatibility data lists `keyboardstatuschanged` as **unsupported on the web**:
- a DOM build reports the keyboard from `visualViewport` and passes it in, and
- everything downstream is the same code.
-- **`keyboardLift({ inset, offset })`** is `max(0, height - inset) + offset`
- while open and exactly zero while closed — the offset included, because a gap
- above a keyboard that is not there is a hole in the layout.
-- **`keepAboveKeyboard()`** measures the focused field against a bounds element
- and answers how far a container has to rise, feeding the rise already applied
- back into the next measurement so moving between fields cannot drop the
- container back to rest. Both rects come from one coordinate space, so nothing
- here needs pixel ratios or a status-bar height — which is the part `lynx-ui`
- pays for with an `androidStatusBarPlusBottomBarHeight` prop.
-- **``** is that wired to a style binding, with
- `behavior='translate'` (rise by the measured overlap) or `'padding'` (reserve
- the keyboard's height for a scroller). It writes only the declaration it owns
- and imposes no layout of its own, and it honours `reducedMotion()` the way
- `RouteStack` does.
-
-`/keyboard` is opt-in and its own module graph, reaching sideways into exactly
-one file — `elements/invoke.ts`, for the rect — which `import-boundary.test.ts`
-now pins as an exact list.
-
-The playground gains a `/keyboard` screen, a `visualViewport` emitter, and
-`__InvokeUIMethod` on its DOM Element PAPI (`boundingClientRect` and
-`scrollIntoView` answered honestly, everything else reported as unimplemented
-rather than invented).
-
-**Not verified on a device.** The arithmetic and the components are covered
-against the fake engine, but the engine event itself, the units it reports, and
-how a real IME animation interacts with the transition are unconfirmed on
-hardware.
diff --git a/.changeset/lynx-location-reverse-geocoding.md b/.changeset/lynx-location-reverse-geocoding.md
deleted file mode 100644
index 07a635c..0000000
--- a/.changeset/lynx-location-reverse-geocoding.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-'@amritk/lynx-location': minor
----
-
-Add `reverseGeocode` to `@amritk/lynx-location` — coordinates to a postal
-address, on both platforms.
-
-`CLGeocoder` on iOS, `android.location.Geocoder` on Android. The Android half
-uses the API 33 callback form where it exists and the blocking form on an
-executor below that, because this library supports devices well under 33 and a
-network round trip on the calling thread is an ANR waiting to happen.
-
-**It needs no permission and never prompts.** Reverse geocoding reads no device
-location — it geocodes the coordinates it is handed — so an app that has been
-refused location outright can still label a saved venue or a map centre. A
-`LocationFix` satisfies the new `Coordinates` type, so pairing it with
-`getCurrentPosition` needs no mapping step.
-
-`GeocodeResult` is a discriminated union like the rest of the package:
-`{ ok: true, addresses }` or `{ ok: false, error, message }`, where `error` is
-`invalidCoordinates`, `notFound`, `network` or `unavailable`. Being throttled is
-`network` rather than a code of its own — Apple rate-limits `CLGeocoder` and
-reports it that way, and Android has no equivalent to report. `notFound` is the
-only spelling of "there is no address there"; an empty `addresses` is never a
-success.
-
-Every `GeocodeAddress` field is nullable, and `formattedAddress` is built by the
-OS — `getAddressLine(0)` on Android, `CNPostalAddressFormatter` on iOS — so it
-places each country's postcode where that country places it. `isoCountryCode` is
-the only field stable across locales.
-
-The iOS half now links `Contacts`, for `CNPostalAddressFormatter` alone. It
-reaches no contact store and needs no permission.
-
-`createFakeLocation` gains `setNextAddresses`, `setGeocoderPresent`,
-`setNetworkAvailable` and `geocodes()`. Nothing here has run on a device; the
-package's `AGENTS.md` lists what reverse geocoding specifically leaves unproven.
diff --git a/.changeset/lynx-native-dialogs.md b/.changeset/lynx-native-dialogs.md
deleted file mode 100644
index 8c92ba6..0000000
--- a/.changeset/lynx-native-dialogs.md
+++ /dev/null
@@ -1,56 +0,0 @@
----
-'@amritk/lynx-dialogs': minor
----
-
-Add `@amritk/lynx-dialogs` — the platform's own date picker, action sheet and
-alert for Lynx.
-
-Lynx ships no picker element and no modal module, and neither published option
-fills the gap: `@lynx-js/lynx-ui-sheet` draws a sheet out of ReactLynx elements
-rather than presenting a native one, and `@sigx/lynx-datetime-picker` is a real
-`UIDatePicker`/`DatePickerDialog` bolted to another framework's bridge. Nothing
-native exists for action sheets at all.
-
-So this is an Android module, an iOS module, and a promise-shaped facade over
-`@amritk/mini-lynx-native`:
-
-```ts
-const date = await presentDatePicker({ mode: 'date', maximum: Date.now() })
-if (date.ok) setDeparture(new Date(date.value))
-
-const choice = await presentActionSheet({
- actions: [{ label: 'Replace' }, { label: 'Delete', destructive: true }],
-})
-if (choice.ok) apply(choice.index)
-
-const confirm = await presentAlert({
- title: 'Delete this photo?',
- buttons: [{ label: 'Cancel', style: 'cancel' }, { label: 'Delete', style: 'destructive' }],
-})
-if (confirm.ok && confirm.index === 1) remove()
-```
-
-`presentDatePicker` covers `date`, `time` and `datetime` with bounds, labels and
-a 12/24-hour override; `presentActionSheet` covers destructive and disabled rows
-and the iPad popover anchor; `presentAlert` covers one to three buttons with
-cancel and destructive styling. `dismissActiveDialog` closes whatever is up so a
-screen can clean up on navigation, and `areDialogsAvailable` reports whether the
-host app linked the module.
-
-`AlertButtons` is a one-to-three tuple rather than an array, because
-`AlertDialog` has exactly three button slots and there is no fourth — so the
-Android cap is a compile error instead of a button that goes missing on half the
-devices an app runs on.
-
-Every outcome is a discriminated union rather than a rejection — cancelling is
-the most likely thing a user does with a dialog, so it is a branch. Only one
-presentation is allowed on screen at a time; a second resolves
-`{ ok: false, reason: 'busy' }` rather than stacking on Android or hanging
-forever on iOS.
-
-Unlike the other two native packages there is no permission, no `Info.plist` key
-and no manifest entry for the host app to add, and the Android half takes no
-dependencies at all — the framework `AlertDialog` rather than Material, so no
-`Theme.Material3` requirement lands on the host's Activity.
-
-`@amritk/lynx-dialogs/testing` ships the native contract as an executable fake.
diff --git a/.changeset/lynx-push-notifications.md b/.changeset/lynx-push-notifications.md
deleted file mode 100644
index 74a126e..0000000
--- a/.changeset/lynx-push-notifications.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-'@amritk/mini-lynx-native': minor
-'@amritk/lynx-notifications': minor
----
-
-Add native modules for Lynx, starting with push notifications.
-
-Lynx ships no notifications module, and Sparkling's `sparkling-notifications` is
-a reserved npm name with no implementation behind it — so the official answer is
-still "write native code and send it into your Lynx code". These two packages
-are that, plus the piece it needs first.
-
-**`@amritk/mini-lynx-native`** is the wire. Lynx's `NativeModules` and
-`GlobalEventEmitter` are background-thread globals, while `@amritk/mini-lynx`
-renders on the main thread because the Element PAPI is a main-thread API — so a
-component reaching for a native module finds `undefined`, with nothing to read.
-The package carries calls one way and events the other: `callNative` and
-`callNativeAsync` for the two shapes a Lynx native method comes in,
-`isNativeModuleAvailable` for feature detection, and `onNativeEvent` for
-`sendGlobalEvent`. Calls made before the background half is installed are queued
-rather than lost, because the main-thread chunk usually runs first. The
-`/background` subpath is the half an app installs in its background chunk, in
-one line; `/testing` ships the fakes both halves run against.
-
-**`@amritk/lynx-notifications`** is the first module built on it: local and
-remote push, with an Android implementation (`NotificationManager`,
-`AlarmManager`, FCM) and an iOS one (`UNUserNotificationCenter`, APNs) declared
-to Lynx's autolinker through `lynx.lib.json`. The JavaScript surface is promises
-and subscriptions rather than signals, deliberately — a second edge onto the
-signal engine is how a consumer ends up with two reactive graphs that cannot see
-each other's writes.
-
-Both native halves are compiled in CI — Gradle against the real
-`org.lynxsdk.lynx:lynx` AAR for Android, `pod lib lint` against the real Lynx pod
-on a macOS runner for iOS — and `src/native-contract.test.ts` pins their method
-names, arities and event strings against the TypeScript, which is the one class
-of drift no compiler on either side can catch. `bun run check:android` runs the
-Android compile locally and skips with an explanation when there is no SDK.
-
-**None of that has run on a device.** Permission flows, `AlarmManager` under
-Doze, APNs registration and FCM delivery are unverified. The caveat is carried in
-the package's `README.md`, `AI.md` and `AGENTS.md`.
diff --git a/.changeset/olive-pots-repeat.md b/.changeset/olive-pots-repeat.md
deleted file mode 100644
index c842a83..0000000
--- a/.changeset/olive-pots-repeat.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'@amritk/lynx-notifications': patch
----
-
-Backport the dangling-selector check from `@amritk/lynx-location`'s parity
-suite: `native-contract.test.ts` now asserts that every selector named in the
-Objective-C `methodLookup` table has a method implementing it.
-
-That is the one cross-language failure nothing else here could see. A selector
-string pointing at no method is not a build error on iOS — `pod lib lint`
-passes — and fails only when Lynx tries to dispatch through it, on a device, as
-a promise that never settles. All eleven of the package's selectors resolve
-today; the check is mutation-verified.
-
-No runtime change.
diff --git a/.changeset/playground-native-modules.md b/.changeset/playground-native-modules.md
deleted file mode 100644
index 3e7239b..0000000
--- a/.changeset/playground-native-modules.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'@amritk/mini-lynx-native': patch
----
-
-Correct the two mis-call failure modes in `AI.md`, and pin them with tests.
-
-The gotchas list had them the wrong way round: it said `callNative` on a
-callback method never settles and `callNativeAsync` on a returning one gives
-you `undefined`. The bridge does the opposite, and it is not a close call —
-the `return` form always replies, so `callNative` always settles, while the
-`callback` form appends a callback that a returning method ignores, so nothing
-ever replies at all.
-
-```ts
-// settles: with `undefined` for a method that stores the callback, or as a
-// rejection when the method reaches for the argument it was not handed
-await callNative('StorageModule', 'loadValue', 'profile')
-
-// never settles: the appended callback is an argument the method ignores
-await callNativeAsync('StorageModule', 'getValue', 'token')
-```
-
-Both are now cases in `channel.test.ts`, because the symmetric-sounding summary
-is the half that sends people looking in the wrong place — a promise that never
-settles reads as a thread problem, and it is a one-word choice at the call site.
-
-Found by wiring the package into `apps/playground-mini-lynx`, which now has a
-screen per native package driving them through each package's own published
-fake.
diff --git a/.changeset/quick-pans-invent.md b/.changeset/quick-pans-invent.md
deleted file mode 100644
index 1379701..0000000
--- a/.changeset/quick-pans-invent.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@amritk/mini-lynx': patch
----
-
-Cut main-thread work out of the paths every screen runs.
-
-This runtime is main-thread because the Element PAPI is, so the work it does
-building and updating a tree is not work a background thread can absorb — it is
-work the frame has to fit around. Five changes, all in the hot paths, none of
-them changing an API:
-
-- `jsx-runtime.ts` walks props with `for…in` instead of `Object.entries`, which
- was allocating an array plus a pair per prop, per element. It was the single
- hottest function in a CPU profile of building a thousand-row list.
-- `apply-prop.ts` remembers the event-prefix parse per prop name. Every prop
- used to pay up to six `startsWith` scans on its way to `__SetAttribute`; a
- given spelling is now scanned once for the life of the app.
-- `style/to-css-name.ts` and `style/to-style-text.ts` do the same for the CSS
- spelling of a style key and the unitless verdict on it. Both ran a regex per
- key per write, which for a reactive style bag — `style={() => ({ paddingBottom:
- keyboardHeight() })}` — meant rediscovering the same answers every frame.
-- `add-event.ts` no longer copies a handler set of one on every delivered event,
- and builds that set empty rather than from an iterable it had to allocate. The
- dispatcher runs on every frame of a scroll, where its garbage competes with
- the layout it is scrolling.
-- `resolve-class.ts` collects into one accumulator instead of
- `map`/`filter`/`join`, which allocated three arrays per array level and four
- per toggle map to produce one string. A reactive `class` is the binding an app
- re-runs most.
-
-Measured against an engine that does nothing but keep the tree — so the number
-is the runtime's own cost and not a host's — building a thousand rows went from
-~9.5ms to ~7ms, about a quarter.
-
-Separately, `testing/create-fake-engine.ts` trims its call log in batches
-instead of on every call. It was doing `calls.slice(-1000)` per PAPI call once
-past the first thousand, so a suite that builds a thousand rows paid twenty-five
-million array writes for a log it keeps a thousand lines of — which also meant
-the package's own reconciler benchmark was mostly measuring the fake. `calls()`
-slices on the way out, so the window a caller sees is unchanged. End to end that
-takes `create 10,000 rows` in `bun run bench:reconciler` from ~840ms to ~170ms,
-and `create 1,000 rows` from ~105ms to ~20ms, with the engine-call counts
-identical.
-
-The core's gzipped byte budget moves 5449 → 5559 to pay for the lookup tables.
-See the note in `core-size-budget.test.ts` for the reasoning; it is the third
-deliberate move.
diff --git a/.changeset/rotten-buttons-grin.md b/.changeset/rotten-buttons-grin.md
deleted file mode 100644
index c956370..0000000
--- a/.changeset/rotten-buttons-grin.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-'@amritk/lynx-deep-linking': minor
----
-
-Add `@amritk/lynx-deep-linking` — deep links for Lynx, as an Android native
-module, an iOS native module and a promise-shaped facade over
-`@amritk/mini-lynx-native`.
-
-Lynx ships no linking module, and the two published community packages are parts
-of frameworks rather than libraries: `@sigx/lynx-linking` needs `@sigx/lynx-core`
-and a `sigx prebuild` step, `@tamer4lynx/tamer-linking` peers on ReactLynx and
-links through the `t4l` CLI, and neither reaches the main thread a `mini-lynx`
-tree renders on.
-
-Inbound, the launch URL is a value (`getInitialURL`) and every later link is an
-event (`onDeepLink`), so an app can handle both without handling one tap twice;
-a link that arrives with no view up is held natively and replayed once. Outbound,
-`openURL`, `canOpenURL` and `openSettings` — the last being the missing half of
-the `denied` states `@amritk/lynx-location` and `@amritk/lynx-notifications`
-report. `parseURL` and `createURL` are pure and need no bridge.
-
-Cold start needs no host wiring on either platform; a link into a running app
-needs one forwarded callback, which no library can observe for itself.
diff --git a/.changeset/typed-routes-and-browser-history.md b/.changeset/typed-routes-and-browser-history.md
deleted file mode 100644
index 02ea991..0000000
--- a/.changeset/typed-routes-and-browser-history.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-'@amritk/mini-helpers': minor
-'@amritk/mini-lynx': minor
-'@amritk/mini': minor
----
-
-Type route params from the pattern, and give `@amritk/mini-lynx` a browser
-history again.
-
-**The pattern is now read at the type level.** `PathParams<'/users/:id'>` is
-`{ id: string }`, and `matchRoute` is generic over its pattern, so
-`matchRoute('/users/:id', path)` hands back `{ id: string } | null` instead of a
-record that answers `string` for every key including the misspelt ones. It lives
-in `@amritk/mini-helpers` beside the matcher it mirrors, because the grammar has
-to be one definition or the value and type worlds drift; `path-params.test.ts`
-pins the two together, down to `/v:major` being a literal segment on both sides.
-
-`PathParams` is `RouteParams`, so this is additive: a table annotated
-`Route[]`, or built at runtime, compiles exactly as before and gets exactly what
-it got before.
-
-**`buildPath` is the inverse.** `buildPath('/users/:id', { id })` cannot spell
-the pattern wrong, forget a param or be left behind when a route is renamed, and
-its values are encoded to round-trip back through `matchRoute`. A `*` wildcard
-is encoded per segment, because `rest` is a path and its slashes are structure.
-`navigate` still takes a plain string, deliberately — a router navigates to
-concrete paths, and one that matches nothing is what a fallback screen is for —
-so the check sits where the string is assembled instead.
-
-**`@amritk/mini-lynx/router` gains `route()` and `AnyRoute`.**
-`route('/users/:id', (params) => …, meta)` keeps the pattern's literal type
-alive so the view is handed a `() => { id: string }`. Its three arguments are
-not a style choice: folded into one object literal, the pattern and the metadata
-compete for inference against an intersection, and the metadata loses — a tab
-bar reading `route.label` would get `unknown` back.
-
-`Route` is now `Route
`, and the router's generics are
-constrained by the new `AnyRoute`. `Route<'/users/:id'>` is deliberately not
-assignable to `Route` — its `view` demands the narrow getter, and the
-widened form can only promise the flat record — so a table of mixed patterns
-needs a constraint that admits all of them. `render-route.ts` owns the single
-cast that reconciles the two. Existing tables keep working: `Route` with no type
-argument is what it always was.
-
-**`createBrowserHistory` is back, on `@amritk/mini-lynx/router/browser`.** It is
-a `RouterHistory` and nothing else, because the rest of routing is already
-target-free — a device build swaps `createMemoryHistory()` back in and changes
-nothing else. It takes a `base` prefix, and it stamps its depth into
-`history.state` rather than reading `window.history.length`, which counts the
-whole tab and is wrong in both directions once the user has gone back. That
-stamp is what survives the two things a memory stack never faces: a reload
-mid-stack and a forward button.
-
-It is the only module in the package that names `window`, and it is quarantined
-accordingly. `src/router/browser/` is excluded from the main compiler pass and
-covered by `tsconfig.dom.json`, so the platform-free rule the rest of the
-package keeps stays something the compiler enforces rather than a convention.
-Hash mode is not included: configuring a host for an SPA fallback is one line,
-where a second URL grammar would be one the device build can never use.
-
-**`stripBase` moved into `@amritk/mini-helpers`,** since both routers now read a
-browser pathname and a base that meant something slightly different on each side
-is exactly the drift that package exists to prevent. `@amritk/mini`'s router
-surface is unchanged apart from re-exporting `buildPath` and `PathParams`.
diff --git a/apps/playground-mini-lynx/CHANGELOG.md b/apps/playground-mini-lynx/CHANGELOG.md
index ac216d3..aa405a7 100644
--- a/apps/playground-mini-lynx/CHANGELOG.md
+++ b/apps/playground-mini-lynx/CHANGELOG.md
@@ -1,5 +1,27 @@
# @amritk/playground-mini-lynx
+## 0.0.4
+
+### Patch Changes
+
+- Updated dependencies [1b6c33d]
+- Updated dependencies [293234c]
+- Updated dependencies [59983c4]
+- Updated dependencies [7c690ed]
+- Updated dependencies [2b07688]
+- Updated dependencies [e025ac7]
+- Updated dependencies [4423822]
+- Updated dependencies [5101aa7]
+- Updated dependencies [d11610b]
+- Updated dependencies [c657444]
+- Updated dependencies [ab6476a]
+ - @amritk/lynx-deep-linking@0.2.0
+ - @amritk/lynx-notifications@0.2.0
+ - @amritk/lynx-dialogs@0.2.0
+ - @amritk/lynx-location@0.2.0
+ - @amritk/mini-lynx@0.4.0
+ - @amritk/mini-lynx-native@0.2.0
+
## 0.0.3
### Patch Changes
diff --git a/apps/playground-mini-lynx/package.json b/apps/playground-mini-lynx/package.json
index 3664d80..10457cd 100644
--- a/apps/playground-mini-lynx/package.json
+++ b/apps/playground-mini-lynx/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/playground-mini-lynx",
- "version": "0.0.3",
+ "version": "0.0.4",
"private": true,
"description": "Kitchen-sink playground for @amritk/mini-lynx and the native modules around it, previewed through a DOM implementation of Lynx's Element PAPI and deployed to Cloudflare Workers as a static SPA.",
"type": "module",
diff --git a/apps/playground-mini/CHANGELOG.md b/apps/playground-mini/CHANGELOG.md
index e32aeaf..d522560 100644
--- a/apps/playground-mini/CHANGELOG.md
+++ b/apps/playground-mini/CHANGELOG.md
@@ -1,5 +1,13 @@
# @amritk/playground-mini
+## 0.0.2
+
+### Patch Changes
+
+- Updated dependencies [1b6c33d]
+- Updated dependencies [ab6476a]
+ - @amritk/mini@0.7.0
+
## 0.0.1
### Patch Changes
diff --git a/apps/playground-mini/package.json b/apps/playground-mini/package.json
index c821c0a..a0d74df 100644
--- a/apps/playground-mini/package.json
+++ b/apps/playground-mini/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/playground-mini",
- "version": "0.0.1",
+ "version": "0.0.2",
"private": true,
"description": "Kitchen-sink playground for @amritk/mini, deployed to Cloudflare Workers as a static SPA.",
"type": "module",
diff --git a/packages/lynx-deep-linking/CHANGELOG.md b/packages/lynx-deep-linking/CHANGELOG.md
new file mode 100644
index 0000000..5d953e6
--- /dev/null
+++ b/packages/lynx-deep-linking/CHANGELOG.md
@@ -0,0 +1,61 @@
+# @amritk/lynx-deep-linking
+
+## 0.2.0
+
+### Minor Changes
+
+- c657444: Add `@amritk/lynx-deep-linking` — deep links for Lynx, as an Android native
+ module, an iOS native module and a promise-shaped facade over
+ `@amritk/mini-lynx-native`.
+
+ Lynx ships no linking module, and the two published community packages are parts
+ of frameworks rather than libraries: `@sigx/lynx-linking` needs `@sigx/lynx-core`
+ and a `sigx prebuild` step, `@tamer4lynx/tamer-linking` peers on ReactLynx and
+ links through the `t4l` CLI, and neither reaches the main thread a `mini-lynx`
+ tree renders on.
+
+ Inbound, the launch URL is a value (`getInitialURL`) and every later link is an
+ event (`onDeepLink`), so an app can handle both without handling one tap twice;
+ a link that arrives with no view up is held natively and replayed once. Outbound,
+ `openURL`, `canOpenURL` and `openSettings` — the last being the missing half of
+ the `denied` states `@amritk/lynx-location` and `@amritk/lynx-notifications`
+ report. `parseURL` and `createURL` are pure and need no bridge.
+
+ Cold start needs no host wiring on either platform; a link into a running app
+ needs one forwarded callback, which no library can observe for itself.
+
+### Patch Changes
+
+- 1b6c33d: Bring every package's shipped `AI.md` back in line with what that package
+ actually publishes, and add `bun run check:ai-docs` so it cannot drift again.
+
+ The files had gone stale in the way generated-and-committed docs always do —
+ silently, and only for the audience that cannot file an issue about it.
+ `@amritk/mini` never documented `watch`, `template`, the typed `matchRoute` /
+ `buildPath` re-exports on `/router`, `Field` on `/forms`, or the `/vite` subpath
+ at all; `@amritk/mini-lynx` was missing `computed` / `effectScope`,
+ `fadeTransition`, `keepAboveKeyboard` and `HANDLER_PREFIX`;
+ `@amritk/lynx-notifications` documented neither its `/testing` subpath nor the
+ fake behind it. All four native packages exported `MODULE` and `EVENTS` with no
+ mention of what they are for, and only `@amritk/lynx-dialogs` showed how to wire
+ a fake into `installNativeBridge` — which is the one thing a consumer testing
+ its own screens needs.
+
+ Two accuracy fixes matter more than the additions. Every native package's
+ _Status_ section claimed the Objective-C compiles against the real Lynx pod; the
+ macOS CI job was disabled on cost, so it now compiles only when somebody runs
+ `pod lib lint` by hand, and the docs say that. And `@amritk/lynx-dialogs` never
+ carried a _Status_ section at all, so nothing in it told a reader that none of
+ it has run on a device.
+
+ `bun run check:ai-docs` reads each package's `exports` and fails on a runtime
+ export, a published subpath, or (for a package shipping native sources) a
+ _Status_ section its `AI.md` never mentions. It runs early in CI, before the
+ build. Exports no consumer ever writes — the tree operations the JSX transform
+ calls, and the like — are listed in `INTERNAL_EXPORTS` with the reason.
+
+- Updated dependencies [e025ac7]
+- Updated dependencies [5101aa7]
+- Updated dependencies [ab6476a]
+ - @amritk/mini-lynx-native@0.2.0
+ - @amritk/mini-helpers@0.2.0
diff --git a/packages/lynx-deep-linking/package.json b/packages/lynx-deep-linking/package.json
index 8053957..fd0629c 100644
--- a/packages/lynx-deep-linking/package.json
+++ b/packages/lynx-deep-linking/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/lynx-deep-linking",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "Deep links for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.",
"type": "module",
"license": "MIT",
diff --git a/packages/lynx-dialogs/CHANGELOG.md b/packages/lynx-dialogs/CHANGELOG.md
new file mode 100644
index 0000000..35584fe
--- /dev/null
+++ b/packages/lynx-dialogs/CHANGELOG.md
@@ -0,0 +1,95 @@
+# @amritk/lynx-dialogs
+
+## 0.2.0
+
+### Minor Changes
+
+- 2b07688: Add `@amritk/lynx-dialogs` — the platform's own date picker, action sheet and
+ alert for Lynx.
+
+ Lynx ships no picker element and no modal module, and neither published option
+ fills the gap: `@lynx-js/lynx-ui-sheet` draws a sheet out of ReactLynx elements
+ rather than presenting a native one, and `@sigx/lynx-datetime-picker` is a real
+ `UIDatePicker`/`DatePickerDialog` bolted to another framework's bridge. Nothing
+ native exists for action sheets at all.
+
+ So this is an Android module, an iOS module, and a promise-shaped facade over
+ `@amritk/mini-lynx-native`:
+
+ ```ts
+ const date = await presentDatePicker({ mode: "date", maximum: Date.now() });
+ if (date.ok) setDeparture(new Date(date.value));
+
+ const choice = await presentActionSheet({
+ actions: [{ label: "Replace" }, { label: "Delete", destructive: true }],
+ });
+ if (choice.ok) apply(choice.index);
+
+ const confirm = await presentAlert({
+ title: "Delete this photo?",
+ buttons: [
+ { label: "Cancel", style: "cancel" },
+ { label: "Delete", style: "destructive" },
+ ],
+ });
+ if (confirm.ok && confirm.index === 1) remove();
+ ```
+
+ `presentDatePicker` covers `date`, `time` and `datetime` with bounds, labels and
+ a 12/24-hour override; `presentActionSheet` covers destructive and disabled rows
+ and the iPad popover anchor; `presentAlert` covers one to three buttons with
+ cancel and destructive styling. `dismissActiveDialog` closes whatever is up so a
+ screen can clean up on navigation, and `areDialogsAvailable` reports whether the
+ host app linked the module.
+
+ `AlertButtons` is a one-to-three tuple rather than an array, because
+ `AlertDialog` has exactly three button slots and there is no fourth — so the
+ Android cap is a compile error instead of a button that goes missing on half the
+ devices an app runs on.
+
+ Every outcome is a discriminated union rather than a rejection — cancelling is
+ the most likely thing a user does with a dialog, so it is a branch. Only one
+ presentation is allowed on screen at a time; a second resolves
+ `{ ok: false, reason: 'busy' }` rather than stacking on Android or hanging
+ forever on iOS.
+
+ Unlike the other two native packages there is no permission, no `Info.plist` key
+ and no manifest entry for the host app to add, and the Android half takes no
+ dependencies at all — the framework `AlertDialog` rather than Material, so no
+ `Theme.Material3` requirement lands on the host's Activity.
+
+ `@amritk/lynx-dialogs/testing` ships the native contract as an executable fake.
+
+### Patch Changes
+
+- 1b6c33d: Bring every package's shipped `AI.md` back in line with what that package
+ actually publishes, and add `bun run check:ai-docs` so it cannot drift again.
+
+ The files had gone stale in the way generated-and-committed docs always do —
+ silently, and only for the audience that cannot file an issue about it.
+ `@amritk/mini` never documented `watch`, `template`, the typed `matchRoute` /
+ `buildPath` re-exports on `/router`, `Field` on `/forms`, or the `/vite` subpath
+ at all; `@amritk/mini-lynx` was missing `computed` / `effectScope`,
+ `fadeTransition`, `keepAboveKeyboard` and `HANDLER_PREFIX`;
+ `@amritk/lynx-notifications` documented neither its `/testing` subpath nor the
+ fake behind it. All four native packages exported `MODULE` and `EVENTS` with no
+ mention of what they are for, and only `@amritk/lynx-dialogs` showed how to wire
+ a fake into `installNativeBridge` — which is the one thing a consumer testing
+ its own screens needs.
+
+ Two accuracy fixes matter more than the additions. Every native package's
+ _Status_ section claimed the Objective-C compiles against the real Lynx pod; the
+ macOS CI job was disabled on cost, so it now compiles only when somebody runs
+ `pod lib lint` by hand, and the docs say that. And `@amritk/lynx-dialogs` never
+ carried a _Status_ section at all, so nothing in it told a reader that none of
+ it has run on a device.
+
+ `bun run check:ai-docs` reads each package's `exports` and fails on a runtime
+ export, a published subpath, or (for a package shipping native sources) a
+ _Status_ section its `AI.md` never mentions. It runs early in CI, before the
+ build. Exports no consumer ever writes — the tree operations the JSX transform
+ calls, and the like — are listed in `INTERNAL_EXPORTS` with the reason.
+
+- Updated dependencies [e025ac7]
+- Updated dependencies [5101aa7]
+ - @amritk/mini-lynx-native@0.2.0
diff --git a/packages/lynx-dialogs/package.json b/packages/lynx-dialogs/package.json
index 356eda9..d6443d3 100644
--- a/packages/lynx-dialogs/package.json
+++ b/packages/lynx-dialogs/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/lynx-dialogs",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "Native date pickers and action sheets for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.",
"type": "module",
"license": "MIT",
diff --git a/packages/lynx-location/CHANGELOG.md b/packages/lynx-location/CHANGELOG.md
new file mode 100644
index 0000000..325b2fe
--- /dev/null
+++ b/packages/lynx-location/CHANGELOG.md
@@ -0,0 +1,98 @@
+# @amritk/lynx-location
+
+## 0.2.0
+
+### Minor Changes
+
+- 293234c: Add `@amritk/lynx-location` — device location for Lynx.
+
+ Lynx ships no location module, Sparkling's built-ins stop at navigation,
+ storage and media, and the one published community package
+ (`@sigx/lynx-location`) calls `NativeModules` directly from its JavaScript half,
+ which is background-thread only — so a main-thread `@amritk/mini-lynx` component
+ importing it gets `undefined`. This is an Android module, an iOS module, and a
+ promise-shaped facade over `@amritk/mini-lynx-native`, built from the shape
+ `@amritk/lynx-notifications` established.
+
+ `getPermissionStatus`, `requestPermission`, `getCurrentPosition`,
+ `getLastKnownPosition`, `isLocationEnabled`, `isLocationAvailable` and
+ `watchPosition`. `CLLocationManager` on iOS, `LocationManager` on Android — not
+ the fused provider, so no Play Services dependency and no non-GMS devices
+ excluded. Foreground only, deliberately: no `ACCESS_BACKGROUND_LOCATION` and no
+ `Always` authorisation.
+
+ Failures are values rather than rejections — `{ ok: true, position }` or
+ `{ ok: false, error, message }` — because Lynx has no error convention for
+ bridge callbacks and a missing permission is an ordinary UI branch.
+
+ `@amritk/lynx-location/testing` ships the native contract as an executable fake.
+ Nothing here has run on a device; see the package's `AGENTS.md` for what the
+ three CI checks do and do not cover.
+
+- 7c690ed: Add `reverseGeocode` to `@amritk/lynx-location` — coordinates to a postal
+ address, on both platforms.
+
+ `CLGeocoder` on iOS, `android.location.Geocoder` on Android. The Android half
+ uses the API 33 callback form where it exists and the blocking form on an
+ executor below that, because this library supports devices well under 33 and a
+ network round trip on the calling thread is an ANR waiting to happen.
+
+ **It needs no permission and never prompts.** Reverse geocoding reads no device
+ location — it geocodes the coordinates it is handed — so an app that has been
+ refused location outright can still label a saved venue or a map centre. A
+ `LocationFix` satisfies the new `Coordinates` type, so pairing it with
+ `getCurrentPosition` needs no mapping step.
+
+ `GeocodeResult` is a discriminated union like the rest of the package:
+ `{ ok: true, addresses }` or `{ ok: false, error, message }`, where `error` is
+ `invalidCoordinates`, `notFound`, `network` or `unavailable`. Being throttled is
+ `network` rather than a code of its own — Apple rate-limits `CLGeocoder` and
+ reports it that way, and Android has no equivalent to report. `notFound` is the
+ only spelling of "there is no address there"; an empty `addresses` is never a
+ success.
+
+ Every `GeocodeAddress` field is nullable, and `formattedAddress` is built by the
+ OS — `getAddressLine(0)` on Android, `CNPostalAddressFormatter` on iOS — so it
+ places each country's postcode where that country places it. `isoCountryCode` is
+ the only field stable across locales.
+
+ The iOS half now links `Contacts`, for `CNPostalAddressFormatter` alone. It
+ reaches no contact store and needs no permission.
+
+ `createFakeLocation` gains `setNextAddresses`, `setGeocoderPresent`,
+ `setNetworkAvailable` and `geocodes()`. Nothing here has run on a device; the
+ package's `AGENTS.md` lists what reverse geocoding specifically leaves unproven.
+
+### Patch Changes
+
+- 1b6c33d: Bring every package's shipped `AI.md` back in line with what that package
+ actually publishes, and add `bun run check:ai-docs` so it cannot drift again.
+
+ The files had gone stale in the way generated-and-committed docs always do —
+ silently, and only for the audience that cannot file an issue about it.
+ `@amritk/mini` never documented `watch`, `template`, the typed `matchRoute` /
+ `buildPath` re-exports on `/router`, `Field` on `/forms`, or the `/vite` subpath
+ at all; `@amritk/mini-lynx` was missing `computed` / `effectScope`,
+ `fadeTransition`, `keepAboveKeyboard` and `HANDLER_PREFIX`;
+ `@amritk/lynx-notifications` documented neither its `/testing` subpath nor the
+ fake behind it. All four native packages exported `MODULE` and `EVENTS` with no
+ mention of what they are for, and only `@amritk/lynx-dialogs` showed how to wire
+ a fake into `installNativeBridge` — which is the one thing a consumer testing
+ its own screens needs.
+
+ Two accuracy fixes matter more than the additions. Every native package's
+ _Status_ section claimed the Objective-C compiles against the real Lynx pod; the
+ macOS CI job was disabled on cost, so it now compiles only when somebody runs
+ `pod lib lint` by hand, and the docs say that. And `@amritk/lynx-dialogs` never
+ carried a _Status_ section at all, so nothing in it told a reader that none of
+ it has run on a device.
+
+ `bun run check:ai-docs` reads each package's `exports` and fails on a runtime
+ export, a published subpath, or (for a package shipping native sources) a
+ _Status_ section its `AI.md` never mentions. It runs early in CI, before the
+ build. Exports no consumer ever writes — the tree operations the JSX transform
+ calls, and the like — are listed in `INTERNAL_EXPORTS` with the reason.
+
+- Updated dependencies [e025ac7]
+- Updated dependencies [5101aa7]
+ - @amritk/mini-lynx-native@0.2.0
diff --git a/packages/lynx-location/package.json b/packages/lynx-location/package.json
index 0195614..d54cf5b 100644
--- a/packages/lynx-location/package.json
+++ b/packages/lynx-location/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/lynx-location",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "Device location for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.",
"type": "module",
"license": "MIT",
diff --git a/packages/lynx-notifications/CHANGELOG.md b/packages/lynx-notifications/CHANGELOG.md
new file mode 100644
index 0000000..fe0eefe
--- /dev/null
+++ b/packages/lynx-notifications/CHANGELOG.md
@@ -0,0 +1,89 @@
+# @amritk/lynx-notifications
+
+## 0.2.0
+
+### Minor Changes
+
+- e025ac7: Add native modules for Lynx, starting with push notifications.
+
+ Lynx ships no notifications module, and Sparkling's `sparkling-notifications` is
+ a reserved npm name with no implementation behind it — so the official answer is
+ still "write native code and send it into your Lynx code". These two packages
+ are that, plus the piece it needs first.
+
+ **`@amritk/mini-lynx-native`** is the wire. Lynx's `NativeModules` and
+ `GlobalEventEmitter` are background-thread globals, while `@amritk/mini-lynx`
+ renders on the main thread because the Element PAPI is a main-thread API — so a
+ component reaching for a native module finds `undefined`, with nothing to read.
+ The package carries calls one way and events the other: `callNative` and
+ `callNativeAsync` for the two shapes a Lynx native method comes in,
+ `isNativeModuleAvailable` for feature detection, and `onNativeEvent` for
+ `sendGlobalEvent`. Calls made before the background half is installed are queued
+ rather than lost, because the main-thread chunk usually runs first. The
+ `/background` subpath is the half an app installs in its background chunk, in
+ one line; `/testing` ships the fakes both halves run against.
+
+ **`@amritk/lynx-notifications`** is the first module built on it: local and
+ remote push, with an Android implementation (`NotificationManager`,
+ `AlarmManager`, FCM) and an iOS one (`UNUserNotificationCenter`, APNs) declared
+ to Lynx's autolinker through `lynx.lib.json`. The JavaScript surface is promises
+ and subscriptions rather than signals, deliberately — a second edge onto the
+ signal engine is how a consumer ends up with two reactive graphs that cannot see
+ each other's writes.
+
+ Both native halves are compiled in CI — Gradle against the real
+ `org.lynxsdk.lynx:lynx` AAR for Android, `pod lib lint` against the real Lynx pod
+ on a macOS runner for iOS — and `src/native-contract.test.ts` pins their method
+ names, arities and event strings against the TypeScript, which is the one class
+ of drift no compiler on either side can catch. `bun run check:android` runs the
+ Android compile locally and skips with an explanation when there is no SDK.
+
+ **None of that has run on a device.** Permission flows, `AlarmManager` under
+ Doze, APNs registration and FCM delivery are unverified. The caveat is carried in
+ the package's `README.md`, `AI.md` and `AGENTS.md`.
+
+### Patch Changes
+
+- 1b6c33d: Bring every package's shipped `AI.md` back in line with what that package
+ actually publishes, and add `bun run check:ai-docs` so it cannot drift again.
+
+ The files had gone stale in the way generated-and-committed docs always do —
+ silently, and only for the audience that cannot file an issue about it.
+ `@amritk/mini` never documented `watch`, `template`, the typed `matchRoute` /
+ `buildPath` re-exports on `/router`, `Field` on `/forms`, or the `/vite` subpath
+ at all; `@amritk/mini-lynx` was missing `computed` / `effectScope`,
+ `fadeTransition`, `keepAboveKeyboard` and `HANDLER_PREFIX`;
+ `@amritk/lynx-notifications` documented neither its `/testing` subpath nor the
+ fake behind it. All four native packages exported `MODULE` and `EVENTS` with no
+ mention of what they are for, and only `@amritk/lynx-dialogs` showed how to wire
+ a fake into `installNativeBridge` — which is the one thing a consumer testing
+ its own screens needs.
+
+ Two accuracy fixes matter more than the additions. Every native package's
+ _Status_ section claimed the Objective-C compiles against the real Lynx pod; the
+ macOS CI job was disabled on cost, so it now compiles only when somebody runs
+ `pod lib lint` by hand, and the docs say that. And `@amritk/lynx-dialogs` never
+ carried a _Status_ section at all, so nothing in it told a reader that none of
+ it has run on a device.
+
+ `bun run check:ai-docs` reads each package's `exports` and fails on a runtime
+ export, a published subpath, or (for a package shipping native sources) a
+ _Status_ section its `AI.md` never mentions. It runs early in CI, before the
+ build. Exports no consumer ever writes — the tree operations the JSX transform
+ calls, and the like — are listed in `INTERNAL_EXPORTS` with the reason.
+
+- 4423822: Backport the dangling-selector check from `@amritk/lynx-location`'s parity
+ suite: `native-contract.test.ts` now asserts that every selector named in the
+ Objective-C `methodLookup` table has a method implementing it.
+
+ That is the one cross-language failure nothing else here could see. A selector
+ string pointing at no method is not a build error on iOS — `pod lib lint`
+ passes — and fails only when Lynx tries to dispatch through it, on a device, as
+ a promise that never settles. All eleven of the package's selectors resolve
+ today; the check is mutation-verified.
+
+ No runtime change.
+
+- Updated dependencies [e025ac7]
+- Updated dependencies [5101aa7]
+ - @amritk/mini-lynx-native@0.2.0
diff --git a/packages/lynx-notifications/package.json b/packages/lynx-notifications/package.json
index 65b01da..6ff3f8b 100644
--- a/packages/lynx-notifications/package.json
+++ b/packages/lynx-notifications/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/lynx-notifications",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "Local and remote push notifications for Lynx: an Android and iOS native module, and a promise-shaped facade that reaches it from the main thread.",
"type": "module",
"license": "MIT",
diff --git a/packages/mini-helpers/CHANGELOG.md b/packages/mini-helpers/CHANGELOG.md
index 5a12248..ce404a3 100644
--- a/packages/mini-helpers/CHANGELOG.md
+++ b/packages/mini-helpers/CHANGELOG.md
@@ -1,5 +1,68 @@
# @amritk/mini-helpers
+## 0.2.0
+
+### Minor Changes
+
+- ab6476a: Type route params from the pattern, and give `@amritk/mini-lynx` a browser
+ history again.
+
+ **The pattern is now read at the type level.** `PathParams<'/users/:id'>` is
+ `{ id: string }`, and `matchRoute` is generic over its pattern, so
+ `matchRoute('/users/:id', path)` hands back `{ id: string } | null` instead of a
+ record that answers `string` for every key including the misspelt ones. It lives
+ in `@amritk/mini-helpers` beside the matcher it mirrors, because the grammar has
+ to be one definition or the value and type worlds drift; `path-params.test.ts`
+ pins the two together, down to `/v:major` being a literal segment on both sides.
+
+ `PathParams` is `RouteParams`, so this is additive: a table annotated
+ `Route[]`, or built at runtime, compiles exactly as before and gets exactly what
+ it got before.
+
+ **`buildPath` is the inverse.** `buildPath('/users/:id', { id })` cannot spell
+ the pattern wrong, forget a param or be left behind when a route is renamed, and
+ its values are encoded to round-trip back through `matchRoute`. A `*` wildcard
+ is encoded per segment, because `rest` is a path and its slashes are structure.
+ `navigate` still takes a plain string, deliberately — a router navigates to
+ concrete paths, and one that matches nothing is what a fallback screen is for —
+ so the check sits where the string is assembled instead.
+
+ **`@amritk/mini-lynx/router` gains `route()` and `AnyRoute`.**
+ `route('/users/:id', (params) => …, meta)` keeps the pattern's literal type
+ alive so the view is handed a `() => { id: string }`. Its three arguments are
+ not a style choice: folded into one object literal, the pattern and the metadata
+ compete for inference against an intersection, and the metadata loses — a tab
+ bar reading `route.label` would get `unknown` back.
+
+ `Route` is now `Route
`, and the router's generics are
+ constrained by the new `AnyRoute`. `Route<'/users/:id'>` is deliberately not
+ assignable to `Route` — its `view` demands the narrow getter, and the
+ widened form can only promise the flat record — so a table of mixed patterns
+ needs a constraint that admits all of them. `render-route.ts` owns the single
+ cast that reconciles the two. Existing tables keep working: `Route` with no type
+ argument is what it always was.
+
+ **`createBrowserHistory` is back, on `@amritk/mini-lynx/router/browser`.** It is
+ a `RouterHistory` and nothing else, because the rest of routing is already
+ target-free — a device build swaps `createMemoryHistory()` back in and changes
+ nothing else. It takes a `base` prefix, and it stamps its depth into
+ `history.state` rather than reading `window.history.length`, which counts the
+ whole tab and is wrong in both directions once the user has gone back. That
+ stamp is what survives the two things a memory stack never faces: a reload
+ mid-stack and a forward button.
+
+ It is the only module in the package that names `window`, and it is quarantined
+ accordingly. `src/router/browser/` is excluded from the main compiler pass and
+ covered by `tsconfig.dom.json`, so the platform-free rule the rest of the
+ package keeps stays something the compiler enforces rather than a convention.
+ Hash mode is not included: configuring a host for an SPA fallback is one line,
+ where a second URL grammar would be one the device build can never use.
+
+ **`stripBase` moved into `@amritk/mini-helpers`,** since both routers now read a
+ browser pathname and a base that meant something slightly different on each side
+ is exactly the drift that package exists to prevent. `@amritk/mini`'s router
+ surface is unchanged apart from re-exporting `buildPath` and `PathParams`.
+
## 0.1.0
### Minor Changes
diff --git a/packages/mini-helpers/package.json b/packages/mini-helpers/package.json
index 45a4686..bd22f08 100644
--- a/packages/mini-helpers/package.json
+++ b/packages/mini-helpers/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/mini-helpers",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "The pure helpers @amritk/mini and @amritk/mini-lynx both need: no reactivity, no platform, no dependencies.",
"type": "module",
"license": "MIT",
diff --git a/packages/mini-lynx-native/CHANGELOG.md b/packages/mini-lynx-native/CHANGELOG.md
new file mode 100644
index 0000000..7756b26
--- /dev/null
+++ b/packages/mini-lynx-native/CHANGELOG.md
@@ -0,0 +1,71 @@
+# @amritk/mini-lynx-native
+
+## 0.2.0
+
+### Minor Changes
+
+- e025ac7: Add native modules for Lynx, starting with push notifications.
+
+ Lynx ships no notifications module, and Sparkling's `sparkling-notifications` is
+ a reserved npm name with no implementation behind it — so the official answer is
+ still "write native code and send it into your Lynx code". These two packages
+ are that, plus the piece it needs first.
+
+ **`@amritk/mini-lynx-native`** is the wire. Lynx's `NativeModules` and
+ `GlobalEventEmitter` are background-thread globals, while `@amritk/mini-lynx`
+ renders on the main thread because the Element PAPI is a main-thread API — so a
+ component reaching for a native module finds `undefined`, with nothing to read.
+ The package carries calls one way and events the other: `callNative` and
+ `callNativeAsync` for the two shapes a Lynx native method comes in,
+ `isNativeModuleAvailable` for feature detection, and `onNativeEvent` for
+ `sendGlobalEvent`. Calls made before the background half is installed are queued
+ rather than lost, because the main-thread chunk usually runs first. The
+ `/background` subpath is the half an app installs in its background chunk, in
+ one line; `/testing` ships the fakes both halves run against.
+
+ **`@amritk/lynx-notifications`** is the first module built on it: local and
+ remote push, with an Android implementation (`NotificationManager`,
+ `AlarmManager`, FCM) and an iOS one (`UNUserNotificationCenter`, APNs) declared
+ to Lynx's autolinker through `lynx.lib.json`. The JavaScript surface is promises
+ and subscriptions rather than signals, deliberately — a second edge onto the
+ signal engine is how a consumer ends up with two reactive graphs that cannot see
+ each other's writes.
+
+ Both native halves are compiled in CI — Gradle against the real
+ `org.lynxsdk.lynx:lynx` AAR for Android, `pod lib lint` against the real Lynx pod
+ on a macOS runner for iOS — and `src/native-contract.test.ts` pins their method
+ names, arities and event strings against the TypeScript, which is the one class
+ of drift no compiler on either side can catch. `bun run check:android` runs the
+ Android compile locally and skips with an explanation when there is no SDK.
+
+ **None of that has run on a device.** Permission flows, `AlarmManager` under
+ Doze, APNs registration and FCM delivery are unverified. The caveat is carried in
+ the package's `README.md`, `AI.md` and `AGENTS.md`.
+
+### Patch Changes
+
+- 5101aa7: Correct the two mis-call failure modes in `AI.md`, and pin them with tests.
+
+ The gotchas list had them the wrong way round: it said `callNative` on a
+ callback method never settles and `callNativeAsync` on a returning one gives
+ you `undefined`. The bridge does the opposite, and it is not a close call —
+ the `return` form always replies, so `callNative` always settles, while the
+ `callback` form appends a callback that a returning method ignores, so nothing
+ ever replies at all.
+
+ ```ts
+ // settles: with `undefined` for a method that stores the callback, or as a
+ // rejection when the method reaches for the argument it was not handed
+ await callNative("StorageModule", "loadValue", "profile");
+
+ // never settles: the appended callback is an argument the method ignores
+ await callNativeAsync("StorageModule", "getValue", "token");
+ ```
+
+ Both are now cases in `channel.test.ts`, because the symmetric-sounding summary
+ is the half that sends people looking in the wrong place — a promise that never
+ settles reads as a thread problem, and it is a one-word choice at the call site.
+
+ Found by wiring the package into `apps/playground-mini-lynx`, which now has a
+ screen per native package driving them through each package's own published
+ fake.
diff --git a/packages/mini-lynx-native/package.json b/packages/mini-lynx-native/package.json
index 745f6f6..04db53a 100644
--- a/packages/mini-lynx-native/package.json
+++ b/packages/mini-lynx-native/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/mini-lynx-native",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "The thread hop between a main-thread mini-lynx tree and Lynx's background-thread NativeModules, as a typed request/reply channel.",
"type": "module",
"license": "MIT",
diff --git a/packages/mini-lynx/CHANGELOG.md b/packages/mini-lynx/CHANGELOG.md
index 9d2e435..77eaa39 100644
--- a/packages/mini-lynx/CHANGELOG.md
+++ b/packages/mini-lynx/CHANGELOG.md
@@ -1,5 +1,205 @@
# @amritk/mini-lynx
+## 0.4.0
+
+### Minor Changes
+
+- 59983c4: Add `@amritk/mini-lynx/keyboard` — the soft keyboard as a signal, and the
+ layouts that move out of its way.
+
+ Lynx does not avoid the keyboard for you. `` does not do it, the docs say
+ so outright, and what the engine offers is a single global event —
+ `keyboardstatuschanged`, carrying `'on' | 'off'` and a height. Every Lynx app
+ with a form writes the layout on top of that event, and the three mistakes are
+ always the same: lifting by the whole keyboard rather than by the overlap,
+ adding the bottom safe-area inset on top of a keyboard that already covers it,
+ and clearing on `blur` — which makes the screen flinch when focus moves between
+ two adjacent fields.
+
+ The wiring is two lines and no provider:
+
+ ```tsx
+ trackKeyboard() // once, next to setEngine
+
+
+
+
+
+ ```
+
+ `ref` is this runtime's element-extension seam, so a control reports its own
+ focus and the container reads it — nothing is threaded between them.
+
+ - **`trackKeyboard(options?)`** subscribes to the engine's `GlobalEventEmitter`
+ and feeds `keyboardHeight()`. The emitter is an option because Lynx's own
+ compatibility data lists `keyboardstatuschanged` as **unsupported on the web**:
+ a DOM build reports the keyboard from `visualViewport` and passes it in, and
+ everything downstream is the same code.
+ - **`keyboardLift({ inset, offset })`** is `max(0, height - inset) + offset`
+ while open and exactly zero while closed — the offset included, because a gap
+ above a keyboard that is not there is a hole in the layout.
+ - **`keepAboveKeyboard()`** measures the focused field against a bounds element
+ and answers how far a container has to rise, feeding the rise already applied
+ back into the next measurement so moving between fields cannot drop the
+ container back to rest. Both rects come from one coordinate space, so nothing
+ here needs pixel ratios or a status-bar height — which is the part `lynx-ui`
+ pays for with an `androidStatusBarPlusBottomBarHeight` prop.
+ - **``** is that wired to a style binding, with
+ `behavior='translate'` (rise by the measured overlap) or `'padding'` (reserve
+ the keyboard's height for a scroller). It writes only the declaration it owns
+ and imposes no layout of its own, and it honours `reducedMotion()` the way
+ `RouteStack` does.
+
+ `/keyboard` is opt-in and its own module graph, reaching sideways into exactly
+ one file — `elements/invoke.ts`, for the rect — which `import-boundary.test.ts`
+ now pins as an exact list.
+
+ The playground gains a `/keyboard` screen, a `visualViewport` emitter, and
+ `__InvokeUIMethod` on its DOM Element PAPI (`boundingClientRect` and
+ `scrollIntoView` answered honestly, everything else reported as unimplemented
+ rather than invented).
+
+ **Not verified on a device.** The arithmetic and the components are covered
+ against the fake engine, but the engine event itself, the units it reports, and
+ how a real IME animation interacts with the transition are unconfirmed on
+ hardware.
+
+- ab6476a: Type route params from the pattern, and give `@amritk/mini-lynx` a browser
+ history again.
+
+ **The pattern is now read at the type level.** `PathParams<'/users/:id'>` is
+ `{ id: string }`, and `matchRoute` is generic over its pattern, so
+ `matchRoute('/users/:id', path)` hands back `{ id: string } | null` instead of a
+ record that answers `string` for every key including the misspelt ones. It lives
+ in `@amritk/mini-helpers` beside the matcher it mirrors, because the grammar has
+ to be one definition or the value and type worlds drift; `path-params.test.ts`
+ pins the two together, down to `/v:major` being a literal segment on both sides.
+
+ `PathParams` is `RouteParams`, so this is additive: a table annotated
+ `Route[]`, or built at runtime, compiles exactly as before and gets exactly what
+ it got before.
+
+ **`buildPath` is the inverse.** `buildPath('/users/:id', { id })` cannot spell
+ the pattern wrong, forget a param or be left behind when a route is renamed, and
+ its values are encoded to round-trip back through `matchRoute`. A `*` wildcard
+ is encoded per segment, because `rest` is a path and its slashes are structure.
+ `navigate` still takes a plain string, deliberately — a router navigates to
+ concrete paths, and one that matches nothing is what a fallback screen is for —
+ so the check sits where the string is assembled instead.
+
+ **`@amritk/mini-lynx/router` gains `route()` and `AnyRoute`.**
+ `route('/users/:id', (params) => …, meta)` keeps the pattern's literal type
+ alive so the view is handed a `() => { id: string }`. Its three arguments are
+ not a style choice: folded into one object literal, the pattern and the metadata
+ compete for inference against an intersection, and the metadata loses — a tab
+ bar reading `route.label` would get `unknown` back.
+
+ `Route` is now `Route
`, and the router's generics are
+ constrained by the new `AnyRoute`. `Route<'/users/:id'>` is deliberately not
+ assignable to `Route` — its `view` demands the narrow getter, and the
+ widened form can only promise the flat record — so a table of mixed patterns
+ needs a constraint that admits all of them. `render-route.ts` owns the single
+ cast that reconciles the two. Existing tables keep working: `Route` with no type
+ argument is what it always was.
+
+ **`createBrowserHistory` is back, on `@amritk/mini-lynx/router/browser`.** It is
+ a `RouterHistory` and nothing else, because the rest of routing is already
+ target-free — a device build swaps `createMemoryHistory()` back in and changes
+ nothing else. It takes a `base` prefix, and it stamps its depth into
+ `history.state` rather than reading `window.history.length`, which counts the
+ whole tab and is wrong in both directions once the user has gone back. That
+ stamp is what survives the two things a memory stack never faces: a reload
+ mid-stack and a forward button.
+
+ It is the only module in the package that names `window`, and it is quarantined
+ accordingly. `src/router/browser/` is excluded from the main compiler pass and
+ covered by `tsconfig.dom.json`, so the platform-free rule the rest of the
+ package keeps stays something the compiler enforces rather than a convention.
+ Hash mode is not included: configuring a host for an SPA fallback is one line,
+ where a second URL grammar would be one the device build can never use.
+
+ **`stripBase` moved into `@amritk/mini-helpers`,** since both routers now read a
+ browser pathname and a base that meant something slightly different on each side
+ is exactly the drift that package exists to prevent. `@amritk/mini`'s router
+ surface is unchanged apart from re-exporting `buildPath` and `PathParams`.
+
+### Patch Changes
+
+- 1b6c33d: Bring every package's shipped `AI.md` back in line with what that package
+ actually publishes, and add `bun run check:ai-docs` so it cannot drift again.
+
+ The files had gone stale in the way generated-and-committed docs always do —
+ silently, and only for the audience that cannot file an issue about it.
+ `@amritk/mini` never documented `watch`, `template`, the typed `matchRoute` /
+ `buildPath` re-exports on `/router`, `Field` on `/forms`, or the `/vite` subpath
+ at all; `@amritk/mini-lynx` was missing `computed` / `effectScope`,
+ `fadeTransition`, `keepAboveKeyboard` and `HANDLER_PREFIX`;
+ `@amritk/lynx-notifications` documented neither its `/testing` subpath nor the
+ fake behind it. All four native packages exported `MODULE` and `EVENTS` with no
+ mention of what they are for, and only `@amritk/lynx-dialogs` showed how to wire
+ a fake into `installNativeBridge` — which is the one thing a consumer testing
+ its own screens needs.
+
+ Two accuracy fixes matter more than the additions. Every native package's
+ _Status_ section claimed the Objective-C compiles against the real Lynx pod; the
+ macOS CI job was disabled on cost, so it now compiles only when somebody runs
+ `pod lib lint` by hand, and the docs say that. And `@amritk/lynx-dialogs` never
+ carried a _Status_ section at all, so nothing in it told a reader that none of
+ it has run on a device.
+
+ `bun run check:ai-docs` reads each package's `exports` and fails on a runtime
+ export, a published subpath, or (for a package shipping native sources) a
+ _Status_ section its `AI.md` never mentions. It runs early in CI, before the
+ build. Exports no consumer ever writes — the tree operations the JSX transform
+ calls, and the like — are listed in `INTERNAL_EXPORTS` with the reason.
+
+- d11610b: Cut main-thread work out of the paths every screen runs.
+
+ This runtime is main-thread because the Element PAPI is, so the work it does
+ building and updating a tree is not work a background thread can absorb — it is
+ work the frame has to fit around. Five changes, all in the hot paths, none of
+ them changing an API:
+
+ - `jsx-runtime.ts` walks props with `for…in` instead of `Object.entries`, which
+ was allocating an array plus a pair per prop, per element. It was the single
+ hottest function in a CPU profile of building a thousand-row list.
+ - `apply-prop.ts` remembers the event-prefix parse per prop name. Every prop
+ used to pay up to six `startsWith` scans on its way to `__SetAttribute`; a
+ given spelling is now scanned once for the life of the app.
+ - `style/to-css-name.ts` and `style/to-style-text.ts` do the same for the CSS
+ spelling of a style key and the unitless verdict on it. Both ran a regex per
+ key per write, which for a reactive style bag — `style={() => ({ paddingBottom:
+keyboardHeight() })}` — meant rediscovering the same answers every frame.
+ - `add-event.ts` no longer copies a handler set of one on every delivered event,
+ and builds that set empty rather than from an iterable it had to allocate. The
+ dispatcher runs on every frame of a scroll, where its garbage competes with
+ the layout it is scrolling.
+ - `resolve-class.ts` collects into one accumulator instead of
+ `map`/`filter`/`join`, which allocated three arrays per array level and four
+ per toggle map to produce one string. A reactive `class` is the binding an app
+ re-runs most.
+
+ Measured against an engine that does nothing but keep the tree — so the number
+ is the runtime's own cost and not a host's — building a thousand rows went from
+ ~9.5ms to ~7ms, about a quarter.
+
+ Separately, `testing/create-fake-engine.ts` trims its call log in batches
+ instead of on every call. It was doing `calls.slice(-1000)` per PAPI call once
+ past the first thousand, so a suite that builds a thousand rows paid twenty-five
+ million array writes for a log it keeps a thousand lines of — which also meant
+ the package's own reconciler benchmark was mostly measuring the fake. `calls()`
+ slices on the way out, so the window a caller sees is unchanged. End to end that
+ takes `create 10,000 rows` in `bun run bench:reconciler` from ~840ms to ~170ms,
+ and `create 1,000 rows` from ~105ms to ~20ms, with the engine-call counts
+ identical.
+
+ The core's gzipped byte budget moves 5449 → 5559 to pay for the lookup tables.
+ See the note in `core-size-budget.test.ts` for the reasoning; it is the third
+ deliberate move.
+
+- Updated dependencies [ab6476a]
+ - @amritk/mini-helpers@0.2.0
+
## 0.3.0
### Minor Changes
diff --git a/packages/mini-lynx/package.json b/packages/mini-lynx/package.json
index 4d1cc68..b9dd960 100644
--- a/packages/mini-lynx/package.json
+++ b/packages/mini-lynx/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/mini-lynx",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "A signals runtime for Lynx: real elements created once and mutated forever, driving Lynx's Element PAPI directly, with no virtual tree.",
"type": "module",
"license": "MIT",
diff --git a/packages/mini/CHANGELOG.md b/packages/mini/CHANGELOG.md
index 2f34ea7..b9ec944 100644
--- a/packages/mini/CHANGELOG.md
+++ b/packages/mini/CHANGELOG.md
@@ -1,5 +1,101 @@
# @amritk/mini
+## 0.7.0
+
+### Minor Changes
+
+- ab6476a: Type route params from the pattern, and give `@amritk/mini-lynx` a browser
+ history again.
+
+ **The pattern is now read at the type level.** `PathParams<'/users/:id'>` is
+ `{ id: string }`, and `matchRoute` is generic over its pattern, so
+ `matchRoute('/users/:id', path)` hands back `{ id: string } | null` instead of a
+ record that answers `string` for every key including the misspelt ones. It lives
+ in `@amritk/mini-helpers` beside the matcher it mirrors, because the grammar has
+ to be one definition or the value and type worlds drift; `path-params.test.ts`
+ pins the two together, down to `/v:major` being a literal segment on both sides.
+
+ `PathParams` is `RouteParams`, so this is additive: a table annotated
+ `Route[]`, or built at runtime, compiles exactly as before and gets exactly what
+ it got before.
+
+ **`buildPath` is the inverse.** `buildPath('/users/:id', { id })` cannot spell
+ the pattern wrong, forget a param or be left behind when a route is renamed, and
+ its values are encoded to round-trip back through `matchRoute`. A `*` wildcard
+ is encoded per segment, because `rest` is a path and its slashes are structure.
+ `navigate` still takes a plain string, deliberately — a router navigates to
+ concrete paths, and one that matches nothing is what a fallback screen is for —
+ so the check sits where the string is assembled instead.
+
+ **`@amritk/mini-lynx/router` gains `route()` and `AnyRoute`.**
+ `route('/users/:id', (params) => …, meta)` keeps the pattern's literal type
+ alive so the view is handed a `() => { id: string }`. Its three arguments are
+ not a style choice: folded into one object literal, the pattern and the metadata
+ compete for inference against an intersection, and the metadata loses — a tab
+ bar reading `route.label` would get `unknown` back.
+
+ `Route` is now `Route
`, and the router's generics are
+ constrained by the new `AnyRoute`. `Route<'/users/:id'>` is deliberately not
+ assignable to `Route` — its `view` demands the narrow getter, and the
+ widened form can only promise the flat record — so a table of mixed patterns
+ needs a constraint that admits all of them. `render-route.ts` owns the single
+ cast that reconciles the two. Existing tables keep working: `Route` with no type
+ argument is what it always was.
+
+ **`createBrowserHistory` is back, on `@amritk/mini-lynx/router/browser`.** It is
+ a `RouterHistory` and nothing else, because the rest of routing is already
+ target-free — a device build swaps `createMemoryHistory()` back in and changes
+ nothing else. It takes a `base` prefix, and it stamps its depth into
+ `history.state` rather than reading `window.history.length`, which counts the
+ whole tab and is wrong in both directions once the user has gone back. That
+ stamp is what survives the two things a memory stack never faces: a reload
+ mid-stack and a forward button.
+
+ It is the only module in the package that names `window`, and it is quarantined
+ accordingly. `src/router/browser/` is excluded from the main compiler pass and
+ covered by `tsconfig.dom.json`, so the platform-free rule the rest of the
+ package keeps stays something the compiler enforces rather than a convention.
+ Hash mode is not included: configuring a host for an SPA fallback is one line,
+ where a second URL grammar would be one the device build can never use.
+
+ **`stripBase` moved into `@amritk/mini-helpers`,** since both routers now read a
+ browser pathname and a base that meant something slightly different on each side
+ is exactly the drift that package exists to prevent. `@amritk/mini`'s router
+ surface is unchanged apart from re-exporting `buildPath` and `PathParams`.
+
+### Patch Changes
+
+- 1b6c33d: Bring every package's shipped `AI.md` back in line with what that package
+ actually publishes, and add `bun run check:ai-docs` so it cannot drift again.
+
+ The files had gone stale in the way generated-and-committed docs always do —
+ silently, and only for the audience that cannot file an issue about it.
+ `@amritk/mini` never documented `watch`, `template`, the typed `matchRoute` /
+ `buildPath` re-exports on `/router`, `Field` on `/forms`, or the `/vite` subpath
+ at all; `@amritk/mini-lynx` was missing `computed` / `effectScope`,
+ `fadeTransition`, `keepAboveKeyboard` and `HANDLER_PREFIX`;
+ `@amritk/lynx-notifications` documented neither its `/testing` subpath nor the
+ fake behind it. All four native packages exported `MODULE` and `EVENTS` with no
+ mention of what they are for, and only `@amritk/lynx-dialogs` showed how to wire
+ a fake into `installNativeBridge` — which is the one thing a consumer testing
+ its own screens needs.
+
+ Two accuracy fixes matter more than the additions. Every native package's
+ _Status_ section claimed the Objective-C compiles against the real Lynx pod; the
+ macOS CI job was disabled on cost, so it now compiles only when somebody runs
+ `pod lib lint` by hand, and the docs say that. And `@amritk/lynx-dialogs` never
+ carried a _Status_ section at all, so nothing in it told a reader that none of
+ it has run on a device.
+
+ `bun run check:ai-docs` reads each package's `exports` and fails on a runtime
+ export, a published subpath, or (for a package shipping native sources) a
+ _Status_ section its `AI.md` never mentions. It runs early in CI, before the
+ build. Exports no consumer ever writes — the tree operations the JSX transform
+ calls, and the like — are listed in `INTERNAL_EXPORTS` with the reason.
+
+- Updated dependencies [ab6476a]
+ - @amritk/mini-helpers@0.2.0
+
## 0.6.0
### Minor Changes
diff --git a/packages/mini/package.json b/packages/mini/package.json
index 3b7ae6f..1abf7c9 100644
--- a/packages/mini/package.json
+++ b/packages/mini/package.json
@@ -1,6 +1,6 @@
{
"name": "@amritk/mini",
- "version": "0.6.0",
+ "version": "0.7.0",
"description": "A deliberately tiny signals-based UI layer: reactive DOM bindings plus a compilerless JSX runtime.",
"type": "module",
"license": "MIT",