@happyvertical/smrt-fields 0.40.61 → 0.40.63

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 (78) hide show
  1. package/AGENTS.md +18 -0
  2. package/README.md +125 -112
  3. package/dist/chunks/FieldPolicyCollection--RxukfCX.js +1218 -0
  4. package/dist/chunks/FieldPolicyCollection--RxukfCX.js.map +1 -0
  5. package/dist/collections/FieldPolicySuggestionCollection.d.ts +160 -0
  6. package/dist/collections/FieldPolicySuggestionCollection.d.ts.map +1 -0
  7. package/dist/collections/FieldUsageCounterCollection.d.ts +143 -0
  8. package/dist/collections/FieldUsageCounterCollection.d.ts.map +1 -0
  9. package/dist/deterministic-id.d.ts +18 -0
  10. package/dist/deterministic-id.d.ts.map +1 -0
  11. package/dist/field-definitions.d.ts +8 -0
  12. package/dist/field-definitions.d.ts.map +1 -1
  13. package/dist/index.d.ts +8 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1565 -1077
  16. package/dist/index.js.map +1 -1
  17. package/dist/manifest.json +1442 -188
  18. package/dist/models/FieldPolicySuggestion.d.ts +132 -0
  19. package/dist/models/FieldPolicySuggestion.d.ts.map +1 -0
  20. package/dist/models/FieldUsageCounter.d.ts +178 -0
  21. package/dist/models/FieldUsageCounter.d.ts.map +1 -0
  22. package/dist/models/FieldUsageReportReceipt.d.ts +25 -0
  23. package/dist/models/FieldUsageReportReceipt.d.ts.map +1 -0
  24. package/dist/smrt-knowledge.json +404 -7
  25. package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +54 -0
  26. package/dist/svelte/__tests__/FormHelp.test.js +8 -0
  27. package/dist/svelte/__tests__/ObjectForm.test.js +131 -0
  28. package/dist/svelte/__tests__/UsageLearning.test.js +140 -0
  29. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +21 -4
  30. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +5 -1
  31. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -1
  32. package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte +23 -0
  33. package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts +10 -0
  34. package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts.map +1 -0
  35. package/dist/svelte/__tests__/playground.test.js +66 -0
  36. package/dist/svelte/components/FieldPolicyControlPanel.svelte +28 -1
  37. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +3 -0
  38. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -1
  39. package/dist/svelte/components/FieldPolicyGearButton.svelte +8 -1
  40. package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -1
  41. package/dist/svelte/components/FieldPolicyGearProvider.svelte +34 -0
  42. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +3 -0
  43. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -1
  44. package/dist/svelte/components/FieldPolicySuggestionQueue.svelte +135 -0
  45. package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts +11 -0
  46. package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts.map +1 -0
  47. package/dist/svelte/components/FormHelp.svelte +16 -1
  48. package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -1
  49. package/dist/svelte/components/ObjectForm.svelte +91 -2
  50. package/dist/svelte/components/ObjectForm.svelte.d.ts +11 -1
  51. package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -1
  52. package/dist/svelte/context.svelte.d.ts +15 -0
  53. package/dist/svelte/context.svelte.d.ts.map +1 -1
  54. package/dist/svelte/context.svelte.js +15 -0
  55. package/dist/svelte/gear-context.svelte.d.ts +2 -0
  56. package/dist/svelte/gear-context.svelte.d.ts.map +1 -1
  57. package/dist/svelte/index.d.ts +6 -1
  58. package/dist/svelte/index.d.ts.map +1 -1
  59. package/dist/svelte/index.js +4 -1
  60. package/dist/svelte/playground/FieldPolicyFormPreview.svelte +18 -12
  61. package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -1
  62. package/dist/svelte/suggestions.d.ts +38 -0
  63. package/dist/svelte/suggestions.d.ts.map +1 -0
  64. package/dist/svelte/suggestions.js +68 -0
  65. package/dist/svelte/usage-capture.d.ts +63 -0
  66. package/dist/svelte/usage-capture.d.ts.map +1 -0
  67. package/dist/svelte/usage-capture.js +61 -0
  68. package/dist/types.d.ts +40 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/types.js +7 -1
  71. package/dist/types.js.map +1 -1
  72. package/dist/usage-learning.d.ts +201 -0
  73. package/dist/usage-learning.d.ts.map +1 -0
  74. package/dist/usage-schedules.d.ts +110 -0
  75. package/dist/usage-schedules.d.ts.map +1 -0
  76. package/dist/users-module.d.ts +30 -0
  77. package/dist/users-module.d.ts.map +1 -0
  78. package/package.json +9 -9
package/AGENTS.md CHANGED
@@ -196,6 +196,24 @@ per-field `{defaultValue, visibility, help, label, order, locked}` for any
196
196
  - `policyToVisibleColumnIds(policy, columns)` feeds smrt-ui `DataTable`'s
197
197
  `visibleColumnIds`; it filters policy-hidden mapped fields, preserves unmapped
198
198
  computed/action columns, and cannot reveal a static `column.hidden` column.
199
+ - **Two-tier context contract.** `PolicyField` alone reads the context with
200
+ `tryGetFieldPolicyContext()`, because outside a Provider it still has the
201
+ caller's children to render verbatim (incremental adoption). The
202
+ provider-only compositions — `ModeSwitch`, `AdvancedFields`, `FormHelp` —
203
+ read it with the throwing `getFieldPolicyContext()`: their whole output is
204
+ derived from the resolved policy, so degrading would render an empty or
205
+ absent control instead of surfacing the missing Provider. Do not convert one
206
+ of them to the non-throwing accessor in isolation (#2272).
207
+ - **One provider per form.** `FieldPolicyProvider` renders only
208
+ `{@render children?.()}`, so a host wanting `FormHelp` in a page header
209
+ extends the SAME provider over the header rather than hoisting the component
210
+ out. It must not add a second: each provider constructs its own
211
+ `FieldPolicyModeStore` and `setContext` shadows for the subtree, so a
212
+ `FormHelp` above a nested provider stops tracking the `ModeSwitch` inside it.
213
+ `ObjectForm` builds its own provider, so never wrap it in another; its
214
+ `actions` snippet renders INSIDE that provider (context resolves at the render
215
+ site), which is the only way to reach `FormHelp` through it — below the
216
+ fields, since there is no header seam (#2289).
199
217
 
200
218
  ## Defaults control panel (#2050)
201
219
 
package/README.md CHANGED
@@ -1,140 +1,153 @@
1
1
  # @happyvertical/smrt-fields
2
2
 
3
- Layered field policy for SMRT objects: personalize per-field defaults,
4
- visibility tiers (basic/advanced/hidden), help text, labels, ordering, and
5
- org locks at app, tenant, and user scope over the code-authored
6
- `@field({ ui })` seed.
3
+ `@happyvertical/smrt-fields` lets an application adjust an object's form
4
+ defaults, labels, help, order, visibility, and locks without changing the
5
+ object's source. Policies layer organization and personal choices over the
6
+ code-authored field definition.
7
7
 
8
- ```typescript
9
- import {
10
- FieldPolicy,
11
- FieldPolicyCollection,
12
- resolveFieldPolicy,
13
- resolveFieldPolicyExplained,
14
- } from '@happyvertical/smrt-fields';
15
-
16
- // An org (tenant) demotes an optional field and sets a default
17
- const policies = await FieldPolicyCollection.create({ db });
18
- await policies.create({
19
- objectRef: '@happyvertical/smrt-content:Article',
20
- fieldName: 'summary',
21
- scopeType: 'tenant',
22
- tenantId,
23
- visibility: 'advanced',
24
- defaultValue: JSON.stringify('TBD'),
25
- });
26
-
27
- // Resolve the effective policy for a user in that tenant
28
- const resolved = await resolveFieldPolicy(
29
- '@happyvertical/smrt-content:Article',
30
- { tenantId, userId, db },
31
- );
32
- resolved.fields.summary.visibility; // 'advanced'
8
+ For the complete application and operator guide, see the
9
+ [field policy guide](https://happyvertical.github.io/smrt/field-policies).
33
10
 
34
- // Explain variant: per-layer contributions for admin/gear UIs
35
- const explained = await resolveFieldPolicyExplained(
36
- '@happyvertical/smrt-content:Article',
37
- { tenantId, userId, db },
38
- );
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @happyvertical/smrt-fields
39
15
  ```
40
16
 
41
- Resolution layers (low high): code seed → app rows → tenant rows
42
- (hierarchy walk via the users tenant loader, or a flat fallback when an
43
- injected hierarchy provider cannot resolve the tenant) → user rows. A NULL column inherits
44
- from the lower layer; resetting a customization is a row delete.
17
+ The package includes `@happyvertical/smrt-users` because policy writes and
18
+ operator actions use its permission and tenant context services.
45
19
 
46
- `@happyvertical/smrt-users` is a required runtime dependency: Field Policy
47
- uses its permission catalog and operation guard for every write and gear
48
- action. The hierarchy fallback concerns tenant ancestry only; it is not a
49
- users-authorization fallback.
20
+ ## Define a safe code seed
50
21
 
51
- Writes are validated against the live `ObjectRegistry`: unknown
52
- objects/fields are rejected, defaults are type-checked, and defaults on
53
- `transient`/`sensitive`/`readPermission`-gated fields are refused. Required
54
- fields can only be demoted from `basic` when a usable default resolves —
55
- and the resolver re-enforces that invariant at read time.
22
+ The decorated model remains the definition of a field. Use its description
23
+ and `ui` hints to supply a useful first form before any policy row exists.
56
24
 
57
- See `AGENTS.md` for the full architecture notes.
25
+ ```typescript
26
+ import { field, SmrtObject, smrt } from '@happyvertical/smrt-core';
27
+
28
+ @smrt({ packageName: '@acme/billing' })
29
+ export class Invoice extends SmrtObject {
30
+ @field({
31
+ required: true,
32
+ description: 'Shown on the customer invoice.',
33
+ ui: { basic: true, group: 'billing', order: 1 },
34
+ })
35
+ title = '';
36
+
37
+ @field({ ui: { basic: false, group: 'billing', order: 2 } })
38
+ internalNotes = '';
39
+ }
40
+ ```
58
41
 
59
- ## Svelte ObjectForm
42
+ If no field has `ui.basic: true`, fields start in the basic view. Once at
43
+ least one field is marked basic, unmarked fields start advanced. `group` and
44
+ `order` are code-owned hints; `locked: true` seeds a lock that can prevent a
45
+ personal override.
60
46
 
61
- `@happyvertical/smrt-fields/svelte` provides provider-free generated forms.
62
- Pass only the generated browser field definitions and the matching resolved
63
- policy; `ObjectForm` renders their safe intersection, honors policy visibility
64
- and ordering, and keeps form state transport-neutral.
47
+ ## Resolve on the server
65
48
 
66
- ```svelte
67
- <script lang="ts">
68
- import { ObjectForm, createFieldInputRegistry } from '@happyvertical/smrt-fields/svelte';
49
+ Use the resolver in trusted server code when rendering SSR or applying a
50
+ server-owned workflow. It merges code → app → tenant ancestry → user.
69
51
 
70
- const inputRegistry = createFieldInputRegistry();
71
- // inputRegistry.registerField(objectRef, 'body', RichTextInput);
72
- </script>
52
+ ```typescript
53
+ import { resolveFieldPolicy } from '@happyvertical/smrt-fields';
73
54
 
74
- <ObjectForm {objectRef} fields={collectionDefinition.fields} {policy}
75
- bind:value={record} {inputRegistry} />
55
+ const policy = await resolveFieldPolicy(
56
+ '@acme/billing:Invoice',
57
+ { tenantId: requestTenant.id, userId: session.user.id, db },
58
+ );
59
+
60
+ const title = policy.fields.title;
76
61
  ```
77
62
 
78
- Pass an `actions` snippet to place host controls inside the owned native form.
79
- The snippet renders after ObjectForm's policy-ordered fields, so a regular
80
- submit button keeps native Enter/click submission and ObjectForm's validation
81
- and `onsubmit` handling without querying the DOM.
63
+ The client-facing generated `resolveBatch` action derives the tenant and user
64
+ from the authenticated request context. Do not accept those identifiers from a
65
+ browser request. It returns only fields that are safe to expose; sensitive,
66
+ transient, and read-permission-gated fields are omitted.
82
67
 
83
- ```svelte
84
- <ObjectForm {objectRef} fields={collectionDefinition.fields} {policy}
85
- bind:value={record} onsubmit={save}>
86
- {#snippet actions()}
87
- <button type="submit">Save</button>
88
- {/snippet}
89
- </ObjectForm>
90
- ```
68
+ ## Use policy-aware Svelte forms
91
69
 
92
- An app can register all of its generated collection definitions once and put
93
- the `resolveBatch` custom-action client behind an `ObjectFormSourceProvider`.
94
- Then forms need only their canonical object reference; the registry validates
95
- the generated definition and the untyped custom-action response before it is
96
- rendered, failing closed with an accessible error state on a missing or
97
- mismatched response.
70
+ For a custom form, provide a resolved policy with `FieldPolicyProvider` and
71
+ wrap each input in `PolicyField`. The wrapper applies visibility, labels, help,
72
+ and new-record defaults while preserving your markup and input component.
98
73
 
99
74
  ```svelte
100
75
  <script lang="ts">
101
76
  import {
102
- ObjectForm,
103
- ObjectFormSourceProvider,
104
- ObjectFormSourceRegistry,
77
+ FieldPolicyProvider,
78
+ PolicyField,
105
79
  } from '@happyvertical/smrt-fields/svelte';
106
80
 
107
- const source = new ObjectFormSourceRegistry(fieldPoliciesClient);
108
- for (const definition of Object.values(collectionDefinitions)) source.register(definition);
81
+ let { policy, invoice = $bindable({}) } = $props();
109
82
  </script>
110
83
 
111
- <ObjectFormSourceProvider {source}>
112
- <ObjectForm objectRef="@happyvertical/smrt-products:Product" bind:value={record} />
113
- </ObjectFormSourceProvider>
84
+ <FieldPolicyProvider {policy}>
85
+ <PolicyField name="title">
86
+ <input id="title" bind:value={invoice.title} />
87
+ </PolicyField>
88
+ </FieldPolicyProvider>
114
89
  ```
115
90
 
116
- The built-ins support text, integer, decimal, boolean, datetime, JSON, and
117
- reference identifiers. `FieldInputRegistry` is per app: a field-specific
118
- renderer takes precedence over a wire-type renderer. SMRT has no `select` wire
119
- type: keep the persisted wire type (normally `text`) and register a
120
- field-specific select-like renderer with `registerField(objectRef, fieldName,
121
- component)`. Reference fields deliberately remain provider-free identifier
122
- inputs unless an app registers its own chooser. `policyToVisibleColumnIds`
123
- adapts the same resolved policy to `DataTable` without hiding unmapped action or
124
- computed columns; static `column.hidden` remains authoritative.
125
-
126
- When a mounted create form starts another record, replace the bound record with
127
- an empty object or change `createSessionKey`; both begin a new default-prefill
128
- session without reapplying defaults after an in-form user clear.
129
-
130
- ## Policy settings gear
131
-
132
- `FieldPolicyGearProvider` makes the context-derived `editor-state` action
133
- available to any policy-aware form without choosing a client transport. Pass an
134
- adapter around the generated collection client's `getEditorState`, `create`,
135
- `update`, and `delete` calls; it must not accept tenant or user identifiers.
136
- Use `FieldPolicyGearButton` where the form wants its affordance, or set
137
- `showPolicyGear` on `ObjectForm`. `registerFieldPolicyFocusTool(shell,
138
- objectRef, tool)` is a structural AdminShell seam: it registers the required
139
- `{ type: 'object-form', id: objectRef }` subject and returns the shell's
140
- disposer without making Fields depend on `smrt-svelte`.
91
+ For generated forms, register the application's generated collection
92
+ definitions once in an `ObjectFormSourceRegistry`, place it in an
93
+ `ObjectFormSourceProvider`, then render an `ObjectForm` by canonical
94
+ `objectRef`. The registry validates both the generated field definition and
95
+ the `resolveBatch` response before rendering. See the guide for the complete
96
+ setup, including field-specific input renderers and policy-aware tables.
97
+
98
+ ## Administration and personal settings
99
+
100
+ Use `FieldPolicyGearProvider` with an adapter to the generated
101
+ `getEditorState`, create, update, and delete actions. It derives identity on
102
+ the server; adapter methods intentionally do not accept tenant or user ids.
103
+ Use `FieldPolicyGearButton` or `showPolicyGear` on `ObjectForm` to expose
104
+ the editor.
105
+
106
+ For a settings destination, the server builds data with
107
+ `buildFieldPolicySettingsCatalog()` and the browser renders it through
108
+ `FieldPolicyControlPanel`. Hosts supply route, transport, confirmation, and
109
+ AdminShell adapters. `fieldPolicyControlPanelNavItem()` and
110
+ `registerFieldPolicyFocusTool()` are structural seams, so Fields does not
111
+ take a dependency on a particular application shell.
112
+
113
+ `fields.policy.manage` permits app and tenant administration.
114
+ `fields.policy.personalize` permits only the current user's personal choices.
115
+ The host must still establish a trusted authenticated principal context and
116
+ enforce the route permission before rendering an operator destination.
117
+
118
+ ## Important behavior
119
+
120
+ - A policy row is sparse: `null` means inherit the lower layer. Delete a row
121
+ to reset that scope completely.
122
+ - `defaultValue` is the JSON-encoded wire channel. Server code with a plain
123
+ value should use `defaultValueRaw` or `setDefaultValue()`.
124
+ - Required fields cannot be hidden or moved to advanced without a usable
125
+ resolved default. Resolution forces a required, default-less field back to
126
+ basic as a safety net.
127
+ - App and tenant rows may lock a field. While the resolved organization policy
128
+ is locked, personal writes are rejected and old personal rows do not apply.
129
+ - Defaults are rejected for sensitive, transient, and read-permission-gated
130
+ fields. Reference defaults must use a valid UUID unless that reference
131
+ declares a text id type.
132
+
133
+ ## Usage learning
134
+
135
+ Optional usage capture records bounded, aggregated submissions from
136
+ authenticated tenant members and turns qualified patterns into
137
+ administrator-reviewed tenant-policy suggestions. It never auto-applies a
138
+ suggestion. `ObjectForm` reports only after its host confirms persistence;
139
+ browser values transit only for low-cardinality boolean and reference fields,
140
+ and telemetry failures never affect the saved submit.
141
+
142
+ Operators enable the dormant maintenance and suggestion schedules explicitly
143
+ with `ensureFieldUsageLearningSchedules({ db })`. The learning loop retains
144
+ aggregates and uses conservative thresholds; its accepted/dismissed suggestion
145
+ queue is restricted to `fields.policy.manage`. See the
146
+ [field policy guide](https://happyvertical.github.io/smrt/field-policies#usage-learning-and-suggestions)
147
+ for the capture, privacy, retention, and schedule contract.
148
+
149
+ ## Example application
150
+
151
+ The [s-m-r-t SaaS starter field-policy walkthrough](https://github.com/happyvertical/smrt-saas-starter/pull/51)
152
+ uses s-m-r-t `0.40.61` and shows the owner/admin controls and the member-facing
153
+ personal form flow in a working application.