@happyvertical/smrt-fields 0.55.5 → 0.55.8
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.
- package/AGENTS.md +20 -0
- package/README.md +11 -0
- package/dist/chunks/{FieldPolicyCollection-NNUaOGwv.js → FieldPolicyCollection-BPQe_eLu.js} +13 -7
- package/dist/chunks/FieldPolicyCollection-BPQe_eLu.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +89 -7
- package/dist/models/FieldPolicy.d.ts.map +1 -1
- package/dist/recipes.d.ts +37 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +89 -11
- package/package.json +10 -9
- package/dist/chunks/FieldPolicyCollection-NNUaOGwv.js.map +0 -1
package/dist/manifest.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-fields",
|
|
5
|
-
"packageVersion": "0.55.
|
|
5
|
+
"packageVersion": "0.55.8",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-fields:FieldUsageLearningAgent": {
|
|
8
8
|
"name": "fieldusagelearningagent",
|
|
9
9
|
"className": "FieldUsageLearningAgent",
|
|
10
10
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageLearningAgent",
|
|
11
11
|
"collection": "fieldusagelearningagents",
|
|
12
|
+
"description": "The registered schedule target for the #2051 learning loop (see the module doc for why it is NOT an smrt-agents Agent subclass).",
|
|
12
13
|
"filePath": "/home/runner/work/smrt/smrt/packages/fields/src/usage-learning.ts",
|
|
13
14
|
"packageName": "@happyvertical/smrt-fields",
|
|
14
15
|
"fields": {},
|
|
@@ -410,6 +411,7 @@
|
|
|
410
411
|
"className": "FieldPolicySuggestionCollection",
|
|
411
412
|
"qualifiedName": "@happyvertical/smrt-fields:FieldPolicySuggestionCollection",
|
|
412
413
|
"collection": "fieldpolicysuggestions",
|
|
414
|
+
"description": "Collection surface for FieldPolicySuggestion plus the three manage-gated actions the gear badge / control-panel queue consume (#2051; UI integration is #2050-follow-up territory — this is the minimal…",
|
|
413
415
|
"filePath": "/home/runner/work/smrt/smrt/packages/fields/src/collections/FieldPolicySuggestionCollection.ts",
|
|
414
416
|
"packageName": "@happyvertical/smrt-fields",
|
|
415
417
|
"fields": {},
|
|
@@ -641,6 +643,7 @@
|
|
|
641
643
|
"className": "FieldUsageCounterCollection",
|
|
642
644
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageCounterCollection",
|
|
643
645
|
"collection": "fieldusagecounters",
|
|
646
|
+
"description": "Collection surface for FieldUsageCounter plus the batched usage ingestion action (#2051).",
|
|
644
647
|
"filePath": "/home/runner/work/smrt/smrt/packages/fields/src/collections/FieldUsageCounterCollection.ts",
|
|
645
648
|
"packageName": "@happyvertical/smrt-fields",
|
|
646
649
|
"fields": {},
|
|
@@ -833,6 +836,7 @@
|
|
|
833
836
|
"className": "FieldUsageReportReceiptCollection",
|
|
834
837
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageReportReceiptCollection",
|
|
835
838
|
"collection": "fieldusagereportreceipts",
|
|
839
|
+
"description": "Internal only — this table has no generated surface.",
|
|
836
840
|
"filePath": "/home/runner/work/smrt/smrt/packages/fields/src/collections/FieldUsageCounterCollection.ts",
|
|
837
841
|
"packageName": "@happyvertical/smrt-fields",
|
|
838
842
|
"fields": {},
|
|
@@ -1040,15 +1044,19 @@
|
|
|
1040
1044
|
"defaultValue": {
|
|
1041
1045
|
"type": "text",
|
|
1042
1046
|
"required": false,
|
|
1047
|
+
"description": "The value a new record starts with for this field.",
|
|
1043
1048
|
"_meta": {
|
|
1044
|
-
"nullable": true
|
|
1049
|
+
"nullable": true,
|
|
1050
|
+
"description": "The value a new record starts with for this field."
|
|
1045
1051
|
}
|
|
1046
1052
|
},
|
|
1047
1053
|
"visibility": {
|
|
1048
1054
|
"type": "text",
|
|
1049
1055
|
"required": false,
|
|
1056
|
+
"description": "Whether the field is always shown, tucked under advanced, or hidden.",
|
|
1050
1057
|
"_meta": {
|
|
1051
|
-
"nullable": true
|
|
1058
|
+
"nullable": true,
|
|
1059
|
+
"description": "Whether the field is always shown, tucked under advanced, or hidden."
|
|
1052
1060
|
},
|
|
1053
1061
|
"enum": [
|
|
1054
1062
|
"basic",
|
|
@@ -1059,29 +1067,37 @@
|
|
|
1059
1067
|
"help": {
|
|
1060
1068
|
"type": "text",
|
|
1061
1069
|
"required": false,
|
|
1070
|
+
"description": "The hint shown beside the field on the form.",
|
|
1062
1071
|
"_meta": {
|
|
1063
|
-
"nullable": true
|
|
1072
|
+
"nullable": true,
|
|
1073
|
+
"description": "The hint shown beside the field on the form."
|
|
1064
1074
|
}
|
|
1065
1075
|
},
|
|
1066
1076
|
"label": {
|
|
1067
1077
|
"type": "text",
|
|
1068
1078
|
"required": false,
|
|
1079
|
+
"description": "The name people see for the field on forms.",
|
|
1069
1080
|
"_meta": {
|
|
1070
|
-
"nullable": true
|
|
1081
|
+
"nullable": true,
|
|
1082
|
+
"description": "The name people see for the field on forms."
|
|
1071
1083
|
}
|
|
1072
1084
|
},
|
|
1073
1085
|
"displayOrder": {
|
|
1074
1086
|
"type": "integer",
|
|
1075
1087
|
"required": false,
|
|
1088
|
+
"description": "Where the field appears on the form; lower numbers come first.",
|
|
1076
1089
|
"_meta": {
|
|
1077
|
-
"nullable": true
|
|
1090
|
+
"nullable": true,
|
|
1091
|
+
"description": "Where the field appears on the form; lower numbers come first."
|
|
1078
1092
|
}
|
|
1079
1093
|
},
|
|
1080
1094
|
"locked": {
|
|
1081
1095
|
"type": "boolean",
|
|
1082
1096
|
"required": false,
|
|
1097
|
+
"description": "Stops individual people from changing this field for themselves.",
|
|
1083
1098
|
"_meta": {
|
|
1084
|
-
"nullable": true
|
|
1099
|
+
"nullable": true,
|
|
1100
|
+
"description": "Stops individual people from changing this field for themselves."
|
|
1085
1101
|
}
|
|
1086
1102
|
},
|
|
1087
1103
|
"updatedBy": {
|
|
@@ -1983,6 +1999,7 @@
|
|
|
1983
1999
|
"className": "FieldUsageReportReceipt",
|
|
1984
2000
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageReportReceipt",
|
|
1985
2001
|
"collection": "fieldusagereportreceipts",
|
|
2002
|
+
"description": "Durable daily receipt for one member's contribution to one field.",
|
|
1986
2003
|
"filePath": "/home/runner/work/smrt/smrt/packages/fields/src/models/FieldUsageReportReceipt.ts",
|
|
1987
2004
|
"packageName": "@happyvertical/smrt-fields",
|
|
1988
2005
|
"fields": {
|
|
@@ -2168,10 +2185,75 @@
|
|
|
2168
2185
|
}
|
|
2169
2186
|
},
|
|
2170
2187
|
"moduleType": "smrt",
|
|
2188
|
+
"recipes": [
|
|
2189
|
+
{
|
|
2190
|
+
"id": "fields.form-customization",
|
|
2191
|
+
"className": "FormCustomizationRecipe",
|
|
2192
|
+
"label": "Form customization",
|
|
2193
|
+
"summary": "Choose what your forms ask for: defaults, labels, hints and which fields are shown.",
|
|
2194
|
+
"synonyms": [
|
|
2195
|
+
"form defaults",
|
|
2196
|
+
"field defaults",
|
|
2197
|
+
"customize forms",
|
|
2198
|
+
"hide fields",
|
|
2199
|
+
"field visibility",
|
|
2200
|
+
"field labels"
|
|
2201
|
+
],
|
|
2202
|
+
"models": [
|
|
2203
|
+
"@happyvertical/smrt-fields:FieldPolicy",
|
|
2204
|
+
"@happyvertical/smrt-fields:FieldPolicySuggestion",
|
|
2205
|
+
"@happyvertical/smrt-fields:FieldUsageCounter",
|
|
2206
|
+
"@happyvertical/smrt-fields:FieldUsageReportReceipt"
|
|
2207
|
+
],
|
|
2208
|
+
"nav": [],
|
|
2209
|
+
"requires": [],
|
|
2210
|
+
"help": {
|
|
2211
|
+
"markdown": "## Overview\n\nForm customization lets you decide what the forms in your app ask for. For any\nfield you can change the name people see, the hint beside it, the value a new\nrecord starts with, where it sits on the form, and whether it is always shown,\ntucked under \"advanced\", or hidden. You can also lock a field so that people\ncannot change it for themselves.\n\nChanges layer. The app starts with sensible settings, your organization can\nchange them for everyone in it, and each person can then adjust their own view\nunless you locked the field. Resetting a field removes your change and brings\nback whatever sits underneath.\n\n## Tasks\n\n### Change what a field is called\n\n1. Open **Form defaults** from your settings and find the form you want.\n2. Choose the field and type a new **{field:label}**.\n3. Save. The new name appears on the form.\n\n### Give people a hint\n\n1. Choose the field in **Form defaults**.\n2. Write the **{field:help}**, one short sentence that says what to enter.\n3. Save. The hint now shows beside the field.\n\n### Pre-fill a value\n\n1. Choose the field in **Form defaults**.\n2. Enter the **{field:defaultValue}** a new record should start with.\n3. Save. People can still type over it unless the field is locked.\n\n### Tidy a long form\n\n1. Choose a field that most people never need.\n2. Set its **{field:visibility}** to advanced to tuck it away, or to hidden to\n remove it.\n3. Save. A field that must always be filled in stays shown unless it also has a\n default value.\n\n### Put fields in a better order\n\n1. Choose a field.\n2. Set its **{field:displayOrder}**. Lower numbers come first.\n3. Save, then check the form.\n\n### Keep everyone on the same setting\n\n1. Choose the field in **Form defaults**.\n2. Turn on **{field:locked}**.\n3. Save. People can no longer change that field for themselves.\n\n### Undo a change\n\nChoose **Reset** on a field to remove your change. Settings from the layers\nbelow, such as the app's own, come back. Resetting asks you to confirm first.\n\n### Review suggestions\n\nWhen enough people fill in the same value, or keep opening the same advanced\nfield, the app suggests a change. Open the suggestions list in **Form defaults**\nand accept or dismiss each one. Nothing changes until you accept.\n",
|
|
2212
|
+
"fieldRefs": [
|
|
2213
|
+
"defaultValue",
|
|
2214
|
+
"displayOrder",
|
|
2215
|
+
"help",
|
|
2216
|
+
"label",
|
|
2217
|
+
"locked",
|
|
2218
|
+
"visibility"
|
|
2219
|
+
]
|
|
2220
|
+
},
|
|
2221
|
+
"surfaces": [
|
|
2222
|
+
{
|
|
2223
|
+
"kind": "settings-panel",
|
|
2224
|
+
"export": "@happyvertical/smrt-fields/svelte#FieldPolicyControlPanel",
|
|
2225
|
+
"label": "Form defaults"
|
|
2226
|
+
}
|
|
2227
|
+
],
|
|
2228
|
+
"runtime": "server",
|
|
2229
|
+
"demo": {
|
|
2230
|
+
"mode": "server",
|
|
2231
|
+
"reasons": [
|
|
2232
|
+
"The recipe declares runtime \"server\".",
|
|
2233
|
+
"Needs a server because its package does not build for a browser: imports core utils/import-workspace-module (node:url, node:fs, node:path); and its package depends on @happyvertical/smrt-profiles, @happyvertical/smrt-tenancy, @happyvertical/smrt-users, which do not build for a browser (#3617, #3618, #3624, #3626)."
|
|
2234
|
+
]
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
],
|
|
2171
2238
|
"smrtDependencies": [
|
|
2172
2239
|
"@happyvertical/smrt-core",
|
|
2173
2240
|
"@happyvertical/smrt-tenancy",
|
|
2174
2241
|
"@happyvertical/smrt-users"
|
|
2175
2242
|
],
|
|
2243
|
+
"browser": {
|
|
2244
|
+
"status": "server-only",
|
|
2245
|
+
"issues": [
|
|
2246
|
+
"#3617",
|
|
2247
|
+
"#3618",
|
|
2248
|
+
"#3624",
|
|
2249
|
+
"#3626"
|
|
2250
|
+
],
|
|
2251
|
+
"reason": "imports core utils/import-workspace-module (node:url, node:fs, node:path)",
|
|
2252
|
+
"via": [
|
|
2253
|
+
"@happyvertical/smrt-profiles",
|
|
2254
|
+
"@happyvertical/smrt-tenancy",
|
|
2255
|
+
"@happyvertical/smrt-users"
|
|
2256
|
+
]
|
|
2257
|
+
},
|
|
2176
2258
|
"stampsConstructors": true
|
|
2177
2259
|
}
|
|
@@ -1 +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;
|
|
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;IAMnF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC,6EAA6E;IAO7E,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAAQ;IAEhD,yEAAyE;IAMzE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE3B,6EAA6E;IAM7E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE5B;;;;OAIG;IAOH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC;;;;OAIG;IAOH,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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared recipes for smrt-fields (#3719): small, user-facing units of
|
|
3
|
+
* functionality an app or agent can pick instead of the whole package.
|
|
4
|
+
* The scanner reads these statics into the `recipes` array of `manifest.json`
|
|
5
|
+
* and `smrt-knowledge.json`; nothing here runs at that point.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { SmrtRecipe } from '@happyvertical/smrt-core';
|
|
10
|
+
import { FieldPolicy } from './models/FieldPolicy.js';
|
|
11
|
+
import { FieldPolicySuggestion } from './models/FieldPolicySuggestion.js';
|
|
12
|
+
import { FieldUsageCounter } from './models/FieldUsageCounter.js';
|
|
13
|
+
import { FieldUsageReportReceipt } from './models/FieldUsageReportReceipt.js';
|
|
14
|
+
/**
|
|
15
|
+
* Form customization: let an organization (and each person) change what the
|
|
16
|
+
* forms ask for. Defaults, labels, hints, order and basic/advanced/hidden
|
|
17
|
+
* visibility are stored as sparse override rows over the code seed.
|
|
18
|
+
*
|
|
19
|
+
* There is no `nav` on purpose: the policy tables have no generated read
|
|
20
|
+
* surface (reads would enumerate every tenant), so the one place people
|
|
21
|
+
* manage this is the `settings-panel` surface below.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FormCustomizationRecipe extends SmrtRecipe {
|
|
24
|
+
static id: string;
|
|
25
|
+
static help: string;
|
|
26
|
+
static label: string;
|
|
27
|
+
static summary: string;
|
|
28
|
+
static synonyms: string[];
|
|
29
|
+
static models: (typeof FieldPolicy | typeof FieldPolicySuggestion | typeof FieldUsageCounter | typeof FieldUsageReportReceipt)[];
|
|
30
|
+
static runtime: "server";
|
|
31
|
+
static surfaces: readonly [{
|
|
32
|
+
readonly kind: "settings-panel";
|
|
33
|
+
readonly export: "@happyvertical/smrt-fields/svelte#FieldPolicyControlPanel";
|
|
34
|
+
readonly label: "Form defaults";
|
|
35
|
+
}];
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=recipes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAE9E;;;;;;;;GAQG;AACH,qBAAa,uBAAwB,SAAQ,UAAU;IACrD,MAAM,CAAC,EAAE,SAA+B;IACxC,MAAM,CAAC,IAAI,SAAoC;IAC/C,MAAM,CAAC,KAAK,SAAwB;IACpC,MAAM,CAAC,OAAO,SAC0E;IACxF,MAAM,CAAC,QAAQ,WAOb;IACF,MAAM,CAAC,MAAM,oHAKX;IAKF,MAAM,CAAC,OAAO,EAAG,QAAQ,CAAU;IACnC,MAAM,CAAC,QAAQ;;;;OAMJ;CACZ"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-fields",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.8",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "b6c0a89d42534d7764d1fd418d016914c7d56034ea185093c013f28a701fd4ab",
|
|
11
|
+
"packageJson": "49eed385149528c029313bd9f7eb3406b5f4563cb9fc7d91ec58800a7b948359",
|
|
12
|
+
"agents": "ae4bf7b69ae3b28a7900b590dd21b53ea0dfc5fd6778f858ceac97dfd8654187"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
15
15
|
".",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@happyvertical/smrt-users": "workspace:*",
|
|
26
26
|
"@happyvertical/sql": "catalog:",
|
|
27
27
|
"@happyvertical/smrt-cli": "workspace:*",
|
|
28
|
+
"@happyvertical/smrt-scanner": "workspace:*",
|
|
28
29
|
"@happyvertical/smrt-vitest": "workspace:*",
|
|
29
30
|
"@sveltejs/package": "^2.5.8",
|
|
30
31
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"smrtDependencies": [
|
|
40
41
|
"@happyvertical/smrt-cli",
|
|
41
42
|
"@happyvertical/smrt-core",
|
|
43
|
+
"@happyvertical/smrt-scanner",
|
|
42
44
|
"@happyvertical/smrt-tenancy",
|
|
43
45
|
"@happyvertical/smrt-ui",
|
|
44
46
|
"@happyvertical/smrt-users",
|
|
@@ -64,6 +66,7 @@
|
|
|
64
66
|
"name": "FieldUsageLearningAgent",
|
|
65
67
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageLearningAgent",
|
|
66
68
|
"collection": "fieldusagelearningagents",
|
|
69
|
+
"description": "The registered schedule target for the #2051 learning loop (see the module doc for why it is NOT an smrt-agents Agent subclass).",
|
|
67
70
|
"tableName": "_smrt_field_usage_learning_agents",
|
|
68
71
|
"packageName": "@happyvertical/smrt-fields",
|
|
69
72
|
"extends": "SmrtObject",
|
|
@@ -248,6 +251,7 @@
|
|
|
248
251
|
"name": "FieldPolicySuggestionCollection",
|
|
249
252
|
"qualifiedName": "@happyvertical/smrt-fields:FieldPolicySuggestionCollection",
|
|
250
253
|
"collection": "fieldpolicysuggestions",
|
|
254
|
+
"description": "Collection surface for FieldPolicySuggestion plus the three manage-gated actions the gear badge / control-panel queue consume (#2051; UI integration is #2050-follow-up territory — this is the minimal…",
|
|
251
255
|
"tableName": "_smrt_field_policy_suggestions",
|
|
252
256
|
"packageName": "@happyvertical/smrt-fields",
|
|
253
257
|
"extends": "SmrtCollection",
|
|
@@ -327,6 +331,7 @@
|
|
|
327
331
|
"name": "FieldUsageCounterCollection",
|
|
328
332
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageCounterCollection",
|
|
329
333
|
"collection": "fieldusagecounters",
|
|
334
|
+
"description": "Collection surface for FieldUsageCounter plus the batched usage ingestion action (#2051).",
|
|
330
335
|
"tableName": "_smrt_field_usage_counters",
|
|
331
336
|
"packageName": "@happyvertical/smrt-fields",
|
|
332
337
|
"extends": "SmrtCollection",
|
|
@@ -389,6 +394,7 @@
|
|
|
389
394
|
"name": "FieldUsageReportReceiptCollection",
|
|
390
395
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageReportReceiptCollection",
|
|
391
396
|
"collection": "fieldusagereportreceipts",
|
|
397
|
+
"description": "Internal only — this table has no generated surface.",
|
|
392
398
|
"tableName": "_smrt_field_usage_report_receipts",
|
|
393
399
|
"packageName": "@happyvertical/smrt-fields",
|
|
394
400
|
"extends": "SmrtCollection",
|
|
@@ -483,37 +489,43 @@
|
|
|
483
489
|
"name": "defaultValue",
|
|
484
490
|
"type": "text",
|
|
485
491
|
"required": false,
|
|
486
|
-
"columnType": "TEXT"
|
|
492
|
+
"columnType": "TEXT",
|
|
493
|
+
"description": "The value a new record starts with for this field."
|
|
487
494
|
},
|
|
488
495
|
{
|
|
489
496
|
"name": "visibility",
|
|
490
497
|
"type": "text",
|
|
491
498
|
"required": false,
|
|
492
|
-
"columnType": "TEXT"
|
|
499
|
+
"columnType": "TEXT",
|
|
500
|
+
"description": "Whether the field is always shown, tucked under advanced, or hidden."
|
|
493
501
|
},
|
|
494
502
|
{
|
|
495
503
|
"name": "help",
|
|
496
504
|
"type": "text",
|
|
497
505
|
"required": false,
|
|
498
|
-
"columnType": "TEXT"
|
|
506
|
+
"columnType": "TEXT",
|
|
507
|
+
"description": "The hint shown beside the field on the form."
|
|
499
508
|
},
|
|
500
509
|
{
|
|
501
510
|
"name": "label",
|
|
502
511
|
"type": "text",
|
|
503
512
|
"required": false,
|
|
504
|
-
"columnType": "TEXT"
|
|
513
|
+
"columnType": "TEXT",
|
|
514
|
+
"description": "The name people see for the field on forms."
|
|
505
515
|
},
|
|
506
516
|
{
|
|
507
517
|
"name": "displayOrder",
|
|
508
518
|
"type": "integer",
|
|
509
519
|
"required": false,
|
|
510
|
-
"columnType": "INTEGER"
|
|
520
|
+
"columnType": "INTEGER",
|
|
521
|
+
"description": "Where the field appears on the form; lower numbers come first."
|
|
511
522
|
},
|
|
512
523
|
{
|
|
513
524
|
"name": "locked",
|
|
514
525
|
"type": "boolean",
|
|
515
526
|
"required": false,
|
|
516
|
-
"columnType": "BOOLEAN"
|
|
527
|
+
"columnType": "BOOLEAN",
|
|
528
|
+
"description": "Stops individual people from changing this field for themselves."
|
|
517
529
|
},
|
|
518
530
|
{
|
|
519
531
|
"name": "updatedBy",
|
|
@@ -1015,6 +1027,7 @@
|
|
|
1015
1027
|
"name": "FieldUsageReportReceipt",
|
|
1016
1028
|
"qualifiedName": "@happyvertical/smrt-fields:FieldUsageReportReceipt",
|
|
1017
1029
|
"collection": "fieldusagereportreceipts",
|
|
1030
|
+
"description": "Durable daily receipt for one member's contribution to one field.",
|
|
1018
1031
|
"tableName": "_smrt_field_usage_report_receipts",
|
|
1019
1032
|
"packageName": "@happyvertical/smrt-fields",
|
|
1020
1033
|
"extends": "SmrtObject",
|
|
@@ -1189,5 +1202,70 @@
|
|
|
1189
1202
|
"polymorphicAssociations": 0,
|
|
1190
1203
|
"uuidColumns": 25
|
|
1191
1204
|
},
|
|
1192
|
-
"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 and restricted\n (`sensitive`/`secret` or `readable: false`) columns, preserves unrestricted\n unmapped computed/action columns, and cannot reveal a static `column.hidden`\n column. Pass `authorizedColumnIds` only when the host explicitly authorizes a\n restricted column; `readable: true` alone does not override sensitivity.\n- `policyToDataSurfaceDescriptor(policy, descriptor)` is the corresponding\n mounted DataSurface adapter. It carries effective labels, help text, order,\n visibility, readable/capability state, and operator/query allowlists into\n discovery metadata while retaining unrestricted computed, row-key, selection,\n and action columns. Static or resolved-policy-hidden structural columns are\n omitted from discovery (the row key remains only as a non-readable identity\n column), and actions that explicitly depend on hidden columns are removed.\n Sensitive/secret and `readable: false` columns also fail closed when omitted\n from the resolved policy; a host must pass `authorizedColumnIds` explicitly\n to expose a restricted descriptor column, and authorization sets the\n effective `readable` metadata to `true` so capabilities and query allowlists\n cannot contradict it.\n- **Two-tier context contract.** `PolicyField` alone reads the context with\n `tryGetFieldPolicyContext()`, because outside a Provider it still has the\n caller's children to render verbatim (incremental adoption). The\n provider-only compositions — `ModeSwitch`, `AdvancedFields`, `FormHelp` —\n read it with the throwing `getFieldPolicyContext()`: their whole output is\n derived from the resolved policy, so degrading would render an empty or\n absent control instead of surfacing the missing Provider. Do not convert one\n of them to the non-throwing accessor in isolation (#2272).\n- **One provider per form.** `FieldPolicyProvider` renders only\n `{@render children?.()}`, so a host wanting `FormHelp` in a page header\n extends the SAME provider over the header rather than hoisting the component\n out. It must not add a second: each provider constructs its own\n `FieldPolicyModeStore` and `setContext` shadows for the subtree, so a\n `FormHelp` above a nested provider stops tracking the `ModeSwitch` inside it.\n `ObjectForm` builds its own provider, so never wrap it in another; its\n `actions` snippet renders INSIDE that provider (context resolves at the render\n site), which is the only way to reach `FormHelp` through it — below the\n fields, since there is no header seam (#2289).\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"
|
|
1205
|
+
"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 and restricted\n (`sensitive`/`secret` or `readable: false`) columns, preserves unrestricted\n unmapped computed/action columns, and cannot reveal a static `column.hidden`\n column. Pass `authorizedColumnIds` only when the host explicitly authorizes a\n restricted column; `readable: true` alone does not override sensitivity.\n- `policyToDataSurfaceDescriptor(policy, descriptor)` is the corresponding\n mounted DataSurface adapter. It carries effective labels, help text, order,\n visibility, readable/capability state, and operator/query allowlists into\n discovery metadata while retaining unrestricted computed, row-key, selection,\n and action columns. Static or resolved-policy-hidden structural columns are\n omitted from discovery (the row key remains only as a non-readable identity\n column), and actions that explicitly depend on hidden columns are removed.\n Sensitive/secret and `readable: false` columns also fail closed when omitted\n from the resolved policy; a host must pass `authorizedColumnIds` explicitly\n to expose a restricted descriptor column, and authorization sets the\n effective `readable` metadata to `true` so capabilities and query allowlists\n cannot contradict it.\n- **Two-tier context contract.** `PolicyField` alone reads the context with\n `tryGetFieldPolicyContext()`, because outside a Provider it still has the\n caller's children to render verbatim (incremental adoption). The\n provider-only compositions — `ModeSwitch`, `AdvancedFields`, `FormHelp` —\n read it with the throwing `getFieldPolicyContext()`: their whole output is\n derived from the resolved policy, so degrading would render an empty or\n absent control instead of surfacing the missing Provider. Do not convert one\n of them to the non-throwing accessor in isolation (#2272).\n- **One provider per form.** `FieldPolicyProvider` renders only\n `{@render children?.()}`, so a host wanting `FormHelp` in a page header\n extends the SAME provider over the header rather than hoisting the component\n out. It must not add a second: each provider constructs its own\n `FieldPolicyModeStore` and `setContext` shadows for the subtree, so a\n `FormHelp` above a nested provider stops tracking the `ModeSwitch` inside it.\n `ObjectForm` builds its own provider, so never wrap it in another; its\n `actions` snippet renders INSIDE that provider (context resolves at the render\n site), which is the only way to reach `FormHelp` through it — below the\n fields, since there is no header seam (#2289).\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## Recipe: `fields.form-customization` (#3719)\n\n`src/recipes.ts` declares `FormCustomizationRecipe`\n(help: `src/form-customization.recipe.md`), emitted in `manifest.json` and\n`smrt-knowledge.json` `recipes`. Contract: [core recipes](../core/agents/recipes.md).\n\n- Models: the four policy/learning tables. No `nav`, deliberately: the tables\n have no generated read surface, so the one management UI is the declared\n `settings-panel` surface `@happyvertical/smrt-fields/svelte#FieldPolicyControlPanel`.\n That component is not zero-prop: the host still injects `SettingsCatalog`,\n the adapter, and loads `buildFieldPolicySettingsCatalog()` data server-side.\n- `runtime = 'server'` is the truthful value while #3618 is open: the root\n entry (resolver, `usage-schedules`) statically imports core's node-only\n `import-workspace-module`. The `/svelte` components are browser-safe because\n they render an already-resolved policy. Flip to `'both'` only when #3618 lands\n and a browser build of the root entry is proven.\n- No `demoSeed` (no fixture exists) and no `playground` surface: the playground\n export is a registry object, not a component. Help field references must\n point at `FieldPolicy` fields that carry `@field({ description })`.\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",
|
|
1206
|
+
"recipes": [
|
|
1207
|
+
{
|
|
1208
|
+
"id": "fields.form-customization",
|
|
1209
|
+
"className": "FormCustomizationRecipe",
|
|
1210
|
+
"label": "Form customization",
|
|
1211
|
+
"summary": "Choose what your forms ask for: defaults, labels, hints and which fields are shown.",
|
|
1212
|
+
"synonyms": [
|
|
1213
|
+
"form defaults",
|
|
1214
|
+
"field defaults",
|
|
1215
|
+
"customize forms",
|
|
1216
|
+
"hide fields",
|
|
1217
|
+
"field visibility",
|
|
1218
|
+
"field labels"
|
|
1219
|
+
],
|
|
1220
|
+
"models": [
|
|
1221
|
+
"@happyvertical/smrt-fields:FieldPolicy",
|
|
1222
|
+
"@happyvertical/smrt-fields:FieldPolicySuggestion",
|
|
1223
|
+
"@happyvertical/smrt-fields:FieldUsageCounter",
|
|
1224
|
+
"@happyvertical/smrt-fields:FieldUsageReportReceipt"
|
|
1225
|
+
],
|
|
1226
|
+
"nav": [],
|
|
1227
|
+
"requires": [],
|
|
1228
|
+
"help": {
|
|
1229
|
+
"markdown": "## Overview\n\nForm customization lets you decide what the forms in your app ask for. For any\nfield you can change the name people see, the hint beside it, the value a new\nrecord starts with, where it sits on the form, and whether it is always shown,\ntucked under \"advanced\", or hidden. You can also lock a field so that people\ncannot change it for themselves.\n\nChanges layer. The app starts with sensible settings, your organization can\nchange them for everyone in it, and each person can then adjust their own view\nunless you locked the field. Resetting a field removes your change and brings\nback whatever sits underneath.\n\n## Tasks\n\n### Change what a field is called\n\n1. Open **Form defaults** from your settings and find the form you want.\n2. Choose the field and type a new **{field:label}**.\n3. Save. The new name appears on the form.\n\n### Give people a hint\n\n1. Choose the field in **Form defaults**.\n2. Write the **{field:help}**, one short sentence that says what to enter.\n3. Save. The hint now shows beside the field.\n\n### Pre-fill a value\n\n1. Choose the field in **Form defaults**.\n2. Enter the **{field:defaultValue}** a new record should start with.\n3. Save. People can still type over it unless the field is locked.\n\n### Tidy a long form\n\n1. Choose a field that most people never need.\n2. Set its **{field:visibility}** to advanced to tuck it away, or to hidden to\n remove it.\n3. Save. A field that must always be filled in stays shown unless it also has a\n default value.\n\n### Put fields in a better order\n\n1. Choose a field.\n2. Set its **{field:displayOrder}**. Lower numbers come first.\n3. Save, then check the form.\n\n### Keep everyone on the same setting\n\n1. Choose the field in **Form defaults**.\n2. Turn on **{field:locked}**.\n3. Save. People can no longer change that field for themselves.\n\n### Undo a change\n\nChoose **Reset** on a field to remove your change. Settings from the layers\nbelow, such as the app's own, come back. Resetting asks you to confirm first.\n\n### Review suggestions\n\nWhen enough people fill in the same value, or keep opening the same advanced\nfield, the app suggests a change. Open the suggestions list in **Form defaults**\nand accept or dismiss each one. Nothing changes until you accept.\n",
|
|
1230
|
+
"fieldRefs": [
|
|
1231
|
+
"defaultValue",
|
|
1232
|
+
"displayOrder",
|
|
1233
|
+
"help",
|
|
1234
|
+
"label",
|
|
1235
|
+
"locked",
|
|
1236
|
+
"visibility"
|
|
1237
|
+
]
|
|
1238
|
+
},
|
|
1239
|
+
"surfaces": [
|
|
1240
|
+
{
|
|
1241
|
+
"kind": "settings-panel",
|
|
1242
|
+
"export": "@happyvertical/smrt-fields/svelte#FieldPolicyControlPanel",
|
|
1243
|
+
"label": "Form defaults"
|
|
1244
|
+
}
|
|
1245
|
+
],
|
|
1246
|
+
"runtime": "server",
|
|
1247
|
+
"demo": {
|
|
1248
|
+
"mode": "server",
|
|
1249
|
+
"reasons": [
|
|
1250
|
+
"The recipe declares runtime \"server\".",
|
|
1251
|
+
"Needs a server because its package does not build for a browser: imports core utils/import-workspace-module (node:url, node:fs, node:path); and its package depends on @happyvertical/smrt-profiles, @happyvertical/smrt-tenancy, @happyvertical/smrt-users, which do not build for a browser (#3617, #3618, #3624, #3626)."
|
|
1252
|
+
]
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
],
|
|
1256
|
+
"browser": {
|
|
1257
|
+
"status": "server-only",
|
|
1258
|
+
"issues": [
|
|
1259
|
+
"#3617",
|
|
1260
|
+
"#3618",
|
|
1261
|
+
"#3624",
|
|
1262
|
+
"#3626"
|
|
1263
|
+
],
|
|
1264
|
+
"reason": "imports core utils/import-workspace-module (node:url, node:fs, node:path)",
|
|
1265
|
+
"via": [
|
|
1266
|
+
"@happyvertical/smrt-profiles",
|
|
1267
|
+
"@happyvertical/smrt-tenancy",
|
|
1268
|
+
"@happyvertical/smrt-users"
|
|
1269
|
+
]
|
|
1270
|
+
}
|
|
1193
1271
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-fields",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.8",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "Layered field policy store and resolver (defaults, visibility tiers, help text) for SMRT objects",
|
|
6
6
|
"type": "module",
|
|
@@ -37,16 +37,17 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@happyvertical/smrt-core": "0.55.
|
|
41
|
-
"@happyvertical/smrt-tenancy": "0.55.
|
|
42
|
-
"@happyvertical/smrt-ui": "0.55.
|
|
43
|
-
"@happyvertical/smrt-users": "0.55.
|
|
44
|
-
"@happyvertical/sql": "^0.
|
|
40
|
+
"@happyvertical/smrt-core": "0.55.8",
|
|
41
|
+
"@happyvertical/smrt-tenancy": "0.55.8",
|
|
42
|
+
"@happyvertical/smrt-ui": "0.55.8",
|
|
43
|
+
"@happyvertical/smrt-users": "0.55.8",
|
|
44
|
+
"@happyvertical/sql": "^0.103.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@happyvertical/smrt-cli": "0.55.
|
|
48
|
-
"@happyvertical/smrt-
|
|
49
|
-
"@happyvertical/smrt-
|
|
47
|
+
"@happyvertical/smrt-cli": "0.55.8",
|
|
48
|
+
"@happyvertical/smrt-scanner": "0.55.8",
|
|
49
|
+
"@happyvertical/smrt-users": "0.55.8",
|
|
50
|
+
"@happyvertical/smrt-vitest": "0.55.8",
|
|
50
51
|
"@sveltejs/package": "^2.5.8",
|
|
51
52
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
52
53
|
"@testing-library/svelte": "^5.4.2",
|