@proveanything/smartlinks 1.11.7 → 1.11.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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.11.7 | Generated: 2026-05-02T09:06:52.034Z
3
+ Version: 1.11.9 | Generated: 2026-05-03T09:07:00.199Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -270,7 +270,7 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
270
270
 
271
271
  #### `records`
272
272
 
273
- Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [Records-Based Admin Pattern](records-admin-pattern.md). Omit if the app does not use scoped records.
273
+ Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [App Records Pattern](app-records-pattern.md). Omit if the app does not use scoped records.
274
274
 
275
275
  The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
276
276
 
@@ -294,7 +294,7 @@ The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils
294
294
  | `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
295
295
  | `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
296
296
 
297
- An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [records-admin-pattern.md](records-admin-pattern.md) for the full admin + public pattern.
297
+ An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [app-records-pattern.md](app-records-pattern.md) for the full admin + public pattern.
298
298
 
299
299
  #### `executor`
300
300
 
@@ -445,7 +445,7 @@ const productComments = await app.threads.list(collectionId, appId, {
445
445
  - **Usage logs** — record product usage metrics
446
446
  - **Audit trails** — immutable logs of actions
447
447
  - **Loyalty points** — track points earned/redeemed
448
- - **Per-product / per-facet configuration** — scoped data that varies by product axis (see [records-admin-pattern.md](records-admin-pattern.md))
448
+ - **Per-product / per-facet configuration** — scoped data that varies by product axis (see [app-records-pattern.md](app-records-pattern.md))
449
449
 
450
450
  ### Key Features
451
451
 
@@ -526,7 +526,7 @@ proof → batch → variant → product → rule(*) → facet(*) →
526
526
  - `facet(*)` — legacy single-facet anchors, walked alphabetically. Prefer `facetRule` for new work.
527
527
  - `collection` — the top of the chain and the catch-all for any record with no anchor fields. **There is no `'global'` tier above collection.**
528
528
 
529
- For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [records-admin-pattern.md §2](records-admin-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
529
+ For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [app-records-pattern.md §2](app-records-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
530
530
 
531
531
  ### Singleton Cardinality
532
532
 
@@ -1,4 +1,4 @@
1
- # SmartLinks Records — Admin & Public Pattern
1
+ # SmartLinks App Records Pattern
2
2
 
3
3
  > Canonical guide for microapps that store **per-product**, **per-facet**, **per-variant**, **per-batch**, or **rule-targeted** data.
4
4
  >
@@ -6,7 +6,8 @@
6
6
  >
7
7
  > Status: **standard**. New apps MUST follow this contract; existing apps SHOULD migrate.
8
8
  >
9
- > SDK: `@proveanything/smartlinks` ≥ **1.11**, `@proveanything/smartlinks-utils-ui` ≥ **0.7.6**.
9
+ > SDK: `@proveanything/smartlinks` ≥ **1.11**.
10
+ > Admin shell (React only): `@proveanything/smartlinks-utils-ui` ≥ **0.7.6** — required for the admin side if using the React shell; not needed in public widgets.
10
11
 
11
12
  ---
12
13
 
@@ -22,10 +23,10 @@ Every records-based app fits into a 2×2:
22
23
  That choice drives **three** things and nothing else:
23
24
 
24
25
  1. **Manifest:** `cardinality: 'singleton' | 'collection'` and `allowFacetRules: boolean`.
25
- 2. **Admin (`<RecordsAdminShell>`):** pass `cardinality` + include `'rule'` in `scopes` if `allowFacetRules`.
26
- 3. **Public hook:** `useResolvedRecord` (best match) **or** `useCollectedRecords` / `useResolveAllRecords` (all matches).
26
+ 2. **Admin:** use `<RecordsAdminShell>` (React) or call the admin SDK functions directly. Pass `cardinality` + include `'rule'` in `scopes` if `allowFacetRules`.
27
+ 3. **Public widget:** call `app.records.match()` (best match / singleton) or `app.records.resolveAll()` (all matches / collection). These are plain SDK calls with no framework dependency.
27
28
 
28
- If you only remember one rule: **never write your own resolver**. The shell, the hooks, and the SDK already agree on resolution order. Reimplementing it is how apps drift.
29
+ If you only remember one rule: **never write your own resolution loop**. The server already walks the chain correctly calling `match()` or `resolveAll()` is the entire public-side implementation.
29
30
 
30
31
  ---
31
32
 
@@ -115,7 +116,11 @@ Declare each record type once in `app.admin.json`. The shell and the platform re
115
116
 
116
117
  ---
117
118
 
118
- ## 4. Admin side — `<RecordsAdminShell>` (the only thing you should be writing)
119
+ ## 4. Admin side
120
+
121
+ > The admin shell and rule editor are part of `@proveanything/smartlinks-utils-ui`, which is a **React-only library**. It is only needed in admin dashboards — never import it in a public widget.
122
+
123
+ ### `<RecordsAdminShell>` (React)
119
124
 
120
125
  The shell owns: scope tabs, browser pane, rule editor, save/discard, dirty navigation, inheritance markers, deletion, CSV, bulk apply, deep linking. **You only supply the editor for one record's `data`.**
121
126
 
@@ -170,64 +175,78 @@ import { FacetRuleEditor } from '@proveanything/smartlinks-utils-ui/facet-rule-e
170
175
 
171
176
  ---
172
177
 
173
- ## 5. Public side — pick the right hook (this is where apps go wrong)
178
+ ## 5. Public side
174
179
 
175
- There is exactly **one decision** to make on the public side, and it follows from the manifest's `cardinality`:
180
+ > **Do not import `@proveanything/smartlinks-utils-ui` in a public widget.** It is a React admin library. Public widgets only need `@proveanything/smartlinks`.
176
181
 
177
- ### 5a. Singleton `useResolvedRecord` (best match wins)
182
+ The SDK is framework-agnostic. Public widgets call two endpoints depending on cardinality:
178
183
 
179
- Use this when the app shows **one** answer for the current product (ingredients, nutrition, washing instructions, warranty terms).
184
+ | Cardinality | Call | What it does |
185
+ |---|---|---|
186
+ | **Singleton** (one answer) | `app.records.match()` | Server walks the chain, returns the best-matching record |
187
+ | **Collection** (all answers) | `app.records.resolveAll()` | Server walks the chain, returns every matching record |
180
188
 
181
- ```tsx
189
+ Neither call requires React or any other framework — wrap them in whatever async pattern your widget uses.
190
+
191
+ > **Admin vs public — the rule is simple:**
192
+ >
193
+ > | Function | Public widget | Admin dashboard |
194
+ > |---|---|---|
195
+ > | `app.records.create(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
196
+ > | `app.records.list(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
197
+ > | `app.records.get(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
198
+ > | `app.records.update(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
199
+ > | `app.records.remove(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
200
+ > | `app.records.aggregate(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
201
+ > | `app.records.match(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
202
+ > | `app.records.resolveAll(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
203
+ > | `app.records.upsert()` | ❌ admin only — no public path | ✅ |
204
+ > | `app.records.bulkUpsert()` | ❌ admin only — no public path | ✅ |
205
+ > | `app.records.bulkDelete()` | ❌ admin only — no public path | ✅ |
206
+ > | `app.records.restore()` | ❌ admin only — no public path | ✅ |
207
+ > | `app.records.previewRule()` | ❌ admin only — no public path | ✅ |
208
+
209
+ ### 5a. Singleton — `app.records.match()` (best match wins)
210
+
211
+ Use when the widget shows **one** answer for the current product (ingredients, nutrition, warranty terms, washing instructions).
212
+
213
+ ```ts
182
214
  import * as SL from '@proveanything/smartlinks';
183
- import { useResolvedRecord } from '@proveanything/smartlinks-utils-ui/records-admin';
184
-
185
- const { data, source, sourceRef, matchedAt, matchedRule, isLoading } =
186
- useResolvedRecord<IngredientsConfig>({
187
- SL,
188
- appId,
189
- collectionId,
190
- recordType: 'ingredients',
191
- productId,
192
- variantId, // optional
193
- batchId, // optional
194
- proofId, // optional
195
- });
196
- ```
197
215
 
198
- The resolver walks `proof → batch → variant → product → rule → facet → collection` and returns the **first match**, plus `matchedAt` so you can render _"From the product record"_ vs _"Matched by rule"_ badges if useful.
216
+ const result = await SL.app.records.match(collectionId, appId, {
217
+ target: { productId, variantId, batchId }, // pass whatever context you have
218
+ strategy: 'best',
219
+ recordType: 'ingredients',
220
+ });
199
221
 
200
- > Wrap this once in your app (e.g. `useResolvedIngredientSet`) so the rest of the codebase reads `{ data, isLoading }` and never sees the resolver.
222
+ // result.best.ingredients the single highest-specificity record
223
+ // result.best.ingredients.matchedAt → 'product' | 'rule' | 'facet' | 'collection' | …
224
+ ```
201
225
 
202
- ### 5b. Collection → `useCollectedRecords` (every match, ordered)
226
+ The server walks `proof batch → variant → product → rule → facet → collection` and returns the first match.
203
227
 
204
- Use this when the app shows **many** answers aggregated across the chain (FAQs, recipes, SOPs, care tips). Most-specific first by default; pass `sort` to override.
228
+ ### 5b. Collection `app.records.resolveAll()` (every match, ordered)
205
229
 
206
- ```tsx
207
- import { useCollectedRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
208
-
209
- const { items, isLoading } = useCollectedRecords<FaqEntry>({
210
- SL,
211
- appId,
212
- collectionId,
213
- recordType: 'faq',
214
- productId,
215
- // sort: { kind: 'field', field: 'order', direction: 'asc' },
230
+ Use when the widget shows **many** answers across the chain (FAQs, recipes, care tips, SOPs).
231
+
232
+ ```ts
233
+ const result = await SL.app.records.resolveAll(collectionId, appId, {
234
+ target: { productId },
235
+ recordTypes: ['faq'],
216
236
  });
217
237
 
218
- // items: CollectedRecord<FaqEntry>[] each has { data, scope, ref, depth }
238
+ // result.records → AppRecord[] sorted most-specific first
239
+ // each record has .matchedAt, .data, .scope
219
240
  ```
220
241
 
221
- ### 5c. Multi-type aggregate `useResolveAllRecords`
242
+ ### 5c. Multi-type `app.records.resolveAll()` with multiple record types
222
243
 
223
- When you need every record of every type that applies to a context (rare; mostly executors and SEO surfaces).
224
-
225
- ```tsx
226
- import { useResolveAllRecords } from '@proveanything/smartlinks-utils-ui/records-admin';
244
+ When you need records of several types in one call (rare; executors, SEO surfaces):
227
245
 
228
- const { entries, isLoading } = useResolveAllRecords({
229
- SL, collectionId, appId,
230
- context: { productId, facets: { brand: 'acme', category: ['bread'] } },
246
+ ```ts
247
+ const result = await SL.app.records.resolveAll(collectionId, appId, {
248
+ target: { productId, facets: { brand: 'acme' } },
249
+ recordTypes: ['ingredients', 'nutrition', 'allergens'],
231
250
  });
232
251
  ```
233
252
 
@@ -235,11 +254,15 @@ const { entries, isLoading } = useResolveAllRecords({
235
254
 
236
255
  | ❌ Anti-pattern | ✅ Do this instead |
237
256
  | ---------------------------------------------------------- | ----------------------------------------------------------------- |
238
- | Calling `SL.app.records.list()` and filtering client-side | `useResolvedRecord` (singleton) or `useCollectedRecords` (collection). The server already knows the chain. |
239
- | Walking the chain by hand with multiple `getConfig` calls | One hook call. The resolver is tested, cached, and includes rules. |
257
+ | Importing anything from `@proveanything/smartlinks-utils-ui` in a public widget | That package is React-only and admin-only. Public widgets only use `@proveanything/smartlinks`. |
258
+ | Calling `SL.app.records.list()` and filtering client-side | `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). The server walks the chain. |
259
+ | Calling `SL.app.records.list(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
260
+ | Calling `SL.app.records.match(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
261
+ | Calling `SL.app.records.resolveAll(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
262
+ | Calling `upsert`, `bulkUpsert`, `bulkDelete`, or `previewRule` from widget code | Those are admin-only. Widget code reads data; it never writes records. |
263
+ | Walking the chain by hand with multiple `get` / `list` calls | One `match()` or `resolveAll()` call. The server handles the resolution order including rules. |
240
264
  | Treating `facet:key:value` refs as the rule mechanism | Use `facetRule` (`{ all: [{ facetKey, anyOf: [...] }] }`). Multi-condition, scored by specificity. |
241
265
  | Reading `matchedAt === 'global'` | There is no `'global'`. The top of the chain is `'collection'`. |
242
- | Building your own `<FacetRuleEditor>` | Use the one from `@proveanything/smartlinks-utils-ui/facet-rule-editor`. |
243
266
 
244
267
  ---
245
268
 
@@ -281,25 +304,37 @@ interface EditorContext<TData> {
281
304
  2. **Add `cardinality` and `allowFacetRules`** to every entry under `records` in `app.admin.json`.
282
305
  3. **Add `'rule'` (and `'collection'` if missing) to `scopes`** wherever `allowFacetRules: true`.
283
306
  4. **Pass `cardinality`** to `<RecordsAdminShell>`.
284
- 5. **Replace any handwritten chain walking** with `useResolvedRecord` (singleton) or `useCollectedRecords` (collection).
285
- 6. **Delete any code that constructs `facet:key:value` refs** for matching. Use `facetRule` via the shell or `<FacetRuleEditor>`.
307
+ 5. **Replace any handwritten chain walking** with `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). If you are using React, the `useResolvedRecord` / `useCollectedRecords` hooks from `@proveanything/smartlinks-utils-ui` wrap these calls — but they are **admin-side React helpers**, not for public widgets.
308
+ 6. **Delete any code that constructs `facet:key:value` refs** for matching. Use `facetRule` via the shell or `<FacetRuleEditor>` (React admin) or pass `facetRule` directly in `upsert()` calls.
286
309
  7. **Search for the word "global"** in your code/docs and rename to "collection" — this is the most common source of confusion.
287
310
 
288
311
  ---
289
312
 
290
313
  ## 8. Where the canonical exports live
291
314
 
292
- | Need | Import from |
293
- | ----------------------------- | ---------------------------------------------------------------------- |
294
- | Admin shell | `@proveanything/smartlinks-utils-ui/records-admin` `RecordsAdminShell` |
295
- | Standalone rule editor | `@proveanything/smartlinks-utils-ui/facet-rule-editor` → `FacetRuleEditor` |
315
+ ### Public widgets (any framework)
316
+
317
+ | Need | Import from |
318
+ | ---- | ----------- |
319
+ | Best-match resolution (singleton) | `@proveanything/smartlinks` → `SL.app.records.match()` |
320
+ | All-matches resolution (collection) | `@proveanything/smartlinks` → `SL.app.records.resolveAll()` |
321
+ | Record CRUD (public path) | `@proveanything/smartlinks` → `SL.app.records.{list, get, create, update, remove, aggregate}` |
322
+
323
+ ### Admin dashboards (React)
324
+
325
+ > All of the following are from `@proveanything/smartlinks-utils-ui`, a **React-only** package. Do not use in public widgets.
326
+
327
+ | Need | Import from |
328
+ | ---- | ----------- |
329
+ | Admin shell | `@proveanything/smartlinks-utils-ui/records-admin` → `RecordsAdminShell` |
330
+ | Standalone rule editor | `@proveanything/smartlinks-utils-ui/facet-rule-editor` → `FacetRuleEditor` |
296
331
  | Conditions editor (non-facet) | `@proveanything/smartlinks-utils-ui/conditions-editor` → `ConditionsEditor` |
297
- | Best-match resolver hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolvedRecord` |
298
- | All-matches hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useCollectedRecords` |
299
- | Multi-type aggregate hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolveAllRecords` |
300
- | Rule preview ("matches N") | `@proveanything/smartlinks-utils-ui/records-admin` → `useRulePreview` |
301
- | Server-side record CRUD | `@proveanything/smartlinks` → `SL.app.records.{upsert, list, remove, match, resolveAll}` |
332
+ | Best-match hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolvedRecord` |
333
+ | All-matches hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useCollectedRecords` |
334
+ | Multi-type hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolveAllRecords` |
335
+ | Rule preview hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useRulePreview` |
336
+ | Admin record CRUD (admin path) | `@proveanything/smartlinks` → `SL.app.records.{upsert, bulkUpsert, bulkDelete, restore, previewRule}` |
302
337
 
303
338
  ---
304
339
 
305
- _End of doc. If anything below the SDK contradicts this file, this file wins — open a PR against the SDK to bring the two back into sync._
340
+ _End of doc. If anything below the SDK contradicts this file, this file wins — open a PR against the SDK to bring the two back into sync._
@@ -70,7 +70,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
70
70
  | **AI Guide Template** | `docs/ai-guide-template.md` | Template for creating `public/ai-guide.md` — customise per app |
71
71
  | **Forms** | `docs/forms.md` | Form definitions, schema-driven rendering, submission patterns |
72
72
  | **Auth Kit** | `docs/auth-kit.md` | End-user sign-in: email/password, magic links, phone OTP, Google OAuth |
73
- | **Records Admin Pattern** | `docs/records-admin-pattern.md` | Standard pattern for per-product/facet/variant/batch admin UIs |
73
+ | **App Records Pattern** | `docs/app-records-pattern.md` | Standard pattern for per-product/facet/variant/batch admin + public widget UIs |
74
74
  | **UI Utils** | `docs/ui-utils.md` | `@proveanything/smartlinks-utils-ui` — React shells, hooks, and primitives for records-based apps |
75
75
 
76
76
  ---
@@ -19,7 +19,7 @@ translate SDK data into consistent admin interfaces.
19
19
 
20
20
  **When do you need it?**
21
21
 
22
- - You are building an admin UI for a records-based microapp (see [records-admin-pattern.md](records-admin-pattern.md))
22
+ - You are building an admin UI for a records-based microapp (see [app-records-pattern.md](app-records-pattern.md))
23
23
  - You need a media asset picker, icon picker, or font picker in an admin panel
24
24
  - You need a recursive rule/conditions editor for targeting or audience logic
25
25
  - You want the standard inheritance/override editor for scoped records
@@ -326,7 +326,7 @@ Props:
326
326
 
327
327
  Free-text facet entry is **not** supported — admins must pick from defined facets.
328
328
 
329
- See [records-admin-pattern.md §4](records-admin-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
329
+ See [app-records-pattern.md §4](app-records-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
330
330
 
331
331
  ---
332
332
 
@@ -505,6 +505,6 @@ your microapp ← domain logic and custom forms
505
505
 
506
506
  ## Further reading
507
507
 
508
- - [records-admin-pattern.md](records-admin-pattern.md) — the data contract that `RecordsAdminShell` implements
508
+ - [app-records-pattern.md](app-records-pattern.md) — the data contract that `RecordsAdminShell` implements
509
509
  - [building-react-components.md](building-react-components.md) — dual-mode rendering rules that apply to all components
510
510
  - [app-manifest.md](app-manifest.md) — the `records` manifest block that drives tab generation
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.11.7 | Generated: 2026-05-02T09:06:52.034Z
3
+ Version: 1.11.9 | Generated: 2026-05-03T09:07:00.199Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -270,7 +270,7 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
270
270
 
271
271
  #### `records`
272
272
 
273
- Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [Records-Based Admin Pattern](records-admin-pattern.md). Omit if the app does not use scoped records.
273
+ Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [App Records Pattern](app-records-pattern.md). Omit if the app does not use scoped records.
274
274
 
275
275
  The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
276
276
 
@@ -294,7 +294,7 @@ The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils
294
294
  | `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
295
295
  | `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
296
296
 
297
- An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [records-admin-pattern.md](records-admin-pattern.md) for the full admin + public pattern.
297
+ An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [app-records-pattern.md](app-records-pattern.md) for the full admin + public pattern.
298
298
 
299
299
  #### `executor`
300
300
 
@@ -445,7 +445,7 @@ const productComments = await app.threads.list(collectionId, appId, {
445
445
  - **Usage logs** — record product usage metrics
446
446
  - **Audit trails** — immutable logs of actions
447
447
  - **Loyalty points** — track points earned/redeemed
448
- - **Per-product / per-facet configuration** — scoped data that varies by product axis (see [records-admin-pattern.md](records-admin-pattern.md))
448
+ - **Per-product / per-facet configuration** — scoped data that varies by product axis (see [app-records-pattern.md](app-records-pattern.md))
449
449
 
450
450
  ### Key Features
451
451
 
@@ -526,7 +526,7 @@ proof → batch → variant → product → rule(*) → facet(*) →
526
526
  - `facet(*)` — legacy single-facet anchors, walked alphabetically. Prefer `facetRule` for new work.
527
527
  - `collection` — the top of the chain and the catch-all for any record with no anchor fields. **There is no `'global'` tier above collection.**
528
528
 
529
- For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [records-admin-pattern.md §2](records-admin-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
529
+ For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [app-records-pattern.md §2](app-records-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
530
530
 
531
531
  ### Singleton Cardinality
532
532
 
@@ -0,0 +1,340 @@
1
+ # SmartLinks App Records Pattern
2
+
3
+ > Canonical guide for microapps that store **per-product**, **per-facet**, **per-variant**, **per-batch**, or **rule-targeted** data.
4
+ >
5
+ > Audience: microapp developers (ingredients, nutrition, allergy, FAQs, recipes, warranty, provenance, …).
6
+ >
7
+ > Status: **standard**. New apps MUST follow this contract; existing apps SHOULD migrate.
8
+ >
9
+ > SDK: `@proveanything/smartlinks` ≥ **1.11**.
10
+ > Admin shell (React only): `@proveanything/smartlinks-utils-ui` ≥ **0.7.6** — required for the admin side if using the React shell; not needed in public widgets.
11
+
12
+ ---
13
+
14
+ ## 0. TL;DR — pick your shape, then copy the snippet
15
+
16
+ Every records-based app fits into a 2×2:
17
+
18
+ | | **Singleton** (one record per scope) | **Collection** (many records per scope) |
19
+ | ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------- |
20
+ | **Best-match (one wins)**| Ingredients, nutrition, washing instructions | _(rare — usually you want all)_ |
21
+ | **All matches (aggregate)** | _(rare — usually you want best)_ | FAQs, recipes, SOPs, care tips, story cards |
22
+
23
+ That choice drives **three** things and nothing else:
24
+
25
+ 1. **Manifest:** `cardinality: 'singleton' | 'collection'` and `allowFacetRules: boolean`.
26
+ 2. **Admin:** use `<RecordsAdminShell>` (React) or call the admin SDK functions directly. Pass `cardinality` + include `'rule'` in `scopes` if `allowFacetRules`.
27
+ 3. **Public widget:** call `app.records.match()` (best match / singleton) or `app.records.resolveAll()` (all matches / collection). These are plain SDK calls with no framework dependency.
28
+
29
+ If you only remember one rule: **never write your own resolution loop**. The server already walks the chain correctly — calling `match()` or `resolveAll()` is the entire public-side implementation.
30
+
31
+ ---
32
+
33
+ ## 1. The data model in one paragraph
34
+
35
+ A microapp owns a typed **records table** keyed by `(appId, recordType, id)`. Each `AppRecord` carries a `data` payload plus **either** a structured `scope` (anchored to a node in the chain) **or** a `facetRule` (matches products dynamically by their facets). The server resolves which record(s) apply to a given product context. There is no "global"; the top of the chain is **collection** — anything not explicitly scoped further applies to the whole collection.
36
+
37
+ ```ts
38
+ import * as SL from '@proveanything/smartlinks';
39
+
40
+ await SL.app.records.upsert(collectionId, appId, {
41
+ recordType: 'ingredients',
42
+ scope: { productId: 'prod_abc', variantId: 'var_500ml' }, // server derives the ref
43
+ data: { /* domain payload */ },
44
+ }, /* admin */ true);
45
+ ```
46
+
47
+ Or, for a rule-targeted record:
48
+
49
+ ```ts
50
+ await SL.app.records.upsert(collectionId, appId, {
51
+ recordType: 'ingredients',
52
+ facetRule: {
53
+ all: [
54
+ { facetKey: 'brand', anyOf: ['acme'] },
55
+ { facetKey: 'category', anyOf: ['bread', 'pastry'] },
56
+ ],
57
+ },
58
+ data: { /* domain payload */ },
59
+ }, true);
60
+ ```
61
+
62
+ `scope` and `facetRule` are **mutually exclusive on save**.
63
+
64
+ ---
65
+
66
+ ## 2. Resolution order (one canonical chain)
67
+
68
+ The server walks **most-specific → least-specific** and stops at the first match (for best-match) or collects every match (for aggregate):
69
+
70
+ ```
71
+ proof → batch → variant → product → rule(*) → facet(*) → collection
72
+ ```
73
+
74
+ - `rule(*)` — facet-rule records are scored by **specificity** (number of clauses + number of constrained values). The most specific rule wins.
75
+ - `facet(*)` — legacy single-facet anchors, walked deterministically (alphabetical).
76
+ - `collection` — the top of the chain. **There is no "global" tier above collection.** A collection-level record is the catch-all for that collection.
77
+
78
+ The resolved value comes back tagged with `matchedAt: 'product' | 'rule' | 'facet' | …` so the UI can say things like _"Matched by rule: brand=Acme AND category=bread"_.
79
+
80
+ > ⚠️ Legacy `scope.facets[]` (colon-delimited single-facet refs) is deprecated and removed in SDK 1.12. Use `facetRule` for everything that isn't a one-off facet pin.
81
+
82
+ ---
83
+
84
+ ## 3. Manifest declaration
85
+
86
+ Declare each record type once in `app.admin.json`. The shell and the platform read this to render the right scope tabs and disable the wrong affordances.
87
+
88
+ ```json
89
+ {
90
+ "records": {
91
+ "ingredients": {
92
+ "label": "Ingredients",
93
+ "cardinality": "singleton",
94
+ "allowFacetRules": true,
95
+ "scopes": ["collection", "facet", "rule", "product", "variant", "batch"],
96
+ "defaultScope": "product"
97
+ },
98
+ "faq": {
99
+ "label": "FAQs",
100
+ "cardinality": "collection",
101
+ "allowFacetRules": true,
102
+ "scopes": ["collection", "rule", "product"],
103
+ "defaultScope": "collection"
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ | Field | Meaning |
110
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
111
+ | `cardinality` | `'singleton'` (one per scope, e.g. ingredients) or `'collection'` (many per scope, e.g. FAQs). Default `'singleton'`. |
112
+ | `allowFacetRules` | `true` to enable the `rule` scope tab + `<FacetRuleEditor>` in the shell. Default `false`. |
113
+ | `scopes` | Allowed scope kinds in **resolution order**. `'rule'` is a synthetic scope that holds rule-targeted records. |
114
+ | `defaultScope` | Where the "Create new" button lands. |
115
+ | `label` | Human-readable label used in headings and toasts. |
116
+
117
+ ---
118
+
119
+ ## 4. Admin side
120
+
121
+ > The admin shell and rule editor are part of `@proveanything/smartlinks-utils-ui`, which is a **React-only library**. It is only needed in admin dashboards — never import it in a public widget.
122
+
123
+ ### `<RecordsAdminShell>` (React)
124
+
125
+ The shell owns: scope tabs, browser pane, rule editor, save/discard, dirty navigation, inheritance markers, deletion, CSV, bulk apply, deep linking. **You only supply the editor for one record's `data`.**
126
+
127
+ ```tsx
128
+ import * as SL from '@proveanything/smartlinks';
129
+ import { RecordsAdminShell } from '@proveanything/smartlinks-utils-ui/records-admin';
130
+
131
+ <RecordsAdminShell<IngredientsConfig>
132
+ SL={SL}
133
+ collectionId={collectionId}
134
+ appId={appId}
135
+ recordType="ingredients"
136
+ label="Ingredients"
137
+ cardinality="singleton" // ← from manifest
138
+ scopes={['collection', 'facet', 'rule', 'product', 'variant', 'batch']}
139
+ defaultScope="product"
140
+ defaultData={() => emptyConfig()}
141
+ renderEditor={(ctx) => (
142
+ <IngredientsEditor
143
+ value={ctx.value}
144
+ onChange={ctx.onChange}
145
+ // For rule-targeted records, the shell hands you the live rule + setter:
146
+ facetRule={ctx.facetRule}
147
+ onFacetRuleChange={ctx.onFacetRuleChange}
148
+ />
149
+ )}
150
+ />
151
+ ```
152
+
153
+ ### What the shell gives you for free
154
+
155
+ - **Scope tabs** including a **`Rule`** tab when `'rule'` is in `scopes`. Selecting it opens `<FacetRuleEditor>` above your editor — no extra wiring.
156
+ - **`EditorContext.facetRule` / `onFacetRuleChange`** for rule-scoped records, plus `canSave: false` until at least one clause has values (avoids server 500s).
157
+ - **Inheritance markers** — when editing a variant, the product baseline is shown; per-field "↩ Inherited" / "● Override" is rendered by the inheritance helpers.
158
+ - **Collection cardinality flow** — set `cardinality="collection"` and the shell turns the right pane into a list of items (table / cards / gallery) with `+ New` and per-item nav.
159
+ - **Telemetry** — `record.save`, `record.delete`, `scope.change`, `csv.import`, `bulk.apply`, `item.create`, etc. via `onTelemetry`.
160
+
161
+ ### Standalone rule editor
162
+
163
+ If you need a rule editor outside the shell (e.g. on a settings page):
164
+
165
+ ```tsx
166
+ import { FacetRuleEditor } from '@proveanything/smartlinks-utils-ui/facet-rule-editor';
167
+
168
+ <FacetRuleEditor
169
+ value={rule}
170
+ onChange={setRule}
171
+ collectionId={collectionId} // lazy-fetches facets via SL.facets.publicList
172
+ preview={rulePreview} // optional — wire from useRulePreview
173
+ />
174
+ ```
175
+
176
+ ---
177
+
178
+ ## 5. Public side
179
+
180
+ > **Do not import `@proveanything/smartlinks-utils-ui` in a public widget.** It is a React admin library. Public widgets only need `@proveanything/smartlinks`.
181
+
182
+ The SDK is framework-agnostic. Public widgets call two endpoints depending on cardinality:
183
+
184
+ | Cardinality | Call | What it does |
185
+ |---|---|---|
186
+ | **Singleton** (one answer) | `app.records.match()` | Server walks the chain, returns the best-matching record |
187
+ | **Collection** (all answers) | `app.records.resolveAll()` | Server walks the chain, returns every matching record |
188
+
189
+ Neither call requires React or any other framework — wrap them in whatever async pattern your widget uses.
190
+
191
+ > **Admin vs public — the rule is simple:**
192
+ >
193
+ > | Function | Public widget | Admin dashboard |
194
+ > |---|---|---|
195
+ > | `app.records.create(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
196
+ > | `app.records.list(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
197
+ > | `app.records.get(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
198
+ > | `app.records.update(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
199
+ > | `app.records.remove(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
200
+ > | `app.records.aggregate(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
201
+ > | `app.records.match(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
202
+ > | `app.records.resolveAll(…, false)` | ✅ default — omit the flag | ✅ pass `true` |
203
+ > | `app.records.upsert()` | ❌ admin only — no public path | ✅ |
204
+ > | `app.records.bulkUpsert()` | ❌ admin only — no public path | ✅ |
205
+ > | `app.records.bulkDelete()` | ❌ admin only — no public path | ✅ |
206
+ > | `app.records.restore()` | ❌ admin only — no public path | ✅ |
207
+ > | `app.records.previewRule()` | ❌ admin only — no public path | ✅ |
208
+
209
+ ### 5a. Singleton — `app.records.match()` (best match wins)
210
+
211
+ Use when the widget shows **one** answer for the current product (ingredients, nutrition, warranty terms, washing instructions).
212
+
213
+ ```ts
214
+ import * as SL from '@proveanything/smartlinks';
215
+
216
+ const result = await SL.app.records.match(collectionId, appId, {
217
+ target: { productId, variantId, batchId }, // pass whatever context you have
218
+ strategy: 'best',
219
+ recordType: 'ingredients',
220
+ });
221
+
222
+ // result.best.ingredients → the single highest-specificity record
223
+ // result.best.ingredients.matchedAt → 'product' | 'rule' | 'facet' | 'collection' | …
224
+ ```
225
+
226
+ The server walks `proof → batch → variant → product → rule → facet → collection` and returns the first match.
227
+
228
+ ### 5b. Collection — `app.records.resolveAll()` (every match, ordered)
229
+
230
+ Use when the widget shows **many** answers across the chain (FAQs, recipes, care tips, SOPs).
231
+
232
+ ```ts
233
+ const result = await SL.app.records.resolveAll(collectionId, appId, {
234
+ target: { productId },
235
+ recordTypes: ['faq'],
236
+ });
237
+
238
+ // result.records → AppRecord[] sorted most-specific first
239
+ // each record has .matchedAt, .data, .scope
240
+ ```
241
+
242
+ ### 5c. Multi-type — `app.records.resolveAll()` with multiple record types
243
+
244
+ When you need records of several types in one call (rare; executors, SEO surfaces):
245
+
246
+ ```ts
247
+ const result = await SL.app.records.resolveAll(collectionId, appId, {
248
+ target: { productId, facets: { brand: 'acme' } },
249
+ recordTypes: ['ingredients', 'nutrition', 'allergens'],
250
+ });
251
+ ```
252
+
253
+ ### Common mistakes (do not do these)
254
+
255
+ | ❌ Anti-pattern | ✅ Do this instead |
256
+ | ---------------------------------------------------------- | ----------------------------------------------------------------- |
257
+ | Importing anything from `@proveanything/smartlinks-utils-ui` in a public widget | That package is React-only and admin-only. Public widgets only use `@proveanything/smartlinks`. |
258
+ | Calling `SL.app.records.list()` and filtering client-side | `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). The server walks the chain. |
259
+ | Calling `SL.app.records.list(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
260
+ | Calling `SL.app.records.match(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
261
+ | Calling `SL.app.records.resolveAll(…, true)` from a public widget | Omit the `admin` flag — it defaults to `false`. |
262
+ | Calling `upsert`, `bulkUpsert`, `bulkDelete`, or `previewRule` from widget code | Those are admin-only. Widget code reads data; it never writes records. |
263
+ | Walking the chain by hand with multiple `get` / `list` calls | One `match()` or `resolveAll()` call. The server handles the resolution order including rules. |
264
+ | Treating `facet:key:value` refs as the rule mechanism | Use `facetRule` (`{ all: [{ facetKey, anyOf: [...] }] }`). Multi-condition, scored by specificity. |
265
+ | Reading `matchedAt === 'global'` | There is no `'global'`. The top of the chain is `'collection'`. |
266
+
267
+ ---
268
+
269
+ ## 6. Reference: the `EditorContext` your `renderEditor` receives
270
+
271
+ ```ts
272
+ interface EditorContext<TData> {
273
+ value: TData;
274
+ onChange: (next: TData) => void;
275
+ source: 'self' | 'inherited' | 'empty';
276
+ recordId?: string;
277
+ parentValue?: TData | null;
278
+ scope: ParsedRef; // { kind: 'product' | 'rule' | …, productId?, … }
279
+
280
+ // Save lifecycle
281
+ isDirty: boolean;
282
+ isSaving?: boolean;
283
+ saveError?: unknown | null;
284
+ canSave?: boolean; // shell flips to false on empty rules
285
+ cannotSaveReason?: string;
286
+ save: () => Promise<void>;
287
+ reset: () => void;
288
+
289
+ // Deletion
290
+ remove: () => Promise<void>;
291
+ canRemove: boolean;
292
+
293
+ // Rule scope only
294
+ facetRule?: FacetRule | null;
295
+ onFacetRuleChange?: (next: FacetRule | null) => void;
296
+ }
297
+ ```
298
+
299
+ ---
300
+
301
+ ## 7. Migration checklist (existing apps)
302
+
303
+ 1. **Update SDKs:** `@proveanything/smartlinks@^1.11`, `@proveanything/smartlinks-utils-ui@^0.7.6`.
304
+ 2. **Add `cardinality` and `allowFacetRules`** to every entry under `records` in `app.admin.json`.
305
+ 3. **Add `'rule'` (and `'collection'` if missing) to `scopes`** wherever `allowFacetRules: true`.
306
+ 4. **Pass `cardinality`** to `<RecordsAdminShell>`.
307
+ 5. **Replace any handwritten chain walking** with `app.records.match()` (singleton) or `app.records.resolveAll()` (collection). If you are using React, the `useResolvedRecord` / `useCollectedRecords` hooks from `@proveanything/smartlinks-utils-ui` wrap these calls — but they are **admin-side React helpers**, not for public widgets.
308
+ 6. **Delete any code that constructs `facet:key:value` refs** for matching. Use `facetRule` via the shell or `<FacetRuleEditor>` (React admin) or pass `facetRule` directly in `upsert()` calls.
309
+ 7. **Search for the word "global"** in your code/docs and rename to "collection" — this is the most common source of confusion.
310
+
311
+ ---
312
+
313
+ ## 8. Where the canonical exports live
314
+
315
+ ### Public widgets (any framework)
316
+
317
+ | Need | Import from |
318
+ | ---- | ----------- |
319
+ | Best-match resolution (singleton) | `@proveanything/smartlinks` → `SL.app.records.match()` |
320
+ | All-matches resolution (collection) | `@proveanything/smartlinks` → `SL.app.records.resolveAll()` |
321
+ | Record CRUD (public path) | `@proveanything/smartlinks` → `SL.app.records.{list, get, create, update, remove, aggregate}` |
322
+
323
+ ### Admin dashboards (React)
324
+
325
+ > All of the following are from `@proveanything/smartlinks-utils-ui`, a **React-only** package. Do not use in public widgets.
326
+
327
+ | Need | Import from |
328
+ | ---- | ----------- |
329
+ | Admin shell | `@proveanything/smartlinks-utils-ui/records-admin` → `RecordsAdminShell` |
330
+ | Standalone rule editor | `@proveanything/smartlinks-utils-ui/facet-rule-editor` → `FacetRuleEditor` |
331
+ | Conditions editor (non-facet) | `@proveanything/smartlinks-utils-ui/conditions-editor` → `ConditionsEditor` |
332
+ | Best-match hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolvedRecord` |
333
+ | All-matches hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useCollectedRecords` |
334
+ | Multi-type hook (React convenience wrapper) | `@proveanything/smartlinks-utils-ui/records-admin` → `useResolveAllRecords` |
335
+ | Rule preview hook | `@proveanything/smartlinks-utils-ui/records-admin` → `useRulePreview` |
336
+ | Admin record CRUD (admin path) | `@proveanything/smartlinks` → `SL.app.records.{upsert, bulkUpsert, bulkDelete, restore, previewRule}` |
337
+
338
+ ---
339
+
340
+ _End of doc. If anything below the SDK contradicts this file, this file wins — open a PR against the SDK to bring the two back into sync._
package/docs/overview.md CHANGED
@@ -70,7 +70,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
70
70
  | **AI Guide Template** | `docs/ai-guide-template.md` | Template for creating `public/ai-guide.md` — customise per app |
71
71
  | **Forms** | `docs/forms.md` | Form definitions, schema-driven rendering, submission patterns |
72
72
  | **Auth Kit** | `docs/auth-kit.md` | End-user sign-in: email/password, magic links, phone OTP, Google OAuth |
73
- | **Records Admin Pattern** | `docs/records-admin-pattern.md` | Standard pattern for per-product/facet/variant/batch admin UIs |
73
+ | **App Records Pattern** | `docs/app-records-pattern.md` | Standard pattern for per-product/facet/variant/batch admin + public widget UIs |
74
74
  | **UI Utils** | `docs/ui-utils.md` | `@proveanything/smartlinks-utils-ui` — React shells, hooks, and primitives for records-based apps |
75
75
 
76
76
  ---
package/docs/ui-utils.md CHANGED
@@ -19,7 +19,7 @@ translate SDK data into consistent admin interfaces.
19
19
 
20
20
  **When do you need it?**
21
21
 
22
- - You are building an admin UI for a records-based microapp (see [records-admin-pattern.md](records-admin-pattern.md))
22
+ - You are building an admin UI for a records-based microapp (see [app-records-pattern.md](app-records-pattern.md))
23
23
  - You need a media asset picker, icon picker, or font picker in an admin panel
24
24
  - You need a recursive rule/conditions editor for targeting or audience logic
25
25
  - You want the standard inheritance/override editor for scoped records
@@ -326,7 +326,7 @@ Props:
326
326
 
327
327
  Free-text facet entry is **not** supported — admins must pick from defined facets.
328
328
 
329
- See [records-admin-pattern.md §4](records-admin-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
329
+ See [app-records-pattern.md §4](app-records-pattern.md#4-admin-side----recordsadminshell-the-only-thing-you-should-be-writing) for the standalone usage example.
330
330
 
331
331
  ---
332
332
 
@@ -505,6 +505,6 @@ your microapp ← domain logic and custom forms
505
505
 
506
506
  ## Further reading
507
507
 
508
- - [records-admin-pattern.md](records-admin-pattern.md) — the data contract that `RecordsAdminShell` implements
508
+ - [app-records-pattern.md](app-records-pattern.md) — the data contract that `RecordsAdminShell` implements
509
509
  - [building-react-components.md](building-react-components.md) — dual-mode rendering rules that apply to all components
510
510
  - [app-manifest.md](app-manifest.md) — the `records` manifest block that drives tab generation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.11.7",
3
+ "version": "1.11.9",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",