@happyvertical/smrt-svelte 0.55.7 → 0.55.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/AGENTS.md +7 -44
  2. package/README.md +112 -1
  3. package/agents/browser-ai.md +47 -0
  4. package/agents/command-palette.md +85 -0
  5. package/agents/import-export.md +62 -0
  6. package/agents/overview-surfaces.md +255 -0
  7. package/agents/screens.md +57 -0
  8. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +5 -0
  9. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -1
  10. package/dist/browser-ai/adapters/tts/browser-synthesis.js +83 -55
  11. package/dist/components/command-palette/CommandPalette.svelte +75 -0
  12. package/dist/components/command-palette/CommandPalette.svelte.d.ts +22 -0
  13. package/dist/components/command-palette/CommandPalette.svelte.d.ts.map +1 -0
  14. package/dist/components/command-palette/CommandPaletteDialog.svelte +406 -0
  15. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts +18 -0
  16. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts.map +1 -0
  17. package/dist/components/command-palette/CommandPaletteTrigger.svelte +91 -0
  18. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts +17 -0
  19. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts.map +1 -0
  20. package/dist/components/command-palette/context.d.ts +18 -0
  21. package/dist/components/command-palette/context.d.ts.map +1 -0
  22. package/dist/components/command-palette/context.js +38 -0
  23. package/dist/components/command-palette/controller.svelte.d.ts +70 -0
  24. package/dist/components/command-palette/controller.svelte.d.ts.map +1 -0
  25. package/dist/components/command-palette/controller.svelte.js +379 -0
  26. package/dist/components/command-palette/hotkeys.d.ts +30 -0
  27. package/dist/components/command-palette/hotkeys.d.ts.map +1 -0
  28. package/dist/components/command-palette/hotkeys.js +161 -0
  29. package/dist/components/command-palette/index.d.ts +19 -0
  30. package/dist/components/command-palette/index.d.ts.map +1 -0
  31. package/dist/components/command-palette/index.js +17 -0
  32. package/dist/components/command-palette/match.d.ts +38 -0
  33. package/dist/components/command-palette/match.d.ts.map +1 -0
  34. package/dist/components/command-palette/match.js +172 -0
  35. package/dist/components/command-palette/providers.d.ts +159 -0
  36. package/dist/components/command-palette/providers.d.ts.map +1 -0
  37. package/dist/components/command-palette/providers.js +345 -0
  38. package/dist/components/command-palette/rank.d.ts +20 -0
  39. package/dist/components/command-palette/rank.d.ts.map +1 -0
  40. package/dist/components/command-palette/rank.js +74 -0
  41. package/dist/components/command-palette/types.d.ts +119 -0
  42. package/dist/components/command-palette/types.d.ts.map +1 -0
  43. package/dist/components/command-palette/types.js +10 -0
  44. package/dist/components/command-palette/use-provider.svelte.d.ts +14 -0
  45. package/dist/components/command-palette/use-provider.svelte.d.ts.map +1 -0
  46. package/dist/components/command-palette/use-provider.svelte.js +23 -0
  47. package/dist/components/import-export/ImportExport.svelte +678 -0
  48. package/dist/components/import-export/ImportExport.svelte.d.ts +5 -0
  49. package/dist/components/import-export/ImportExport.svelte.d.ts.map +1 -0
  50. package/dist/components/import-export/collection-adapter.d.ts +35 -0
  51. package/dist/components/import-export/collection-adapter.d.ts.map +1 -0
  52. package/dist/components/import-export/collection-adapter.js +42 -0
  53. package/dist/components/import-export/csv.d.ts +60 -0
  54. package/dist/components/import-export/csv.d.ts.map +1 -0
  55. package/dist/components/import-export/csv.js +192 -0
  56. package/dist/components/import-export/download.d.ts +4 -0
  57. package/dist/components/import-export/download.d.ts.map +1 -0
  58. package/dist/components/import-export/download.js +17 -0
  59. package/dist/components/import-export/export.d.ts +25 -0
  60. package/dist/components/import-export/export.d.ts.map +1 -0
  61. package/dist/components/import-export/export.js +68 -0
  62. package/dist/components/import-export/fields.d.ts +48 -0
  63. package/dist/components/import-export/fields.d.ts.map +1 -0
  64. package/dist/components/import-export/fields.js +103 -0
  65. package/dist/components/import-export/index.d.ts +12 -0
  66. package/dist/components/import-export/index.d.ts.map +1 -0
  67. package/dist/components/import-export/index.js +9 -0
  68. package/dist/components/import-export/mapping.d.ts +12 -0
  69. package/dist/components/import-export/mapping.d.ts.map +1 -0
  70. package/dist/components/import-export/mapping.js +38 -0
  71. package/dist/components/import-export/props.d.ts +34 -0
  72. package/dist/components/import-export/props.d.ts.map +1 -0
  73. package/dist/components/import-export/props.js +1 -0
  74. package/dist/components/import-export/report.d.ts +13 -0
  75. package/dist/components/import-export/report.d.ts.map +1 -0
  76. package/dist/components/import-export/report.js +50 -0
  77. package/dist/components/import-export/runner.d.ts +17 -0
  78. package/dist/components/import-export/runner.d.ts.map +1 -0
  79. package/dist/components/import-export/runner.js +39 -0
  80. package/dist/components/import-export/types.d.ts +100 -0
  81. package/dist/components/import-export/types.d.ts.map +1 -0
  82. package/dist/components/import-export/types.js +6 -0
  83. package/dist/components/import-export/validate.d.ts +39 -0
  84. package/dist/components/import-export/validate.d.ts.map +1 -0
  85. package/dist/components/import-export/validate.js +268 -0
  86. package/dist/components/overview/OverviewGrid.svelte +579 -0
  87. package/dist/components/overview/OverviewGrid.svelte.d.ts +5 -0
  88. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -0
  89. package/dist/components/overview/WidgetOptionsForm.svelte +193 -0
  90. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts +5 -0
  91. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -0
  92. package/dist/components/overview/controller.svelte.d.ts +92 -0
  93. package/dist/components/overview/controller.svelte.d.ts.map +1 -0
  94. package/dist/components/overview/controller.svelte.js +336 -0
  95. package/dist/components/overview/format.d.ts +13 -0
  96. package/dist/components/overview/format.d.ts.map +1 -0
  97. package/dist/components/overview/format.js +45 -0
  98. package/dist/components/overview/grid-types.d.ts +52 -0
  99. package/dist/components/overview/grid-types.d.ts.map +1 -0
  100. package/dist/components/overview/grid-types.js +1 -0
  101. package/dist/components/overview/index.d.ts +24 -0
  102. package/dist/components/overview/index.d.ts.map +1 -0
  103. package/dist/components/overview/index.js +22 -0
  104. package/dist/components/overview/load.d.ts +30 -0
  105. package/dist/components/overview/load.d.ts.map +1 -0
  106. package/dist/components/overview/load.js +92 -0
  107. package/dist/components/overview/markdown.d.ts +53 -0
  108. package/dist/components/overview/markdown.d.ts.map +1 -0
  109. package/dist/components/overview/markdown.js +203 -0
  110. package/dist/components/overview/model.d.ts +100 -0
  111. package/dist/components/overview/model.d.ts.map +1 -0
  112. package/dist/components/overview/model.js +460 -0
  113. package/dist/components/overview/recipe-widgets.d.ts +77 -0
  114. package/dist/components/overview/recipe-widgets.d.ts.map +1 -0
  115. package/dist/components/overview/recipe-widgets.js +151 -0
  116. package/dist/components/overview/registry.d.ts +45 -0
  117. package/dist/components/overview/registry.d.ts.map +1 -0
  118. package/dist/components/overview/registry.js +112 -0
  119. package/dist/components/overview/resize.d.ts +25 -0
  120. package/dist/components/overview/resize.d.ts.map +1 -0
  121. package/dist/components/overview/resize.js +41 -0
  122. package/dist/components/overview/schema.d.ts +34 -0
  123. package/dist/components/overview/schema.d.ts.map +1 -0
  124. package/dist/components/overview/schema.js +180 -0
  125. package/dist/components/overview/server.d.ts +14 -0
  126. package/dist/components/overview/server.d.ts.map +1 -0
  127. package/dist/components/overview/server.js +13 -0
  128. package/dist/components/overview/types.d.ts +235 -0
  129. package/dist/components/overview/types.d.ts.map +1 -0
  130. package/dist/components/overview/types.js +17 -0
  131. package/dist/components/overview/widgets/ChartWidget.svelte +111 -0
  132. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +6 -0
  133. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -0
  134. package/dist/components/overview/widgets/MetricWidget.svelte +86 -0
  135. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts +6 -0
  136. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts.map +1 -0
  137. package/dist/components/overview/widgets/NoteWidget.svelte +61 -0
  138. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts +5 -0
  139. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts.map +1 -0
  140. package/dist/components/overview/widgets/RecordListWidget.svelte +91 -0
  141. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts +6 -0
  142. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -0
  143. package/dist/components/overview/widgets/ShortcutsWidget.svelte +80 -0
  144. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts +6 -0
  145. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts.map +1 -0
  146. package/dist/components/overview/widgets/core.d.ts +44 -0
  147. package/dist/components/overview/widgets/core.d.ts.map +1 -0
  148. package/dist/components/overview/widgets/core.js +222 -0
  149. package/dist/components/overview/widgets/data.d.ts +76 -0
  150. package/dist/components/overview/widgets/data.d.ts.map +1 -0
  151. package/dist/components/overview/widgets/data.js +40 -0
  152. package/dist/components/screens/DetailScreen.svelte +220 -0
  153. package/dist/components/screens/DetailScreen.svelte.d.ts +5 -0
  154. package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -0
  155. package/dist/components/screens/EditForm.svelte +313 -0
  156. package/dist/components/screens/EditForm.svelte.d.ts +5 -0
  157. package/dist/components/screens/EditForm.svelte.d.ts.map +1 -0
  158. package/dist/components/screens/ListScreen.svelte +191 -0
  159. package/dist/components/screens/ListScreen.svelte.d.ts +5 -0
  160. package/dist/components/screens/ListScreen.svelte.d.ts.map +1 -0
  161. package/dist/components/screens/RecipeScreens.svelte +237 -0
  162. package/dist/components/screens/RecipeScreens.svelte.d.ts +5 -0
  163. package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -0
  164. package/dist/components/screens/fields.d.ts +54 -0
  165. package/dist/components/screens/fields.d.ts.map +1 -0
  166. package/dist/components/screens/fields.js +229 -0
  167. package/dist/components/screens/index.d.ts +8 -0
  168. package/dist/components/screens/index.d.ts.map +1 -0
  169. package/dist/components/screens/index.js +6 -0
  170. package/dist/components/screens/types.d.ts +215 -0
  171. package/dist/components/screens/types.d.ts.map +1 -0
  172. package/dist/components/screens/types.js +1 -0
  173. package/dist/components/screens/values.d.ts +62 -0
  174. package/dist/components/screens/values.d.ts.map +1 -0
  175. package/dist/components/screens/values.js +289 -0
  176. package/dist/components/sortable/controller.svelte.d.ts +10 -0
  177. package/dist/components/sortable/controller.svelte.d.ts.map +1 -1
  178. package/dist/components/sortable/controller.svelte.js +29 -7
  179. package/dist/components/workspace/manifest-nav.d.ts +20 -0
  180. package/dist/components/workspace/manifest-nav.d.ts.map +1 -1
  181. package/dist/components/workspace/manifest-nav.js +42 -13
  182. package/dist/i18n/server.d.ts +6 -0
  183. package/dist/i18n/server.d.ts.map +1 -1
  184. package/dist/i18n/server.js +6 -0
  185. package/dist/i18n/strings.import-export.d.ts +88 -0
  186. package/dist/i18n/strings.import-export.d.ts.map +1 -0
  187. package/dist/i18n/strings.import-export.js +88 -0
  188. package/dist/i18n/strings.overview.d.ts +100 -0
  189. package/dist/i18n/strings.overview.d.ts.map +1 -0
  190. package/dist/i18n/strings.overview.js +107 -0
  191. package/dist/i18n/strings.palette.d.ts +18 -0
  192. package/dist/i18n/strings.palette.d.ts.map +1 -0
  193. package/dist/i18n/strings.palette.js +18 -0
  194. package/dist/i18n/strings.screens.d.ts +31 -0
  195. package/dist/i18n/strings.screens.d.ts.map +1 -0
  196. package/dist/i18n/strings.screens.js +31 -0
  197. package/package.json +45 -16
package/AGENTS.md CHANGED
@@ -13,11 +13,15 @@ subpath you are editing. This file keeps what holds in every module.
13
13
  | `src/i18n/` (`./i18n` + `./i18n/server`) | `defineMessages` / `useI18n` / `<Trans>` / `buildI18nSnapshot`, the template-vs-render split, and hardcoded-string enforcement | [agents/i18n.md](agents/i18n.md) |
14
14
  | `src/themes/` + `src/theme/` | which theme system is canonical and the full `--smrt-*` design-token vocabulary with its alias rules | [agents/themes.md](agents/themes.md) |
15
15
  | `src/test-support/` + `__tests__/` | the golden-test harness and pattern for Svelte component tests | [agents/testing.md](agents/testing.md) |
16
+ | `src/components/import-export/` (`./import-export`) | `ImportExport` CSV/TSV import (column mapping, validation preview, error report) and export (column selection) driven by collection definitions + resolved field policy, over a pure Node-tested CSV layer (#3715) | [agents/import-export.md](agents/import-export.md) |
16
17
  | `src/components/settings/` (`./settings`) | `SettingsCatalog`, `paginateSettingsCatalog`, and the summary-vs-detail scalability contract | [agents/settings.md](agents/settings.md) |
18
+ | `src/components/screens/` (`./screens`) | `RecipeScreens` / `ListScreen` / `DetailScreen` / `EditForm`: list, view, create and edit derived from a generated web definition plus resolved field policy; pure `deriveScreenFields` selection, minor-unit money handling, transport-neutral source (#3718) | [agents/screens.md](agents/screens.md) |
17
19
  | `src/components/app/` (`./app`) | `AppShell` (Provider + ThemeProvider + theme CSS + AdminShell + nav/dock slots), `OwnerSetupForm` (props-driven first-run owner form), `ShellSettingsPage`, `RuntimeDiagnosticsWebMcp`. Browser-only; no `$app/*` or server imports | [agents/workspace.md](agents/workspace.md#app-shell-and-owner-setup-app) |
18
20
  | `src/components/workspace/` (`./workspace` + `./web`) | the AdminShell family and its principles, the legacy ToolsDock surface, the `./web` activity-feed and `updateAvailable` adapters, and server-side dock gates | [agents/workspace.md](agents/workspace.md) |
21
+ | `src/components/command-palette/` (`./command-palette`) | `CommandPalette` trigger + dialog, `PaletteProvider` contract, nav and manifest providers (#3713) | [agents/command-palette.md](agents/command-palette.md) |
22
+ | `src/components/overview/` (`./overview` + `./overview/server`) | customizable overview surfaces: `OverviewGrid` flow grid (span 1-4, keyboard reorder/resize) joined to the shell's layout edit mode, host-owned `createOverview` controller, widget registry with versioned option schemas validated on save and load, the server `load(options, ctx)` contract (`loadOverview`), the metric/chart/records/shortcuts/note core widgets, and `registerRecipeWidgets` (recipe `widget` surfaces to registry registrations, injected module resolver) (#3727) | [agents/overview-surfaces.md](agents/overview-surfaces.md) |
19
23
  | `src/web/remote-query.svelte.ts` | Svelte 5 binding for query-shaped remote pages: rows, page, totals, loading/refreshing/stale/error, retry, last-updated, and query-scoped live subscriptions (#2445) | — |
20
- | `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | One-call registration of an existing, non-`DataTable` list (already mirroring a headless `DataTableController`) as a mounted `DataSurfaceDescriptor` — a same-behavior port of `registerContentListDataSurface`'s registration/translation logic (`@happyvertical/smrt-content/svelte`), generalized off ContentList's view-mode concept, so any custom list markup can adopt it directly instead of hand-mirroring the registry contract. Not yet unified behind a shared `@happyvertical/smrt-ui/data` implementation (#2917) — this copy is currently ahead; see its own module doc comment for the current, authoritative list of divergences #2917 must adopt rather than assume parity (#2906) | — |
24
+ | `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | Registers an existing non-`DataTable` list (mirroring a `DataTableController`) as a mounted `DataSurfaceDescriptor`; a same-behavior port of `registerContentListDataSurface`, not yet unified behind smrt-ui (#2917). The module doc comment is the authoritative list of divergences (#2906) | — |
21
25
  | `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
22
26
  | `src/web/webmcp-ui.ts` | Fixed, low-cardinality WebMCP adapter over the Provider's mounted form-control and data-surface registries (#2521) | — |
23
27
  | `src/web/webmcp.svelte.ts` (`useWebMcpTool`) | Component-owned bespoke WebMCP tool. Routes through `@happyvertical/smrt-web`'s `registerWebMcpBespokeTool`, so it shares the fail-closed effect classification and `effects` exposure policy of generated tools — undeclared `annotations` classify destructive and are excluded by default. `namespace` and `maxTools` never apply to a bespoke tool (#2586) | — |
@@ -206,49 +210,8 @@ for a data-surface one — for exactly the component's lifetime.
206
210
  - **Preload strategies**: `none`, `eager`, `idle` (recommended), `on-visible`
207
211
  - **Warm client cache**: module-level Map survives navigation/remounts -- avoids re-downloading WASM/models
208
212
  - **Adapters**: STT (browser-speech, whisper-cpp, whisper-local = alias whisper-wasm), TTS (browser-synthesis), LLM (webllm, transformers-llm)
209
- - **On-device speech** (`whisper-local` = alias `whisper-wasm`, and `moonshine`): thin adapters over
210
- `@happyvertical/speech/local` (`LocalTranscriber`, or its worker host `serveLocalTranscriber`).
211
- Push-to-talk (record on `start()`, transcribe on `stop()`, one final result) plus `transcribePcm()`
212
- for smrt-ui's hands-free mode. Default `onnx-community/whisper-tiny.en` q8 (~45 MB, Cache
213
- Storage); `model: 'moonshine-tiny'` (~32 MB) / `'moonshine-base'` (~67 MB) or any HF id
214
- (`LOCAL_SPEECH_MODELS`); WebGPU else single-thread WASM (no COOP/COEP needed). The package never
215
- imports the peers: a host passes `createWorker` (build it from
216
- `@happyvertical/smrt-svelte/browser-ai/whisper-worker?worker`, alias `speech-worker`, the one module
217
- that imports `@happyvertical/speech/local` and `@huggingface/transformers` statically) plus
218
- `loadSpeech: () => import('@happyvertical/speech/local')`, or `loadModule` for the page thread.
219
- `createLocalSpeechModel()` (alias `createWhisperLocalModel()`) / `adapter.model` is the consent API
220
- (`estimateSize`, `isCached`, `load({ onProgress, signal })` with `downloading` -> `extracting` ->
221
- `complete`); share it via `modelHandle`. English-only handling of `language`/`task` lives in the speech
222
- package. `probeBrowserSpeech()` -> `works | missing | unreliable` (no network, no mic prompt)
223
- decides whether to offer it. Sources set `stopTimeoutMs` so `Dictation` waits for the transcription.
224
213
  - Cache API: `getCachedSTT()`, `getCachedTTS()`, `getCachedLLM()`, `getCacheStats()`, `clearAllCaches()`
225
- - **Inference backends (browser-ai)**: two `@happyvertical/smrt-web/ai` `InferenceBackend`s, both
226
- `kind: 'local'`, both refusing `tool`/`function` messages, non-string content, `tool_calls` and
227
- `options.tools` with `InferencePathError` (`unsupported_message`) through the SHARED
228
- `assertTextOnlyTurn`, so a per-call switch between backends never changes what a turn means.
229
- - `createWebLlmInferenceBackend()` wraps an `LLMAdapter` — `status` derives from the adapter's
230
- `initState` (plus an explicit in-flight flag, because the adapter only flips to `initializing`
231
- after its own awaits), `progress` mirrors `DownloadProgressInfo`, and `load()`/`unload()` drive
232
- `ensureInitialized`/`unloadModel`. It reports `unavailable` without WebGPU so an `auto` path skips
233
- it. Pass `adapter` to share the app-state-managed warm-cached adapter; construct once, so two
234
- callers cannot each start an unshared adapter and a second download.
235
- Its `loadModule` option matters: the default `importOptional()` resolves a VARIABLE specifier,
236
- which a browser cannot resolve (`TypeError: Failed to resolve module specifier`) — a host that has
237
- `@mlc-ai/web-llm` must pass `() => import('@mlc-ai/web-llm')` so the bundler rewrites a static one.
238
- It does NOT forward `signal` (the adapter has no cancellation seam).
239
- - `createBitGpuInferenceBackend()` wraps `bitgpu` (MIT, optional peer), the engine behind the
240
- Bonsai WebGPU demo PrismML's own docs list as their in-browser option. `loadBitGpu`/`loadChat`
241
- are REQUIRED loaders (`() => import('bitgpu')`, `() => import('bitgpu/chat')`) — a static import
242
- here would fail the build for every consumer without the optional peer, and a bare specifier is
243
- not resolvable by a browser either. `bitgpu`'s `LoadProgress.phase` maps to `InferenceProgress`
244
- (`manifest`/`weights` → `downloading`, `pipelines` → `extracting`). Unlike the WebLLM backend it
245
- **forwards `signal`** — bitgpu aborts a live generation — and it does NOT lazily load: weights
246
- run to GBs, so a turn before `load()` fails with a named reason. `model` in the per-call options
247
- is ignored (one engine binds one model), and bitgpu's tools/JSON-schema/think support is
248
- deliberately left unexposed for now so the text-only invariant holds across backends.
249
- - **`ModelStatusControl.svelte`**: the user-facing model lifecycle surface (state, Load/Unload,
250
- download progress) driven entirely by a backend's own `status`/`progress`/`subscribe`, so it works
251
- without a `<Provider>` ancestor. `unavailable` is a first-class state that offers NO load action.
214
+ - **On-device speech and inference backends** (`whisper-local`/`moonshine` adapters, `createWebLlmInferenceBackend`, `createBitGpuInferenceBackend`, `ModelStatusControl`): see [agents/browser-ai.md](agents/browser-ai.md).
252
215
 
253
216
  ## Permission Action
254
217
 
@@ -272,5 +235,5 @@ for a data-surface one — for exactly the component's lifetime.
272
235
  - `@happyvertical/smrt-types` (shared types) — includes the identity data contracts (`User`, `Role`, `Membership`, `Tenant`) the role/membership components type against, so no dependency on `smrt-users` / `smrt-profiles` is needed
273
236
  - `@happyvertical/smrt-ui` (UI runtime: primitives, theme system, i18n client, module registry) is a hard `dependency`. The agent-admin shells that used to type against `@happyvertical/smrt-agents/ui` moved to `@happyvertical/smrt-agents/svelte` (#1589), so `smrt-agents` is no longer a dependency here — this drops smrt-svelte below smrt-agents in the package DAG.
274
237
  - `@happyvertical/smrt-languages` is a hard `dependency` (not an optional peer): the Node-only `/i18n/server` subpath imports its resolver. The browser bundle still excludes it — the client `/i18n` layer never imports the languages root, so it tree-shakes out.
275
- - `@happyvertical/logger` (SDK) is a `dependency` — the console logger used for voice/AI error reporting in the form components. Consume it **only** through `src/internal/logger.ts`, never `createLogger()` at module scope: `createLogger()` reads `HAVE_LOGGER_LEVEL` from `process.env`, so a top-level call throws `ReferenceError: process is not defined` in the browser and kills client-side hydration under `vite dev` (prod builds tree-shake/define it away, so this only bites in dev). The `internal/logger` wrapper constructs the logger lazily and falls back to a bare `ConsoleLogger` when `process.env` is absent, keeping this browser-reachable module (imported by `Provider` + the form primitives) safe.
238
+ - `@happyvertical/logger` (SDK) is a `dependency` for voice/AI error reporting in the form components. Consume it **only** through `src/internal/logger.ts`, never `createLogger()` at module scope: it reads `process.env`, which throws `ReferenceError` in the browser under `vite dev` and kills hydration. The wrapper builds the logger lazily and falls back to a bare `ConsoleLogger`.
276
239
  - Peer (all optional): `svelte` >=5.18.2, plus the browser-AI engines (`@happyvertical/speech`, `@huggingface/transformers`, `@mlc-ai/web-llm`, `@remotion/whisper-web`) and `chrono-node`.
package/README.md CHANGED
@@ -32,6 +32,50 @@ pipeline (`opportunity.stage`) without importing `@happyvertical/smrt-projects`.
32
32
 
33
33
  Svelte 5 component library for the s-m-r-t framework. Provides UI components, browser AI integration (STT/TTS/LLM with warm cache), a theme system, permission-aware rendering, and module UI registry for agent admin panels.
34
34
 
35
+ ## Import and export
36
+
37
+ `@happyvertical/smrt-svelte/import-export` provides `ImportExport`, a CSV and
38
+ TSV import/export panel for any collection. Spreadsheet users save as CSV;
39
+ there is no `.xlsx` reader. Import is a guided flow: pick a file, map each
40
+ column to a field (auto-detected from the field name, label, or alias),
41
+ check a validation preview, then import. The preview is the dry run and writes
42
+ nothing. Rows that fail validation are listed by file line with the offending
43
+ value, and are downloadable as a CSV error report. Valid rows are created one
44
+ request each, so a server rejection fails only that row. Export lets the user
45
+ choose columns, delimiter, and header style; the default `name` headers
46
+ re-import cleanly.
47
+
48
+ The columns come from the same sources as the generated forms:
49
+ `fieldsFromCollectionDefinition(definition, { policy })` reads the collection
50
+ definition (the manifest) and a resolved field policy. A policy-`hidden` field
51
+ is neither imported nor exported, a `locked` field is never overwritten, and a
52
+ resolved default fills an empty or unmapped cell. `createCollectionImportExport`
53
+ binds the panel to the generated CRUD fetchers, so import and export go through
54
+ the same authorized REST routes as every other client.
55
+
56
+ ```svelte
57
+ <script lang="ts">
58
+ import {
59
+ ImportExport,
60
+ createCollectionImportExport,
61
+ } from '@happyvertical/smrt-svelte/import-export';
62
+
63
+ const io = createCollectionImportExport({ definition, fetchers, fieldOptions: { policy } });
64
+ </script>
65
+
66
+ <ImportExport
67
+ fields={io.fields}
68
+ createRecord={io.createRecord}
69
+ loadRows={io.loadRows}
70
+ filename="products"
71
+ />
72
+ ```
73
+
74
+ The CSV layer (`parseTable`, `validateRows`, `buildExportFile`, `runImport`)
75
+ is pure and runs in Node. Exports neutralize spreadsheet formulas in text
76
+ cells. See [agents/import-export.md](./agents/import-export.md) for the
77
+ contract.
78
+
35
79
  ## Sortable and shell layout
36
80
 
37
81
  `@happyvertical/smrt-svelte/sortable` provides `Sortable`, an accessible
@@ -590,10 +634,67 @@ keys the shell renders exactly as before.
590
634
  - **Migration guide** (first-generation `WorkspaceShell`/`RoleShell` →
591
635
  `AdminShell`; adoption is additive and non-breaking):
592
636
  [`src/components/workspace/MIGRATION.md`](./src/components/workspace/MIGRATION.md)
593
- - **Playground demos**: `playground/src/routes/admin-shell` exercises all four
637
+ - **Playground demos**: `playground/src/routes/command-palette` shows the palette in the header slot;
638
+ `playground/src/routes/admin-shell` exercises all four
594
639
  scopes, focus tools, and activities; `admin-shell-activity-feed` and
595
640
  `admin-shell-system-feed` show live feeds.
596
641
 
642
+ ### Command palette and global search
643
+
644
+ `@happyvertical/smrt-svelte/command-palette` is a keyboard-first "find anything"
645
+ dialog (Ctrl/Cmd+K) with a provider contract. Anything can register rows: the
646
+ shell navigation, "New invoice" commands and cross-model record search derived
647
+ from the manifest, or your own commands.
648
+
649
+ ```ts
650
+ import {
651
+ CommandPalette,
652
+ createCommandPalette,
653
+ createModelProviders,
654
+ createNavigationProvider,
655
+ } from '@happyvertical/smrt-svelte/command-palette';
656
+
657
+ const palette = createCommandPalette({
658
+ navigate: goto,
659
+ providers: [
660
+ createNavigationProvider({ nav, groups }),
661
+ ...createModelProviders({ manifest, pagesBasePath: '/app' }),
662
+ ],
663
+ });
664
+ ```
665
+
666
+ Render `<CommandPalette {palette} />` as an `AppShell` `slotItems` entry
667
+ (usually `header.center`). See
668
+ [`agents/command-palette.md`](./agents/command-palette.md) for the provider
669
+ contract, accessibility behavior, and the record-search limits.
670
+
671
+ ### Customizable overviews
672
+
673
+ `@happyvertical/smrt-svelte/overview` turns an overview page into a grid of
674
+ registered widgets (metric, chart, record list, shortcuts, note) that admins
675
+ reorder, resize, configure, add and remove in the shell's layout edit mode,
676
+ inside an allowed set the page declares. A saved overview is declarative data:
677
+ widget options are validated against versioned schemas on save and on load, and
678
+ widget data comes from a server `load(options, ctx)` that runs inside the page's
679
+ load with the user's permissions, so the page renders fully on the server.
680
+ Persistence is host-owned (the same pattern as the shell layout).
681
+
682
+ ```ts
683
+ import {
684
+ createOverview,
685
+ OverviewGrid,
686
+ registerCoreWidgets,
687
+ } from '@happyvertical/smrt-svelte/overview';
688
+ import {
689
+ defineOverview,
690
+ loadOverview,
691
+ resolveOverview,
692
+ } from '@happyvertical/smrt-svelte/overview/server';
693
+ ```
694
+
695
+ See [`agents/overview-surfaces.md`](./agents/overview-surfaces.md) for the data
696
+ model, the validation rules, the server load contract and the extension points.
697
+
597
698
  ## Exports
598
699
 
599
700
  ### Entry Points
@@ -605,10 +706,15 @@ importable, even if it appears in `dist/`.
605
706
  |-------------|----------|
606
707
  | `@happyvertical/smrt-svelte` | `Provider`, hooks (`useAppState`, `useAuth`, `useLLM`, `useSocket`, `useSTT`, `useTheme`, `useTTS`), app state/context, `ModulePanel`, and the form components below |
607
708
  | `@happyvertical/smrt-svelte/forms` | Form inputs (TextInput, Select, MoneyInput, DateTimeInput, Toggle, etc.) |
709
+ | `@happyvertical/smrt-svelte/import-export` | `ImportExport` (CSV/TSV import with column mapping, validation preview and error report; export with column selection), `createCollectionImportExport`, `fieldsFromCollectionDefinition`, and the pure CSV layer |
608
710
  | `@happyvertical/smrt-svelte/settings` | Server-paged settings search, selection, and list/detail layout (`SettingsCatalog`, `paginateSettingsCatalog`) |
711
+ | `@happyvertical/smrt-svelte/screens` | List, view, create and edit screens derived from a generated web definition and resolved field policy (`RecipeScreens`, `ListScreen`, `DetailScreen`, `EditForm`) |
609
712
  | `@happyvertical/smrt-svelte/workspace` | AdminShell, ShellState, tenant nav, focus tools, settings, activities, and system/app panels |
610
713
  | `@happyvertical/smrt-svelte/app` | `AppShell` (Provider + themes + AdminShell + nav/dock slots, `dockToggles` buttons and host `slots` for the header/footer/sidebar regions), `OwnerSetupForm` (first-run owner setup), `ShellSettingsPage`, `RuntimeDiagnosticsWebMcp` |
611
714
  | `@happyvertical/smrt-svelte/app/runtime-diagnostics` | Svelte-free diagnostics WebMCP registration and its tool name/endpoint constants, importable from server routes |
715
+ | `@happyvertical/smrt-svelte/command-palette` | Global search and command palette: `CommandPalette` (shell-slot trigger plus dialog), `createCommandPalette`, the provider contract, and the navigation and manifest-driven model providers |
716
+ | `@happyvertical/smrt-svelte/overview` | Customizable overview surfaces: `OverviewGrid` (flow grid with keyboard reorder and resize), `createOverview` (host-owned controller), the widget registry (`registerWidget`), `registerCoreWidgets` (metric, chart, record list, shortcuts, note) and `shortcutsFromNav` |
717
+ | `@happyvertical/smrt-svelte/overview/server` | Svelte-free half of the overview surface: the data model (`resolveOverview`, `checkOverviewOverride`), option-schema validation, `WidgetRegistry`, and `loadOverview` for a page's server load |
612
718
  | `@happyvertical/smrt-svelte/workspace/legacy` | Opt-in ToolsDock compatibility surface for applications migrating to AdminShell |
613
719
  | `@happyvertical/smrt-svelte/workspace/server` | Server-side workspace helpers (Node only) |
614
720
  | `@happyvertical/smrt-svelte/workspace/live` | `systemFeed` — the AdminShell system scope (jobs/schedules/dispatch) polled from an app status endpoint; deliberately carries no `smrt-web` dependency |
@@ -704,6 +810,11 @@ all other currencies render their ISO code for deterministic SSR hydration.
704
810
 
705
811
  `getCachedSTT`, `getCachedTTS`, `getCachedLLM`, `getCacheStats`, `clearAllCaches`
706
812
 
813
+ Browser speech synthesis honors `stop()`, replacement `speak()` calls, and
814
+ `dispose()` while voices are loading. Canceled `speak()` promises resolve even
815
+ when the browser emits no completion event; `onEnd` is reserved for natural
816
+ completion. Events from canceled utterances are ignored.
817
+
707
818
  ## Dependencies
708
819
 
709
820
  - `@happyvertical/smrt-types` -- shared type definitions
@@ -0,0 +1,47 @@
1
+ # smrt-svelte/browser-ai
2
+
3
+ Module semantics for `src/browser-ai/`. Package orientation and cross-module
4
+ invariants live in [../AGENTS.md](../AGENTS.md); read that first.
5
+
6
+ - **On-device speech** (`whisper-local` = alias `whisper-wasm`, and `moonshine`): thin adapters over
7
+ `@happyvertical/speech/local` (`LocalTranscriber`, or its worker host `serveLocalTranscriber`).
8
+ Push-to-talk (record on `start()`, transcribe on `stop()`, one final result) plus `transcribePcm()`
9
+ for smrt-ui's hands-free mode. Default `onnx-community/whisper-tiny.en` q8 (~45 MB, Cache
10
+ Storage); `model: 'moonshine-tiny'` (~32 MB) / `'moonshine-base'` (~67 MB) or any HF id
11
+ (`LOCAL_SPEECH_MODELS`); WebGPU else single-thread WASM (no COOP/COEP needed). The package never
12
+ imports the peers: a host passes `createWorker` (build it from
13
+ `@happyvertical/smrt-svelte/browser-ai/whisper-worker?worker`, alias `speech-worker`, the one module
14
+ that imports `@happyvertical/speech/local` and `@huggingface/transformers` statically) plus
15
+ `loadSpeech: () => import('@happyvertical/speech/local')`, or `loadModule` for the page thread.
16
+ `createLocalSpeechModel()` (alias `createWhisperLocalModel()`) / `adapter.model` is the consent API
17
+ (`estimateSize`, `isCached`, `load({ onProgress, signal })` with `downloading` -> `extracting` ->
18
+ `complete`); share it via `modelHandle`. English-only handling of `language`/`task` lives in the speech
19
+ package. `probeBrowserSpeech()` -> `works | missing | unreliable` (no network, no mic prompt)
20
+ decides whether to offer it. Sources set `stopTimeoutMs` so `Dictation` waits for the transcription.
21
+ - **Inference backends (browser-ai)**: two `@happyvertical/smrt-web/ai` `InferenceBackend`s, both
22
+ `kind: 'local'`, both refusing `tool`/`function` messages, non-string content, `tool_calls` and
23
+ `options.tools` with `InferencePathError` (`unsupported_message`) through the SHARED
24
+ `assertTextOnlyTurn`, so a per-call switch between backends never changes what a turn means.
25
+ - `createWebLlmInferenceBackend()` wraps an `LLMAdapter` — `status` derives from the adapter's
26
+ `initState` (plus an explicit in-flight flag, because the adapter only flips to `initializing`
27
+ after its own awaits), `progress` mirrors `DownloadProgressInfo`, and `load()`/`unload()` drive
28
+ `ensureInitialized`/`unloadModel`. It reports `unavailable` without WebGPU so an `auto` path skips
29
+ it. Pass `adapter` to share the app-state-managed warm-cached adapter; construct once, so two
30
+ callers cannot each start an unshared adapter and a second download.
31
+ Its `loadModule` option matters: the default `importOptional()` resolves a VARIABLE specifier,
32
+ which a browser cannot resolve (`TypeError: Failed to resolve module specifier`) — a host that has
33
+ `@mlc-ai/web-llm` must pass `() => import('@mlc-ai/web-llm')` so the bundler rewrites a static one.
34
+ It does NOT forward `signal` (the adapter has no cancellation seam).
35
+ - `createBitGpuInferenceBackend()` wraps `bitgpu` (MIT, optional peer), the engine behind the
36
+ Bonsai WebGPU demo PrismML's own docs list as their in-browser option. `loadBitGpu`/`loadChat`
37
+ are REQUIRED loaders (`() => import('bitgpu')`, `() => import('bitgpu/chat')`) — a static import
38
+ here would fail the build for every consumer without the optional peer, and a bare specifier is
39
+ not resolvable by a browser either. `bitgpu`'s `LoadProgress.phase` maps to `InferenceProgress`
40
+ (`manifest`/`weights` → `downloading`, `pipelines` → `extracting`). Unlike the WebLLM backend it
41
+ **forwards `signal`** — bitgpu aborts a live generation — and it does NOT lazily load: weights
42
+ run to GBs, so a turn before `load()` fails with a named reason. `model` in the per-call options
43
+ is ignored (one engine binds one model), and bitgpu's tools/JSON-schema/think support is
44
+ deliberately left unexposed for now so the text-only invariant holds across backends.
45
+ - **`ModelStatusControl.svelte`**: the user-facing model lifecycle surface (state, Load/Unload,
46
+ download progress) driven entirely by a backend's own `status`/`progress`/`subscribe`, so it works
47
+ without a `<Provider>` ancestor. `unavailable` is a first-class state that offers NO load action.
@@ -0,0 +1,85 @@
1
+ # smrt-svelte/command-palette
2
+
3
+ Module semantics for `src/components/command-palette/` (`./command-palette`).
4
+ Package orientation and cross-module invariants live in
5
+ [../AGENTS.md](../AGENTS.md); the shell it sits in is in
6
+ [workspace.md](workspace.md).
7
+
8
+ ## Shape
9
+
10
+ - **Controller** (`controller.svelte.ts`): `createCommandPalette({ providers,
11
+ navigate, onError, searchDebounceMs, maxPerGroup })`. Owns open/query state,
12
+ the provider registry, local and remote rows, the highlight, and `activate`.
13
+ `setCommandPalette(controller)` puts it on context (call it above the
14
+ `AppShell`) so any descendant can `useCommandPaletteProvider(provider)`; that
15
+ hook is a no-op without a palette.
16
+ - **Components**: `CommandPalette` = `CommandPaletteTrigger` + `CommandPaletteDialog`.
17
+ Mount it **once**: the dialog owns the global shortcut (`hotkeys`, default
18
+ `['Mod+K']`; `false` for none). Place it with `AppShell` `slotItems`
19
+ (`header.center` is the usual slot); it is a movable item like any other.
20
+ Without a `palette` prop or context it builds one from `providers`/`navigate`.
21
+ - **Pure core**: `match.ts` (accent/case-insensitive, multi-term, word-start
22
+ then in-order fuzzy; returns title ranges), `rank.ts` (groups, limits,
23
+ boost), `hotkeys.ts` (`Mod` = Ctrl or Meta). Node-testable, no Svelte.
24
+
25
+ ## Provider contract (`types.ts`)
26
+
27
+ A `PaletteProvider` is `{ id, label, order?, items?, search?, minQueryLength?,
28
+ limit? }`.
29
+
30
+ - `items(ctx)` rows are re-read **every time the palette opens** and matched
31
+ locally by the palette. Sync results land in the same tick; async ones fill
32
+ in (`busy`).
33
+ - `search(query, { signal, limit })` runs after the debounce with an abort
34
+ signal that fires on the next keystroke or close. Its rows are shown as
35
+ returned (not re-filtered); earlier rows stay until the new ones arrive. A
36
+ superseded result is dropped, never shown.
37
+ - Registering an id again replaces the provider; a stale disposer is a no-op.
38
+ The replaced provider's cached rows are cleared and its in-flight `items` or
39
+ `search` results (and failures) are dropped, never shown or reported.
40
+ - Authorization stays with the provider. A failing provider is reported through
41
+ `onError` (default: logger warning) and marked in `failedProviders`; the
42
+ palette keeps working. The palette is not a security boundary.
43
+ - A row with `run` runs; else a row with `href` goes through `navigate`
44
+ (default `window.location.assign`; SvelteKit apps pass `goto`). The palette
45
+ closes first, then runs.
46
+
47
+ ## Shipped providers (`providers.ts`)
48
+
49
+ - `createNavigationProvider({ nav, groups })`: rows for the shell nav. Pass
50
+ getters over `useShellLayout().applied` to honor sections a user hid.
51
+ - `createModelProviders({ manifest, ... })` returns `[commands, records]`:
52
+ - eligibility is `navigableManifestEntries` (exported from
53
+ `workspace/manifest-nav.ts`), so it agrees with manifest-derived nav;
54
+ - commands: "New <noun>" for models whose REST `create` route exists;
55
+ destinations come from `hrefs` or the `<pagesBasePath>/<collection>[/new|/id]`
56
+ convention (the app owns its page routes);
57
+ - records: `GET <apiBasePath>/<collection>?<field>[like]=%q%` per model, one
58
+ text field each (first of `title`, `name`, `displayName`, ... that is a
59
+ non-sensitive, non-enum text field; override with `searchFields`), capped by
60
+ `maxModels` and `concurrency`, interleaved so one model cannot crowd out
61
+ the rest. The default fetch reads the generated list envelope
62
+ `{ items, count, limit, offset }` (a bare array also works; any other body
63
+ counts as no results). 401/403/404 mean "no results" for that model; the search throws
64
+ only when **every** request fails.
65
+ - Known limit: `LIKE` is case-insensitive on SQLite and case-sensitive on
66
+ PostgreSQL (sdk#1192). Supply `searchRows` to use a server search endpoint
67
+ instead.
68
+
69
+ ## Accessibility and keyboard
70
+
71
+ WAI-ARIA combobox: focus stays in the search box; `aria-activedescendant`
72
+ names the highlighted option; options sit in labelled groups inside a
73
+ `listbox`; a polite status region announces counts and "no results". Up/Down
74
+ (wrapping, skipping disabled rows), PageUp/PageDown, Enter, Escape (the
75
+ `Modal` closes). Disabled rows stay visible with their reason as the tooltip.
76
+ A bare-key shortcut (`/`) never fires while typing in a field or during IME
77
+ composition; a modified one (`Mod+K`) does. Strings are in
78
+ `src/i18n/strings.palette.ts` (registered in `i18n/server.ts`); provider group
79
+ labels default to English and are options on the shipped providers.
80
+
81
+ ## Not yet wired
82
+
83
+ Recipe surfaces (#3708): a recipe will declare the palette as a shell widget
84
+ and its own providers (for example an assistant provider) once non-model
85
+ surfaces exist. Until then hosts mount `CommandPalette` themselves.
@@ -0,0 +1,62 @@
1
+ # smrt-svelte/import-export
2
+
3
+ Module semantics for `src/components/import-export/` (`./import-export`).
4
+ Package orientation and cross-module invariants live in
5
+ [../AGENTS.md](../AGENTS.md); read that first.
6
+
7
+ ## Shape
8
+
9
+ `ImportExport.svelte` is a thin shell over a pure layer. The pure files import
10
+ no Svelte or DOM API and are unit-tested in Node; keep logic there, not in the
11
+ component.
12
+
13
+ | File | Owns |
14
+ |------|------|
15
+ | `csv.ts` | RFC 4180 parse/format, delimiter detection, BOM, formula neutralization |
16
+ | `validate.ts` | `coerceCell`, `validateMapping`, `validateRows` (the dry run) |
17
+ | `mapping.ts` | `autoMapColumns`, `setColumnTarget` |
18
+ | `fields.ts` | `fieldsFromCollectionDefinition`: manifest + resolved field policy to `ImportExportField[]` |
19
+ | `collection-adapter.ts` | `createCollectionImportExport`: generated CRUD fetchers to `createRecord` / `loadRows` |
20
+ | `runner.ts` | `runImport`: bounded-concurrency create, per-row failures, abort |
21
+ | `export.ts` / `report.ts` | `buildExportFile`, `buildErrorReport` |
22
+ | `download.ts` | the only DOM touch: Blob download (overridable via `ondownload`) |
23
+
24
+ ## Invariants
25
+
26
+ - **Field policy is a hard boundary.** A supplied resolved policy is an
27
+ allowlist: a field it omits is neither importable nor exportable (manifest
28
+ defaults apply only with no policy). `hidden` fields are neither importable
29
+ nor exportable; `locked` fields are not importable but keep their resolved
30
+ default; tenant fields (`tenantId`) are never imported (the server sets
31
+ them). `id` is export-only. Do not add a path that writes a non-importable
32
+ field: only importable and `locked` fields take a default; hidden and tenant
33
+ defaults never enter the payload.
34
+ - **Nothing is written during validation.** `validateRows` returns no records
35
+ while `mappingIssues` is non-empty (an unmapped required field would be
36
+ written without its value). `records` only ever holds fully valid rows;
37
+ there is no partial-row import.
38
+ - **Strict typing.** Integers must be JavaScript-safe integers (money is
39
+ integer minor units, never decimals); datetimes are strict ISO 8601 with a
40
+ missing offset meaning UTC; `3/5/2026` is rejected, not guessed. Booleans
41
+ accept `true/false/yes/no/y/n/1/0/on/off`. Enum matching is
42
+ case-insensitive and writes the canonical option.
43
+ - **Defaults fill empties.** An empty or unmapped cell takes the field's
44
+ resolved default; a required field with a default is satisfied.
45
+ - **Exports are spreadsheet-safe.** Text cells starting with `= + - @`, tab or
46
+ CR are prefixed with `'`; numeric and boolean columns are exempt so negative
47
+ numbers survive. Import strips that prefix back off text cells. The error
48
+ report is neutralized too, because it echoes user data.
49
+ - **Export never truncates silently.** `loadRows` pages with `limit`/`offset`
50
+ and throws `ExportLimitError` past `maxRows` (default 50,000).
51
+ - **Row failures are per row.** `runImport` continues past a rejected row and
52
+ reports it by file line; abort stops starting new rows and counts in-flight
53
+ ones.
54
+ - **Messages are i18n keys.** Issue text lives under `ui.importExport.issue.*`
55
+ in `src/i18n/strings.import-export.ts` (registered in `i18n/server.ts`);
56
+ `describeIssue` falls back to English defaults for the pure layer.
57
+
58
+ ## Not here
59
+
60
+ Native `.xlsx` read/write (save as CSV first), server-side dry-run endpoints,
61
+ upsert-by-key, and recipe/nav wiring (offering the panel on list screens waits
62
+ on the recipe surface contract, #3708).