@happyvertical/smrt-fields 0.40.59 → 0.40.61

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 (184) hide show
  1. package/AGENTS.md +58 -12
  2. package/README.md +90 -2
  3. package/dist/__smrt-register__.d.ts +2 -0
  4. package/dist/__smrt-register__.d.ts.map +1 -0
  5. package/dist/cache.d.ts +18 -0
  6. package/dist/cache.d.ts.map +1 -0
  7. package/dist/collections/FieldPolicyCollection.d.ts +95 -0
  8. package/dist/collections/FieldPolicyCollection.d.ts.map +1 -0
  9. package/dist/field-definitions.d.ts +77 -0
  10. package/dist/field-definitions.d.ts.map +1 -0
  11. package/dist/field-policy-resolver.d.ts +46 -0
  12. package/dist/field-policy-resolver.d.ts.map +1 -0
  13. package/dist/index.d.ts +9 -532
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +752 -55
  16. package/dist/index.js.map +1 -1
  17. package/dist/manifest.json +45 -3
  18. package/dist/models/FieldPolicy.d.ts +163 -0
  19. package/dist/models/FieldPolicy.d.ts.map +1 -0
  20. package/dist/permissions.d.ts +9 -0
  21. package/dist/permissions.d.ts.map +1 -0
  22. package/dist/playground.d.ts +2 -0
  23. package/dist/playground.d.ts.map +1 -0
  24. package/dist/playground.js +39 -0
  25. package/dist/playground.js.map +1 -0
  26. package/dist/settings-catalog.d.ts +73 -0
  27. package/dist/settings-catalog.d.ts.map +1 -0
  28. package/dist/smrt-knowledge.json +50 -7
  29. package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +223 -0
  30. package/dist/svelte/__tests__/FieldPolicyGear.test.js +503 -0
  31. package/dist/svelte/__tests__/FormHelp.test.js +138 -0
  32. package/dist/svelte/__tests__/ModeUX.test.js +109 -0
  33. package/dist/svelte/__tests__/ObjectForm.test.js +556 -0
  34. package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
  35. package/dist/svelte/__tests__/PolicyField.test.js +373 -0
  36. package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte +30 -0
  37. package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts +9 -0
  38. package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts.map +1 -0
  39. package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte +33 -0
  40. package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts +10 -0
  41. package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts.map +1 -0
  42. package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte +19 -0
  43. package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts +10 -0
  44. package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts.map +1 -0
  45. package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte +18 -0
  46. package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts +11 -0
  47. package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts.map +1 -0
  48. package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte +11 -0
  49. package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts +19 -0
  50. package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts.map +1 -0
  51. package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte +18 -0
  52. package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts +8 -0
  53. package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts.map +1 -0
  54. package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte +24 -0
  55. package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts +13 -0
  56. package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts.map +1 -0
  57. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +20 -0
  58. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +11 -0
  59. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -0
  60. package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte +17 -0
  61. package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts +10 -0
  62. package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts.map +1 -0
  63. package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte +25 -0
  64. package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts +10 -0
  65. package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts.map +1 -0
  66. package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte +34 -0
  67. package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts +10 -0
  68. package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts.map +1 -0
  69. package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte +29 -0
  70. package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts +11 -0
  71. package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts.map +1 -0
  72. package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte +33 -0
  73. package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts +9 -0
  74. package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts.map +1 -0
  75. package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte +40 -0
  76. package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts +8 -0
  77. package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts.map +1 -0
  78. package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte +26 -0
  79. package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts +13 -0
  80. package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts.map +1 -0
  81. package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte +39 -0
  82. package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts +17 -0
  83. package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts.map +1 -0
  84. package/dist/svelte/__tests__/fixtures/RegistryInput.svelte +15 -0
  85. package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts +5 -0
  86. package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts.map +1 -0
  87. package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte +29 -0
  88. package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts +14 -0
  89. package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts.map +1 -0
  90. package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte +24 -0
  91. package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts +14 -0
  92. package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts.map +1 -0
  93. package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte +28 -0
  94. package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts +13 -0
  95. package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts.map +1 -0
  96. package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte +14 -0
  97. package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts +19 -0
  98. package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts.map +1 -0
  99. package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte +25 -0
  100. package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts +14 -0
  101. package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts.map +1 -0
  102. package/dist/svelte/__tests__/settings-catalog.test.js +161 -0
  103. package/dist/svelte/components/AdvancedFields.svelte +49 -0
  104. package/dist/svelte/components/AdvancedFields.svelte.d.ts +17 -0
  105. package/dist/svelte/components/AdvancedFields.svelte.d.ts.map +1 -0
  106. package/dist/svelte/components/FieldInput.svelte +159 -0
  107. package/dist/svelte/components/FieldInput.svelte.d.ts +5 -0
  108. package/dist/svelte/components/FieldInput.svelte.d.ts.map +1 -0
  109. package/dist/svelte/components/FieldPolicyControlPanel.svelte +261 -0
  110. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +26 -0
  111. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -0
  112. package/dist/svelte/components/FieldPolicyEditor.svelte +417 -0
  113. package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts +18 -0
  114. package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts.map +1 -0
  115. package/dist/svelte/components/FieldPolicyGearButton.svelte +17 -0
  116. package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts +8 -0
  117. package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -0
  118. package/dist/svelte/components/FieldPolicyGearProvider.svelte +135 -0
  119. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +17 -0
  120. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -0
  121. package/dist/svelte/components/FieldPolicyProvider.svelte +89 -0
  122. package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts +35 -0
  123. package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts.map +1 -0
  124. package/dist/svelte/components/FormHelp.svelte +157 -0
  125. package/dist/svelte/components/FormHelp.svelte.d.ts +15 -0
  126. package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -0
  127. package/dist/svelte/components/ModeSwitch.svelte +47 -0
  128. package/dist/svelte/components/ModeSwitch.svelte.d.ts +10 -0
  129. package/dist/svelte/components/ModeSwitch.svelte.d.ts.map +1 -0
  130. package/dist/svelte/components/ObjectForm.svelte +351 -0
  131. package/dist/svelte/components/ObjectForm.svelte.d.ts +40 -0
  132. package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -0
  133. package/dist/svelte/components/ObjectFormSourceProvider.svelte +18 -0
  134. package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts +11 -0
  135. package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts.map +1 -0
  136. package/dist/svelte/components/PolicyField.svelte +338 -0
  137. package/dist/svelte/components/PolicyField.svelte.d.ts +80 -0
  138. package/dist/svelte/components/PolicyField.svelte.d.ts.map +1 -0
  139. package/dist/svelte/context.svelte.d.ts +54 -0
  140. package/dist/svelte/context.svelte.d.ts.map +1 -0
  141. package/dist/svelte/context.svelte.js +55 -0
  142. package/dist/svelte/data-table.d.ts +17 -0
  143. package/dist/svelte/data-table.d.ts.map +1 -0
  144. package/dist/svelte/data-table.js +19 -0
  145. package/dist/svelte/field-policy-editor.d.ts +81 -0
  146. package/dist/svelte/field-policy-editor.d.ts.map +1 -0
  147. package/dist/svelte/field-policy-editor.js +120 -0
  148. package/dist/svelte/gear-context.svelte.d.ts +14 -0
  149. package/dist/svelte/gear-context.svelte.d.ts.map +1 -0
  150. package/dist/svelte/gear-context.svelte.js +15 -0
  151. package/dist/svelte/index.d.ts +58 -0
  152. package/dist/svelte/index.d.ts.map +1 -0
  153. package/dist/svelte/index.js +38 -0
  154. package/dist/svelte/input-registry.d.ts +21 -0
  155. package/dist/svelte/input-registry.d.ts.map +1 -0
  156. package/dist/svelte/input-registry.js +26 -0
  157. package/dist/svelte/object-form-source-context.svelte.d.ts +4 -0
  158. package/dist/svelte/object-form-source-context.svelte.d.ts.map +1 -0
  159. package/dist/svelte/object-form-source-context.svelte.js +8 -0
  160. package/dist/svelte/object-form-source.svelte.d.ts +18 -0
  161. package/dist/svelte/object-form-source.svelte.d.ts.map +1 -0
  162. package/dist/svelte/object-form-source.svelte.js +99 -0
  163. package/dist/svelte/object-form.d.ts +6 -0
  164. package/dist/svelte/object-form.d.ts.map +1 -0
  165. package/dist/svelte/object-form.js +38 -0
  166. package/dist/svelte/playground/FieldPolicyFormPreview.svelte +212 -0
  167. package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts +4 -0
  168. package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -0
  169. package/dist/svelte/playground/ObjectFormPreview.svelte +129 -0
  170. package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts +4 -0
  171. package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts.map +1 -0
  172. package/dist/svelte/playground.d.ts +20 -0
  173. package/dist/svelte/playground.d.ts.map +1 -0
  174. package/dist/svelte/playground.js +38 -0
  175. package/dist/svelte/settings-catalog.d.ts +54 -0
  176. package/dist/svelte/settings-catalog.d.ts.map +1 -0
  177. package/dist/svelte/settings-catalog.js +116 -0
  178. package/dist/svelte/types.d.ts +101 -0
  179. package/dist/svelte/types.d.ts.map +1 -0
  180. package/dist/svelte/types.js +4 -0
  181. package/dist/types.d.ts +276 -203
  182. package/dist/types.d.ts.map +1 -0
  183. package/dist/types.js.map +1 -1
  184. package/package.json +33 -8
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-fields",
5
- "packageVersion": "0.40.59",
5
+ "packageVersion": "0.40.61",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-fields:FieldPolicyCollection": {
8
8
  "name": "fieldpolicycollection",
@@ -13,6 +13,20 @@
13
13
  "packageName": "@happyvertical/smrt-fields",
14
14
  "fields": {},
15
15
  "methods": {
16
+ "policyAudit": {
17
+ "name": "policyAudit",
18
+ "async": true,
19
+ "parameters": [
20
+ {
21
+ "name": "options",
22
+ "type": "object",
23
+ "optional": true
24
+ }
25
+ ],
26
+ "returnType": "Promise<FieldPolicyAuditSnapshot>",
27
+ "isStatic": false,
28
+ "isPublic": true
29
+ },
16
30
  "getAppRows": {
17
31
  "name": "getAppRows",
18
32
  "async": true,
@@ -78,6 +92,20 @@
78
92
  "returnType": "Promise<FieldPolicyBatchResult>",
79
93
  "isStatic": false,
80
94
  "isPublic": true
95
+ },
96
+ "getEditorState": {
97
+ "name": "getEditorState",
98
+ "async": true,
99
+ "parameters": [
100
+ {
101
+ "name": "options",
102
+ "type": "object",
103
+ "optional": true
104
+ }
105
+ ],
106
+ "returnType": "Promise<FieldPolicyEditorStateResult>",
107
+ "isStatic": false,
108
+ "isPublic": true
81
109
  }
82
110
  },
83
111
  "decoratorConfig": {
@@ -92,13 +120,26 @@
92
120
  "create",
93
121
  "update",
94
122
  "delete",
95
- "resolveBatch"
123
+ "resolveBatch",
124
+ "getEditorState",
125
+ "policyAudit"
96
126
  ],
127
+ "principalContext": true,
97
128
  "routes": {
98
129
  "resolveBatch": {
99
130
  "scope": "collection",
100
131
  "method": "POST",
101
132
  "path": "resolve"
133
+ },
134
+ "getEditorState": {
135
+ "scope": "collection",
136
+ "method": "POST",
137
+ "path": "editor-state"
138
+ },
139
+ "policyAudit": {
140
+ "scope": "collection",
141
+ "method": "POST",
142
+ "path": "policy-audit"
102
143
  }
103
144
  }
104
145
  },
@@ -327,7 +368,8 @@
327
368
  "create",
328
369
  "update",
329
370
  "delete"
330
- ]
371
+ ],
372
+ "principalContext": true
331
373
  },
332
374
  "cli": {
333
375
  "include": [
@@ -0,0 +1,163 @@
1
+ import { SmrtObject } from '@happyvertical/smrt-core';
2
+ import { FieldPolicyOptions, FieldPolicyScopeType, FieldPolicyVisibility } from '../types.js';
3
+ /**
4
+ * Sparse, layered field policy override (epic #2045, issue #2047).
5
+ *
6
+ * One row personalizes a subset of `{defaultValue, visibility, help, label,
7
+ * displayOrder, locked}` for a single `(objectRef, fieldName)` at one scope
8
+ * tier. A NULL column means "inherit from the lower layer" (code seed → app →
9
+ * tenant → user), so resetting a customization is a row DELETE — later
10
+ * lower-layer changes then flow through (sparse-delta rationale, #1770).
11
+ *
12
+ * Rows are validated at write time against the live `ObjectRegistry` (the
13
+ * manifest is the definition registry): unknown objects/fields are rejected,
14
+ * defaults are type-checked, and the security rail (`sensitive` /
15
+ * `readPermission` / `transient`) refuses stored defaults outright.
16
+ */
17
+ export declare class FieldPolicy extends SmrtObject {
18
+ /** Qualified class name of the target object (`@package/name:ClassName`). */
19
+ objectRef: string;
20
+ /** Field name on the target object (validated against the registry). */
21
+ fieldName: string;
22
+ /** Scope tier this row belongs to ('app' | 'tenant' | 'user'). */
23
+ scopeType: FieldPolicyScopeType;
24
+ /** Owning tenant for tenant-scope rows; NULL otherwise (native UUID on PG). */
25
+ tenantId: string | null;
26
+ /** Owning user for user-scope rows; NULL otherwise. */
27
+ userId: string | null;
28
+ /**
29
+ * Computed uniqueness key (`userId ?? tenantId ?? '__app__'`), set in
30
+ * `save()`. Used ONLY by `conflictColumns` so the unique index stays total
31
+ * while `tenantId`/`userId` are nullable — mirrors `PromptOverride.context`.
32
+ * Never read it for scoping logic; `scopeType` + the typed columns own that.
33
+ */
34
+ scopeKey: string;
35
+ /** JSON-encoded default value; NULL = inherit. JSON `null` = "default to null". */
36
+ defaultValue: string | null;
37
+ /** Visibility override ('basic' | 'advanced' | 'hidden'); NULL = inherit. */
38
+ visibility: FieldPolicyVisibility | null;
39
+ /** Help text override; NULL = inherit (code seed: field description). */
40
+ help: string | null;
41
+ /** Label override; NULL = inherit (consumers derive from the field name). */
42
+ label: string | null;
43
+ /**
44
+ * Sort-order override; NULL = inherit (code seed: `ui.order`). Named
45
+ * `displayOrder` because a column literally named `order` is an SQL keyword
46
+ * the runtime INSERT path does not quote; resolved output exposes `order`.
47
+ */
48
+ displayOrder: number | null;
49
+ /**
50
+ * Org lock (app/tenant rows only): when the effective lock is true, the
51
+ * user tier may not override this field. NULL = inherit (code seed:
52
+ * `ui.locked`); org rows may set `false` to explicitly unlock.
53
+ */
54
+ locked: boolean | null;
55
+ /** Audit attribution for #2050 ("who changed what"); not validated. */
56
+ updatedBy: string | null;
57
+ constructor(options?: FieldPolicyOptions);
58
+ /** Parse the stored JSON default. `undefined` = no stored default (inherit). */
59
+ getDefaultValue(): unknown;
60
+ /** Serialize a default value; `undefined` clears the override (inherit). */
61
+ setDefaultValue(value: unknown): void;
62
+ save(): Promise<this>;
63
+ private saveAfterIdentityChange;
64
+ private saveAfterIdentityChangeInTransaction;
65
+ private saveAfterIdentityChangeWithDeferredDelete;
66
+ delete(): Promise<void>;
67
+ private validateFieldPolicy;
68
+ /**
69
+ * Effective org-tier (code → app → tenant hierarchy) policy for this row's
70
+ * field, computed by the RESOLVER so write-time checks share the one
71
+ * precedence implementation — including ancestor-tenant cascades via the
72
+ * default hierarchy loader. The chain tenant is the row's own tenant for
73
+ * tenant-scope rows, the ambient context tenant for user-scope rows, and
74
+ * none for app-scope rows (their only lower layer is the code seed).
75
+ *
76
+ * During an update, the persisted row is excluded from the lower-layer
77
+ * projection. That makes a clear-default + required-field demotion validate
78
+ * the post-write policy rather than accidentally counting the row being
79
+ * replaced.
80
+ */
81
+ private resolveOrgTierFieldPolicy;
82
+ /**
83
+ * Exactly-one-owner scope shape: app rows carry neither id, tenant rows
84
+ * carry only `tenantId`, user rows carry only `userId` (the user tier is
85
+ * keyed by user alone so preferences follow the user across tenants).
86
+ */
87
+ private validateScopeConsistency;
88
+ /** `userId ?? tenantId ?? '__app__'` — the `conflictColumns` scope key. */
89
+ private computeScopeKey;
90
+ /**
91
+ * Derive a scope-owner column the transport could not carry (#2047).
92
+ *
93
+ * Core's mass-assignment guard treats `tenantId` as server-managed and
94
+ * strips it from EVERY generated create/update body, and `FieldPolicy` is
95
+ * deliberately not `@TenantScoped`, so the tenancy interceptor never
96
+ * repopulates it. A `POST {scopeType:'tenant', tenantId}` therefore always
97
+ * reached `validateScopeConsistency` with `tenantId === null`, making the
98
+ * org tier write-dead over every generated surface — the model, as the
99
+ * single validation authority for these rows, fills it in instead.
100
+ *
101
+ * This grants nothing: `assertScopeOwnedByAmbientContext` already requires
102
+ * a non-bypass caller's tenant/user rows to name exactly the ambient
103
+ * tenant/user, so the derived value is the ONLY value that could ever have
104
+ * been accepted. An explicit value is never overwritten (a super-admin
105
+ * bypass caller keeps naming other scopes), and with neither tenancy nor
106
+ * authenticated-session context nothing is derived — scope-shape validation
107
+ * rejects the row as before.
108
+ * Consequence: a bypass caller writing ANOTHER tenant's row must pass
109
+ * `tenantId` through a server-side model call, because the generated routes
110
+ * still strip it.
111
+ */
112
+ private attributeScopeToAmbientContext;
113
+ /**
114
+ * Fail-closed write boundary against the ambient tenant context, applied to
115
+ * the NEW scope on save and to the PERSISTED scope on save/delete of an
116
+ * existing row.
117
+ *
118
+ * With NO ambient identity at all (neither tenant ALS nor authenticated
119
+ * session-permission ALS), only app-scope rows are accepted: tenant- and
120
+ * user-scope rows are unattributable without a context, so they are
121
+ * rejected outright rather than allowed by default. Inside a non-bypass
122
+ * context, a caller may only touch rows for its own tenant, and user rows
123
+ * only for its own user id — a context that carries NO user id may not
124
+ * touch the user tier at all.
125
+ * App-wide rows then require super-admin bypass (or a context-less/system
126
+ * caller).
127
+ *
128
+ * Package rule: a missing identity component DENIES, it never skips.
129
+ */
130
+ private assertScopeOwnedByAmbientContext;
131
+ private validateTenantContextBoundary;
132
+ /**
133
+ * Permission boundary for policy mutations. Scope ownership is checked
134
+ * separately (and first) because permission never grants cross-tenant or
135
+ * cross-user access. Existing rows are checked against their persisted
136
+ * scope before an identity-changing save can mutate or delete them.
137
+ */
138
+ private assertScopePermission;
139
+ private validateDefaultAgainstSecurityRail;
140
+ private parseDefaultValueOrThrow;
141
+ /**
142
+ * The persisted row this save/delete would replace, looked up by primary
143
+ * key and — when that misses — by the NATURAL key.
144
+ *
145
+ * The natural-key fallback is load-bearing for authorization: every
146
+ * generated create arrives with a freshly minted UUID, so a primary-key
147
+ * lookup always misses, yet the `conflictColumns` upsert still overwrites
148
+ * whatever row already occupies `(objectRef, fieldName, scopeType,
149
+ * scopeKey)`. Authorizing on the primary key alone therefore skipped the
150
+ * persisted-scope guard on exactly the path that can replace an existing
151
+ * row's contents.
152
+ */
153
+ private getPersistedIdentity;
154
+ private getPersistedRowById;
155
+ /**
156
+ * The row occupying this row's `conflictColumns` tuple, if any. Column
157
+ * names are the physical snake_case ones: this reads the driver directly
158
+ * rather than through a collection (the model has no collection handle).
159
+ */
160
+ private getPersistedRowByNaturalKey;
161
+ private normalizeDefaultValueForPersistence;
162
+ }
163
+ //# sourceMappingURL=FieldPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldPolicy.d.ts","sourceRoot":"","sources":["../../src/models/FieldPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEX,MAAM,0BAA0B,CAAC;AA4BlC,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAE3B,MAAM,aAAa,CAAC;AA6GrB;;;;;;;;;;;;;GAaG;AASH,qBAgBa,WAAY,SAAQ,UAAU;IACzC,6EAA6E;IAE7E,SAAS,EAAE,MAAM,CAAM;IAEvB,wEAAwE;IAExE,SAAS,EAAE,MAAM,CAAM;IAEvB,kEAAkE;IAElE,SAAS,EAAE,oBAAoB,CAAS;IAExC,+EAA+E;IAE/E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B,uDAAuD;IAEvD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE7B;;;;;OAKG;IAEH,QAAQ,EAAE,MAAM,CAAM;IAEtB,mFAAmF;IAEnF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC,6EAA6E;IAE7E,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAAQ;IAEhD,yEAAyE;IAEzE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE3B,6EAA6E;IAE7E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE5B;;;;OAIG;IAEH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC;;;;OAIG;IAEH,MAAM,EAAE,OAAO,GAAG,IAAI,CAAQ;IAE9B,uEAAuE;IAEvE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAEpB,OAAO,GAAE,kBAAuB;IAsC5C,gFAAgF;IAChF,eAAe,IAAI,OAAO;IAW1B,4EAA4E;IAC5E,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAkDtB,uBAAuB;YAQvB,oCAAoC;YA+BpC,yCAAyC;IA6BxC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;YAiBxB,mBAAmB;IA8HjC;;;;;;;;;;;;OAYG;YACW,yBAAyB;IAuBvC;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAwBhC,2EAA2E;IAC3E,OAAO,CAAC,eAAe;IAIvB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,CAAC,8BAA8B;IAatC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,gCAAgC;IA0ExC,OAAO,CAAC,6BAA6B;IAWrC;;;;;OAKG;YACW,qBAAqB;IAoBnC,OAAO,CAAC,kCAAkC;IAwB1C,OAAO,CAAC,wBAAwB;IAkBhC;;;;;;;;;;;OAWG;YACW,oBAAoB;YAkCpB,mBAAmB;IAQjC;;;;OAIG;YACW,2BAA2B;IAgBzC,OAAO,CAAC,mCAAmC;CAiB5C"}
@@ -0,0 +1,9 @@
1
+ import { PermissionDefinition } from '@happyvertical/smrt-users';
2
+ /** Authorizes app- and tenant-scope field-policy administration. */
3
+ export declare const MANAGE_FIELD_POLICY_PERMISSION = "fields.policy.manage";
4
+ /** Authorizes a principal to maintain their own user-scope field policy. */
5
+ export declare const PERSONALIZE_FIELD_POLICY_PERMISSION = "fields.policy.personalize";
6
+ export declare const FIELD_POLICY_PERMISSION_DEFINITIONS: PermissionDefinition[];
7
+ /** Register field-policy permissions once when smrt-fields is loaded. */
8
+ export declare function ensureFieldPolicyPermissionsRegistered(): void;
9
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,2BAA2B,CAAC;AAEnC,oEAAoE;AACpE,eAAO,MAAM,8BAA8B,yBAAyB,CAAC;AAErE,4EAA4E;AAC5E,eAAO,MAAM,mCAAmC,8BAA8B,CAAC;AAE/E,eAAO,MAAM,mCAAmC,EAAE,oBAAoB,EAarE,CAAC;AAIF,yEAAyE;AACzE,wBAAgB,sCAAsC,IAAI,IAAI,CAM7D"}
@@ -0,0 +1,2 @@
1
+ export { default } from './svelte/playground.js';
2
+ //# sourceMappingURL=playground.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../src/playground.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,39 @@
1
+ //#region src/svelte/playground.ts
2
+ var loadFieldPolicyForm = () => import("./svelte/playground/FieldPolicyFormPreview.svelte");
3
+ var loadObjectForm = () => import("./svelte/playground/ObjectFormPreview.svelte");
4
+ var playground_default = {
5
+ packageName: "@happyvertical/smrt-fields",
6
+ displayName: "Field Policies",
7
+ description: "Headless form primitives: FieldPolicyProvider, PolicyField, basic/advanced modes, and manifest-sourced help.",
8
+ entries: [{
9
+ id: "policy-form",
10
+ title: "Policy-Driven Form",
11
+ description: "A hand-written product form adopting PolicyField on a subset of fields — visibility by mode, default prefill, help hints, and the form-level glossary.",
12
+ loadComponent: loadFieldPolicyForm,
13
+ order: 1,
14
+ tags: [
15
+ "forms",
16
+ "fields",
17
+ "policy",
18
+ "defaults"
19
+ ],
20
+ modes: { mock: { label: "Mock" } }
21
+ }, {
22
+ id: "object-form",
23
+ title: "Generated ObjectForm",
24
+ description: "A reusable create/edit form from generated web fields and resolved policy, including advanced groups and a per-field custom renderer.",
25
+ loadComponent: loadObjectForm,
26
+ order: 2,
27
+ tags: [
28
+ "forms",
29
+ "fields",
30
+ "object-form",
31
+ "registry"
32
+ ],
33
+ modes: { mock: { label: "Mock" } }
34
+ }]
35
+ };
36
+ //#endregion
37
+ export { playground_default as default };
38
+
39
+ //# sourceMappingURL=playground.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.js","names":[],"sources":["../src/svelte/playground.ts"],"sourcesContent":["/**\n * Field-policy playground previews (#2048).\n *\n * Registered with the shared playground host via the auto-discovered\n * \"packages/STAR/src/svelte/playground.ts\" convention (smrt-playground vite\n * plugin).\n */\nconst loadFieldPolicyForm = () =>\n import('./playground/FieldPolicyFormPreview.svelte');\nconst loadObjectForm = () => import('./playground/ObjectFormPreview.svelte');\n\nexport default {\n packageName: '@happyvertical/smrt-fields',\n displayName: 'Field Policies',\n description:\n 'Headless form primitives: FieldPolicyProvider, PolicyField, basic/advanced modes, and manifest-sourced help.',\n entries: [\n {\n id: 'policy-form',\n title: 'Policy-Driven Form',\n description:\n 'A hand-written product form adopting PolicyField on a subset of fields — visibility by mode, default prefill, help hints, and the form-level glossary.',\n loadComponent: loadFieldPolicyForm,\n order: 1,\n tags: ['forms', 'fields', 'policy', 'defaults'],\n modes: {\n mock: {\n label: 'Mock',\n },\n },\n },\n {\n id: 'object-form',\n title: 'Generated ObjectForm',\n description:\n 'A reusable create/edit form from generated web fields and resolved policy, including advanced groups and a per-field custom renderer.',\n loadComponent: loadObjectForm,\n order: 2,\n tags: ['forms', 'fields', 'object-form', 'registry'],\n modes: { mock: { label: 'Mock' } },\n },\n ],\n};\n"],"mappings":";AAOA,IAAM,4BACJ,OAAO;AACT,IAAM,uBAAuB,OAAO;AAEpC,IAAA,qBAAe;CACb,aAAa;CACb,aAAa;CACb,aACE;CACF,SAAS,CACP;EACE,IAAI;EACJ,OAAO;EACP,aACE;EACF,eAAe;EACf,OAAO;EACP,MAAM;GAAC;GAAS;GAAU;GAAU;EAAU;EAC9C,OAAO,EACL,MAAM,EACJ,OAAO,OACT,EACF;CACF,GACA;EACE,IAAI;EACJ,OAAO;EACP,aACE;EACF,eAAe;EACf,OAAO;EACP,MAAM;GAAC;GAAS;GAAU;GAAe;EAAU;EACnD,OAAO,EAAE,MAAM,EAAE,OAAO,OAAO,EAAE;CACnC,CACF;AACF"}
@@ -0,0 +1,73 @@
1
+ import { SmrtClassOptions } from '@happyvertical/smrt-core';
2
+ import { FieldPolicyCollection } from './collections/FieldPolicyCollection.js';
3
+ import { FieldPolicyAuditSnapshot } from './types.js';
4
+ export interface FieldPolicyCatalogField {
5
+ type: 'text' | 'integer' | 'decimal' | 'boolean' | 'datetime' | 'json' | 'foreignKey' | 'crossPackageRef';
6
+ required?: boolean;
7
+ default?: unknown;
8
+ description?: string;
9
+ ui?: {
10
+ basic?: boolean;
11
+ group?: string;
12
+ order?: number;
13
+ locked?: boolean;
14
+ };
15
+ }
16
+ export interface FieldPolicySummaryItem {
17
+ id: string;
18
+ label: string;
19
+ description?: string;
20
+ eyebrow?: string;
21
+ status?: string;
22
+ objectRef: string;
23
+ fieldName: string;
24
+ className: string;
25
+ packageName: string;
26
+ }
27
+ export interface FieldPolicyDetailItem extends FieldPolicySummaryItem {
28
+ fields: Record<string, FieldPolicyCatalogField>;
29
+ }
30
+ /** Structural SettingsCatalogPage mirror; `SettingsCatalog` accepts it directly. */
31
+ export interface FieldPolicySettingsCatalogPage {
32
+ items: FieldPolicySummaryItem[];
33
+ selected: FieldPolicyDetailItem | null;
34
+ query: string;
35
+ page: number;
36
+ pageSize: number;
37
+ total: number;
38
+ }
39
+ export interface FieldPolicyCatalogObjectSummary {
40
+ objectRef: string;
41
+ className: string;
42
+ packageName: string;
43
+ fieldCount: number;
44
+ }
45
+ export interface FieldPolicySettingsCatalogData {
46
+ page: FieldPolicySettingsCatalogPage;
47
+ audit: FieldPolicyAuditSnapshot;
48
+ objects: FieldPolicyCatalogObjectSummary[];
49
+ packages: string[];
50
+ filters: {
51
+ packageFilter: string | null;
52
+ objectFilter: string | null;
53
+ customizedOnly: boolean;
54
+ };
55
+ }
56
+ export interface FieldPolicySettingsCatalogQuery {
57
+ query?: string | null;
58
+ page?: number | null;
59
+ pageSize?: number | null;
60
+ selectedId?: string | null;
61
+ packageFilter?: string | null;
62
+ objectFilter?: string | null;
63
+ customizedOnly?: boolean;
64
+ }
65
+ export interface BuildFieldPolicySettingsCatalogOptions extends FieldPolicySettingsCatalogQuery {
66
+ db?: SmrtClassOptions['db'];
67
+ collection?: FieldPolicyCollection;
68
+ objectRefs?: string[];
69
+ }
70
+ export declare function fieldPolicyCatalogItemId(objectRef: string, fieldName: string): string;
71
+ export declare function parseFieldPolicyCatalogQuery(params: URLSearchParams): FieldPolicySettingsCatalogQuery;
72
+ export declare function buildFieldPolicySettingsCatalog(options: BuildFieldPolicySettingsCatalogOptions): Promise<FieldPolicySettingsCatalogData>;
73
+ //# sourceMappingURL=settings-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-catalog.d.ts","sourceRoot":"","sources":["../src/settings-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,OAAO,EACL,qBAAqB,EAEtB,MAAM,wCAAwC,CAAC;AAQhD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AA4B3D,MAAM,WAAW,uBAAuB;IACtC,IAAI,EACA,MAAM,GACN,SAAS,GACT,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,YAAY,GACZ,iBAAiB,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC5E;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAsB,SAAQ,sBAAsB;IACnE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CACjD;AAED,oFAAoF;AACpF,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,sBAAsB,EAAE,CAAC;IAChC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,8BAA8B,CAAC;IACrC,KAAK,EAAE,wBAAwB,CAAC;IAChC,OAAO,EAAE,+BAA+B,EAAE,CAAC;IAC3C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE;QACP,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,+BAA+B;IAC9C,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,sCACf,SAAQ,+BAA+B;IACvC,EAAE,CAAC,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAC5B,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AASD,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,eAAe,GACtB,+BAA+B,CAUjC;AAED,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,sCAAsC,GAC9C,OAAO,CAAC,8BAA8B,CAAC,CAgHzC"}
@@ -2,27 +2,35 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedAt": "1970-01-01T00:00:00.000Z",
4
4
  "packageName": "@happyvertical/smrt-fields",
5
- "packageVersion": "0.40.59",
5
+ "packageVersion": "0.40.61",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "6ecb2cee7792ee1e7d091c4a1c79f331e89bc148a324fb3123b4f89bcb59f54e",
10
- "packageJson": "a347821b7feaf36c9e748e86eddb3abcf4eab892646e1e700de9154d215ec4b9",
11
- "agents": "efe605cdabdde966a22fc43d94af0275bfb1ce5e2027b52809c69837e3d5dc0c"
9
+ "manifest": "7bda54a2c54dad0a830732576d2880a42cb75e291729b23a1604243775a735c7",
10
+ "packageJson": "6a20590d3e6f0e8aa1f5e5adb1e862ac103f10ef4bda818a6212e3625a17d1f8",
11
+ "agents": "6e3f776e4a585c5d21f0d9168687bf2eafefe17508c69a1c8870e9034ac2ec72"
12
12
  },
13
13
  "exports": [
14
14
  ".",
15
15
  "./manifest",
16
- "./manifest.json"
16
+ "./manifest.json",
17
+ "./playground",
18
+ "./svelte"
17
19
  ],
18
20
  "dependencies": {
19
21
  "@happyvertical/smrt-core": "workspace:*",
20
22
  "@happyvertical/smrt-tenancy": "workspace:*",
23
+ "@happyvertical/smrt-ui": "workspace:*",
24
+ "@happyvertical/smrt-users": "workspace:*",
21
25
  "@happyvertical/sql": "catalog:",
22
26
  "@happyvertical/smrt-cli": "workspace:*",
23
- "@happyvertical/smrt-users": "workspace:*",
24
27
  "@happyvertical/smrt-vitest": "workspace:*",
28
+ "@sveltejs/package": "^2.5.8",
29
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
30
+ "@testing-library/svelte": "^5.4.2",
25
31
  "@types/node": "24.13.2",
32
+ "svelte": "^5.56.4",
33
+ "svelte-check": "^4.7.1",
26
34
  "typescript": "5.9.3",
27
35
  "vite": "8.1.4",
28
36
  "vitest": "4.1.10"
@@ -31,6 +39,7 @@
31
39
  "@happyvertical/smrt-cli",
32
40
  "@happyvertical/smrt-core",
33
41
  "@happyvertical/smrt-tenancy",
42
+ "@happyvertical/smrt-ui",
34
43
  "@happyvertical/smrt-users",
35
44
  "@happyvertical/smrt-vitest"
36
45
  ],
@@ -51,8 +60,10 @@
51
60
  "relationships": [],
52
61
  "methods": [
53
62
  "getAppRows",
63
+ "getEditorState",
54
64
  "getTenantRows",
55
65
  "getUserRows",
66
+ "policyAudit",
56
67
  "resolveBatch"
57
68
  ],
58
69
  "surfaces": [
@@ -87,6 +98,22 @@
87
98
  "objectName": "@happyvertical/smrt-fields:FieldPolicyCollection",
88
99
  "path": "/fieldpolicies/resolveBatch",
89
100
  "method": "POST"
101
+ },
102
+ {
103
+ "kind": "api",
104
+ "name": "fieldpolicies.getEditorState",
105
+ "operation": "getEditorState",
106
+ "objectName": "@happyvertical/smrt-fields:FieldPolicyCollection",
107
+ "path": "/fieldpolicies/getEditorState",
108
+ "method": "POST"
109
+ },
110
+ {
111
+ "kind": "api",
112
+ "name": "fieldpolicies.policyAudit",
113
+ "operation": "policyAudit",
114
+ "objectName": "@happyvertical/smrt-fields:FieldPolicyCollection",
115
+ "path": "/fieldpolicies/policyAudit",
116
+ "method": "POST"
90
117
  }
91
118
  ],
92
119
  "relationshipFeatures": [
@@ -291,6 +318,22 @@
291
318
  "path": "/fieldpolicies/resolveBatch",
292
319
  "method": "POST"
293
320
  },
321
+ {
322
+ "kind": "api",
323
+ "name": "fieldpolicies.getEditorState",
324
+ "operation": "getEditorState",
325
+ "objectName": "@happyvertical/smrt-fields:FieldPolicyCollection",
326
+ "path": "/fieldpolicies/getEditorState",
327
+ "method": "POST"
328
+ },
329
+ {
330
+ "kind": "api",
331
+ "name": "fieldpolicies.policyAudit",
332
+ "operation": "policyAudit",
333
+ "objectName": "@happyvertical/smrt-fields:FieldPolicyCollection",
334
+ "path": "/fieldpolicies/policyAudit",
335
+ "method": "POST"
336
+ },
294
337
  {
295
338
  "kind": "api",
296
339
  "name": "fieldpolicies.create",
@@ -343,5 +386,5 @@
343
386
  "polymorphicAssociations": 0,
344
387
  "uuidColumns": 5
345
388
  },
346
- "agentDoc": "# smrt-fields\n\nLayered field policy store and resolution engine (epic #2045). Personalizes\nper-field `{defaultValue, visibility, help, label, order, locked}` for any\n`@smrt()` object at app, tenant, and user scope over the code seed.\n\n## Core pieces\n\n- `FieldPolicy` (`_smrt_field_policies` table) — sparse override rows keyed\n `(objectRef, fieldName, scopeType, scopeKey)`; NULL column = inherit from\n the lower layer, reset = row DELETE (later lower-layer changes flow through)\n- `FieldPolicyCollection` — write surface plus the `resolveBatch` custom\n collection-scoped action (`POST /<collection>/resolve`). NO generated\n surface exposes read verbs: API `list`/`get` on this non-tenant-scoped\n model would enumerate every tenant's and user's rows; the model's CLI is\n writes-only (the generated CLI invokes over HTTP, and the cli↔api\n coherence gate rejects CLI entries without API routes); the runtime\n CLI/MCP surfaces are closed by the collection config (ContentContributions\n precedent). Reads go through `resolveBatch` (context-scoped) or the\n server-side resolver. Keep the api include lists in lockstep: a decorated\n collection's config is the RUNTIME registry authority for its item class,\n while build-time generation reads each manifest object's own config. Both\n transports dispatch the action: generated SvelteKit routes natively, and\n core's runtime `APIGenerator` via its decorator-route dispatch\n (single-segment collection-scoped paths; multi-segment custom paths remain\n SvelteKit-only).\n- `resolveFieldPolicy(objectRef, { tenantId, userId, db })` — merged policy\n- `resolveFieldPolicyExplained(...)` — merged policy plus ordered per-layer\n contributions (code/app/tenant/user) for gear/admin UIs (#2049/#2050)\n\n## Resolution layers (priority low → high)\n\n1. Code seed — manifest field defaults, `description` as help, `_meta.ui`\n hints (#2046: `basic`/`group`/`order`/`locked`; cold-start rule: no\n `basic` markers ⇒ everything basic, any marker ⇒ unmarked fields advanced)\n2. App rows — `scopeType: 'app'`, `tenantId`/`userId` null\n3. Tenant rows — hierarchy walk root → leaf via an injected\n `tenantHierarchyLoader` (smrt-features shape); the default loader\n dynamic-imports `@happyvertical/smrt-users` (missing-package failures —\n `ERR_MODULE_NOT_FOUND` / \"Cannot find package\" across the cause chain —\n fall back to a flat single-tenant chain). A node that breaks permission\n inheritance discards every earlier tenant contribution (chain-structural),\n so only the suffix from the LAST break participates — in merging AND in\n the explained layers, which therefore replay to the merged result\n4. User rows — keyed by `userId` alone (preferences follow the user); both\n defaults AND visibility resolve through this tier\n\n## Invariants\n\n- **Scope shape**: app ⇒ `tenantId`+`userId` null; tenant ⇒ only `tenantId`;\n user ⇒ only `userId`. `scopeKey` (`userId ?? tenantId ?? '__app__'`) exists\n ONLY to keep the `conflictColumns` unique index total — never read it for\n scoping logic.\n- **Manifest as definition registry**: writes validate against the live\n `ObjectRegistry` (never checked-in manifest.json artifacts): unknown\n objectRef/fieldName rejected; defaults type-checked against the field type;\n system fields, relationship pseudo-fields, and STI meta storage fields are\n not policy-addressable (the resolver excludes them, so rows would silently\n never apply).\n- **Security rail**: defaults are refused on `transient`, `sensitive`, and\n `readPermission`-gated fields (both top-level and `_meta` flags checked).\n Reference-field (`foreignKey`/`crossPackageRef`) defaults must be UUID\n strings unless the field declares `idType: 'text'` — the columns are\n native UUID on PostgreSQL/DuckDB and a non-UUID default would fail at\n insert time. `resolveBatch` responses omit sensitive/read-permission-gated\n fields for every caller (fail closed) and transient fields\n (client-emission parity).\n- **Required-field invariant**: demoting a required field to\n advanced/hidden requires a usable resolved default (not null/empty) —\n enforced at write time AND re-enforced at resolution (a required field with\n no usable default always resolves `basic`, flagged `visibilityForced`).\n- **Org lock**: `locked` may be set on app/tenant rows only; while the\n code/app/tenant tiers resolve locked, user-scope writes are rejected and\n existing user rows are skipped at resolution.\n- **Write-time org checks reuse the resolver**: the required-demotion default\n and the user-write lock are computed by `resolveFieldPolicy` over the org\n tiers (default hierarchy loader), so cascading ancestor-tenant defaults and\n locks are honored at save time too. On updates the resolver sees the row's\n persisted version (no self-exclusion), so the resolver-side safety net\n remains the authoritative enforcement at read time.\n- **Isolation — a MISSING identity component DENIES, it never skips.** This\n is the package rule; both the write guard\n (`FieldPolicy.assertScopeOwnedByAmbientContext`) and the read guard\n (`assertResolutionAllowedInContext`) obey it. A non-bypass tenant context\n may only resolve/write its own tenant, and the user tier only for its own\n user id — a context carrying permissions but NO `userId` (no `resolveUserId`\n hook configured: API-key auth, service principals, background jobs, a bare\n `withTenant({ tenantId })`) may not touch the user tier at all. Skipping\n that check instead of denying it was a live ownership bypass: user rows are\n `tenantId: null` by design, so nothing else contains such a write. App-scope\n writes inside a tenant context require super-admin bypass. With NO ambient\n identity at all (tenancy ALS never entered), only app-scope writes/deletes\n are accepted; context-LESS *reads* stay allowed because\n `resolveFieldPolicy` is a trusted server-side API. Residual: ALS-less\n deployments can still write APP rows with any authenticated principal until\n the #2049 permission layer adds `fields:policy:manage`. `save()`/`delete()`\n on an existing row additionally authorize against the row's PERSISTED\n scope, looked up by primary key AND — because a generated create always\n mints a fresh UUID while the `conflictColumns` upsert still replaces the\n occupant — by NATURAL key. `resolveBatch` takes identity exclusively from\n the ambient context; the request body cannot select another tenant or user.\n- **Scope attribution**: inside an ambient context the model DERIVES a\n missing `tenantId` (tenant rows) or `userId` (user rows) from that context,\n and always stamps `updatedBy` from it. Core's mass-assignment guard treats\n `tenantId` as server-managed and strips it from every generated write body\n while `FieldPolicy` is deliberately not `@TenantScoped`, so without this the\n org tier is write-dead over REST/SvelteKit (scope-shape validation throws).\n Deriving grants nothing — the ownership guard already pinned the value to\n the ambient one. An explicit value is never overwritten, so a super-admin\n bypass caller writing ANOTHER tenant's row must go through a server-side\n model call; the generated routes still strip it.\n\n## Caching and invalidation\n\n- Resolver results cached per\n `(dbNamespace, objectRef, tenantId, userId, hierarchyLoader)` with a 30s TTL\n (`cache.ts` mirrors smrt-prompts' `getDbNamespace`). The loader identity is\n part of the key because an injected `tenantHierarchyLoader` yields a\n different ancestor chain — and so different defaults/locks — for the same\n `(db, objectRef, tenant, user)`; it goes LAST so the `(db, objectRef)`\n prefix scan still invalidates every loader's entries.\n- `FieldPolicy.save()`/`.delete()` invalidate ALL entries for the row's\n `(db, objectRef)` — coarser than prompts because tenant hierarchy makes a\n parent-tenant row affect every descendant's resolution.\n- `_smrt_field_policies` rows do NOT ride the client change feed: core's\n change-feed writer deliberately skips `_smrt_`-prefixed system tables, and\n the emit side is private. Live client invalidation is a core-side decision;\n do not add custom push/emit paths here.\n\n## Gotchas\n\n- Defaults have TWO explicit constructor channels, never sniffed:\n `defaultValue` is ALREADY JSON-encoded (the wire contract — generated write\n routes hand the request body straight to the constructor, and #2049's gear\n posts `JSON.stringify(draft.defaultValue)`), while `defaultValueRaw` is a\n plain value that is always serialized, strings included. Passing both throws.\n One option cannot carry both meanings: `'\"TBD\"'` and `'TBD'` are\n indistinguishable, so `{ defaultValue: 'Net 30' }` is a parse error that\n names `defaultValueRaw` in its message. `setDefaultValue()` is the\n method-level plain channel.\n- The sort-order column is `displayOrder` (resolved output exposes `order`):\n a column literally named `order` is an SQL keyword the runtime INSERT path\n does not quote.\n- `FieldPolicy` deliberately has NO class-level `@TenantScoped` (the\n prompts/features precedent): resolution legitimately reads app rows and\n ancestor-tenant rows, which the tenancy interceptor would block. Isolation\n is enforced at the resolver/save boundaries instead.\n- Identity changes (objectRef/fieldName/scope) on a persisted row go through\n delete-then-insert (transactional when the driver supports it) because the\n natural-key upsert would otherwise collide with the primary key.\n- `FieldPolicyCollection` MUST repeat FieldPolicy's `conflictColumns`. A\n decorated collection emits its OWN manifest schema for the item's table, and\n without the natural key that schema falls back to SmrtObject's default unique\n `(slug, context)` index; manifest-driven migrations aggregate both onto\n `_smrt_field_policies`, where the stray index rejects legitimate layered rows\n (all policy rows have NULL slug/context). The runtime registry cannot catch\n this — `getAllSchemas()` is keyed by TABLE name, so the two schemas collapse\n into one entry. Pinned against the generated manifest in\n `generated-surfaces.test.ts`.\n- `withSystemContext()` does NOT unlock tenant/user-scope writes:\n `getCurrentTenant()` returns undefined inside it, so the context-absent rule\n rejects those tiers before any bypass check. Seeds and migrations that must\n write org/user rows use a `superAdminBypass` context instead.\n- The MODEL's `cli`/`mcp` decorator config is dead at runtime: the registry\n re-registers the item slot with the COLLECTION's config wholesale, so\n `FieldPolicyCollection`'s `cli: false, mcp: false` is what actually closes\n those surfaces. Keep both in lockstep anyway — build-time generation reads\n the model's own config. Related: `ObjectRegistry.getTableName(\n 'FieldPolicyCollection')` resolves to the UNPREFIXED collection fallback\n `field_policies`; persistence uses the item class\n (`_smrt_field_policies`), and both are pinned in\n `generated-surfaces.test.ts`.\n\n## Related\n\n- `@happyvertical/smrt-prompts` / `smrt-languages` / `smrt-features` — the\n same architecture family (override rows + layered resolver + TTL cache)\n- Core `FieldUIHints` (#2046) — the `@field({ ui })` code seed this package\n resolves over\n"
389
+ "agentDoc": "# smrt-fields\n\nLayered field policy store and resolution engine (epic #2045). Personalizes\nper-field `{defaultValue, visibility, help, label, order, locked}` for any\n`@smrt()` object at app, tenant, and user scope over the code seed.\n\n## Core pieces\n\n- `FieldPolicy` (`_smrt_field_policies` table) — sparse override rows keyed\n `(objectRef, fieldName, scopeType, scopeKey)`; NULL column = inherit from\n the lower layer, reset = row DELETE (later lower-layer changes flow through)\n- `FieldPolicyCollection` — write surface plus the `resolveBatch` custom\n collection-scoped action (`POST /<collection>/resolve`). NO generated\n surface exposes read verbs: API `list`/`get` on this non-tenant-scoped\n model would enumerate every tenant's and user's rows; the model's CLI is\n writes-only (the generated CLI invokes over HTTP, and the cli↔api\n coherence gate rejects CLI entries without API routes); the runtime\n CLI/MCP surfaces are closed by the collection config (ContentContributions\n precedent). Reads go through `resolveBatch` (context-scoped) or the\n server-side resolver. Keep the api include lists in lockstep: a decorated\n collection's config is the RUNTIME registry authority for its item class,\n while build-time generation reads each manifest object's own config. Both\n transports dispatch the action: generated SvelteKit routes natively, and\n core's runtime `APIGenerator` via its decorator-route dispatch\n (single-segment collection-scoped paths; multi-segment custom paths remain\n SvelteKit-only).\n- `resolveFieldPolicy(objectRef, { tenantId, userId, db })` — merged policy\n- `resolveFieldPolicyExplained(...)` — merged policy plus ordered per-layer\n contributions (code/app/tenant/user) for gear/admin UIs (#2049/#2050)\n\n## Resolution layers (priority low → high)\n\n1. Code seed — manifest field defaults, `description` as help, `_meta.ui`\n hints (#2046: `basic`/`group`/`order`/`locked`; cold-start rule: no\n `basic` markers ⇒ everything basic, any marker ⇒ unmarked fields advanced)\n2. App rows — `scopeType: 'app'`, `tenantId`/`userId` null\n3. Tenant rows — hierarchy walk root → leaf via an injected\n `tenantHierarchyLoader` (smrt-features shape); the default loader\n uses `@happyvertical/smrt-users`' `TenantCollection`; an injected loader\n may return no provider (or no chain for a tenant), which falls back to a\n flat single-tenant chain. `smrt-users` itself is a required Fields runtime\n dependency for policy authorization, not an optional hierarchy dependency.\n A node that breaks permission\n inheritance discards every earlier tenant contribution (chain-structural),\n so only the suffix from the LAST break participates — in merging AND in\n the explained layers, which therefore replay to the merged result\n4. User rows — keyed by `userId` alone (preferences follow the user); both\n defaults AND visibility resolve through this tier\n\n## Invariants\n\n- **Scope shape**: app ⇒ `tenantId`+`userId` null; tenant ⇒ only `tenantId`;\n user ⇒ only `userId`. `scopeKey` (`userId ?? tenantId ?? '__app__'`) exists\n ONLY to keep the `conflictColumns` unique index total — never read it for\n scoping logic.\n- **Manifest as definition registry**: writes validate against the live\n `ObjectRegistry` (never checked-in manifest.json artifacts): unknown\n objectRef/fieldName rejected; defaults type-checked against the field type;\n system fields, relationship pseudo-fields, and STI meta storage fields are\n not policy-addressable (the resolver excludes them, so rows would silently\n never apply).\n- **Security rail**: defaults are refused on `transient`, `sensitive`, and\n `readPermission`-gated fields (both top-level and `_meta` flags checked).\n Reference-field (`foreignKey`/`crossPackageRef`) defaults must be UUID\n strings unless the field declares `idType: 'text'` — the columns are\n native UUID on PostgreSQL/DuckDB and a non-UUID default would fail at\n insert time. `resolveBatch` responses omit sensitive/read-permission-gated\n fields for every caller (fail closed) and transient fields\n (client-emission parity).\n- **Required-field invariant**: demoting a required field to\n advanced/hidden requires a usable resolved default (not null/empty) —\n enforced at write time AND re-enforced at resolution (a required field with\n no usable default always resolves `basic`, flagged `visibilityForced`).\n- **Org lock**: `locked` may be set on app/tenant rows only; while the\n code/app/tenant tiers resolve locked, user-scope writes are rejected and\n existing user rows are skipped at resolution.\n- **Write-time org checks reuse the resolver**: the required-demotion default\n and the user-write lock are computed by `resolveFieldPolicy` over the org\n tiers (default hierarchy loader), so cascading ancestor-tenant defaults and\n locks are honored at save time too. On updates the persisted row is excluded\n from projected lower-layer resolution, so clearing the row's only default\n while demoting a required field is rejected before mutation; that projected\n lookup bypasses the shared cache. The resolver-side safety net remains\n authoritative when a different row is later deleted.\n- **Isolation — a MISSING identity component DENIES, it never skips.** This\n is the package rule; both the write guard\n (`FieldPolicy.assertScopeOwnedByAmbientContext`) and the read guard\n (`assertResolutionAllowedInContext`) obey it. A non-bypass tenant context\n may only resolve/write its own tenant, and the user tier only for its own\n user id — a context carrying permissions but NO `userId` (no `resolveUserId`\n hook configured: API-key auth, service principals, background jobs, a bare\n `withTenant({ tenantId })`) may not touch the user tier at all. Skipping\n that check instead of denying it was a live ownership bypass: user rows are\n `tenantId: null` by design, so nothing else contains such a write. App-scope\n writes inside a tenant context require super-admin bypass. `fields.policy.manage`\n authorizes app/tenant policy mutations; `fields.policy.personalize` authorizes\n only the caller's user tier (default-seeded for built-in roles). With NO ambient\n identity at all (tenancy ALS never entered), policy writes/deletes fail closed;\n trusted system and super-admin contexts retain their explicit bypasses.\n Context-LESS *reads* stay allowed because `resolveFieldPolicy` is a trusted\n server-side API. `save()`/`delete()`\n on an existing row additionally authorize against the row's PERSISTED\n scope, looked up by primary key AND — because a generated create always\n mints a fresh UUID while the `conflictColumns` upsert still replaces the\n occupant — by NATURAL key. `resolveBatch` takes identity exclusively from\n the ambient context; the request body cannot select another tenant or user.\n- **Scope attribution**: inside an ambient context the model DERIVES a\n missing `tenantId` (tenant rows) or `userId` (user rows) from that context,\n and always stamps `updatedBy` from it. Core's mass-assignment guard treats\n `tenantId` as server-managed and strips it from every generated write body\n while `FieldPolicy` is deliberately not `@TenantScoped`, so without this the\n org tier is write-dead over REST/SvelteKit (scope-shape validation throws).\n Deriving grants nothing — the ownership guard already pinned the value to\n the ambient one. An explicit value is never overwritten, so a super-admin\n bypass caller writing ANOTHER tenant's row must go through a server-side\n model call; the generated routes still strip it.\n\n## Caching and invalidation\n\n- Resolver results cached per\n `(dbNamespace, objectRef, tenantId, userId, hierarchyLoader)` with a 30s TTL\n (`cache.ts` mirrors smrt-prompts' `getDbNamespace`). The loader identity is\n part of the key because an injected `tenantHierarchyLoader` yields a\n different ancestor chain — and so different defaults/locks — for the same\n `(db, objectRef, tenant, user)`; it goes LAST so the `(db, objectRef)`\n prefix scan still invalidates every loader's entries.\n- `FieldPolicy.save()`/`.delete()` invalidate ALL entries for the row's\n `(db, objectRef)` — coarser than prompts because tenant hierarchy makes a\n parent-tenant row affect every descendant's resolution.\n- `_smrt_field_policies` rows do NOT ride the client change feed: core's\n change-feed writer deliberately skips `_smrt_`-prefixed system tables, and\n the emit side is private. Live client invalidation is a core-side decision;\n do not add custom push/emit paths here.\n\n## Gotchas\n\n- Defaults have TWO explicit constructor channels, never sniffed:\n `defaultValue` is ALREADY JSON-encoded (the wire contract — generated write\n routes hand the request body straight to the constructor, and #2049's gear\n posts `JSON.stringify(draft.defaultValue)`), while `defaultValueRaw` is a\n plain value that is always serialized, strings included. Passing both throws.\n One option cannot carry both meanings: `'\"TBD\"'` and `'TBD'` are\n indistinguishable, so `{ defaultValue: 'Net 30' }` is a parse error that\n names `defaultValueRaw` in its message. `setDefaultValue()` is the\n method-level plain channel.\n- The sort-order column is `displayOrder` (resolved output exposes `order`):\n a column literally named `order` is an SQL keyword the runtime INSERT path\n does not quote.\n- `FieldPolicy` deliberately has NO class-level `@TenantScoped` (the\n prompts/features precedent): resolution legitimately reads app rows and\n ancestor-tenant rows, which the tenancy interceptor would block. Isolation\n is enforced at the resolver/save boundaries instead.\n- Identity changes (objectRef/fieldName/scope) on a persisted row go through\n delete-then-insert (transactional when the driver supports it) because the\n natural-key upsert would otherwise collide with the primary key.\n- `FieldPolicyCollection` MUST repeat FieldPolicy's `conflictColumns`. A\n decorated collection emits its OWN manifest schema for the item's table, and\n without the natural key that schema falls back to SmrtObject's default unique\n `(slug, context)` index; manifest-driven migrations aggregate both onto\n `_smrt_field_policies`, where the stray index rejects legitimate layered rows\n (all policy rows have NULL slug/context). The runtime registry cannot catch\n this — `getAllSchemas()` is keyed by TABLE name, so the two schemas collapse\n into one entry. Pinned against the generated manifest in\n `generated-surfaces.test.ts`.\n- `withSystemContext()` does NOT unlock tenant/user-scope writes:\n `getCurrentTenant()` returns undefined inside it, so the context-absent rule\n rejects those tiers before any bypass check. Seeds and migrations that must\n write org/user rows use a `superAdminBypass` context instead.\n- The MODEL's `cli`/`mcp` decorator config is dead at runtime: the registry\n re-registers the item slot with the COLLECTION's config wholesale, so\n `FieldPolicyCollection`'s `cli: false, mcp: false` is what actually closes\n those surfaces. Keep both in lockstep anyway — build-time generation reads\n the model's own config. Related: `ObjectRegistry.getTableName(\n 'FieldPolicyCollection')` resolves to the UNPREFIXED collection fallback\n `field_policies`; persistence uses the item class\n (`_smrt_field_policies`), and both are pinned in\n `generated-surfaces.test.ts`.\n\n## Svelte generated forms (#2049)\n\n- `ObjectForm` accepts direct generated `fields` plus a resolved `policy` for\n SSR and explicit hosts, or accepts only `objectRef` beneath an\n `ObjectFormSourceProvider`. `ObjectFormSourceRegistry` is per app and maps\n canonical refs to generated web collection definitions before calling the\n generated `resolveBatch` custom action. It uses structural types only: this\n package must never import `smrt-web`.\n- Generated custom-action clients resolve `Promise<any>`; validate their\n `policies[objectRef]` response at the registry boundary and fail closed on a\n missing/mismatched definition or policy. The component renders an accessible\n loading state and alert rather than a partial form.\n- Browser manifest types are `text`, `integer`, `decimal`, `boolean`,\n `datetime`, `json`, `foreignKey`, and `crossPackageRef`; there is no `select`\n wire type. Apps use the per-app `FieldInputRegistry.registerField` seam for\n select-like widgets. Reference fields intentionally default to identifier\n inputs unless an app supplies a chooser.\n- `policyToVisibleColumnIds(policy, columns)` feeds smrt-ui `DataTable`'s\n `visibleColumnIds`; it filters policy-hidden mapped fields, preserves unmapped\n computed/action columns, and cannot reveal a static `column.hidden` column.\n\n## Defaults control panel (#2050)\n\n- `buildFieldPolicySettingsCatalog()` is the server-side, URL/GET-driven\n catalog builder. It structurally targets `SettingsCatalog`, but this package\n must not import `smrt-svelte`; hosts inject `SettingsCatalog` into\n `FieldPolicyControlPanel` and retain their own route and transport adapters.\n- `policyAudit` is the only routed organization roll-up. It requires\n `fields.policy.manage`, returns only the caller tenant's editable rows and\n read-only app summaries, and represents other users strictly as per-field\n counts. It resolves only requested page refs; never pre-resolve the catalog.\n- Display code/app/org values by replaying the explained resolver layers, not\n by independently calculating precedence. Reset and drift prune are ordinary\n model deletes, so existing scope/permission validation remains authoritative.\n The panel asks for an explicit confirmation before either destructive action;\n SSR hosts can inject that confirmation decision instead of relying on\n `window.confirm`.\n- `fieldPolicyControlPanelNavItem()` is a structural AdminShell tenant-nav\n seam. It never imports `smrt-svelte` or `smrt-web`; the host supplies the\n returned entry and must enforce its real route permission server-side.\n\n## Related\n\n- `@happyvertical/smrt-prompts` / `smrt-languages` / `smrt-features` — the\n same architecture family (override rows + layered resolver + TTL cache)\n- Core `FieldUIHints` (#2046) — the `@field({ ui })` code seed this package\n resolves over\n"
347
390
  }