@happyvertical/smrt-fields 0.40.61 → 0.40.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/AGENTS.md +18 -0
  2. package/README.md +125 -112
  3. package/dist/chunks/FieldPolicyCollection--RxukfCX.js +1218 -0
  4. package/dist/chunks/FieldPolicyCollection--RxukfCX.js.map +1 -0
  5. package/dist/collections/FieldPolicySuggestionCollection.d.ts +160 -0
  6. package/dist/collections/FieldPolicySuggestionCollection.d.ts.map +1 -0
  7. package/dist/collections/FieldUsageCounterCollection.d.ts +143 -0
  8. package/dist/collections/FieldUsageCounterCollection.d.ts.map +1 -0
  9. package/dist/deterministic-id.d.ts +18 -0
  10. package/dist/deterministic-id.d.ts.map +1 -0
  11. package/dist/field-definitions.d.ts +8 -0
  12. package/dist/field-definitions.d.ts.map +1 -1
  13. package/dist/index.d.ts +8 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1565 -1077
  16. package/dist/index.js.map +1 -1
  17. package/dist/manifest.json +1442 -188
  18. package/dist/models/FieldPolicySuggestion.d.ts +132 -0
  19. package/dist/models/FieldPolicySuggestion.d.ts.map +1 -0
  20. package/dist/models/FieldUsageCounter.d.ts +178 -0
  21. package/dist/models/FieldUsageCounter.d.ts.map +1 -0
  22. package/dist/models/FieldUsageReportReceipt.d.ts +25 -0
  23. package/dist/models/FieldUsageReportReceipt.d.ts.map +1 -0
  24. package/dist/smrt-knowledge.json +404 -7
  25. package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +54 -0
  26. package/dist/svelte/__tests__/FormHelp.test.js +8 -0
  27. package/dist/svelte/__tests__/ObjectForm.test.js +131 -0
  28. package/dist/svelte/__tests__/UsageLearning.test.js +140 -0
  29. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +21 -4
  30. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +5 -1
  31. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -1
  32. package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte +23 -0
  33. package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts +10 -0
  34. package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts.map +1 -0
  35. package/dist/svelte/__tests__/playground.test.js +66 -0
  36. package/dist/svelte/components/FieldPolicyControlPanel.svelte +28 -1
  37. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +3 -0
  38. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -1
  39. package/dist/svelte/components/FieldPolicyGearButton.svelte +8 -1
  40. package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -1
  41. package/dist/svelte/components/FieldPolicyGearProvider.svelte +34 -0
  42. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +3 -0
  43. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -1
  44. package/dist/svelte/components/FieldPolicySuggestionQueue.svelte +135 -0
  45. package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts +11 -0
  46. package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts.map +1 -0
  47. package/dist/svelte/components/FormHelp.svelte +16 -1
  48. package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -1
  49. package/dist/svelte/components/ObjectForm.svelte +91 -2
  50. package/dist/svelte/components/ObjectForm.svelte.d.ts +11 -1
  51. package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -1
  52. package/dist/svelte/context.svelte.d.ts +15 -0
  53. package/dist/svelte/context.svelte.d.ts.map +1 -1
  54. package/dist/svelte/context.svelte.js +15 -0
  55. package/dist/svelte/gear-context.svelte.d.ts +2 -0
  56. package/dist/svelte/gear-context.svelte.d.ts.map +1 -1
  57. package/dist/svelte/index.d.ts +6 -1
  58. package/dist/svelte/index.d.ts.map +1 -1
  59. package/dist/svelte/index.js +4 -1
  60. package/dist/svelte/playground/FieldPolicyFormPreview.svelte +18 -12
  61. package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -1
  62. package/dist/svelte/suggestions.d.ts +38 -0
  63. package/dist/svelte/suggestions.d.ts.map +1 -0
  64. package/dist/svelte/suggestions.js +68 -0
  65. package/dist/svelte/usage-capture.d.ts +63 -0
  66. package/dist/svelte/usage-capture.d.ts.map +1 -0
  67. package/dist/svelte/usage-capture.js +61 -0
  68. package/dist/types.d.ts +40 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/types.js +7 -1
  71. package/dist/types.js.map +1 -1
  72. package/dist/usage-learning.d.ts +201 -0
  73. package/dist/usage-learning.d.ts.map +1 -0
  74. package/dist/usage-schedules.d.ts +110 -0
  75. package/dist/usage-schedules.d.ts.map +1 -0
  76. package/dist/users-module.d.ts +30 -0
  77. package/dist/users-module.d.ts.map +1 -0
  78. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -1,1211 +1,1202 @@
1
- import { APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES } from "./types.js";
1
+ import { C as setCachedFieldPolicy, D as ensureFieldPolicyPermissionsRegistered, E as PERSONALIZE_FIELD_POLICY_PERMISSION, O as __exportAll, S as invalidateFieldPolicyCache, T as MANAGE_FIELD_POLICY_PERMISSION, _ as requireRegisteredObject, a as buildCodeSeedDelta, b as getCachedFieldPolicy, c as getCodeSeedGroup, d as isPolicyAddressableField, f as isRequiredField, g as isUsableRequiredDefault, h as isTransientField, i as assertDefaultValueMatchesFieldType, l as getFieldReadPermission, m as isStorableReferenceId, o as buildCodeSeedVisibility, p as isSensitiveField, r as FieldPolicy, s as getCodeDefault, t as FieldPolicyCollection, u as getObjectFieldMap, v as sanitizeFieldUIHints, w as FIELD_POLICY_PERMISSION_DEFINITIONS, x as getFieldPolicyCacheTtlMs, y as clearFieldPolicyCache } from "./chunks/FieldPolicyCollection--RxukfCX.js";
2
+ import { APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_SUGGESTION_KINDS, FIELD_POLICY_SUGGESTION_STATUSES, FIELD_POLICY_VISIBILITIES } from "./types.js";
2
3
  import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
3
- import { MembershipCollection, assertOperationPermission, checkOperationPermission, getCurrentSessionPermissionContext, registerPermissionDefinitions } from "@happyvertical/smrt-users";
4
+ import { assertOperationPermission } from "@happyvertical/smrt-users";
4
5
  import { TenantIsolationError, assertTenantReadAllowed, getCurrentTenant, isSuperAdminBypass, tenantId } from "@happyvertical/smrt-tenancy";
5
6
  import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
6
- //#region \0rolldown/runtime.js
7
- var __defProp$2 = Object.defineProperty;
8
- var __exportAll = (all, no_symbols) => {
9
- let target = {};
10
- for (var name in all) __defProp$2(target, name, {
11
- get: all[name],
12
- enumerable: true
13
- });
14
- if (!no_symbols) __defProp$2(target, Symbol.toStringTag, { value: "Module" });
15
- return target;
16
- };
17
- //#endregion
18
7
  //#region src/__smrt-register__.ts
19
- ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-fields\",\"packageVersion\":\"0.40.61\",\"objects\":{\"@happyvertical/smrt-fields:FieldPolicyCollection\":{\"name\":\"fieldpolicycollection\",\"className\":\"FieldPolicyCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicyCollection\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldPolicyCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"policyAudit\":{\"name\":\"policyAudit\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyAuditSnapshot>\",\"isStatic\":false,\"isPublic\":true},\"getAppRows\":{\"name\":\"getAppRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"getTenantRows\":{\"name\":\"getTenantRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"tenantIds\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"Promise<Map<string, Map<string, FieldPolicy>>>\",\"isStatic\":false,\"isPublic\":true},\"getUserRows\":{\"name\":\"getUserRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"userId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"resolveBatch\":{\"name\":\"resolveBatch\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyBatchResult>\",\"isStatic\":false,\"isPublic\":true},\"getEditorState\":{\"name\":\"getEditorState\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyEditorStateResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\",\"resolveBatch\",\"getEditorState\",\"policyAudit\"],\"principalContext\":true,\"routes\":{\"resolveBatch\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"resolve\"},\"getEditorState\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"editor-state\"},\"policyAudit\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"policy-audit\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_policies\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldPolicy\",\"exportName\":\"FieldPolicyCollection\",\"collectionExportName\":\"FieldPolicyCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"13aa748c\"}},\"@happyvertical/smrt-fields:FieldPolicy\":{\"name\":\"fieldpolicy\",\"className\":\"FieldPolicy\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicy\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldPolicy.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"scopeType\":{\"type\":\"text\",\"required\":true,\"default\":\"app\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true}}},\"userId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}},\"scopeKey\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}},\"defaultValue\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"visibility\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"help\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"label\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"displayOrder\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"locked\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{\"nullable\":true}},\"updatedBy\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}}},\"methods\":{\"getDefaultValue\":{\"name\":\"getDefaultValue\",\"async\":false,\"parameters\":[],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"setDefaultValue\":{\"name\":\"setDefaultValue\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"any\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_policies\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\"],\"principalContext\":true},\"cli\":{\"include\":[\"create\",\"update\",\"delete\"],\"exclude\":[\"getDefaultValue\",\"setDefaultValue\"]},\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldPolicy\",\"collectionExportName\":\"FieldPolicyCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeType\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"scope_type\\\" TEXT NOT NULL DEFAULT 'app',\\n \\\"tenant_id\\\" UUID,\\n \\\"user_id\\\" UUID,\\n \\\"scope_key\\\" TEXT NOT NULL,\\n \\\"default_value\\\" TEXT,\\n \\\"visibility\\\" TEXT,\\n \\\"help\\\" TEXT,\\n \\\"label\\\" TEXT,\\n \\\"display_order\\\" INTEGER,\\n \\\"locked\\\" BOOLEAN,\\n \\\"updated_by\\\" UUID\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"scope_type\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"app\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"user_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"scope_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false},\"default_value\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"visibility\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"help\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"label\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"display_order\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"locked\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"updated_by\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"34b355cc\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\",\"@happyvertical/smrt-users\"]}"));
20
- //#endregion
21
- //#region src/permissions.ts
22
- var MANAGE_FIELD_POLICY_PERMISSION = "fields.policy.manage";
23
- var PERSONALIZE_FIELD_POLICY_PERMISSION = "fields.policy.personalize";
24
- var FIELD_POLICY_PERMISSION_DEFINITIONS = [{
25
- slug: MANAGE_FIELD_POLICY_PERMISSION,
26
- category: "fields",
27
- name: "Manage Field Policies",
28
- description: "Manage application and tenant field-policy overrides"
29
- }, {
30
- slug: PERSONALIZE_FIELD_POLICY_PERMISSION,
31
- category: "fields",
32
- name: "Personalize Field Policies",
33
- description: "Manage the caller’s own field-policy overrides"
34
- }];
35
- var registered = false;
36
- function ensureFieldPolicyPermissionsRegistered() {
37
- if (registered) return;
38
- registered = true;
39
- registerPermissionDefinitions(FIELD_POLICY_PERMISSION_DEFINITIONS);
40
- }
8
+ ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-fields\",\"packageVersion\":\"0.40.63\",\"objects\":{\"@happyvertical/smrt-fields:FieldUsageLearningAgent\":{\"name\":\"fieldusagelearningagent\",\"className\":\"FieldUsageLearningAgent\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageLearningAgent\",\"collection\":\"fieldusagelearningagents\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/usage-learning.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"runUsageMaintenance\":{\"name\":\"runUsageMaintenance\",\"async\":true,\"parameters\":[{\"name\":\"args\",\"type\":\"Record<string>\",\"optional\":true}],\"returnType\":\"Promise<FieldUsageMaintenanceSummary>\",\"isStatic\":false,\"isPublic\":true},\"runSuggestionGeneration\":{\"name\":\"runSuggestionGeneration\",\"async\":true,\"parameters\":[{\"name\":\"args\",\"type\":\"Record<string>\",\"optional\":true}],\"returnType\":\"Promise<FieldUsageSuggestionRunSummary>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_usage_learning_agents\",\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldUsageLearningAgent\",\"collectionExportName\":\"FieldUsageLearningAgentCollection\",\"schema\":{\"tableName\":\"_smrt_field_usage_learning_agents\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_learning_agents\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_learning_agents_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_learning_agents_slug_context_idx\",\"columns\":[\"slug\",\"context\"],\"unique\":true}],\"version\":\"183ee1d8\"}},\"@happyvertical/smrt-fields:FieldPolicyCollection\":{\"name\":\"fieldpolicycollection\",\"className\":\"FieldPolicyCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicyCollection\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldPolicyCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"policyAudit\":{\"name\":\"policyAudit\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyAuditSnapshot>\",\"isStatic\":false,\"isPublic\":true},\"getAppRows\":{\"name\":\"getAppRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"getTenantRows\":{\"name\":\"getTenantRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"tenantIds\",\"type\":\"string[]\",\"optional\":false}],\"returnType\":\"Promise<Map<string, Map<string, FieldPolicy>>>\",\"isStatic\":false,\"isPublic\":true},\"getUserRows\":{\"name\":\"getUserRows\",\"async\":true,\"parameters\":[{\"name\":\"objectRef\",\"type\":\"string\",\"optional\":false},{\"name\":\"userId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Map<string, FieldPolicy>>\",\"isStatic\":false,\"isPublic\":true},\"resolveBatch\":{\"name\":\"resolveBatch\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyBatchResult>\",\"isStatic\":false,\"isPublic\":true},\"getEditorState\":{\"name\":\"getEditorState\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldPolicyEditorStateResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\",\"resolveBatch\",\"getEditorState\",\"policyAudit\"],\"principalContext\":true,\"routes\":{\"resolveBatch\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"resolve\"},\"getEditorState\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"editor-state\"},\"policyAudit\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"policy-audit\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_policies\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldPolicy\",\"exportName\":\"FieldPolicyCollection\",\"collectionExportName\":\"FieldPolicyCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"13aa748c\"}},\"@happyvertical/smrt-fields:FieldPolicySuggestionCollection\":{\"name\":\"fieldpolicysuggestioncollection\",\"className\":\"FieldPolicySuggestionCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicySuggestionCollection\",\"collection\":\"fieldpolicysuggestions\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldPolicySuggestionCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"pendingSuggestions\":{\"name\":\"pendingSuggestions\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<PendingFieldPolicySuggestionsResult>\",\"isStatic\":false,\"isPublic\":true},\"acceptSuggestion\":{\"name\":\"acceptSuggestion\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<AcceptFieldPolicySuggestionResult>\",\"isStatic\":false,\"isPublic\":true},\"dismissSuggestion\":{\"name\":\"dismissSuggestion\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<DismissFieldPolicySuggestionResult>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"api\":{\"include\":[\"pendingSuggestions\",\"acceptSuggestion\",\"dismissSuggestion\"],\"principalContext\":true,\"routes\":{\"pendingSuggestions\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"pending\"},\"acceptSuggestion\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"accept\"},\"dismissSuggestion\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"dismiss\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_policy_suggestions\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldPolicySuggestion\",\"exportName\":\"FieldPolicySuggestionCollection\",\"collectionExportName\":\"FieldPolicySuggestionCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_policy_suggestions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policy_suggestions\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"_smrt_field_policy_suggestions_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policy_suggestions_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"unique\":true}],\"version\":\"853df86d\"}},\"@happyvertical/smrt-fields:FieldUsageCounterCollection\":{\"name\":\"fieldusagecountercollection\",\"className\":\"FieldUsageCounterCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageCounterCollection\",\"collection\":\"fieldusagecounters\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldUsageCounterCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"reportUsage\":{\"name\":\"reportUsage\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":true}],\"returnType\":\"Promise<FieldUsageReportResult>\",\"isStatic\":false,\"isPublic\":true},\"listWindow\":{\"name\":\"listWindow\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":false}],\"returnType\":\"Promise<FieldUsageCounter[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"api\":{\"include\":[\"reportUsage\"],\"principalContext\":true,\"routes\":{\"reportUsage\":{\"scope\":\"collection\",\"method\":\"POST\",\"path\":\"report\"}}},\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_usage_counters\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldUsageCounter\",\"exportName\":\"FieldUsageCounterCollection\",\"collectionExportName\":\"FieldUsageCounterCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_usage_counters\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_counters\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_counters_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_counters_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"unique\":true}],\"version\":\"8bd9beae\"}},\"@happyvertical/smrt-fields:FieldUsageReportReceiptCollection\":{\"name\":\"fieldusagereportreceiptcollection\",\"className\":\"FieldUsageReportReceiptCollection\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageReportReceiptCollection\",\"collection\":\"fieldusagereportreceipts\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/collections/FieldUsageCounterCollection.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{},\"methods\":{\"claim\":{\"name\":\"claim\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"object\",\"optional\":false}],\"returnType\":\"Promise<boolean>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"conflictColumns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"api\":false,\"cli\":false,\"mcp\":false,\"tableName\":\"_smrt_field_usage_report_receipts\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"FieldUsageReportReceipt\",\"exportName\":\"FieldUsageReportReceiptCollection\",\"collectionExportName\":\"FieldUsageReportReceiptCollectionCollection\",\"schema\":{\"tableName\":\"_smrt_field_usage_report_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_report_receipts\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_report_receipts_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_report_receipts_tenant_id_user_id_idx\",\"columns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"unique\":true}],\"version\":\"2e77b775\"}},\"@happyvertical/smrt-fields:FieldPolicy\":{\"name\":\"fieldpolicy\",\"className\":\"FieldPolicy\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicy\",\"collection\":\"fieldpolicies\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldPolicy.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"scopeType\":{\"type\":\"text\",\"required\":true,\"default\":\"app\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true}}},\"userId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}},\"scopeKey\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}},\"defaultValue\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"visibility\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"help\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"label\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"displayOrder\":{\"type\":\"integer\",\"required\":false,\"_meta\":{\"nullable\":true}},\"locked\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{\"nullable\":true}},\"updatedBy\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}}},\"methods\":{\"getDefaultValue\":{\"name\":\"getDefaultValue\",\"async\":false,\"parameters\":[],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"setDefaultValue\":{\"name\":\"setDefaultValue\",\"async\":false,\"parameters\":[{\"name\":\"value\",\"type\":\"any\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_policies\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"api\":{\"include\":[\"create\",\"update\",\"delete\"],\"principalContext\":true},\"cli\":{\"include\":[\"create\",\"update\",\"delete\"],\"exclude\":[\"getDefaultValue\",\"setDefaultValue\"]},\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldPolicy\",\"collectionExportName\":\"FieldPolicyCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeType\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"scopeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_policies\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policies\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"scope_type\\\" TEXT NOT NULL DEFAULT 'app',\\n \\\"tenant_id\\\" UUID,\\n \\\"user_id\\\" UUID,\\n \\\"scope_key\\\" TEXT NOT NULL,\\n \\\"default_value\\\" TEXT,\\n \\\"visibility\\\" TEXT,\\n \\\"help\\\" TEXT,\\n \\\"label\\\" TEXT,\\n \\\"display_order\\\" INTEGER,\\n \\\"locked\\\" BOOLEAN,\\n \\\"updated_by\\\" UUID\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"scope_type\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"app\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"user_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"scope_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false},\"default_value\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"visibility\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"help\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"label\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"display_order\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"locked\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"updated_by\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_policies_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policies_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"scope_type\",\"scope_key\"],\"unique\":true}],\"version\":\"34b355cc\"}},\"@happyvertical/smrt-fields:FieldPolicySuggestion\":{\"name\":\"fieldpolicysuggestion\",\"className\":\"FieldPolicySuggestion\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldPolicySuggestion\",\"collection\":\"fieldpolicysuggestions\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldPolicySuggestion.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":true,\"autoPopulate\":true,\"nullable\":false}}},\"kind\":{\"type\":\"text\",\"required\":true,\"default\":\"promote\",\"_meta\":{\"required\":true}},\"proposedValue\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"evidence\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"status\":{\"type\":\"text\",\"required\":true,\"default\":\"pending\",\"_meta\":{\"required\":true}},\"activeKey\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}},\"cooldownUntil\":{\"type\":\"datetime\",\"required\":false,\"_meta\":{\"nullable\":true}},\"decidedBy\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\",\"_meta\":{\"nullable\":true}},\"decidedAt\":{\"type\":\"datetime\",\"required\":false,\"_meta\":{\"nullable\":true}}},\"methods\":{\"getEvidence\":{\"name\":\"getEvidence\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string>\",\"isStatic\":false,\"isPublic\":true},\"setEvidence\":{\"name\":\"setEvidence\",\"async\":false,\"parameters\":[{\"name\":\"evidence\",\"type\":\"Record<string>\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getProposedValue\":{\"name\":\"getProposedValue\",\"async\":false,\"parameters\":[],\"returnType\":\"any\",\"isStatic\":false,\"isPublic\":true},\"toSuggestionData\":{\"name\":\"toSuggestionData\",\"async\":false,\"parameters\":[],\"returnType\":\"FieldPolicySuggestionData\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_policy_suggestions\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldPolicySuggestion\",\"collectionExportName\":\"FieldPolicySuggestionCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"tenantId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"kind\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"status\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"activeKey\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_policy_suggestions\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_policy_suggestions\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"tenant_id\\\" UUID NOT NULL,\\n \\\"kind\\\" TEXT NOT NULL DEFAULT 'promote',\\n \\\"proposed_value\\\" TEXT,\\n \\\"evidence\\\" TEXT,\\n \\\"status\\\" TEXT NOT NULL DEFAULT 'pending',\\n \\\"active_key\\\" TEXT NOT NULL,\\n \\\"cooldown_until\\\" TIMESTAMP,\\n \\\"decided_by\\\" UUID,\\n \\\"decided_at\\\" TIMESTAMP\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":true,\"unique\":false},\"kind\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"promote\"},\"proposed_value\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"evidence\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"status\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"pending\"},\"active_key\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false},\"cooldown_until\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"decided_by\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"decided_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_policy_suggestions_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_policy_suggestions_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"kind\",\"active_key\"],\"unique\":true}],\"version\":\"1bc8c6e5\"}},\"@happyvertical/smrt-fields:FieldUsageCounter\":{\"name\":\"fieldusagecounter\",\"className\":\"FieldUsageCounter\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageCounter\",\"collection\":\"fieldusagecounters\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldUsageCounter.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"tenantId\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":true,\"autoPopulate\":true,\"nullable\":false}}},\"period\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"submissionCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"setCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"distinctUserCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"distinctUserIds\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"distinctUsersOverflowed\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"valueHistogram\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"nullable\":true}},\"valueHistogramOverflowed\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}}},\"methods\":{\"getDistinctUserIds\":{\"name\":\"getDistinctUserIds\",\"async\":false,\"parameters\":[],\"returnType\":\"string[]\",\"isStatic\":false,\"isPublic\":true},\"addDistinctUser\":{\"name\":\"addDistinctUser\",\"async\":false,\"parameters\":[{\"name\":\"userId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"getValueHistogram\":{\"name\":\"getValueHistogram\",\"async\":false,\"parameters\":[],\"returnType\":\"Record<string, number>\",\"isStatic\":false,\"isPublic\":true},\"recordHistogramSample\":{\"name\":\"recordHistogramSample\",\"async\":false,\"parameters\":[{\"name\":\"key\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"void\",\"isStatic\":false,\"isPublic\":true},\"isLegacyBucket\":{\"name\":\"isLegacyBucket\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise\",\"isStatic\":false,\"isPublic\":true},\"delete\":{\"name\":\"delete\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"_smrt_field_usage_counters\",\"conflictColumns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldUsageCounter\",\"collectionExportName\":\"FieldUsageCounterCollection\",\"validationRules\":[{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"tenantId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"period\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_usage_counters\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_counters\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"tenant_id\\\" UUID NOT NULL,\\n \\\"period\\\" TEXT NOT NULL DEFAULT '',\\n \\\"submission_count\\\" INTEGER,\\n \\\"set_count\\\" INTEGER,\\n \\\"distinct_user_count\\\" INTEGER,\\n \\\"distinct_user_ids\\\" TEXT,\\n \\\"distinct_users_overflowed\\\" BOOLEAN,\\n \\\"value_histogram\\\" TEXT,\\n \\\"value_histogram_overflowed\\\" BOOLEAN\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":true,\"unique\":false},\"period\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"submission_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"set_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"distinct_user_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"distinct_user_ids\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"distinct_users_overflowed\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"value_histogram\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"value_histogram_overflowed\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"_smrt_field_usage_counters_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_counters_object_ref_field_name_idx\",\"columns\":[\"object_ref\",\"field_name\",\"tenant_id\",\"period\"],\"unique\":true}],\"version\":\"46fcebc6\"}},\"@happyvertical/smrt-fields:FieldUsageReportReceipt\":{\"name\":\"fieldusagereportreceipt\",\"className\":\"FieldUsageReportReceipt\",\"qualifiedName\":\"@happyvertical/smrt-fields:FieldUsageReportReceipt\",\"collection\":\"fieldusagereportreceipts\",\"filePath\":\"/home/runner/_work/smrt/smrt/packages/fields/src/models/FieldUsageReportReceipt.ts\",\"packageName\":\"@happyvertical/smrt-fields\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"sqlType\":\"UUID\",\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":true,\"autoPopulate\":true,\"nullable\":false}}},\"userId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-users:User\"},\"objectRef\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"fieldName\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}},\"period\":{\"type\":\"text\",\"required\":true,\"default\":\"\",\"_meta\":{\"required\":true}}},\"methods\":{},\"decoratorConfig\":{\"tableName\":\"_smrt_field_usage_report_receipts\",\"conflictColumns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"api\":{\"include\":[]},\"cli\":false,\"mcp\":{\"include\":[]}},\"extends\":\"SmrtObject\",\"exportName\":\"FieldUsageReportReceipt\",\"collectionExportName\":\"FieldUsageReportReceiptCollection\",\"validationRules\":[{\"field\":\"tenantId\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"objectRef\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"fieldName\",\"rule\":\"required\",\"fieldType\":\"text\"},{\"field\":\"period\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"_smrt_field_usage_report_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"_smrt_field_usage_report_receipts\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID NOT NULL,\\n \\\"user_id\\\" UUID,\\n \\\"object_ref\\\" TEXT NOT NULL DEFAULT '',\\n \\\"field_name\\\" TEXT NOT NULL DEFAULT '',\\n \\\"period\\\" TEXT NOT NULL DEFAULT ''\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":true,\"unique\":false},\"user_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"object_ref\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"field_name\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"},\"period\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false,\"default\":\"\"}},\"indexes\":[{\"name\":\"_smrt_field_usage_report_receipts_id_idx\",\"columns\":[\"id\"]},{\"name\":\"_smrt_field_usage_report_receipts_tenant_id_user_id_idx\",\"columns\":[\"tenant_id\",\"user_id\",\"object_ref\",\"field_name\",\"period\"],\"unique\":true}],\"version\":\"c3119047\"}}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\",\"@happyvertical/smrt-users\"]}"));
41
9
  //#endregion
42
- //#region src/cache.ts
43
- var FIELD_POLICY_CACHE_TTL_MS = 3e4;
44
- var fieldPolicyCache = /* @__PURE__ */ new Map();
45
- var dbInstanceIds = /* @__PURE__ */ new WeakMap();
46
- var nextDbId = 1;
47
- function getDbNamespace(db) {
48
- if (!db) return "no-db";
49
- if (typeof db === "string") return `db:${db}`;
50
- if (typeof db === "object") {
51
- const dbObject = db;
52
- if (typeof dbObject.query === "function") {
53
- if (!dbInstanceIds.has(dbObject)) dbInstanceIds.set(dbObject, `db-instance:${nextDbId++}`);
54
- const namespace = dbInstanceIds.get(dbObject);
55
- if (namespace) return namespace;
56
- return "db-instance:unknown";
57
- }
58
- try {
59
- return `db-config:${JSON.stringify(dbObject)}`;
60
- } catch {
61
- return "db-config:opaque";
62
- }
63
- }
64
- return "db:unknown";
65
- }
66
- var hierarchyLoaderIds = /* @__PURE__ */ new WeakMap();
67
- var nextLoaderId = 1;
68
- function getHierarchyLoaderNamespace(loader) {
69
- if (typeof loader !== "function") return "loader:default";
70
- const key = loader;
71
- if (!hierarchyLoaderIds.has(key)) hierarchyLoaderIds.set(key, `loader:${nextLoaderId++}`);
72
- return hierarchyLoaderIds.get(key) ?? "loader:unknown";
73
- }
74
- function buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader) {
75
- return `${getDbNamespace(db)}::${objectRef}::${tenantId ?? "app"}::${userId ?? "user:none"}::${getHierarchyLoaderNamespace(hierarchyLoader)}`;
76
- }
77
- function getFieldPolicyCacheTtlMs() {
78
- return FIELD_POLICY_CACHE_TTL_MS;
79
- }
80
- function getCachedFieldPolicy(objectRef, tenantId, userId, db, hierarchyLoader) {
81
- const cacheKey = buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader);
82
- const cached = fieldPolicyCache.get(cacheKey);
83
- if (!cached) return null;
84
- if (cached.expiresAt <= Date.now()) {
85
- fieldPolicyCache.delete(cacheKey);
86
- return null;
87
- }
88
- return cached.value;
89
- }
90
- function setCachedFieldPolicy(objectRef, tenantId, userId, db, value, hierarchyLoader) {
91
- fieldPolicyCache.set(buildCacheKey(objectRef, tenantId, userId, db, hierarchyLoader), {
92
- expiresAt: Date.now() + FIELD_POLICY_CACHE_TTL_MS,
93
- value
94
- });
95
- }
96
- function invalidateFieldPolicyCache(objectRef, db) {
97
- const keyPrefix = `${getDbNamespace(db)}::${objectRef}::`;
98
- for (const cacheKey of fieldPolicyCache.keys()) if (cacheKey.startsWith(keyPrefix)) fieldPolicyCache.delete(cacheKey);
99
- }
100
- function clearFieldPolicyCache() {
101
- fieldPolicyCache.clear();
102
- }
103
- //#endregion
104
- //#region src/field-definitions.ts
105
- function requireRegisteredObject(objectRef) {
106
- if (!objectRef?.includes(":")) throw new Error(`Field policy objectRef must be a qualified class name ("@package/name:ClassName"), got "${objectRef}"`);
107
- if (!ObjectRegistry.getClassByQualifiedName(objectRef)) throw new Error(`Unknown field policy objectRef "${objectRef}": no class with that qualified name is registered`);
108
- }
109
- async function getObjectFieldMap(objectRef) {
110
- requireRegisteredObject(objectRef);
111
- return ObjectRegistry.getAllFields(objectRef);
112
- }
113
- function sanitizeFieldUIHints(value) {
114
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
115
- const raw = value;
116
- const ui = {
117
- ...typeof raw.basic === "boolean" ? { basic: raw.basic } : {},
118
- ...typeof raw.group === "string" ? { group: raw.group } : {},
119
- ...typeof raw.order === "number" && Number.isFinite(raw.order) ? { order: raw.order } : {},
120
- ...typeof raw.locked === "boolean" ? { locked: raw.locked } : {}
121
- };
122
- return Object.keys(ui).length > 0 ? ui : void 0;
123
- }
124
- function isSensitiveField(field) {
125
- return field.sensitive === true || field._meta?.sensitive === true;
126
- }
127
- function getFieldReadPermission(field) {
128
- if (typeof field.readPermission === "string") return field.readPermission;
129
- const metaPermission = field._meta?.readPermission;
130
- return typeof metaPermission === "string" ? metaPermission : void 0;
131
- }
132
- function isTransientField(field) {
133
- return field.transient === true || field._meta?.transient === true;
134
- }
135
- function isPolicyAddressableField(field) {
136
- return field._meta?.__smrtSystemField !== true && field.type !== "oneToMany" && field.type !== "manyToMany" && field.type !== "meta";
137
- }
138
- function isRequiredField(field) {
139
- if (field.nullable === true || field._meta?.nullable === true) return false;
140
- return field.required === true || field._meta?.required === true;
141
- }
142
- function getCodeDefault(field) {
143
- if (field.default !== void 0) return { value: field.default };
144
- if (field._meta?.default !== void 0) return { value: field._meta.default };
145
- }
146
- function isUsableRequiredDefault(defaultBox) {
147
- if (!defaultBox) return false;
148
- return defaultBox.value !== null && defaultBox.value !== "";
149
- }
150
- function buildCodeSeedVisibility(fields) {
151
- let hasBasicMarkers = false;
152
- const hints = /* @__PURE__ */ new Map();
153
- for (const [name, field] of fields) {
154
- const ui = sanitizeFieldUIHints(field._meta?.ui);
155
- hints.set(name, ui);
156
- if (ui?.basic === true) hasBasicMarkers = true;
157
- }
158
- const visibility = /* @__PURE__ */ new Map();
159
- for (const [name] of fields) {
160
- const ui = hints.get(name);
161
- if (ui?.basic === true) visibility.set(name, "basic");
162
- else if (ui?.basic === false || hasBasicMarkers) visibility.set(name, "advanced");
163
- else visibility.set(name, "basic");
164
- }
165
- return visibility;
166
- }
167
- function buildCodeSeedDelta(field, visibility) {
168
- const ui = sanitizeFieldUIHints(field._meta?.ui);
169
- const help = typeof field.description === "string" ? field.description : typeof field._meta?.description === "string" ? field._meta.description : void 0;
170
- const codeDefault = getCodeDefault(field);
171
- return {
172
- ...codeDefault ? { default: codeDefault } : {},
173
- visibility,
174
- ...help !== void 0 ? { help } : {},
175
- ...ui?.order !== void 0 ? { order: ui.order } : {},
176
- ...ui?.locked === true ? { locked: true } : {}
177
- };
178
- }
179
- function getCodeSeedGroup(field) {
180
- return sanitizeFieldUIHints(field._meta?.ui)?.group ?? null;
181
- }
182
- var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
183
- function usesTextIdStorage(field) {
184
- return field.idType === "text" || field._meta?.idType === "text";
185
- }
186
- function assertDefaultValueMatchesFieldType(objectRef, fieldName, field, value) {
187
- const label = `Field policy default for "${objectRef}.${fieldName}"`;
188
- if (value === null) {
189
- if (isRequiredField(field)) throw new Error(`${label} may not be null: the field is required, so a null default is never a usable default`);
190
- return;
191
- }
192
- switch (field.type) {
193
- case "text":
194
- if (typeof value !== "string") throw new Error(`${label} must be a string (field type "text")`);
195
- return;
196
- case "integer":
197
- if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label} must be an integer (field type "integer")`);
198
- return;
199
- case "decimal":
200
- if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a finite number (field type "decimal")`);
201
- return;
202
- case "boolean":
203
- if (typeof value !== "boolean") throw new Error(`${label} must be a boolean (field type "boolean")`);
204
- return;
205
- case "datetime":
206
- if (typeof value !== "string" || Number.isNaN(Date.parse(value))) throw new Error(`${label} must be a date-parseable string (field type "datetime")`);
207
- return;
208
- case "json": return;
209
- case "foreignKey":
210
- case "crossPackageRef":
211
- if (typeof value !== "string") throw new Error(`${label} must be a string id (field type "${field.type}")`);
212
- if (!usesTextIdStorage(field) && !UUID_PATTERN.test(value)) throw new Error(`${label} must be a UUID string (the reference column stores native UUIDs; declare idType 'text' on the field to store arbitrary ids)`);
213
- return;
214
- default: throw new Error(`${label} is not supported: defaults cannot target "${String(field.type)}" fields`);
215
- }
10
+ //#region src/deterministic-id.ts
11
+ async function deterministicFieldsUuid(parts) {
12
+ const bytes = new TextEncoder().encode(JSON.stringify(parts));
13
+ const uuid = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)).slice(0, 16);
14
+ uuid[6] = uuid[6] & 15 | 80;
15
+ uuid[8] = uuid[8] & 63 | 128;
16
+ const hex = Array.from(uuid, (byte) => byte.toString(16).padStart(2, "0")).join("");
17
+ return [
18
+ hex.slice(0, 8),
19
+ hex.slice(8, 12),
20
+ hex.slice(12, 16),
21
+ hex.slice(16, 20),
22
+ hex.slice(20)
23
+ ].join("-");
216
24
  }
217
25
  //#endregion
218
- //#region src/models/FieldPolicy.ts
219
- var __defProp$1 = Object.defineProperty;
220
- var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
221
- var __decorateClass$1 = (decorators, target, key, kind) => {
222
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
26
+ //#region src/models/FieldPolicySuggestion.ts
27
+ var __defProp$5 = Object.defineProperty;
28
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
29
+ var __decorateClass$5 = (decorators, target, key, kind) => {
30
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
223
31
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
224
- if (kind && result) __defProp$1(target, key, result);
32
+ if (kind && result) __defProp$5(target, key, result);
225
33
  return result;
226
34
  };
227
- ensureFieldPolicyPermissionsRegistered();
228
- var FIELD_POLICY_SCOPE_DENIAL_MESSAGE = "Field policy scope is not permitted for this principal.";
229
- function getFieldPolicyAmbientContext() {
230
- const tenantContext = getCurrentTenant();
231
- if (tenantContext) return {
232
- permissions: tenantContext.permissions,
233
- superAdminBypass: isSuperAdminBypass(),
234
- tenantId: tenantContext.tenantId,
235
- userId: tenantContext.userId
236
- };
237
- const sessionContext = getCurrentSessionPermissionContext();
238
- if (!sessionContext?.tenantId) return;
239
- return {
240
- permissions: sessionContext.permissionSet,
241
- superAdminBypass: sessionContext.superAdminBypass,
242
- tenantId: sessionContext.tenantId,
243
- userId: sessionContext.userId ?? void 0
244
- };
245
- }
246
- var FieldPolicyAuthorizationError = class extends Error {
247
- code = "permission_denied";
248
- status = 403;
249
- constructor(message) {
250
- super(message);
251
- this.name = "FieldPolicyAuthorizationError";
252
- }
253
- };
254
- var FieldPolicyTenantIsolationError = class extends TenantIsolationError {
255
- publicMessage = FIELD_POLICY_SCOPE_DENIAL_MESSAGE;
256
- status = 403;
257
- constructor(diagnosticMessage, details) {
258
- super(FIELD_POLICY_SCOPE_DENIAL_MESSAGE);
259
- Object.defineProperty(this, "cause", {
260
- configurable: true,
261
- enumerable: false,
262
- value: new Error(diagnosticMessage),
263
- writable: false
264
- });
265
- Object.defineProperty(this, "diagnostic", {
266
- configurable: true,
267
- enumerable: false,
268
- value: {
269
- ...details,
270
- message: diagnosticMessage
271
- },
272
- writable: false
273
- });
274
- }
275
- };
276
- var FieldPolicy = class extends SmrtObject {
35
+ var ACTIVE_SUGGESTION_KEY = "active";
36
+ var FieldPolicySuggestion = class extends SmrtObject {
277
37
  objectRef = "";
278
38
  fieldName = "";
279
- scopeType = "app";
280
- tenantId = null;
281
- userId = null;
282
- scopeKey = "";
283
- defaultValue = null;
284
- visibility = null;
285
- help = null;
286
- label = null;
287
- displayOrder = null;
288
- locked = null;
289
- updatedBy = null;
39
+ tenantId;
40
+ kind = "promote";
41
+ proposedValue = null;
42
+ evidence = "{}";
43
+ status = "pending";
44
+ activeKey = ACTIVE_SUGGESTION_KEY;
45
+ cooldownUntil = null;
46
+ decidedBy = null;
47
+ decidedAt = null;
290
48
  constructor(options = {}) {
291
49
  super(options);
292
50
  if (options.objectRef !== void 0) this.objectRef = options.objectRef;
293
51
  if (options.fieldName !== void 0) this.fieldName = options.fieldName;
294
- if (options.scopeType !== void 0) this.scopeType = options.scopeType;
295
52
  if (options.tenantId !== void 0) this.tenantId = options.tenantId;
296
- if (options.userId !== void 0) this.userId = options.userId;
297
- if (options.defaultValue !== void 0 && options.defaultValueRaw !== void 0) throw new Error("FieldPolicy accepts either defaultValue (already JSON-encoded) or defaultValueRaw (a plain value to serialize), not both");
298
- if (options.defaultValueRaw !== void 0) this.setDefaultValue(options.defaultValueRaw);
299
- else if (options.defaultValue !== void 0) this.defaultValue = options.defaultValue;
300
- if (options.visibility !== void 0) this.visibility = options.visibility;
301
- if (options.help !== void 0) this.help = options.help;
302
- if (options.label !== void 0) this.label = options.label;
303
- if (options.displayOrder !== void 0) this.displayOrder = options.displayOrder;
304
- if (options.locked !== void 0) this.locked = options.locked;
305
- if (options.updatedBy !== void 0) this.updatedBy = options.updatedBy;
53
+ if (options.kind !== void 0) this.kind = options.kind;
54
+ if (options.proposedValue !== void 0) this.proposedValue = options.proposedValue;
55
+ if (options.evidence !== void 0) this.evidence = options.evidence;
56
+ if (options.status !== void 0) this.status = options.status;
57
+ if (options.cooldownUntil !== void 0) this.cooldownUntil = options.cooldownUntil;
58
+ if (options.decidedBy !== void 0) this.decidedBy = options.decidedBy;
59
+ if (options.decidedAt !== void 0) this.decidedAt = options.decidedAt;
306
60
  }
307
- /** Parse the stored JSON default. `undefined` = no stored default (inherit). */
308
- getDefaultValue() {
309
- if (this.defaultValue === null || this.defaultValue === void 0) return;
61
+ /** Parse the stored evidence object (guarded; junk parses as empty). */
62
+ getEvidence() {
310
63
  try {
311
- return JSON.parse(this.defaultValue);
64
+ const parsed = JSON.parse(this.evidence);
65
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
312
66
  } catch {
313
- return;
67
+ return {};
314
68
  }
315
69
  }
316
- /** Serialize a default value; `undefined` clears the override (inherit). */
317
- setDefaultValue(value) {
318
- this.defaultValue = value === void 0 ? null : JSON.stringify(value);
70
+ /** Serialize an evidence object into the stored JSON string. */
71
+ setEvidence(evidence) {
72
+ this.evidence = JSON.stringify(evidence);
319
73
  }
320
- async save() {
321
- this.attributeScopeToAmbientContext();
322
- const previousIdentity = await this.getPersistedIdentity();
323
- if (previousIdentity) {
324
- this.assertScopeOwnedByAmbientContext(previousIdentity, "save");
325
- await this.assertScopePermission(previousIdentity);
74
+ /** Parse the proposed value; `undefined` when none is stored. */
75
+ getProposedValue() {
76
+ if (this.proposedValue === null || this.proposedValue === void 0) return;
77
+ try {
78
+ return JSON.parse(this.proposedValue);
79
+ } catch {
80
+ return;
326
81
  }
327
- this.assertScopeOwnedByAmbientContext(this, "save");
328
- await this.assertScopePermission(this);
329
- const ambientContext = getFieldPolicyAmbientContext();
330
- if (ambientContext) this.updatedBy = ambientContext.userId ?? null;
331
- this.normalizeDefaultValueForPersistence();
332
- await this.validateFieldPolicy(previousIdentity);
333
- this.scopeKey = this.computeScopeKey();
334
- const identityChanged = previousIdentity && (previousIdentity.objectRef !== this.objectRef || previousIdentity.fieldName !== this.fieldName || previousIdentity.scopeType !== this.scopeType || previousIdentity.scopeKey !== this.scopeKey);
335
- const result = identityChanged && previousIdentity ? await this.saveAfterIdentityChange() : await super.save();
336
- if (identityChanged && previousIdentity) invalidateFieldPolicyCache(previousIdentity.objectRef, this.db);
337
- invalidateFieldPolicyCache(this.objectRef, this.db);
338
- return result;
339
82
  }
340
- async saveAfterIdentityChange() {
341
- if (typeof this.db.beginTransaction === "function") return this.saveAfterIdentityChangeInTransaction();
342
- return this.saveAfterIdentityChangeWithDeferredDelete();
83
+ /** Serialized row shape for the collection actions. */
84
+ toSuggestionData() {
85
+ return {
86
+ id: String(this.id),
87
+ objectRef: this.objectRef,
88
+ fieldName: this.fieldName,
89
+ tenantId: String(this.tenantId ?? ""),
90
+ kind: this.kind,
91
+ proposedValue: this.proposedValue ?? null,
92
+ evidence: this.getEvidence(),
93
+ status: this.status,
94
+ cooldownUntil: toIsoOrNull(this.cooldownUntil),
95
+ decidedBy: this.decidedBy ?? null,
96
+ decidedAt: toIsoOrNull(this.decidedAt)
97
+ };
343
98
  }
344
- async saveAfterIdentityChangeInTransaction() {
345
- const originalDb = this._db;
346
- const originalOptionsDb = this.options.db;
347
- const tx = await this.db.beginTransaction?.();
348
- if (!tx) return this.saveAfterIdentityChangeWithDeferredDelete();
349
- try {
350
- this._db = tx;
351
- this.options.db = tx;
352
- await super.delete();
353
- const result = await super.save();
354
- await tx.commit();
355
- return result;
356
- } catch (error) {
357
- try {
358
- await tx.rollback();
359
- } catch {}
360
- throw error;
361
- } finally {
362
- this._db = originalDb;
363
- this.options.db = originalOptionsDb;
364
- }
99
+ async save() {
100
+ await this.assertRowOwnedByAmbientContext("save");
101
+ await this.validateFieldPolicySuggestion();
102
+ this.applyActiveKey();
103
+ return super.save();
365
104
  }
366
- async saveAfterIdentityChangeWithDeferredDelete() {
367
- const previousId = this.id;
368
- if (!previousId) return super.save();
369
- const replacementId = crypto.randomUUID();
370
- let replacementSaved = false;
371
- this.id = replacementId;
372
- try {
373
- const result = await super.save();
374
- replacementSaved = true;
375
- await this.db.delete(this.tableName, { id: previousId });
376
- return result;
377
- } catch (error) {
378
- if (replacementSaved) try {
379
- await this.db.delete(this.tableName, { id: replacementId });
380
- } catch {}
381
- this.id = previousId;
382
- throw error;
105
+ /**
106
+ * Recompute the lifecycle-slot key: the shared sentinel while pending (so
107
+ * the unique index admits exactly one), the row's own id once settled (so
108
+ * history never competes for the slot and the freed slot allows a
109
+ * post-cool-down regeneration). A settled row that has not been persisted
110
+ * yet is assigned its id here — the key must be unique from the first write.
111
+ */
112
+ applyActiveKey() {
113
+ if (this.status === "pending") {
114
+ this.activeKey = ACTIVE_SUGGESTION_KEY;
115
+ return;
383
116
  }
117
+ if (!this.id) this.id = crypto.randomUUID();
118
+ this.activeKey = String(this.id);
384
119
  }
385
120
  async delete() {
386
- const persisted = await this.getPersistedIdentity();
387
- if (persisted) this.assertScopeOwnedByAmbientContext(persisted, "delete");
388
- await this.assertScopePermission(persisted ?? this);
389
- const objectRef = persisted?.objectRef ?? this.objectRef;
121
+ await this.assertRowOwnedByAmbientContext("delete");
390
122
  await super.delete();
391
- invalidateFieldPolicyCache(objectRef, this.db);
392
- if (this.objectRef && this.objectRef !== objectRef) invalidateFieldPolicyCache(this.objectRef, this.db);
393
123
  }
394
- async validateFieldPolicy(previousIdentity) {
395
- if (!this.objectRef || this.objectRef.trim() === "") throw new Error("FieldPolicy.objectRef is required");
396
- if (!this.fieldName || this.fieldName.trim() === "") throw new Error("FieldPolicy.fieldName is required");
397
- if (!FIELD_POLICY_SCOPE_TYPES.includes(this.scopeType)) throw new Error(`FieldPolicy.scopeType must be one of ${FIELD_POLICY_SCOPE_TYPES.join(", ")}; got "${this.scopeType}"`);
398
- if (this.visibility !== null && !FIELD_POLICY_VISIBILITIES.includes(this.visibility)) throw new Error(`FieldPolicy.visibility must be null or one of ${FIELD_POLICY_VISIBILITIES.join(", ")}; got "${this.visibility}"`);
399
- if (this.displayOrder !== null && (typeof this.displayOrder !== "number" || !Number.isInteger(this.displayOrder))) throw new Error("FieldPolicy.displayOrder must be null or an integer");
400
- this.validateScopeConsistency();
401
- this.validateTenantContextBoundary();
124
+ /**
125
+ * The "normal validation" the #1885 seam promises producers: registry-known
126
+ * field, policy-addressable, never sensitive/read-permission-gated/transient
127
+ * (those fields are count-only in usage data and get no suggestions), valid
128
+ * kind/status, and for `default` suggestions a JSON proposed value that
129
+ * type-checks against the manifest field type.
130
+ */
131
+ async validateFieldPolicySuggestion() {
132
+ if (!this.objectRef || this.objectRef.trim() === "") throw new Error("FieldPolicySuggestion.objectRef is required");
133
+ if (!this.fieldName || this.fieldName.trim() === "") throw new Error("FieldPolicySuggestion.fieldName is required");
134
+ if (!this.tenantId) throw new Error("FieldPolicySuggestion.tenantId is required");
135
+ if (!FIELD_POLICY_SUGGESTION_KINDS.includes(this.kind)) throw new Error(`FieldPolicySuggestion.kind must be one of ${FIELD_POLICY_SUGGESTION_KINDS.join(", ")}; got "${this.kind}"`);
136
+ if (!FIELD_POLICY_SUGGESTION_STATUSES.includes(this.status)) throw new Error(`FieldPolicySuggestion.status must be one of ${FIELD_POLICY_SUGGESTION_STATUSES.join(", ")}; got "${this.status}"`);
402
137
  const fieldDef = (await getObjectFieldMap(this.objectRef)).get(this.fieldName);
403
138
  if (!fieldDef) throw new Error(`Unknown field "${this.fieldName}" on "${this.objectRef}"`);
404
- if (fieldDef._meta?.__smrtSystemField === true) throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is a framework system field and is not policy-addressable`);
405
- if (fieldDef.type === "oneToMany" || fieldDef.type === "manyToMany") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is a relationship pseudo-field and is not policy-addressable`);
406
- if (fieldDef.type === "meta") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is STI meta storage and is not policy-addressable`);
407
- if (this.locked !== null && this.scopeType === "user") throw new Error("FieldPolicy.locked may only be set on org rows (app or tenant scope)");
408
- if (this.defaultValue !== null) {
409
- this.validateDefaultAgainstSecurityRail(fieldDef);
410
- const parsed = this.parseDefaultValueOrThrow();
139
+ if (fieldDef._meta?.__smrtSystemField === true || fieldDef.type === "oneToMany" || fieldDef.type === "manyToMany" || fieldDef.type === "meta") throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is not policy-addressable, so it cannot carry a suggestion`);
140
+ if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) throw new Error(`Field "${this.fieldName}" on "${this.objectRef}" is sensitive, read-permission-gated, or transient; usage data for it is count-only and it cannot carry a suggestion`);
141
+ if (this.kind === "default") {
142
+ if (this.proposedValue === null) throw new Error("FieldPolicySuggestion of kind 'default' requires a proposedValue");
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(this.proposedValue);
146
+ } catch (error) {
147
+ throw new Error(`FieldPolicySuggestion.proposedValue is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
148
+ }
411
149
  assertDefaultValueMatchesFieldType(this.objectRef, this.fieldName, fieldDef, parsed);
412
- }
413
- const demotesRequiredField = (this.visibility === "advanced" || this.visibility === "hidden") && isRequiredField(fieldDef);
414
- const ownDefault = demotesRequiredField && this.defaultValue !== null ? { value: this.parseDefaultValueOrThrow() } : void 0;
415
- const needsOrgDefault = demotesRequiredField && !isUsableRequiredDefault(ownDefault);
416
- const needsLockCheck = this.scopeType === "user";
417
- if (needsOrgDefault || needsLockCheck) {
418
- const orgPolicy = await this.resolveOrgTierFieldPolicy(previousIdentity);
419
- if (needsOrgDefault) {
420
- if (!isUsableRequiredDefault(orgPolicy?.hasDefault ? { value: orgPolicy.defaultValue } : void 0)) throw new Error(`Cannot set visibility "${this.visibility}" for required field "${this.objectRef}.${this.fieldName}": no resolved default exists at or below this layer`);
150
+ } else if (this.proposedValue !== null) throw new Error("FieldPolicySuggestion of kind 'promote' must not carry a proposedValue");
151
+ }
152
+ /**
153
+ * Tenant write boundary (the FieldPolicy posture): inside a non-bypass
154
+ * tenant context a caller may only touch its own tenant's suggestions —
155
+ * checked against BOTH the in-memory scope and, for persisted rows, the
156
+ * PERSISTED tenant (a foreign row cannot be re-scoped into the caller's
157
+ * tenant). Trusted execution (no context / bypass) is exempt — that is what
158
+ * lets the scheduled generation job and platform flows operate.
159
+ */
160
+ async assertRowOwnedByAmbientContext(operation) {
161
+ const context = getCurrentTenant();
162
+ if (!context || isSuperAdminBypass()) return;
163
+ if (this.tenantId !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldPolicySuggestion.${operation}: context tenant is '${context.tenantId}' but the row belongs to '${this.tenantId}'`, {
164
+ tenantId: context.tenantId,
165
+ attemptedTenantId: this.tenantId ?? void 0
166
+ });
167
+ if (this.id) {
168
+ const persisted = await this.db.get(this.tableName, { id: this.id });
169
+ if (persisted) {
170
+ const row = persisted;
171
+ const persistedTenant = row.tenantId ?? row.tenant_id ?? this.tenantId ?? null;
172
+ if (persistedTenant !== null && String(persistedTenant) !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldPolicySuggestion.${operation}: the persisted row belongs to '${String(persistedTenant)}'`, {
173
+ tenantId: context.tenantId,
174
+ attemptedTenantId: String(persistedTenant)
175
+ });
421
176
  }
422
- if (needsLockCheck && orgPolicy?.locked) throw new FieldPolicyAuthorizationError(`Field "${this.objectRef}.${this.fieldName}" is locked by org policy; user-scope overrides are not allowed`);
423
177
  }
424
178
  }
179
+ };
180
+ __decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "objectRef", 2);
181
+ __decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "fieldName", 2);
182
+ __decorateClass$5([tenantId()], FieldPolicySuggestion.prototype, "tenantId", 2);
183
+ __decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "kind", 2);
184
+ __decorateClass$5([field({
185
+ type: "text",
186
+ nullable: true
187
+ })], FieldPolicySuggestion.prototype, "proposedValue", 2);
188
+ __decorateClass$5([field({ type: "text" })], FieldPolicySuggestion.prototype, "evidence", 2);
189
+ __decorateClass$5([field({ required: true })], FieldPolicySuggestion.prototype, "status", 2);
190
+ __decorateClass$5([field({
191
+ type: "text",
192
+ required: true
193
+ })], FieldPolicySuggestion.prototype, "activeKey", 2);
194
+ __decorateClass$5([field({
195
+ type: "datetime",
196
+ nullable: true
197
+ })], FieldPolicySuggestion.prototype, "cooldownUntil", 2);
198
+ __decorateClass$5([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicySuggestion.prototype, "decidedBy", 2);
199
+ __decorateClass$5([field({
200
+ type: "datetime",
201
+ nullable: true
202
+ })], FieldPolicySuggestion.prototype, "decidedAt", 2);
203
+ FieldPolicySuggestion = __decorateClass$5([smrt({
204
+ tableName: "_smrt_field_policy_suggestions",
205
+ conflictColumns: [
206
+ "object_ref",
207
+ "field_name",
208
+ "tenant_id",
209
+ "kind",
210
+ "active_key"
211
+ ],
212
+ api: { include: [] },
213
+ cli: false,
214
+ mcp: { include: [] }
215
+ })], FieldPolicySuggestion);
216
+ function toIsoOrNull(value) {
217
+ if (value === null || value === void 0) return null;
218
+ if (value instanceof Date) return value.toISOString();
219
+ const parsed = Date.parse(value);
220
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
221
+ }
222
+ //#endregion
223
+ //#region src/collections/FieldPolicySuggestionCollection.ts
224
+ var __defProp$4 = Object.defineProperty;
225
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
226
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$4(obj, key, {
227
+ enumerable: true,
228
+ configurable: true,
229
+ writable: true,
230
+ value
231
+ }) : obj[key] = value;
232
+ var __decorateClass$4 = (decorators, target, key, kind) => {
233
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
234
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
235
+ if (kind && result) __defProp$4(target, key, result);
236
+ return result;
237
+ };
238
+ var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
239
+ var FieldPolicySuggestionConflictError = class extends Error {
240
+ /** Core's generated-REST status contract (own integer property). */
241
+ httpStatus = 409;
242
+ /** The users-style shape mirrored by this package's authorization errors. */
243
+ status = 409;
244
+ constructor(operation, suggestionId) {
245
+ super(`${operation}: suggestion "${suggestionId}" was already decided by another request (it is no longer pending)`);
246
+ this.name = "FieldPolicySuggestionConflictError";
247
+ }
248
+ };
249
+ var FieldPolicySuggestionRequestContextError = class extends TenantIsolationError {
250
+ httpStatus = 403;
251
+ status = 403;
252
+ constructor(message, details) {
253
+ super(message, details);
254
+ this.name = "FieldPolicySuggestionRequestContextError";
255
+ }
256
+ };
257
+ function transactionBoundOptions(db) {
258
+ return {
259
+ db,
260
+ _reuseInitializedDb: true,
261
+ _deferRuntimeInitialization: true
262
+ };
263
+ }
264
+ async function claimSuggestionTransition(db, suggestionId, patch) {
265
+ return ((await db.query(`UPDATE _smrt_field_policy_suggestions
266
+ SET status = ?,
267
+ active_key = ?,
268
+ decided_at = ?,
269
+ decided_by = ?,
270
+ cooldown_until = ?
271
+ WHERE id = ? AND status = 'pending'
272
+ RETURNING id`, patch.status, patch.activeKey, patch.decidedAt.toISOString(), patch.decidedBy, patch.cooldownUntil ? patch.cooldownUntil.toISOString() : null, suggestionId))?.rows?.length ?? 0) > 0;
273
+ }
274
+ async function revertSuggestionToPending(db, suggestionId) {
275
+ await db.query(`UPDATE _smrt_field_policy_suggestions
276
+ SET status = 'pending',
277
+ active_key = ?,
278
+ decided_at = NULL,
279
+ decided_by = NULL,
280
+ cooldown_until = NULL
281
+ WHERE id = ?`, ACTIVE_SUGGESTION_KEY, suggestionId);
282
+ }
283
+ async function settleSuggestionActiveKey(db, suggestionId) {
284
+ await db.query(`UPDATE _smrt_field_policy_suggestions
285
+ SET active_key = ?
286
+ WHERE id = ?`, suggestionId, suggestionId);
287
+ }
288
+ var FieldPolicySuggestionCompensationError = class extends Error {
289
+ httpStatus = 500;
290
+ status = 500;
291
+ revertCause;
292
+ constructor(suggestionId, cause, revertCause) {
293
+ super(`acceptSuggestion: the policy write for suggestion "${suggestionId}" failed AND reverting it to pending also failed; the suggestion may be left accepted without its policy row and needs reconciliation (policy error: ${cause instanceof Error ? cause.message : String(cause)}; revert error: ${revertCause instanceof Error ? revertCause.message : String(revertCause)})`, { cause });
294
+ this.name = "FieldPolicySuggestionCompensationError";
295
+ this.revertCause = revertCause;
296
+ }
297
+ };
298
+ function fieldPolicyRowId(tenantId, objectRef, fieldName) {
299
+ return deterministicFieldsUuid([
300
+ "field-policy-row",
301
+ "tenant",
302
+ tenantId,
303
+ objectRef,
304
+ fieldName
305
+ ]);
306
+ }
307
+ async function updatePolicyColumn(db, target) {
308
+ const id = ((await db.query(`UPDATE _smrt_field_policies
309
+ SET ${target.column} = ?,
310
+ updated_by = ?,
311
+ updated_at = ?
312
+ WHERE object_ref = ?
313
+ AND field_name = ?
314
+ AND scope_type = 'tenant'
315
+ AND tenant_id = ?
316
+ RETURNING id`, target.value, target.decidedBy, (/* @__PURE__ */ new Date()).toISOString(), target.objectRef, target.fieldName, target.tenantId))?.rows ?? [])[0]?.id;
317
+ return id === void 0 || id === null ? null : String(id);
318
+ }
319
+ async function assertAcceptedDefaultStillAllowed(suggestion) {
320
+ const fieldDef = await assertAcceptedPolicyTargetStillAllowed(suggestion);
321
+ if (suggestion.proposedValue === null) throw new Error("FieldPolicySuggestion of kind 'default' requires a proposedValue");
322
+ let parsed;
323
+ try {
324
+ parsed = JSON.parse(suggestion.proposedValue);
325
+ } catch (error) {
326
+ throw new Error(`FieldPolicySuggestion.proposedValue is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
327
+ }
328
+ assertDefaultValueMatchesFieldType(suggestion.objectRef, suggestion.fieldName, fieldDef, parsed);
329
+ }
330
+ async function assertAcceptedPolicyTargetStillAllowed(suggestion) {
331
+ const fieldDef = (await getObjectFieldMap(suggestion.objectRef)).get(suggestion.fieldName);
332
+ if (!fieldDef || !isPolicyAddressableField(fieldDef)) throw new Error(`Field "${suggestion.fieldName}" on "${suggestion.objectRef}" is not policy-addressable`);
333
+ if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) throw new Error(`Cannot apply a policy for "${suggestion.objectRef}.${suggestion.fieldName}": the field is sensitive, read-permission-gated, or transient`);
334
+ return fieldDef;
335
+ }
336
+ var DEFAULT_SUGGESTION_COOL_DOWN_MS = 720 * 60 * 60 * 1e3;
337
+ var MIN_SUGGESTION_COOL_DOWN_MS = 3600 * 1e3;
338
+ var MAX_SUGGESTION_COOL_DOWN_MS = 365 * 24 * 60 * 60 * 1e3;
339
+ var MAX_PENDING_OBJECT_REFS = 100;
340
+ var FieldPolicySuggestionCollection = class extends SmrtCollection {
425
341
  /**
426
- * Effective org-tier (code app tenant hierarchy) policy for this row's
427
- * field, computed by the RESOLVER so write-time checks share the one
428
- * precedence implementation — including ancestor-tenant cascades via the
429
- * default hierarchy loader. The chain tenant is the row's own tenant for
430
- * tenant-scope rows, the ambient context tenant for user-scope rows, and
431
- * none for app-scope rows (their only lower layer is the code seed).
432
- *
433
- * During an update, the persisted row is excluded from the lower-layer
434
- * projection. That makes a clear-default + required-field demotion validate
435
- * the post-write policy rather than accidentally counting the row being
436
- * replaced.
342
+ * The caller's tenant's PENDING suggestions (optionally filtered to a set
343
+ * of objectRefs), newest first, plus the total for the gear badge.
437
344
  */
438
- async resolveOrgTierFieldPolicy(previousIdentity) {
439
- const tenantIdForChain = this.scopeType === "tenant" ? this.tenantId : this.scopeType === "user" ? getFieldPolicyAmbientContext()?.tenantId ?? null : null;
440
- const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
441
- return (await resolveFieldPolicy(this.objectRef, {
442
- tenantId: tenantIdForChain,
443
- db: this.options.db ?? this.options.persistence,
444
- ...previousIdentity ? { excludePolicyIds: /* @__PURE__ */ new Set([previousIdentity.id]) } : {}
445
- })).fields[this.fieldName];
345
+ async pendingSuggestions(options = {}) {
346
+ const tenantId = await this.requireManageContext("pendingSuggestions");
347
+ const objectRefs = normalizeObjectRefsFilter(options.objectRefs);
348
+ const where = {
349
+ tenantId,
350
+ status: "pending"
351
+ };
352
+ if (objectRefs) where["objectRef in"] = objectRefs;
353
+ const rows = await this.list({
354
+ where,
355
+ orderBy: "created_at DESC"
356
+ });
357
+ return {
358
+ suggestions: rows.map((row) => row.toSuggestionData()),
359
+ total: rows.length
360
+ };
446
361
  }
447
362
  /**
448
- * Exactly-one-owner scope shape: app rows carry neither id, tenant rows
449
- * carry only `tenantId`, user rows carry only `userId` (the user tier is
450
- * keyed by user alone so preferences follow the user across tenants).
363
+ * Accept a pending suggestion: CLAIM the pending→accepted transition, then
364
+ * write the corresponding org-scope `FieldPolicy` row THROUGH NORMAL
365
+ * VALIDATION (registry check, type check, security rail, required-field
366
+ * invariant, ownership boundary, and the #2049 permission split — the
367
+ * ambient caller must hold `fields.policy.manage`, which this action also
368
+ * asserts up front).
369
+ *
370
+ * An existing tenant row for the same `(objectRef, fieldName)` is UPDATED
371
+ * (read-modify-write), preserving its other sparse columns — never
372
+ * duplicated through the natural-key upsert.
373
+ *
374
+ * Concurrency: the claim is a compare-and-set on `status` (the jobs
375
+ * `writeOwnedJob` precedent — a guarded UPDATE with `RETURNING id`), so an
376
+ * overlapping accept/dismiss pair cannot both win; the loser throws
377
+ * {@link FieldPolicySuggestionConflictError} (409). Claim and policy write
378
+ * run in ONE transaction when the driver supports it, so a rejected policy
379
+ * write can never leave the suggestion settled without its policy (and vice
380
+ * versa). Drivers without transactions get an explicit compensating revert.
451
381
  */
452
- validateScopeConsistency() {
453
- if (this.scopeType === "app") {
454
- if (this.tenantId !== null || this.userId !== null) throw new Error("App-scope field policy rows must have tenantId and userId null");
455
- return;
382
+ async acceptSuggestion(options = {}) {
383
+ const tenantId = await this.requireManageContext("acceptSuggestion");
384
+ const suggestion = await this.loadOwnedPendingSuggestion("acceptSuggestion", options.id, tenantId);
385
+ const suggestionId = String(suggestion.id);
386
+ const decidedBy = getCurrentTenant()?.userId ?? null;
387
+ const decidedAt = /* @__PURE__ */ new Date();
388
+ const claim = {
389
+ status: "accepted",
390
+ activeKey: suggestionId,
391
+ decidedAt,
392
+ decidedBy,
393
+ cooldownUntil: null
394
+ };
395
+ const tx = await this.beginTransactionIfSupported();
396
+ if (tx) {
397
+ let policyRowId2;
398
+ try {
399
+ if (!await claimSuggestionTransition(tx, suggestionId, claim)) throw new FieldPolicySuggestionConflictError("acceptSuggestion", suggestionId);
400
+ policyRowId2 = await this.applyAcceptedPolicy(tx, suggestion, tenantId, decidedBy);
401
+ await tx.commit();
402
+ } catch (error) {
403
+ try {
404
+ await tx.rollback();
405
+ } catch {}
406
+ throw error;
407
+ }
408
+ invalidateFieldPolicyCache(suggestion.objectRef, this.db);
409
+ suggestion.status = "accepted";
410
+ suggestion.decidedAt = decidedAt;
411
+ suggestion.decidedBy = decidedBy;
412
+ return {
413
+ suggestion: suggestion.toSuggestionData(),
414
+ policyRowId: policyRowId2
415
+ };
456
416
  }
457
- if (this.scopeType === "tenant") {
458
- if (!this.tenantId || this.userId !== null) throw new Error("Tenant-scope field policy rows must set tenantId and leave userId null");
459
- return;
417
+ if (!await claimSuggestionTransition(this.db, suggestionId, {
418
+ ...claim,
419
+ activeKey: "active"
420
+ })) throw new FieldPolicySuggestionConflictError("acceptSuggestion", suggestionId);
421
+ let policyRowId;
422
+ try {
423
+ policyRowId = await this.applyAcceptedPolicy(this.db, suggestion, tenantId, decidedBy);
424
+ } catch (error) {
425
+ try {
426
+ await revertSuggestionToPending(this.db, suggestionId);
427
+ } catch (revertError) {
428
+ throw new FieldPolicySuggestionCompensationError(suggestionId, error, revertError);
429
+ }
430
+ throw error;
460
431
  }
461
- if (!this.userId || this.tenantId !== null) throw new Error("User-scope field policy rows must set userId and leave tenantId null");
462
- }
463
- /** `userId ?? tenantId ?? '__app__'` — the `conflictColumns` scope key. */
464
- computeScopeKey() {
465
- return this.userId ?? this.tenantId ?? "__app__";
432
+ await settleSuggestionActiveKey(this.db, suggestionId);
433
+ invalidateFieldPolicyCache(suggestion.objectRef, this.db);
434
+ suggestion.status = "accepted";
435
+ suggestion.decidedAt = decidedAt;
436
+ suggestion.decidedBy = decidedBy;
437
+ return {
438
+ suggestion: suggestion.toSuggestionData(),
439
+ policyRowId
440
+ };
466
441
  }
467
442
  /**
468
- * Derive a scope-owner column the transport could not carry (#2047).
469
- *
470
- * Core's mass-assignment guard treats `tenantId` as server-managed and
471
- * strips it from EVERY generated create/update body, and `FieldPolicy` is
472
- * deliberately not `@TenantScoped`, so the tenancy interceptor never
473
- * repopulates it. A `POST {scopeType:'tenant', tenantId}` therefore always
474
- * reached `validateScopeConsistency` with `tenantId === null`, making the
475
- * org tier write-dead over every generated surface — the model, as the
476
- * single validation authority for these rows, fills it in instead.
443
+ * Dismiss a pending suggestion: set status + a cool-down until which the
444
+ * generation job will NOT regenerate the same
445
+ * `(objectRef, fieldName, tenantId, kind)` suggestion.
477
446
  *
478
- * This grants nothing: `assertScopeOwnedByAmbientContext` already requires
479
- * a non-bypass caller's tenant/user rows to name exactly the ambient
480
- * tenant/user, so the derived value is the ONLY value that could ever have
481
- * been accepted. An explicit value is never overwritten (a super-admin
482
- * bypass caller keeps naming other scopes), and with neither tenancy nor
483
- * authenticated-session context nothing is derived scope-shape validation
484
- * rejects the row as before.
485
- * Consequence: a bypass caller writing ANOTHER tenant's row must pass
486
- * `tenantId` through a server-side model call, because the generated routes
487
- * still strip it.
447
+ * Validates OWNERSHIP and PENDING STATUS ONLY — deliberately NOT continued
448
+ * proposal eligibility. A queued suggestion whose field has since been
449
+ * removed, turned sensitive/`readPermission`-gated/transient, or retyped is
450
+ * exactly the row an operator most needs to clear; re-running proposal
451
+ * validation on the way out would reject the dismissal and, because pending
452
+ * rows are never pruned, wedge it in the queue forever. The claim is the same
453
+ * compare-and-set as accept, so it also bypasses the model's proposal
454
+ * validation by construction.
488
455
  */
489
- attributeScopeToAmbientContext() {
490
- const context = getFieldPolicyAmbientContext();
491
- if (!context) return;
492
- if (this.scopeType === "tenant" && this.tenantId === null) this.tenantId = context.tenantId;
493
- if (this.scopeType === "user" && this.userId === null) this.userId = context.userId ?? null;
456
+ async dismissSuggestion(options = {}) {
457
+ const tenantId = await this.requireManageContext("dismissSuggestion");
458
+ const suggestion = await this.loadOwnedPendingSuggestion("dismissSuggestion", options.id, tenantId);
459
+ const suggestionId = String(suggestion.id);
460
+ const coolDownMs = normalizeCoolDownMs(options.coolDownMs);
461
+ const decidedAt = /* @__PURE__ */ new Date();
462
+ const decidedBy = getCurrentTenant()?.userId ?? null;
463
+ const cooldownUntil = new Date(decidedAt.getTime() + coolDownMs);
464
+ if (!await claimSuggestionTransition(this.db, suggestionId, {
465
+ status: "dismissed",
466
+ activeKey: suggestionId,
467
+ decidedAt,
468
+ decidedBy,
469
+ cooldownUntil
470
+ })) throw new FieldPolicySuggestionConflictError("dismissSuggestion", suggestionId);
471
+ suggestion.status = "dismissed";
472
+ suggestion.cooldownUntil = cooldownUntil;
473
+ suggestion.decidedAt = decidedAt;
474
+ suggestion.decidedBy = decidedBy;
475
+ return { suggestion: suggestion.toSuggestionData() };
494
476
  }
495
477
  /**
496
- * Fail-closed write boundary against the ambient tenant context, applied to
497
- * the NEW scope on save and to the PERSISTED scope on save/delete of an
498
- * existing row.
478
+ * Write the org-scope policy column an accepted suggestion implies, bound to
479
+ * `db` so it participates in the caller's transaction.
499
480
  *
500
- * With NO ambient identity at all (neither tenant ALS nor authenticated
501
- * session-permission ALS), only app-scope rows are accepted: tenant- and
502
- * user-scope rows are unattributable without a context, so they are
503
- * rejected outright rather than allowed by default. Inside a non-bypass
504
- * context, a caller may only touch rows for its own tenant, and user rows
505
- * only for its own user id a context that carries NO user id may not
506
- * touch the user tier at all.
507
- * App-wide rows then require super-admin bypass (or a context-less/system
508
- * caller).
481
+ * A `promote` and a `default` suggestion for the SAME field share one sparse
482
+ * `FieldPolicy` row but own DIFFERENT columns, and both may be accepted
483
+ * concurrently. A read-modify-save of the whole row therefore loses updates:
484
+ * each acceptance reads the row (or its absence) before the other's save
485
+ * lands, and the later full-row write erases the sibling's column and, via
486
+ * the natural-key upsert, can even replace the row id, dangling the earlier
487
+ * caller's `policyRowId`. (Distinct from the suggestion-row race fixed
488
+ * earlier: that one guards the pending→settled transition; this one is on the
489
+ * shared policy row.) So this NEVER writes a whole row over an existing one:
509
490
  *
510
- * Package rule: a missing identity component DENIES, it never skips.
491
+ * 1. An ATOMIC partial UPDATE sets only this acceptance's column, keyed by
492
+ * the policy natural key and `RETURNING id` — one statement, so no
493
+ * interleaving can drop the sibling's column, and the returned id is
494
+ * always the surviving row.
495
+ * 2. Only when no row exists yet does it CREATE through the model, so full
496
+ * validation (registry, type check, security rail, required-field
497
+ * invariant, ownership + permission split) runs on the row that is
498
+ * actually inserted. The row carries a DETERMINISTIC id under strict
499
+ * insert, so two concurrent creates cannot each mint a row: the loser
500
+ * collides and falls back to step 1, applying only its own column.
501
+ *
502
+ * The update path skips the model, so BOTH kinds re-check live target
503
+ * addressability through the shared helpers (a field can turn sensitive,
504
+ * gated, transient, or disappear between queueing and acceptance). Defaults
505
+ * additionally re-check their value/type rail; a promotion to `basic` has no
506
+ * value payload, and the required-field invariant restricts only
507
+ * advanced/hidden. Org locks constrain the user tier, not org rows.
511
508
  */
512
- assertScopeOwnedByAmbientContext(scope, operation) {
513
- const context = getFieldPolicyAmbientContext();
514
- if (!context) {
515
- if (scope.scopeType === "tenant" || scope.scopeType === "user") throw new FieldPolicyTenantIsolationError(`${scope.scopeType === "tenant" ? "Tenant" : "User"}-scope field policy ${operation}s require an ambient tenant context (or super-admin bypass); without one the caller identity is unattributable`);
516
- return;
517
- }
518
- if (context.superAdminBypass) return;
519
- if (scope.scopeType === "app") throw new FieldPolicyTenantIsolationError(`App-scope field policy ${operation}s are not allowed inside a tenant context (use a system context or super-admin bypass)`, { tenantId: context.tenantId });
520
- if (scope.scopeType === "tenant" && scope.tenantId !== context.tenantId) throw new FieldPolicyTenantIsolationError(`Tenant isolation violation in FieldPolicy.${operation}: context tenant is '${context.tenantId}' but the row belongs to '${scope.tenantId}'`, {
521
- tenantId: context.tenantId,
522
- attemptedTenantId: scope.tenantId ?? void 0
523
- });
524
- if (scope.scopeType === "user") {
525
- if (context.userId === void 0) throw new FieldPolicyTenantIsolationError(`User-scope field policy ${operation}s require an ambient context that carries a user id; this context cannot attribute a user-scope write`, { tenantId: context.tenantId });
526
- if (scope.userId !== context.userId) throw new FieldPolicyTenantIsolationError(`Tenant isolation violation in FieldPolicy.${operation}: context user is '${context.userId}' but the row belongs to '${scope.userId}'`, { tenantId: context.tenantId });
509
+ async applyAcceptedPolicy(db, suggestion, tenantId, decidedBy) {
510
+ if (suggestion.kind === "default") await assertAcceptedDefaultStillAllowed(suggestion);
511
+ else await assertAcceptedPolicyTargetStillAllowed(suggestion);
512
+ const target = {
513
+ objectRef: suggestion.objectRef,
514
+ fieldName: suggestion.fieldName,
515
+ tenantId,
516
+ column: suggestion.kind === "promote" ? "visibility" : "default_value",
517
+ value: suggestion.kind === "promote" ? "basic" : suggestion.proposedValue,
518
+ decidedBy
519
+ };
520
+ const updatedId = await updatePolicyColumn(db, target);
521
+ if (updatedId) return updatedId;
522
+ const { FieldPolicyCollection } = await import("./chunks/FieldPolicyCollection--RxukfCX.js").then((n) => n.n);
523
+ const policies = await FieldPolicyCollection.create(transactionBoundOptions(db));
524
+ const deterministicId = await fieldPolicyRowId(tenantId, suggestion.objectRef, suggestion.fieldName);
525
+ try {
526
+ const created = await policies.create({
527
+ id: deterministicId,
528
+ objectRef: suggestion.objectRef,
529
+ fieldName: suggestion.fieldName,
530
+ scopeType: "tenant",
531
+ tenantId,
532
+ ...suggestion.kind === "promote" ? { visibility: "basic" } : { defaultValue: suggestion.proposedValue },
533
+ updatedBy: decidedBy,
534
+ _insertOnly: true
535
+ });
536
+ return String(created.id);
537
+ } catch (error) {
538
+ const racedId = await updatePolicyColumn(db, target);
539
+ if (racedId) return racedId;
540
+ throw error;
527
541
  }
528
542
  }
529
- validateTenantContextBoundary() {
530
- this.assertScopeOwnedByAmbientContext({
531
- scopeType: this.scopeType,
532
- tenantId: this.tenantId,
533
- userId: this.userId
534
- }, "save");
543
+ /**
544
+ * A driver transaction handle, or `null` when the driver has none (the
545
+ * `FieldPolicy.saveAfterIdentityChange` probe, same shape).
546
+ */
547
+ async beginTransactionIfSupported() {
548
+ if (typeof this.db.beginTransaction !== "function") return null;
549
+ return await this.db.beginTransaction() ?? null;
535
550
  }
536
551
  /**
537
- * Permission boundary for policy mutations. Scope ownership is checked
538
- * separately (and first) because permission never grants cross-tenant or
539
- * cross-user access. Existing rows are checked against their persisted
540
- * scope before an identity-changing save can mutate or delete them.
552
+ * Ambient manage gate shared by the three actions: an ambient tenant
553
+ * context is required (no context no identity fail closed — the
554
+ * suggestion queue is never an anonymous surface), and the caller must hold
555
+ * `fields.policy.manage` unless running under super-admin bypass.
541
556
  */
542
- async assertScopePermission(scope) {
543
- const context = getFieldPolicyAmbientContext();
544
- const [, , operation] = (scope.scopeType === "user" ? PERSONALIZE_FIELD_POLICY_PERMISSION : MANAGE_FIELD_POLICY_PERMISSION).split(".");
545
- await assertOperationPermission({
557
+ async requireManageContext(action) {
558
+ const context = getCurrentTenant();
559
+ if (!context?.tenantId) throw new FieldPolicySuggestionRequestContextError(`${action} requires an ambient tenant context (fail closed): the suggestion queue is scoped to the caller tenant`);
560
+ if (!isSuperAdminBypass()) await assertOperationPermission({
546
561
  collection: "fields.policy",
547
- action: operation ?? "manage",
548
- db: this.options.db ?? this.options.persistence,
549
- tenantId: context?.tenantId ?? null,
550
- userId: context?.userId ?? null,
551
- permissionSet: context?.permissions
562
+ action: "fields.policy.manage".split(".").at(-1) ?? "manage",
563
+ db: this.db,
564
+ tenantId: context.tenantId,
565
+ userId: context.userId ?? null,
566
+ permissionSet: context.permissions
567
+ });
568
+ return context.tenantId;
569
+ }
570
+ async loadOwnedPendingSuggestion(action, id, tenantId) {
571
+ if (typeof id !== "string" || id.trim() === "") throw new Error(`${action} requires a suggestion "id" string`);
572
+ const suggestion = await this.get(id);
573
+ if (!suggestion) throw new Error(`${action}: no suggestion found for id "${id}"`);
574
+ if (suggestion.tenantId !== tenantId) throw new TenantIsolationError(`Tenant isolation violation in ${action}: the suggestion belongs to another tenant`, {
575
+ tenantId,
576
+ attemptedTenantId: suggestion.tenantId ?? void 0
552
577
  });
578
+ if (suggestion.status !== "pending") throw new FieldPolicySuggestionConflictError(action, id);
579
+ return suggestion;
553
580
  }
554
- validateDefaultAgainstSecurityRail(fieldDef) {
555
- if (isTransientField(fieldDef)) throw new Error(`Cannot store a default for transient field "${this.objectRef}.${this.fieldName}"`);
556
- if (isSensitiveField(fieldDef)) throw new Error(`Cannot store a default for sensitive field "${this.objectRef}.${this.fieldName}"`);
557
- const readPermission = getFieldReadPermission(fieldDef);
558
- if (readPermission) throw new Error(`Cannot store a default for read-permission-gated field "${this.objectRef}.${this.fieldName}" (requires "${readPermission}")`);
581
+ };
582
+ __publicField$2(FieldPolicySuggestionCollection, "_itemClass", FieldPolicySuggestion);
583
+ FieldPolicySuggestionCollection = __decorateClass$4([smrt({
584
+ conflictColumns: [
585
+ "object_ref",
586
+ "field_name",
587
+ "tenant_id",
588
+ "kind",
589
+ "active_key"
590
+ ],
591
+ api: {
592
+ include: [
593
+ "pendingSuggestions",
594
+ "acceptSuggestion",
595
+ "dismissSuggestion"
596
+ ],
597
+ principalContext: true,
598
+ routes: {
599
+ pendingSuggestions: {
600
+ scope: "collection",
601
+ method: "POST",
602
+ path: "pending"
603
+ },
604
+ acceptSuggestion: {
605
+ scope: "collection",
606
+ method: "POST",
607
+ path: "accept"
608
+ },
609
+ dismissSuggestion: {
610
+ scope: "collection",
611
+ method: "POST",
612
+ path: "dismiss"
613
+ }
614
+ }
615
+ },
616
+ cli: false,
617
+ mcp: false
618
+ })], FieldPolicySuggestionCollection);
619
+ function normalizeObjectRefsFilter(rawRefs) {
620
+ if (rawRefs === void 0) return null;
621
+ if (!Array.isArray(rawRefs) || rawRefs.length === 0) throw new Error("pendingSuggestions \"objectRefs\" must be a non-empty string array when provided");
622
+ if (rawRefs.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error("pendingSuggestions objectRefs must be non-empty strings");
623
+ const objectRefs = [...new Set(rawRefs)];
624
+ if (objectRefs.length > MAX_PENDING_OBJECT_REFS) throw new Error(`pendingSuggestions accepts at most ${MAX_PENDING_OBJECT_REFS} objectRefs per call (got ${objectRefs.length})`);
625
+ return objectRefs;
626
+ }
627
+ function normalizeCoolDownMs(raw) {
628
+ if (raw === void 0) return DEFAULT_SUGGESTION_COOL_DOWN_MS;
629
+ if (typeof raw !== "number" || !Number.isFinite(raw)) throw new Error("dismissSuggestion coolDownMs must be a finite number");
630
+ return Math.min(Math.max(raw, MIN_SUGGESTION_COOL_DOWN_MS), MAX_SUGGESTION_COOL_DOWN_MS);
631
+ }
632
+ //#endregion
633
+ //#region src/models/FieldUsageCounter.ts
634
+ var __defProp$3 = Object.defineProperty;
635
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
636
+ var __decorateClass$3 = (decorators, target, key, kind) => {
637
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
638
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
639
+ if (kind && result) __defProp$3(target, key, result);
640
+ return result;
641
+ };
642
+ var MAX_DISTINCT_USERS_PER_BUCKET = 100;
643
+ var MAX_VALUE_HISTOGRAM_BUCKETS = 25;
644
+ var FIELD_USAGE_PERIOD_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
645
+ function fieldUsagePeriodForDate(date) {
646
+ return date.toISOString().slice(0, 10);
647
+ }
648
+ function emptyHistogram() {
649
+ return /* @__PURE__ */ Object.create(null);
650
+ }
651
+ var FieldUsageCounter = class extends SmrtObject {
652
+ objectRef = "";
653
+ fieldName = "";
654
+ tenantId;
655
+ period = "";
656
+ submissionCount = 0;
657
+ setCount = 0;
658
+ distinctUserCount = 0;
659
+ distinctUserIds = "[]";
660
+ distinctUsersOverflowed = false;
661
+ valueHistogram = null;
662
+ valueHistogramOverflowed = false;
663
+ constructor(options = {}) {
664
+ super(options);
665
+ if (options.objectRef !== void 0) this.objectRef = options.objectRef;
666
+ if (options.fieldName !== void 0) this.fieldName = options.fieldName;
667
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
668
+ if (options.period !== void 0) this.period = options.period;
669
+ if (options.submissionCount !== void 0) this.submissionCount = options.submissionCount;
670
+ if (options.setCount !== void 0) this.setCount = options.setCount;
671
+ if (options.distinctUserCount !== void 0) this.distinctUserCount = options.distinctUserCount;
672
+ if (options.distinctUserIds !== void 0) this.distinctUserIds = options.distinctUserIds;
673
+ if (options.distinctUsersOverflowed !== void 0) this.distinctUsersOverflowed = options.distinctUsersOverflowed;
674
+ if (options.valueHistogram !== void 0) this.valueHistogram = options.valueHistogram;
675
+ if (options.valueHistogramOverflowed !== void 0) this.valueHistogramOverflowed = options.valueHistogramOverflowed;
559
676
  }
560
- parseDefaultValueOrThrow() {
677
+ /** Parse the stored distinct-user set (guarded; junk parses as empty). */
678
+ getDistinctUserIds() {
561
679
  try {
562
- return JSON.parse(this.defaultValue);
563
- } catch (error) {
564
- throw new Error(`FieldPolicy default for "${this.objectRef}.${this.fieldName}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}. The "defaultValue" option is the already-encoded channel; pass a plain value as "defaultValueRaw" (or call setDefaultValue()).`);
680
+ const parsed = JSON.parse(this.distinctUserIds);
681
+ return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
682
+ } catch {
683
+ return [];
565
684
  }
566
685
  }
567
686
  /**
568
- * The persisted row this save/delete would replace, looked up by primary
569
- * key and when that misses by the NATURAL key.
687
+ * Add a user to the distinct set, honoring the cap. At the cap the id is
688
+ * NOT added and the overflow marker is set instead, keeping
689
+ * {@link distinctUserCount} an honest lower bound.
690
+ */
691
+ addDistinctUser(userId) {
692
+ const ids = this.getDistinctUserIds();
693
+ if (ids.includes(userId)) return;
694
+ if (ids.length >= 100) {
695
+ this.distinctUsersOverflowed = true;
696
+ return;
697
+ }
698
+ ids.push(userId);
699
+ this.distinctUserIds = JSON.stringify(ids);
700
+ this.distinctUserCount = ids.length;
701
+ }
702
+ /**
703
+ * Parse the stored histogram (guarded; junk parses as empty).
570
704
  *
571
- * The natural-key fallback is load-bearing for authorization: every
572
- * generated create arrives with a freshly minted UUID, so a primary-key
573
- * lookup always misses, yet the `conflictColumns` upsert still overwrites
574
- * whatever row already occupies `(objectRef, fieldName, scopeType,
575
- * scopeKey)`. Authorizing on the primary key alone therefore skipped the
576
- * persisted-scope guard on exactly the path that can replace an existing
577
- * row's contents.
705
+ * Returns a NULL-PROTOTYPE object. Histogram keys are user-supplied ids
706
+ * an `idType: 'text'` reference may legitimately be `constructor`,
707
+ * `toString`, or `__proto__` and on a plain object those inherit truthy
708
+ * prototype values (so a missing bucket reads as present) or, for
709
+ * `__proto__`, hit the prototype setter instead of creating an own key.
710
+ * Both would silently corrupt counts. See {@link emptyHistogram}.
578
711
  */
579
- async getPersistedIdentity() {
580
- const existing = await this.getPersistedRowById() ?? await this.getPersistedRowByNaturalKey();
581
- if (!existing) return null;
582
- const row = existing;
583
- const read = (camel, snake, fallback) => {
584
- if (row[camel] !== void 0 && row[camel] !== null) return String(row[camel]);
585
- if (row[snake] !== void 0 && row[snake] !== null) return String(row[snake]);
586
- return fallback;
587
- };
588
- const readNullable = (camel, snake) => {
589
- const value = row[camel] !== void 0 ? row[camel] : row[snake];
590
- return value === void 0 || value === null ? null : String(value);
591
- };
592
- return {
593
- id: read("id", "id", String(this.id ?? "")),
594
- objectRef: read("objectRef", "object_ref", this.objectRef),
595
- fieldName: read("fieldName", "field_name", this.fieldName),
596
- scopeType: read("scopeType", "scope_type", this.scopeType),
597
- scopeKey: read("scopeKey", "scope_key", this.scopeKey),
598
- tenantId: readNullable("tenantId", "tenant_id"),
599
- userId: readNullable("userId", "user_id")
600
- };
712
+ getValueHistogram() {
713
+ const histogram = emptyHistogram();
714
+ if (!this.valueHistogram) return histogram;
715
+ try {
716
+ const parsed = JSON.parse(this.valueHistogram);
717
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return histogram;
718
+ for (const [key, count] of Object.entries(parsed)) if (typeof count === "number" && Number.isFinite(count) && count > 0) histogram[key] = count;
719
+ return histogram;
720
+ } catch {
721
+ return emptyHistogram();
722
+ }
601
723
  }
602
- async getPersistedRowById() {
603
- if (!this.id) return null;
604
- return await this.db.get(this.tableName, { id: this.id }) ?? null;
724
+ /**
725
+ * Record one histogram sample under an already-serialized key, honoring the
726
+ * bucket cap (a NEW key past the cap is dropped and the overflow marker
727
+ * set; existing keys keep counting).
728
+ *
729
+ * Bucket presence is an OWN-key test, never a truthiness/`undefined` read,
730
+ * so prototype-shaped ids behave like any other key.
731
+ */
732
+ recordHistogramSample(key) {
733
+ if (key.length === 0 || key.length > 64) return;
734
+ const histogram = this.getValueHistogram();
735
+ if (!Object.hasOwn(histogram, key)) {
736
+ if (Object.keys(histogram).length >= 25) {
737
+ this.valueHistogramOverflowed = true;
738
+ return;
739
+ }
740
+ histogram[key] = 1;
741
+ } else histogram[key] += 1;
742
+ this.valueHistogram = JSON.stringify(histogram);
605
743
  }
606
744
  /**
607
- * The row occupying this row's `conflictColumns` tuple, if any. Column
608
- * names are the physical snake_case ones: this reads the driver directly
609
- * rather than through a collection (the model has no collection handle).
745
+ * Whether this bucket predates the {@link submissionCount} column (or was
746
+ * corrupted): it records deviations without a total, so no honest dominance
747
+ * ratio can be computed from it. The generation job skips `default`
748
+ * suggestions for any group containing such a bucket.
610
749
  */
611
- async getPersistedRowByNaturalKey() {
612
- if (!this.objectRef || !this.fieldName) return null;
613
- return await this.db.get(this.tableName, {
614
- object_ref: this.objectRef,
615
- field_name: this.fieldName,
616
- scope_type: this.scopeType,
617
- scope_key: this.computeScopeKey()
618
- }) ?? null;
750
+ isLegacyBucket() {
751
+ return this.submissionCount < this.setCount;
619
752
  }
620
- normalizeDefaultValueForPersistence() {
621
- const raw = this.defaultValue;
622
- if (raw === null || raw === void 0) {
623
- this.defaultValue = null;
624
- return;
625
- }
626
- if (typeof raw === "string") {
627
- this.defaultValue = raw;
628
- return;
629
- }
630
- this.defaultValue = JSON.stringify(raw);
753
+ async save() {
754
+ await this.assertRowOwnedByAmbientContext("save");
755
+ this.validateFieldUsageCounter();
756
+ return super.save();
757
+ }
758
+ async delete() {
759
+ await this.assertRowOwnedByAmbientContext("delete");
760
+ await super.delete();
761
+ }
762
+ validateFieldUsageCounter() {
763
+ if (!this.objectRef || this.objectRef.trim() === "") throw new Error("FieldUsageCounter.objectRef is required");
764
+ if (!this.fieldName || this.fieldName.trim() === "") throw new Error("FieldUsageCounter.fieldName is required");
765
+ if (!this.tenantId) throw new Error("FieldUsageCounter.tenantId is required");
766
+ if (!FIELD_USAGE_PERIOD_PATTERN.test(this.period)) throw new Error(`FieldUsageCounter.period must be a UTC day bucket (YYYY-MM-DD); got "${this.period}"`);
767
+ if (!Number.isInteger(this.submissionCount) || this.submissionCount < 0) throw new Error("FieldUsageCounter.submissionCount must be a non-negative integer");
768
+ if (!Number.isInteger(this.setCount) || this.setCount < 0) throw new Error("FieldUsageCounter.setCount must be a non-negative integer");
769
+ if (!Number.isInteger(this.distinctUserCount) || this.distinctUserCount < 0) throw new Error("FieldUsageCounter.distinctUserCount must be a non-negative integer");
770
+ }
771
+ /**
772
+ * Tenant write boundary (the FieldPolicy posture — no class-level
773
+ * `@TenantScoped`, because the learning jobs legitimately operate
774
+ * cross-tenant in trusted execution): inside a non-bypass tenant context a
775
+ * caller may only touch rows of its own tenant; without a context (system/
776
+ * job execution) writes are trusted.
777
+ *
778
+ * Checked against BOTH the in-memory tenant and — for a row that already
779
+ * exists — the PERSISTED one. The persisted check is what makes the boundary
780
+ * real: bucket ids are deterministic and the deriving helper is exported, so
781
+ * a foreign row is trivially addressable, and an in-memory-only check would
782
+ * let a caller load it, re-stamp `tenantId` with its own, and adopt or delete
783
+ * another tenant's counters (the #2047 FieldPolicy pattern).
784
+ */
785
+ async assertRowOwnedByAmbientContext(operation) {
786
+ const context = getCurrentTenant();
787
+ if (!context || isSuperAdminBypass()) return;
788
+ if (this.tenantId !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldUsageCounter.${operation}: context tenant is '${context.tenantId}' but the row belongs to '${this.tenantId}'`, {
789
+ tenantId: context.tenantId,
790
+ attemptedTenantId: this.tenantId ?? void 0
791
+ });
792
+ const persistedTenantId = await this.getPersistedTenantId();
793
+ if (persistedTenantId !== null && persistedTenantId !== context.tenantId) throw new TenantIsolationError(`Tenant isolation violation in FieldUsageCounter.${operation}: the persisted row belongs to '${persistedTenantId}'`, {
794
+ tenantId: context.tenantId,
795
+ attemptedTenantId: persistedTenantId
796
+ });
797
+ }
798
+ /** The stored tenant for this row's id; `null` when it is not persisted. */
799
+ async getPersistedTenantId() {
800
+ if (!this.id) return null;
801
+ const existing = await this.db.get(this.tableName, { id: this.id });
802
+ if (!existing) return null;
803
+ const row = existing;
804
+ const value = row.tenantId ?? row.tenant_id;
805
+ return value === void 0 || value === null ? null : String(value);
631
806
  }
632
807
  };
633
- __decorateClass$1([field({ required: true })], FieldPolicy.prototype, "objectRef", 2);
634
- __decorateClass$1([field({ required: true })], FieldPolicy.prototype, "fieldName", 2);
635
- __decorateClass$1([field({ required: true })], FieldPolicy.prototype, "scopeType", 2);
636
- __decorateClass$1([tenantId({ nullable: true })], FieldPolicy.prototype, "tenantId", 2);
637
- __decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicy.prototype, "userId", 2);
638
- __decorateClass$1([field({
639
- type: "text",
640
- required: true
641
- })], FieldPolicy.prototype, "scopeKey", 2);
642
- __decorateClass$1([field({
643
- type: "text",
644
- nullable: true
645
- })], FieldPolicy.prototype, "defaultValue", 2);
646
- __decorateClass$1([field({
808
+ __decorateClass$3([field({ required: true })], FieldUsageCounter.prototype, "objectRef", 2);
809
+ __decorateClass$3([field({ required: true })], FieldUsageCounter.prototype, "fieldName", 2);
810
+ __decorateClass$3([tenantId()], FieldUsageCounter.prototype, "tenantId", 2);
811
+ __decorateClass$3([field({ required: true })], FieldUsageCounter.prototype, "period", 2);
812
+ __decorateClass$3([field({ type: "integer" })], FieldUsageCounter.prototype, "submissionCount", 2);
813
+ __decorateClass$3([field({ type: "integer" })], FieldUsageCounter.prototype, "setCount", 2);
814
+ __decorateClass$3([field({ type: "integer" })], FieldUsageCounter.prototype, "distinctUserCount", 2);
815
+ __decorateClass$3([field({ type: "text" })], FieldUsageCounter.prototype, "distinctUserIds", 2);
816
+ __decorateClass$3([field({ type: "boolean" })], FieldUsageCounter.prototype, "distinctUsersOverflowed", 2);
817
+ __decorateClass$3([field({
647
818
  type: "text",
648
819
  nullable: true
649
- })], FieldPolicy.prototype, "visibility", 2);
650
- __decorateClass$1([field({
651
- type: "text",
652
- nullable: true
653
- })], FieldPolicy.prototype, "help", 2);
654
- __decorateClass$1([field({
655
- type: "text",
656
- nullable: true
657
- })], FieldPolicy.prototype, "label", 2);
658
- __decorateClass$1([field({
659
- type: "integer",
660
- nullable: true
661
- })], FieldPolicy.prototype, "displayOrder", 2);
662
- __decorateClass$1([field({
663
- type: "boolean",
664
- nullable: true
665
- })], FieldPolicy.prototype, "locked", 2);
666
- __decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { nullable: true })], FieldPolicy.prototype, "updatedBy", 2);
667
- FieldPolicy = __decorateClass$1([smrt({
668
- tableName: "_smrt_field_policies",
820
+ })], FieldUsageCounter.prototype, "valueHistogram", 2);
821
+ __decorateClass$3([field({ type: "boolean" })], FieldUsageCounter.prototype, "valueHistogramOverflowed", 2);
822
+ FieldUsageCounter = __decorateClass$3([smrt({
823
+ tableName: "_smrt_field_usage_counters",
669
824
  conflictColumns: [
670
825
  "object_ref",
671
826
  "field_name",
672
- "scope_type",
673
- "scope_key"
827
+ "tenant_id",
828
+ "period"
674
829
  ],
675
- api: {
676
- include: [
677
- "create",
678
- "update",
679
- "delete"
680
- ],
681
- principalContext: true
682
- },
683
- cli: {
684
- include: [
685
- "create",
686
- "update",
687
- "delete"
688
- ],
689
- exclude: ["getDefaultValue", "setDefaultValue"]
690
- },
830
+ api: { include: [] },
831
+ cli: false,
832
+ mcp: { include: [] }
833
+ })], FieldUsageCounter);
834
+ //#endregion
835
+ //#region src/models/FieldUsageReportReceipt.ts
836
+ var __defProp$2 = Object.defineProperty;
837
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
838
+ var __decorateClass$2 = (decorators, target, key, kind) => {
839
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
840
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
841
+ if (kind && result) __defProp$2(target, key, result);
842
+ return result;
843
+ };
844
+ var FieldUsageReportReceipt = class extends SmrtObject {
845
+ tenantId;
846
+ userId = "";
847
+ objectRef = "";
848
+ fieldName = "";
849
+ period = "";
850
+ constructor(options = {}) {
851
+ super(options);
852
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
853
+ if (options.userId !== void 0) this.userId = options.userId;
854
+ if (options.objectRef !== void 0) this.objectRef = options.objectRef;
855
+ if (options.fieldName !== void 0) this.fieldName = options.fieldName;
856
+ if (options.period !== void 0) this.period = options.period;
857
+ }
858
+ };
859
+ __decorateClass$2([tenantId()], FieldUsageReportReceipt.prototype, "tenantId", 2);
860
+ __decorateClass$2([crossPackageRef("@happyvertical/smrt-users:User")], FieldUsageReportReceipt.prototype, "userId", 2);
861
+ __decorateClass$2([field({ required: true })], FieldUsageReportReceipt.prototype, "objectRef", 2);
862
+ __decorateClass$2([field({ required: true })], FieldUsageReportReceipt.prototype, "fieldName", 2);
863
+ __decorateClass$2([field({ required: true })], FieldUsageReportReceipt.prototype, "period", 2);
864
+ FieldUsageReportReceipt = __decorateClass$2([smrt({
865
+ tableName: "_smrt_field_usage_report_receipts",
866
+ conflictColumns: [
867
+ "tenant_id",
868
+ "user_id",
869
+ "object_ref",
870
+ "field_name",
871
+ "period"
872
+ ],
873
+ api: { include: [] },
874
+ cli: false,
691
875
  mcp: { include: [] }
692
- })], FieldPolicy);
876
+ })], FieldUsageReportReceipt);
693
877
  //#endregion
694
- //#region src/collections/FieldPolicyCollection.ts
695
- var __defProp = Object.defineProperty;
696
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
697
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
878
+ //#region src/collections/FieldUsageCounterCollection.ts
879
+ var __defProp$1 = Object.defineProperty;
880
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
881
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
698
882
  enumerable: true,
699
883
  configurable: true,
700
884
  writable: true,
701
885
  value
702
886
  }) : obj[key] = value;
703
- var __decorateClass = (decorators, target, key, kind) => {
704
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
887
+ var __decorateClass$1 = (decorators, target, key, kind) => {
888
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
705
889
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
706
- if (kind && result) __defProp(target, key, result);
890
+ if (kind && result) __defProp$1(target, key, result);
707
891
  return result;
708
892
  };
709
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
710
- var MAX_BATCH_OBJECT_REFS = 100;
711
- MAX_BATCH_OBJECT_REFS * 2;
712
- var FieldPolicyCollection = class extends SmrtCollection {
893
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
894
+ function usageValuesEqual(a, b) {
895
+ if (a === b) return true;
896
+ try {
897
+ return JSON.stringify(a) === JSON.stringify(b);
898
+ } catch {
899
+ return false;
900
+ }
901
+ }
902
+ var MAX_USAGE_REPORT_ENTRIES = 100;
903
+ var FieldUsageRequestContextError = class extends TenantIsolationError {
904
+ httpStatus = 403;
905
+ status = 403;
906
+ constructor(message, details) {
907
+ super(message, details);
908
+ this.name = "FieldUsageRequestContextError";
909
+ }
910
+ };
911
+ var FieldUsageCounterCollection = class extends SmrtCollection {
713
912
  /**
714
- * Permission-gated, tenant-scoped audit read for the defaults control
715
- * panel. It returns editable own-tenant rows, read-only app summaries,
716
- * projected user override counts, and identity-only manifest drift rows.
913
+ * Record a batch of field submissions into the current UTC-day counters.
914
+ *
915
+ * Identity is AMBIENT-ONLY (the resolveBatch posture): the tenant AND the
916
+ * user come exclusively from the tenant context established by the app's
917
+ * auth hook — the request body cannot attribute usage to another tenant or
918
+ * user. Both are REQUIRED and the call fails closed without them (#2047
919
+ * round-3 posture):
920
+ *
921
+ * - No ambient tenant ⇒ the report is unattributable.
922
+ * - No ambient USER ⇒ the caller is an unauthenticated principal (a
923
+ * deployment that resolves the tenant from host/header alone). Such
924
+ * callers could otherwise inflate counts and histograms without bound
925
+ * (the batch cap is per call), and `distinctUsers` — the promote signal —
926
+ * is meaningless without a principal. Consequence, accepted deliberately:
927
+ * ANONYMOUS/PUBLIC FORMS DO NOT CONTRIBUTE USAGE. The learning loop is
928
+ * for authenticated org users.
929
+ *
930
+ * Any authenticated in-tenant principal may report (no manage slug needed).
931
+ * A durable receipt admits at most one `(tenant, user, object, field, UTC
932
+ * day)` contribution, and the batch stays bounded per call
933
+ * ({@link MAX_USAGE_REPORT_ENTRIES}).
934
+ *
935
+ * Server-side derivation (the client is never the authority):
936
+ * - Fields are validated against the live `ObjectRegistry`; unknown or
937
+ * non-usage-addressable entries (system, relationship pseudo-fields, STI
938
+ * meta storage, transient) are DROPPED and counted, never failing the
939
+ * batch (stale clients after a redeploy are expected).
940
+ * - Sensitivity comes from BOTH `field.sensitive` and `field._meta.sensitive`
941
+ * (and `readPermission` in both places): such fields are recorded
942
+ * COUNT-ONLY — their values are never persisted anywhere in usage data,
943
+ * default-matching or not.
944
+ * - DEVIATION (`setCount`, distinct users) is decided by comparing the
945
+ * submitted value against the default RESOLVED HERE for the calling
946
+ * identity — never against a client claim. Every accepted entry also
947
+ * increments `submissionCount`, the dominance denominator.
948
+ * - Values are histogrammed only for low-cardinality field types
949
+ * (`boolean`, `foreignKey`, `crossPackageRef`) with type-checked samples;
950
+ * free text is never histogrammed.
951
+ *
952
+ * Bounded trust in `matchedDefault`: an entry may omit `value` entirely
953
+ * (`collectFieldUsageEntries({ includeValues: false })` — apps that prefer
954
+ * no value transit). Only then is the entry's `matchedDefault` flag read, to
955
+ * decide the deviation bit alone. It can never create a histogram entry,
956
+ * never bypass the count-only rail, and never manufacture distinct users
957
+ * beyond the caller's own id. The once-per-field/day receipt means a lying
958
+ * client gets only one such contribution, so repeated POSTs cannot inflate
959
+ * thresholds or steer a `default` suggestion.
717
960
  */
718
- async policyAudit(options = {}) {
719
- const objectRefs = normalizeAuditRefs(options.objectRefs, "objectRefs", 100);
720
- const countObjectRefs = normalizeAuditRefs(options.countObjectRefs ?? objectRefs, "countObjectRefs", 200);
961
+ async reportUsage(options = {}) {
962
+ const entries = validateEntriesInput(options.entries);
721
963
  const context = getCurrentTenant();
722
- const tenantId = context?.tenantId ?? null;
723
- const userId = context?.userId ?? null;
724
- const permissionOptions = {
725
- collection: "fields.policy",
726
- db: this.db,
727
- tenantId,
728
- userId,
729
- permissionSet: context?.permissions,
730
- onDeny: "return"
731
- };
732
- const manage = tenantId ? await checkOperationPermission({
733
- ...permissionOptions,
734
- action: "fields.policy.manage".split(".").at(-1) ?? "manage"
735
- }) : { allowed: false };
736
- const personalize = userId ? await checkOperationPermission({
737
- ...permissionOptions,
738
- action: "fields.policy.personalize".split(".").at(-1) ?? "personalize"
739
- }) : { allowed: false };
740
- const caller = {
741
- tenantId,
742
- userId,
743
- canManageOrg: manage.allowed,
744
- canPersonalize: personalize.allowed && userId !== null
745
- };
746
- if (!caller.canManageOrg || tenantId === null) return emptyAudit(caller);
747
- if (options.summaryOnly === true) return emptyAudit(caller);
748
- const fieldMaps = /* @__PURE__ */ new Map();
749
- const fieldMapFor = (objectRef) => {
750
- let loaded = fieldMaps.get(objectRef);
751
- if (!loaded) {
752
- loaded = getObjectFieldMap(objectRef).catch(() => null);
753
- fieldMaps.set(objectRef, loaded);
964
+ const tenantId = context?.tenantId;
965
+ if (!tenantId) throw new FieldUsageRequestContextError("reportUsage requires an ambient tenant context: usage is unattributable without one, so the call fails closed");
966
+ const userId = context.userId;
967
+ if (!userId) throw new FieldUsageRequestContextError("reportUsage requires an ambient AUTHENTICATED user: an unauthenticated caller could inflate counters without bound and distinct-user thresholds are meaningless without a principal, so the call fails closed (anonymous forms do not contribute usage)", { tenantId });
968
+ const period = fieldUsagePeriodForDate(/* @__PURE__ */ new Date());
969
+ const receipts = await FieldUsageReportReceiptCollection.create({ db: this.db });
970
+ const groups = /* @__PURE__ */ new Map();
971
+ for (const entry of entries) {
972
+ let byField = groups.get(entry.objectRef);
973
+ if (!byField) {
974
+ byField = /* @__PURE__ */ new Map();
975
+ groups.set(entry.objectRef, byField);
754
976
  }
755
- return loaded;
756
- };
757
- const classify = async (row) => {
758
- const fields = await fieldMapFor(row.objectRef);
759
- if (!fields) return "unknown-object";
760
- const definition = fields.get(row.fieldName);
761
- if (!definition) return "unknown-field";
762
- if (isSensitiveField(definition) || getFieldReadPermission(definition) !== void 0 || isTransientField(definition)) return "gated";
763
- return isPolicyAddressableField(definition) ? "ok" : "excluded-field";
764
- };
765
- const [appRows, orgRows, ownUserRows] = objectRefs.length ? await Promise.all([
766
- this.list({ where: {
767
- scopeType: "app",
768
- "objectRef in": objectRefs
769
- } }),
770
- this.list({ where: {
771
- scopeType: "tenant",
772
- tenantId,
773
- "objectRef in": objectRefs
774
- } }),
775
- userId ? this.list({ where: {
776
- scopeType: "user",
777
- userId,
778
- "objectRef in": objectRefs
779
- } }) : Promise.resolve([])
780
- ]) : [
781
- [],
782
- [],
783
- []
784
- ];
785
- const audit = {
786
- ...emptyAudit(caller),
787
- policies: options.countsOnly ? {} : await this.resolveAuditPolicies(objectRefs, tenantId, fieldMapFor)
788
- };
789
- const append = async (row, target, driftPrunable) => {
790
- const state = await classify(row);
791
- if (state === "ok") target.push(toAuditRow(row));
792
- else if (state !== "gated") audit.driftRows.push({
793
- ...toDriftIdentity(row),
794
- reason: state,
795
- prunable: driftPrunable
796
- });
797
- };
798
- for (const row of orgRows) await append(row, audit.orgRows, true);
799
- for (const row of appRows) await append(row, audit.appRows, isSuperAdminBypass());
800
- for (const row of ownUserRows) {
801
- const state = await classify(row);
802
- if (state !== "ok" && state !== "gated") audit.driftRows.push({
803
- ...toDriftIdentity(row),
804
- reason: state,
805
- prunable: caller.canPersonalize
806
- });
977
+ const samples = byField.get(entry.fieldName);
978
+ if (samples) samples.push(entry);
979
+ else byField.set(entry.fieldName, [entry]);
807
980
  }
808
- if (options.includeDrift === true) {
809
- const [driftAppRows, driftOrgRows, driftUserRows] = await Promise.all([
810
- this.list({
811
- where: { scopeType: "app" },
812
- limit: 100
813
- }),
814
- this.list({
815
- where: {
816
- scopeType: "tenant",
817
- tenantId
818
- },
819
- limit: 100
820
- }),
821
- userId ? this.list({
822
- where: {
823
- scopeType: "user",
824
- userId
825
- },
826
- limit: 100
827
- }) : Promise.resolve([])
828
- ]);
829
- const knownIds = new Set([
830
- ...appRows,
831
- ...orgRows,
832
- ...ownUserRows
833
- ].map((row) => String(row.id)));
834
- for (const [rows, prunable] of [
835
- [driftAppRows, isSuperAdminBypass()],
836
- [driftOrgRows, true],
837
- [driftUserRows, caller.canPersonalize]
838
- ]) for (const row of rows) {
839
- if (knownIds.has(String(row.id))) continue;
840
- const state = await classify(row);
841
- if (state !== "ok" && state !== "gated") audit.driftRows.push({
842
- ...toDriftIdentity(row),
843
- reason: state,
844
- prunable
845
- });
981
+ let accepted = 0;
982
+ let dropped = 0;
983
+ for (const [objectRef, byField] of groups) {
984
+ let fieldMap;
985
+ try {
986
+ fieldMap = await getObjectFieldMap(objectRef);
987
+ } catch {
988
+ for (const samples of byField.values()) dropped += samples.length;
989
+ continue;
846
990
  }
847
- }
848
- const memberships = await MembershipCollection.create({ db: this.db });
849
- const memberIds = [...new Set((await memberships.findActiveByTenant(tenantId)).map((membership) => membership.userId?.trim()).filter((id) => Boolean(id)))];
850
- const projected = countObjectRefs.length && memberIds.length ? await this.list({
851
- where: {
852
- scopeType: "user",
853
- "objectRef in": countObjectRefs,
854
- "userId in": memberIds
855
- },
856
- select: ["objectRef", "fieldName"]
857
- }) : [];
858
- for (const row of projected) {
859
- if (await classify(row) !== "ok") continue;
860
- const byField = audit.userOverrideCounts[row.objectRef] ?? {};
861
- audit.userOverrideCounts[row.objectRef] = byField;
862
- byField[row.fieldName] = (byField[row.fieldName] ?? 0) + 1;
863
- }
864
- const { resolveSurvivingTenantChainIds } = await Promise.resolve().then(() => field_policy_resolver_exports);
865
- const ancestorIds = (await resolveSurvivingTenantChainIds(tenantId, { db: this.db })).filter((id) => id !== tenantId);
866
- if (ancestorIds.length && objectRefs.length) {
867
- const ancestors = await this.list({ where: {
868
- scopeType: "tenant",
869
- "tenantId in": ancestorIds,
870
- "objectRef in": objectRefs
871
- } });
872
- for (const row of ancestors) {
873
- if (await classify(row) !== "ok") continue;
874
- const names = audit.inheritedOrgKeys[row.objectRef] ?? [];
875
- audit.inheritedOrgKeys[row.objectRef] = names;
876
- if (!names.includes(row.fieldName)) names.push(row.fieldName);
991
+ const resolvedFields = await this.resolveDefaultsForCaller(objectRef, tenantId, userId);
992
+ if (!resolvedFields) {
993
+ for (const samples of byField.values()) dropped += samples.length;
994
+ continue;
877
995
  }
878
- }
879
- audit.orgRows.sort(sortAuditRows);
880
- audit.appRows.sort(sortAuditRows);
881
- audit.driftRows.sort(sortDriftRows);
882
- for (const names of Object.values(audit.inheritedOrgKeys)) names.sort();
883
- return audit;
884
- }
885
- async resolveAuditPolicies(objectRefs, tenantId, fieldMapFor) {
886
- const { resolveFieldPolicyExplained } = await Promise.resolve().then(() => field_policy_resolver_exports);
887
- const policies = {};
888
- for (const objectRef of objectRefs) {
889
- if (!await fieldMapFor(objectRef)) continue;
890
- const resolved = await resolveFieldPolicyExplained(objectRef, {
891
- tenantId,
892
- db: this.db
893
- });
894
- policies[objectRef] = await this.gateExplainedForEditorResponse(resolved, {
895
- manage: true,
896
- personalize: false
897
- }, true);
898
- }
899
- return policies;
900
- }
901
- /** All app-scope rows for an object, keyed by field name. */
902
- async getAppRows(objectRef) {
903
- const rows = await this.list({ where: {
904
- objectRef,
905
- scopeType: "app"
906
- } });
907
- const byField = /* @__PURE__ */ new Map();
908
- for (const row of rows) byField.set(row.fieldName, row);
909
- return byField;
910
- }
911
- /**
912
- * All tenant-scope rows for an object across a tenant chain, keyed
913
- * `tenantId → fieldName → row`.
914
- */
915
- async getTenantRows(objectRef, tenantIds) {
916
- const byTenant = /* @__PURE__ */ new Map();
917
- if (tenantIds.length === 0) return byTenant;
918
- const rows = await this.list({ where: {
919
- objectRef,
920
- scopeType: "tenant",
921
- "tenantId in": tenantIds
922
- } });
923
- for (const row of rows) {
924
- if (!row.tenantId) continue;
925
- let byField = byTenant.get(row.tenantId);
926
- if (!byField) {
927
- byField = /* @__PURE__ */ new Map();
928
- byTenant.set(row.tenantId, byField);
996
+ for (const [fieldName, samples] of byField) {
997
+ const fieldDef = fieldMap.get(fieldName);
998
+ if (!fieldDef || !isUsageAddressableField(fieldDef)) {
999
+ dropped += samples.length;
1000
+ continue;
1001
+ }
1002
+ if (!await receipts.claim({
1003
+ tenantId,
1004
+ userId,
1005
+ objectRef,
1006
+ fieldName,
1007
+ period
1008
+ })) continue;
1009
+ const sample = samples[0];
1010
+ const histogramEligible = !(isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0) && isHistogramEligibleField(fieldDef);
1011
+ const resolved = resolvedFields[fieldName];
1012
+ const hasDefault = resolved?.hasDefault === true;
1013
+ const defaultValue = hasDefault ? resolved?.defaultValue : void 0;
1014
+ let deviations = 0;
1015
+ const histogramKeys = [];
1016
+ if ("value" in sample) {
1017
+ if (!hasDefault || !usageValuesEqual(sample.value, defaultValue)) deviations += 1;
1018
+ if (histogramEligible) {
1019
+ const key = serializeHistogramSample(fieldDef, sample.value);
1020
+ if (key !== null) histogramKeys.push(key);
1021
+ }
1022
+ } else if (sample.matchedDefault !== true) deviations += 1;
1023
+ await this.mergeIntoBucket({
1024
+ objectRef,
1025
+ fieldName,
1026
+ tenantId,
1027
+ period,
1028
+ userId,
1029
+ submissionCount: 1,
1030
+ deviationCount: deviations,
1031
+ histogramKeys
1032
+ });
1033
+ accepted += 1;
929
1034
  }
930
- byField.set(row.fieldName, row);
931
1035
  }
932
- return byTenant;
933
- }
934
- /** All user-scope rows for an object and user, keyed by field name. */
935
- async getUserRows(objectRef, userId) {
936
- const rows = await this.list({ where: {
937
- objectRef,
938
- scopeType: "user",
939
- userId
940
- } });
941
- const byField = /* @__PURE__ */ new Map();
942
- for (const row of rows) byField.set(row.fieldName, row);
943
- return byField;
1036
+ return {
1037
+ accepted,
1038
+ dropped
1039
+ };
944
1040
  }
945
1041
  /**
946
- * Resolve merged field policy for a set of objectRefs for the CURRENT
947
- * caller, gated for public consumption.
1042
+ * The defaults the CALLER's forms would have prefilled the deviation
1043
+ * authority. Resolved for the ambient `(tenant, user)` identity (the
1044
+ * resolver's own TTL cache keeps repeat batches cheap).
948
1045
  *
949
- * Caller identity comes exclusively from the ambient tenant context
950
- * (established by the app's auth hook, e.g. smrt-users' session context) —
951
- * the request body cannot select another tenant or user (fail closed).
952
- * Server-side consumers wanting explicit identities call
953
- * `resolveFieldPolicy()` directly instead.
954
- *
955
- * Field gating mirrors the REST serializer's derivation (`sensitive` /
956
- * `readPermission` read from both the top level and `_meta`): sensitive and
957
- * read-permission-gated fields are ABSENT from the response for every
958
- * caller (the generated action route cannot convey per-caller grants to
959
- * this method, so gated fields fail closed), and transient fields are
960
- * stripped for parity with generated client field definitions.
1046
+ * Ingestion is fire-and-forget, so a stored-layer read failure degrades to
1047
+ * the CODE-SEED-only resolution (no db) rather than failing the request;
1048
+ * `null` (both attempts failed) makes the caller drop the group.
961
1049
  */
962
- async resolveBatch(options = {}) {
963
- const rawRefs = options.objectRefs;
964
- if (!Array.isArray(rawRefs) || rawRefs.length === 0) throw new Error("resolveBatch requires a non-empty \"objectRefs\" string array");
965
- if (rawRefs.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error("resolveBatch objectRefs must be non-empty strings");
966
- const objectRefs = [...new Set(rawRefs)];
967
- if (objectRefs.length > MAX_BATCH_OBJECT_REFS) throw new Error(`resolveBatch accepts at most ${MAX_BATCH_OBJECT_REFS} objectRefs per call (got ${objectRefs.length})`);
968
- const context = getCurrentTenant();
969
- const tenantId = context?.tenantId ?? null;
970
- const userId = context?.userId ?? null;
1050
+ async resolveDefaultsForCaller(objectRef, tenantId, userId) {
971
1051
  const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
972
- const policies = {};
973
- for (const objectRef of objectRefs) {
974
- const resolved = await resolveFieldPolicy(objectRef, {
1052
+ try {
1053
+ return (await resolveFieldPolicy(objectRef, {
975
1054
  tenantId,
976
1055
  userId,
977
1056
  db: this.db
978
- });
979
- policies[objectRef] = await this.gateResolvedForPublicResponse(resolved);
1057
+ })).fields;
1058
+ } catch {
1059
+ try {
1060
+ return (await resolveFieldPolicy(objectRef, {
1061
+ tenantId,
1062
+ userId
1063
+ })).fields;
1064
+ } catch {
1065
+ return null;
1066
+ }
980
1067
  }
981
- return { policies };
982
1068
  }
983
1069
  /**
984
- * Context-derived bootstrap data for the policy gear. This is deliberately
985
- * a collection action instead of reopening list/get: a caller can inspect
986
- * only their safe effective policy and rows/layers they are permitted to
987
- * edit, never arbitrary tenant or user policy rows.
1070
+ * Counter rows within a period window (inclusive bounds, lexicographic ISO
1071
+ * day comparison), optionally restricted to one tenant the read the
1072
+ * suggestion-generation job consumes.
988
1073
  */
989
- async getEditorState(options = {}) {
990
- const objectRef = options.objectRef;
991
- if (typeof objectRef !== "string" || objectRef.trim() === "") throw new Error("getEditorState requires a non-empty \"objectRef\" string");
992
- const context = getCurrentTenant();
993
- const permissionOptions = {
994
- collection: "fields.policy",
995
- db: this.db,
996
- tenantId: context?.tenantId ?? null,
997
- userId: context?.userId ?? null,
998
- permissionSet: context?.permissions,
999
- onDeny: "return"
1000
- };
1001
- const manage = await checkOperationPermission({
1002
- ...permissionOptions,
1003
- action: "fields.policy.manage".split(".").at(-1) ?? "manage"
1004
- });
1005
- const personalize = await checkOperationPermission({
1006
- ...permissionOptions,
1007
- action: "fields.policy.personalize".split(".").at(-1) ?? "personalize"
1074
+ async listWindow(options) {
1075
+ const where = { "period >=": options.fromPeriod };
1076
+ if (options.toPeriod) where["period <="] = options.toPeriod;
1077
+ if (options.tenantId) where.tenantId = options.tenantId;
1078
+ return this.list({
1079
+ where,
1080
+ orderBy: "period ASC"
1008
1081
  });
1009
- const personalizeAllowed = personalize.allowed && typeof context?.userId === "string" && context.userId.length > 0;
1010
- if (!manage.allowed && !personalizeAllowed) return {
1011
- code: "permission_denied",
1012
- message: `Permission denied for '${personalize.permission ?? "fields.policy.manage"}'.`,
1013
- ok: false,
1014
- status: 403
1015
- };
1016
- const { resolveFieldPolicyExplained } = await Promise.resolve().then(() => field_policy_resolver_exports);
1017
- const resolved = await resolveFieldPolicyExplained(objectRef, {
1018
- tenantId: context?.tenantId ?? null,
1019
- userId: context?.userId ?? null,
1020
- db: this.db
1021
- });
1022
- const policy = await this.gateExplainedForEditorResponse(resolved, {
1023
- manage: manage.allowed,
1024
- personalize: personalizeAllowed
1025
- });
1026
- const fieldNames = new Set(Object.keys(policy.fields));
1027
- const personalLowerDefaultUsable = personalizeAllowed ? await this.getPersonalLowerDefaultUsability(objectRef, context?.tenantId ?? null, fieldNames) : {};
1028
- const appRows = manage.allowed ? this.toEditorRows(await this.getAppRows(objectRef), fieldNames) : [];
1029
- const tenantRows = manage.allowed && context?.tenantId ? this.toEditorRows((await this.getTenantRows(objectRef, [context.tenantId])).get(context.tenantId) ?? /* @__PURE__ */ new Map(), fieldNames) : [];
1030
- const userRows = personalizeAllowed && context?.userId ? this.toEditorRows(await this.getUserRows(objectRef, context.userId), fieldNames) : [];
1031
- return {
1032
- capabilities: {
1033
- manage: manage.allowed,
1034
- personalize: personalizeAllowed
1035
- },
1036
- personalLowerDefaultUsable,
1037
- policy,
1038
- rows: {
1039
- app: appRows,
1040
- tenant: tenantRows,
1041
- user: userRows
1042
- }
1043
- };
1044
1082
  }
1045
1083
  /**
1046
- * Compute the non-disclosing fallback signal used only by personal drafts.
1047
- * Passing no user id resolves exactly code/app/tenant precedence, including
1048
- * the real tenant hierarchy, while keeping every lower-layer value server
1049
- * side. The public map is limited to fields already admitted by the editor
1050
- * response's security rail.
1084
+ * Idempotent read-modify-write merge into the deterministic day bucket.
1085
+ * Counters are approximate by design: concurrent cross-process merges may
1086
+ * lose an increment (documented on the model), while the deterministic id
1087
+ * plus the natural-key unique index keep concurrent creates converging on
1088
+ * one row.
1051
1089
  */
1052
- async getPersonalLowerDefaultUsability(objectRef, tenantId, fieldNames) {
1053
- const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
1054
- const lowerPolicy = await resolveFieldPolicy(objectRef, {
1055
- tenantId,
1056
- db: this.db
1090
+ async mergeIntoBucket(options) {
1091
+ const id = await fieldUsageCounterId(options.tenantId, options.objectRef, options.fieldName, options.period);
1092
+ const existing = await this.get(id);
1093
+ const counter = existing ?? new FieldUsageCounter({
1094
+ db: this.db,
1095
+ id,
1096
+ objectRef: options.objectRef,
1097
+ fieldName: options.fieldName,
1098
+ tenantId: options.tenantId,
1099
+ period: options.period
1057
1100
  });
1058
- return Object.fromEntries(Array.from(fieldNames, (fieldName) => {
1059
- const field = lowerPolicy.fields[fieldName];
1060
- return [fieldName, isUsableRequiredDefault(field?.hasDefault ? { value: field.defaultValue } : void 0)];
1061
- }));
1062
- }
1063
- async gateResolvedForPublicResponse(resolved) {
1064
- const fieldMap = await getObjectFieldMap(resolved.objectRef);
1065
- const fields = {};
1066
- for (const [fieldName, policy] of Object.entries(resolved.fields)) {
1067
- const fieldDef = fieldMap.get(fieldName);
1068
- if (!fieldDef) continue;
1069
- if (isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
1070
- fields[fieldName] = policy;
1071
- }
1072
- return {
1073
- objectRef: resolved.objectRef,
1074
- fields
1075
- };
1076
- }
1077
- async gateExplainedForEditorResponse(resolved, capabilities, includeCode = false) {
1078
- const fieldMap = await getObjectFieldMap(resolved.objectRef);
1079
- const fields = {};
1080
- const layers = {};
1081
- for (const [fieldName, policy] of Object.entries(resolved.fields)) {
1082
- const fieldDef = fieldMap.get(fieldName);
1083
- if (!fieldDef || isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) continue;
1084
- fields[fieldName] = policy;
1085
- layers[fieldName] = (resolved.layers[fieldName] ?? []).filter((layer) => includeCode && layer.layer === "code" || capabilities.manage && (layer.layer === "app" || layer.layer === "tenant") || capabilities.personalize && layer.layer === "user");
1086
- }
1087
- return {
1088
- objectRef: resolved.objectRef,
1089
- fields,
1090
- layers
1091
- };
1092
- }
1093
- toEditorRows(rows, allowedFieldNames) {
1094
- return Array.from(rows.values()).filter((row) => allowedFieldNames.has(row.fieldName)).map((row) => ({
1095
- defaultValue: row.defaultValue,
1096
- displayOrder: row.displayOrder,
1097
- fieldName: row.fieldName,
1098
- help: row.help,
1099
- id: String(row.id),
1100
- label: row.label,
1101
- locked: row.locked,
1102
- scopeType: row.scopeType,
1103
- tenantId: row.tenantId,
1104
- updatedBy: row.updatedBy,
1105
- userId: row.userId,
1106
- visibility: row.visibility
1107
- })).sort((left, right) => left.fieldName.localeCompare(right.fieldName));
1101
+ counter.submissionCount += options.submissionCount;
1102
+ counter.setCount += options.deviationCount;
1103
+ if (options.deviationCount > 0) counter.addDistinctUser(options.userId);
1104
+ for (const key of options.histogramKeys) counter.recordHistogramSample(key);
1105
+ if (!existing) await counter.initialize();
1106
+ await counter.save();
1108
1107
  }
1109
1108
  };
1110
- __publicField(FieldPolicyCollection, "_itemClass", FieldPolicy);
1111
- FieldPolicyCollection = __decorateClass([smrt({
1109
+ __publicField$1(FieldUsageCounterCollection, "_itemClass", FieldUsageCounter);
1110
+ FieldUsageCounterCollection = __decorateClass$1([smrt({
1112
1111
  conflictColumns: [
1113
1112
  "object_ref",
1114
1113
  "field_name",
1115
- "scope_type",
1116
- "scope_key"
1114
+ "tenant_id",
1115
+ "period"
1117
1116
  ],
1118
1117
  api: {
1119
- include: [
1120
- "create",
1121
- "update",
1122
- "delete",
1123
- "resolveBatch",
1124
- "getEditorState",
1125
- "policyAudit"
1126
- ],
1118
+ include: ["reportUsage"],
1127
1119
  principalContext: true,
1128
- routes: {
1129
- resolveBatch: {
1130
- scope: "collection",
1131
- method: "POST",
1132
- path: "resolve"
1133
- },
1134
- getEditorState: {
1135
- scope: "collection",
1136
- method: "POST",
1137
- path: "editor-state"
1138
- },
1139
- policyAudit: {
1140
- scope: "collection",
1141
- method: "POST",
1142
- path: "policy-audit"
1143
- }
1144
- }
1120
+ routes: { reportUsage: {
1121
+ scope: "collection",
1122
+ method: "POST",
1123
+ path: "report"
1124
+ } }
1145
1125
  },
1146
1126
  cli: false,
1147
1127
  mcp: false
1148
- })], FieldPolicyCollection);
1149
- function normalizeAuditRefs(value, label, maximum) {
1150
- if (value === void 0) return [];
1151
- if (!Array.isArray(value)) throw new Error(`policyAudit ${label} must be a string array`);
1152
- if (value.some((ref) => typeof ref !== "string" || ref.trim() === "")) throw new Error(`policyAudit ${label} must contain non-empty strings`);
1153
- const refs = [...new Set(value)];
1154
- if (refs.length > maximum) throw new Error(`policyAudit ${label} accepts at most ${maximum} objectRefs`);
1155
- return refs;
1156
- }
1157
- function emptyAudit(caller) {
1158
- return {
1159
- orgRows: [],
1160
- appRows: [],
1161
- inheritedOrgKeys: {},
1162
- userOverrideCounts: {},
1163
- driftRows: [],
1164
- policies: {},
1165
- caller
1166
- };
1128
+ })], FieldUsageCounterCollection);
1129
+ var FieldUsageReportReceiptCollection = class extends SmrtCollection {
1130
+ async claim(options) {
1131
+ const id = await fieldUsageReportReceiptId(options.tenantId, options.userId, options.objectRef, options.fieldName, options.period);
1132
+ if (await this.get(id)) return false;
1133
+ try {
1134
+ await this.create({
1135
+ ...options,
1136
+ id,
1137
+ _insertOnly: true
1138
+ });
1139
+ return true;
1140
+ } catch (error) {
1141
+ if (await this.get(id)) return false;
1142
+ throw error;
1143
+ }
1144
+ }
1145
+ };
1146
+ __publicField$1(FieldUsageReportReceiptCollection, "_itemClass", FieldUsageReportReceipt);
1147
+ FieldUsageReportReceiptCollection = __decorateClass$1([smrt({
1148
+ conflictColumns: [
1149
+ "tenant_id",
1150
+ "user_id",
1151
+ "object_ref",
1152
+ "field_name",
1153
+ "period"
1154
+ ],
1155
+ api: false,
1156
+ cli: false,
1157
+ mcp: false
1158
+ })], FieldUsageReportReceiptCollection);
1159
+ async function fieldUsageCounterId(tenantId, objectRef, fieldName, period) {
1160
+ return deterministicFieldsUuid([
1161
+ "field-usage-counter",
1162
+ tenantId,
1163
+ objectRef,
1164
+ fieldName,
1165
+ period
1166
+ ]);
1167
1167
  }
1168
- function dateText(value) {
1169
- if (value instanceof Date) return value.toISOString();
1170
- return typeof value === "string" ? value : null;
1168
+ async function fieldUsageReportReceiptId(tenantId, userId, objectRef, fieldName, period) {
1169
+ return deterministicFieldsUuid([
1170
+ "field-usage-report-receipt",
1171
+ tenantId,
1172
+ userId,
1173
+ objectRef,
1174
+ fieldName,
1175
+ period
1176
+ ]);
1171
1177
  }
1172
- function toAuditRow(row) {
1173
- return {
1174
- objectRef: row.objectRef,
1175
- defaultValue: row.defaultValue,
1176
- displayOrder: row.displayOrder,
1177
- fieldName: row.fieldName,
1178
- help: row.help,
1179
- id: String(row.id),
1180
- label: row.label,
1181
- locked: row.locked,
1182
- scopeType: row.scopeType,
1183
- tenantId: row.tenantId,
1184
- updatedBy: row.updatedBy,
1185
- userId: row.userId,
1186
- visibility: row.visibility,
1187
- createdAt: dateText(row.createdAt),
1188
- updatedAt: dateText(row.updatedAt)
1189
- };
1178
+ function isUsageAddressableField(field) {
1179
+ if (field._meta?.__smrtSystemField === true) return false;
1180
+ if (field.type === "oneToMany" || field.type === "manyToMany" || field.type === "meta") return false;
1181
+ return !isTransientField(field);
1190
1182
  }
1191
- function toDriftIdentity(row) {
1192
- return {
1193
- id: String(row.id),
1194
- objectRef: row.objectRef,
1195
- fieldName: row.fieldName,
1196
- scopeType: row.scopeType,
1197
- tenantId: row.tenantId,
1198
- userId: row.userId,
1199
- updatedBy: row.updatedBy,
1200
- createdAt: dateText(row.createdAt),
1201
- updatedAt: dateText(row.updatedAt)
1202
- };
1183
+ function isHistogramEligibleField(field) {
1184
+ return field.type === "boolean" || field.type === "foreignKey" || field.type === "crossPackageRef";
1203
1185
  }
1204
- function sortAuditRows(left, right) {
1205
- return left.objectRef.localeCompare(right.objectRef) || left.fieldName.localeCompare(right.fieldName);
1186
+ function serializeHistogramSample(field, value) {
1187
+ if (field.type === "boolean") return typeof value === "boolean" ? String(value) : null;
1188
+ if (isStorableReferenceId(field, value) && value.length <= 64) return value;
1189
+ return null;
1206
1190
  }
1207
- function sortDriftRows(left, right) {
1208
- return left.objectRef.localeCompare(right.objectRef) || left.fieldName.localeCompare(right.fieldName) || left.scopeType.localeCompare(right.scopeType);
1191
+ function decodeHistogramKey(field, key) {
1192
+ if (field.type === "boolean") return key === "true";
1193
+ return key;
1194
+ }
1195
+ function validateEntriesInput(rawEntries) {
1196
+ if (!Array.isArray(rawEntries) || rawEntries.length === 0) throw new Error("reportUsage requires a non-empty \"entries\" array of { objectRef, fieldName, value?, matchedDefault? } samples");
1197
+ if (rawEntries.length > 100) throw new Error(`reportUsage accepts at most 100 entries per call (got ${rawEntries.length})`);
1198
+ for (const entry of rawEntries) if (!entry || typeof entry !== "object" || typeof entry.objectRef !== "string" || entry.objectRef.trim() === "" || typeof entry.fieldName !== "string" || entry.fieldName.trim() === "") throw new Error("reportUsage entries must carry non-empty objectRef and fieldName strings");
1199
+ return rawEntries;
1209
1200
  }
1210
1201
  //#endregion
1211
1202
  //#region src/field-policy-resolver.ts
@@ -1399,7 +1390,7 @@ async function defaultTenantHierarchyLoader(options) {
1399
1390
  throw error;
1400
1391
  }
1401
1392
  }
1402
- var MISSING_MODULE_TARGET_PATTERN = /Cannot find (?:package|module) '([^']+)'/;
1393
+ var MISSING_MODULE_TARGET_PATTERN$1 = /Cannot find (?:package|module) '([^']+)'/;
1403
1394
  function isUsersSpecifier(target) {
1404
1395
  return target === "@happyvertical/smrt-users" || target.startsWith("@happyvertical/smrt-users/");
1405
1396
  }
@@ -1408,7 +1399,7 @@ function isMissingUsersDependency(error) {
1408
1399
  const seen = /* @__PURE__ */ new Set();
1409
1400
  while (current instanceof Error && !seen.has(current)) {
1410
1401
  seen.add(current);
1411
- const match = current.message.match(MISSING_MODULE_TARGET_PATTERN);
1402
+ const match = current.message.match(MISSING_MODULE_TARGET_PATTERN$1);
1412
1403
  if (match && isUsersSpecifier(match[1])) return true;
1413
1404
  if (current.message.includes("Failed to load @happyvertical/smrt-users for")) return true;
1414
1405
  current = current.cause;
@@ -1625,9 +1616,506 @@ function spaced(value) {
1625
1616
  return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ");
1626
1617
  }
1627
1618
  //#endregion
1619
+ //#region src/usage-learning.ts
1620
+ var __defProp = Object.defineProperty;
1621
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1622
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
1623
+ enumerable: true,
1624
+ configurable: true,
1625
+ writable: true,
1626
+ value
1627
+ }) : obj[key] = value;
1628
+ var __decorateClass = (decorators, target, key, kind) => {
1629
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1630
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1631
+ if (kind && result) __defProp(target, key, result);
1632
+ return result;
1633
+ };
1634
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
1635
+ var DAY_MS = 1440 * 60 * 1e3;
1636
+ var FIELD_USAGE_MAINTENANCE_DEFAULTS = {
1637
+ counterMaxAgeMs: 90 * DAY_MS,
1638
+ counterMaxRows: 1e5,
1639
+ suggestionAcceptedMaxAgeMs: 180 * DAY_MS
1640
+ };
1641
+ var FIELD_USAGE_SUGGESTION_DEFAULTS = {
1642
+ windowDays: 30,
1643
+ minDistinctUsers: 5,
1644
+ minSetCount: 10,
1645
+ defaultDominanceRatio: .8
1646
+ };
1647
+ async function pruneFieldUsageCounters(db, retention) {
1648
+ const { maxAgeMs, maxRows } = retention;
1649
+ if (maxAgeMs == null && maxRows == null) throw new Error("pruneFieldUsageCounters requires maxAgeMs and/or maxRows");
1650
+ if (maxAgeMs != null && (!Number.isFinite(maxAgeMs) || maxAgeMs < 0)) throw new Error(`pruneFieldUsageCounters maxAgeMs must be >= 0, got ${maxAgeMs}`);
1651
+ if (maxRows != null && (!Number.isFinite(maxRows) || maxRows < 0)) throw new Error(`pruneFieldUsageCounters maxRows must be >= 0, got ${maxRows}`);
1652
+ const tenantCondition = retention.tenantId ? " AND tenant_id = ?" : "";
1653
+ const tenantParams = retention.tenantId ? [retention.tenantId] : [];
1654
+ let pruned = 0;
1655
+ if (maxAgeMs != null) {
1656
+ const cutoffDay = fieldUsagePeriodForDate(new Date(Date.now() - maxAgeMs));
1657
+ pruned += await deleteCounted(db, `period < ?${tenantCondition}`, [cutoffDay, ...tenantParams]);
1658
+ }
1659
+ if (maxRows != null) {
1660
+ const excess = numberFromRow(getQueryRows(await db.query(`SELECT COUNT(*) AS total FROM _smrt_field_usage_counters${retention.tenantId ? " WHERE tenant_id = ?" : ""}`, ...tenantParams))[0] ?? {}, "total") - Math.floor(maxRows);
1661
+ if (excess > 0) {
1662
+ await db.query(`DELETE FROM _smrt_field_usage_counters
1663
+ WHERE id IN (
1664
+ SELECT id FROM _smrt_field_usage_counters
1665
+ ${retention.tenantId ? "WHERE tenant_id = ?" : ""}
1666
+ ORDER BY period ASC, id ASC
1667
+ LIMIT ${excess}
1668
+ )`, ...tenantParams);
1669
+ pruned += excess;
1670
+ }
1671
+ }
1672
+ return { pruned };
1673
+ }
1674
+ async function pruneFieldUsageReportReceipts(db, options) {
1675
+ if (!Number.isFinite(options.maxAgeMs) || options.maxAgeMs < 0) throw new Error(`pruneFieldUsageReportReceipts maxAgeMs must be >= 0, got ${options.maxAgeMs}`);
1676
+ const cutoffDay = fieldUsagePeriodForDate(new Date(Date.now() - options.maxAgeMs));
1677
+ const tenantCondition = options.tenantId ? " AND tenant_id = ?" : "";
1678
+ const tenantParams = options.tenantId ? [options.tenantId] : [];
1679
+ return { pruned: await deleteCounted(db, `period < ?${tenantCondition}`, [cutoffDay, ...tenantParams], "_smrt_field_usage_report_receipts") };
1680
+ }
1681
+ async function pruneFieldPolicySuggestions(db, retention) {
1682
+ const { acceptedMaxAgeMs } = retention;
1683
+ if (!Number.isFinite(acceptedMaxAgeMs) || acceptedMaxAgeMs < 0) throw new Error(`pruneFieldPolicySuggestions acceptedMaxAgeMs must be >= 0, got ${acceptedMaxAgeMs}`);
1684
+ const now = retention.now ?? /* @__PURE__ */ new Date();
1685
+ const acceptedCutoff = new Date(now.getTime() - acceptedMaxAgeMs);
1686
+ const tenantCondition = retention.tenantId ? " AND tenant_id = ?" : "";
1687
+ const tenantParams = retention.tenantId ? [retention.tenantId] : [];
1688
+ return { pruned: await deleteCounted(db, `((status = 'dismissed' AND cooldown_until IS NOT NULL AND cooldown_until <= ?) OR (status = 'accepted' AND decided_at IS NOT NULL AND decided_at <= ?))${tenantCondition}`, [
1689
+ now.toISOString(),
1690
+ acceptedCutoff.toISOString(),
1691
+ ...tenantParams
1692
+ ], "_smrt_field_policy_suggestions") };
1693
+ }
1694
+ async function runFieldUsageMaintenance(options) {
1695
+ const config = normalizeMaintenanceConfig(options);
1696
+ const ambientTenantId = restrictingTenantId();
1697
+ const counters = await pruneFieldUsageCounters(options.db, {
1698
+ maxAgeMs: config.counterMaxAgeMs,
1699
+ maxRows: config.counterMaxRows,
1700
+ tenantId: ambientTenantId
1701
+ });
1702
+ const receipts = await pruneFieldUsageReportReceipts(options.db, {
1703
+ maxAgeMs: config.counterMaxAgeMs,
1704
+ tenantId: ambientTenantId
1705
+ });
1706
+ const suggestions = await pruneFieldPolicySuggestions(options.db, {
1707
+ acceptedMaxAgeMs: config.suggestionAcceptedMaxAgeMs,
1708
+ tenantId: ambientTenantId
1709
+ });
1710
+ return {
1711
+ countersPruned: counters.pruned,
1712
+ receiptsPruned: receipts.pruned,
1713
+ suggestionsPruned: suggestions.pruned
1714
+ };
1715
+ }
1716
+ async function runFieldPolicySuggestionGeneration(options) {
1717
+ const config = normalizeSuggestionConfig(options);
1718
+ const now = options.now ?? /* @__PURE__ */ new Date();
1719
+ const ambientTenantId = restrictingTenantId();
1720
+ const counters = await FieldUsageCounterCollection.create({ db: options.db });
1721
+ const suggestions = await FieldPolicySuggestionCollection.create({ db: options.db });
1722
+ const fromPeriod = fieldUsagePeriodForDate(/* @__PURE__ */ new Date(now.getTime() - (config.windowDays - 1) * DAY_MS));
1723
+ const toPeriod = fieldUsagePeriodForDate(now);
1724
+ const groups = groupCounters(await counters.listWindow({
1725
+ fromPeriod,
1726
+ toPeriod,
1727
+ tenantId: ambientTenantId
1728
+ }));
1729
+ const fieldMaps = /* @__PURE__ */ new Map();
1730
+ const resolvedPolicies = /* @__PURE__ */ new Map();
1731
+ const summary = {
1732
+ created: 0,
1733
+ suppressed: 0,
1734
+ groupsConsidered: 0,
1735
+ groupsFailed: 0,
1736
+ failures: []
1737
+ };
1738
+ for (const group of groups) {
1739
+ summary.groupsConsidered += 1;
1740
+ try {
1741
+ await evaluateGroup(group);
1742
+ } catch (error) {
1743
+ summary.groupsFailed += 1;
1744
+ if (summary.failures.length < 5) summary.failures.push(`${group.tenantId} ${group.objectRef}.${group.fieldName}: ${error instanceof Error ? error.message : String(error)}`);
1745
+ }
1746
+ }
1747
+ return summary;
1748
+ async function evaluateGroup(group) {
1749
+ let fieldMap = fieldMaps.get(group.objectRef);
1750
+ if (fieldMap === void 0) {
1751
+ try {
1752
+ fieldMap = await getObjectFieldMap(group.objectRef);
1753
+ } catch {
1754
+ fieldMap = null;
1755
+ }
1756
+ fieldMaps.set(group.objectRef, fieldMap);
1757
+ }
1758
+ if (!fieldMap) return;
1759
+ const fieldDef = fieldMap.get(group.fieldName);
1760
+ if (!fieldDef || isSensitiveField(fieldDef) || getFieldReadPermission(fieldDef) !== void 0 || isTransientField(fieldDef)) return;
1761
+ const stats = mergeGroupStats(group.buckets);
1762
+ const policyKey = `${group.tenantId}\0${group.objectRef}`;
1763
+ let orgFields = resolvedPolicies.get(policyKey);
1764
+ if (!orgFields) {
1765
+ const { resolveFieldPolicy } = await Promise.resolve().then(() => field_policy_resolver_exports);
1766
+ orgFields = (await resolveFieldPolicy(group.objectRef, {
1767
+ tenantId: group.tenantId,
1768
+ db: options.db
1769
+ })).fields;
1770
+ resolvedPolicies.set(policyKey, orgFields);
1771
+ }
1772
+ const fieldPolicy = orgFields[group.fieldName];
1773
+ if (!fieldPolicy) return;
1774
+ if (stats.distinctUsers >= config.minDistinctUsers && fieldPolicy.visibility !== "basic") if (await createUnlessSuppressed(suggestions, {
1775
+ tenantId: group.tenantId,
1776
+ objectRef: group.objectRef,
1777
+ fieldName: group.fieldName,
1778
+ kind: "promote",
1779
+ proposedValue: null,
1780
+ evidence: buildFieldUsageEvidence({
1781
+ kind: "promote",
1782
+ fieldName: group.fieldName,
1783
+ objectRef: group.objectRef,
1784
+ windowStart: fromPeriod,
1785
+ windowEnd: toPeriod,
1786
+ distinctUsers: stats.distinctUsers,
1787
+ distinctUsersAtLeast: stats.distinctUsersOverflowed,
1788
+ setCount: stats.setCount,
1789
+ submissionCount: stats.submissionTotalKnown ? stats.submissionCount : void 0,
1790
+ threshold: config.minDistinctUsers
1791
+ }),
1792
+ now
1793
+ })) summary.created += 1;
1794
+ else summary.suppressed += 1;
1795
+ if (isHistogramEligibleField(fieldDef) && stats.submissionTotalKnown && stats.submissionCount >= config.minSetCount) {
1796
+ const top = topHistogramEntry(stats.histogram);
1797
+ if (top) {
1798
+ const share = top.count / stats.submissionCount;
1799
+ const proposed = decodeHistogramKey(fieldDef, top.key);
1800
+ const currentDefault = fieldPolicy.hasDefault ? fieldPolicy.defaultValue : void 0;
1801
+ if (share >= config.defaultDominanceRatio && !sameProposedValue(proposed, currentDefault)) if (await createUnlessSuppressed(suggestions, {
1802
+ tenantId: group.tenantId,
1803
+ objectRef: group.objectRef,
1804
+ fieldName: group.fieldName,
1805
+ kind: "default",
1806
+ proposedValue: JSON.stringify(proposed),
1807
+ evidence: buildFieldUsageEvidence({
1808
+ kind: "default",
1809
+ fieldName: group.fieldName,
1810
+ objectRef: group.objectRef,
1811
+ windowStart: fromPeriod,
1812
+ windowEnd: toPeriod,
1813
+ distinctUsers: stats.distinctUsers,
1814
+ distinctUsersAtLeast: stats.distinctUsersOverflowed,
1815
+ setCount: stats.setCount,
1816
+ submissionCount: stats.submissionCount,
1817
+ threshold: config.minSetCount,
1818
+ topValue: proposed,
1819
+ topValueShare: share
1820
+ }),
1821
+ now
1822
+ })) summary.created += 1;
1823
+ else summary.suppressed += 1;
1824
+ }
1825
+ }
1826
+ }
1827
+ }
1828
+ var FieldUsageLearningAgent = class extends SmrtObject {
1829
+ constructor(options = {}) {
1830
+ super(options);
1831
+ }
1832
+ /** Schedule entry point for {@link runFieldUsageMaintenance}. */
1833
+ async runUsageMaintenance(args = {}) {
1834
+ return runFieldUsageMaintenance({
1835
+ db: this.db,
1836
+ ...pickFiniteNumbers(args, [
1837
+ "counterMaxAgeMs",
1838
+ "counterMaxRows",
1839
+ "suggestionAcceptedMaxAgeMs"
1840
+ ])
1841
+ });
1842
+ }
1843
+ /** Schedule entry point for {@link runFieldPolicySuggestionGeneration}. */
1844
+ async runSuggestionGeneration(args = {}) {
1845
+ return runFieldPolicySuggestionGeneration({
1846
+ db: this.db,
1847
+ ...pickFiniteNumbers(args, [
1848
+ "windowDays",
1849
+ "minDistinctUsers",
1850
+ "minSetCount",
1851
+ "defaultDominanceRatio"
1852
+ ])
1853
+ });
1854
+ }
1855
+ };
1856
+ /**
1857
+ * The smrt-jobs opt-in background allowlist (S5 contract): ONLY these two
1858
+ * methods are reachable from a persisted job/schedule row.
1859
+ */
1860
+ __publicField(FieldUsageLearningAgent, "backgroundEligibleMethods", ["runUsageMaintenance", "runSuggestionGeneration"]);
1861
+ FieldUsageLearningAgent = __decorateClass([smrt({
1862
+ tableName: "_smrt_field_usage_learning_agents",
1863
+ api: { include: [] },
1864
+ cli: false,
1865
+ mcp: { include: [] }
1866
+ })], FieldUsageLearningAgent);
1867
+ function buildFieldUsageEvidence(input) {
1868
+ const users = `${input.distinctUsersAtLeast ? "at least " : ""}${input.distinctUsers} user${input.distinctUsers === 1 && !input.distinctUsersAtLeast ? "" : "s"}`;
1869
+ const deviations = `${input.setCount} of ` + (input.submissionCount === void 0 ? "an unrecorded number of submissions" : `${input.submissionCount} submission${input.submissionCount === 1 ? "" : "s"}`);
1870
+ const base = `${users} set "${input.fieldName}" to a non-default value in ${deviations} between ${input.windowStart} and ${input.windowEnd}.`;
1871
+ return {
1872
+ summary: input.kind === "default" ? `${base} ${Math.round((input.topValueShare ?? 0) * 100)}% of all ${input.submissionCount ?? 0} submissions used the value ${JSON.stringify(input.topValue)}.` : base,
1873
+ objectRef: input.objectRef,
1874
+ fieldName: input.fieldName,
1875
+ windowStart: input.windowStart,
1876
+ windowEnd: input.windowEnd,
1877
+ distinctUsers: input.distinctUsers,
1878
+ ...input.distinctUsersAtLeast ? { distinctUsersAtLeast: true } : {},
1879
+ setCount: input.setCount,
1880
+ ...input.submissionCount !== void 0 ? { submissionCount: input.submissionCount } : { submissionCountUnknown: true },
1881
+ threshold: input.threshold,
1882
+ ...input.topValue !== void 0 ? { topValue: input.topValue } : {},
1883
+ ...input.topValueShare !== void 0 ? { topValueShare: Number(input.topValueShare.toFixed(4)) } : {}
1884
+ };
1885
+ }
1886
+ function groupCounters(rows) {
1887
+ const byKey = /* @__PURE__ */ new Map();
1888
+ for (const row of rows) {
1889
+ if (!row.tenantId) continue;
1890
+ const key = `${row.tenantId}\0${row.objectRef}\0${row.fieldName}`;
1891
+ let group = byKey.get(key);
1892
+ if (!group) {
1893
+ group = {
1894
+ tenantId: row.tenantId,
1895
+ objectRef: row.objectRef,
1896
+ fieldName: row.fieldName,
1897
+ buckets: []
1898
+ };
1899
+ byKey.set(key, group);
1900
+ }
1901
+ group.buckets.push(row);
1902
+ }
1903
+ return [...byKey.values()];
1904
+ }
1905
+ function mergeGroupStats(buckets) {
1906
+ let submissionCount = 0;
1907
+ let submissionTotalKnown = true;
1908
+ let setCount = 0;
1909
+ let overflowed = false;
1910
+ const users = /* @__PURE__ */ new Set();
1911
+ const histogram = emptyHistogram();
1912
+ for (const bucket of buckets) {
1913
+ submissionCount += bucket.submissionCount;
1914
+ if (bucket.isLegacyBucket()) submissionTotalKnown = false;
1915
+ setCount += bucket.setCount;
1916
+ overflowed = overflowed || bucket.distinctUsersOverflowed;
1917
+ for (const id of bucket.getDistinctUserIds()) users.add(id);
1918
+ for (const [key, count] of Object.entries(bucket.getValueHistogram())) histogram[key] = (Object.hasOwn(histogram, key) ? histogram[key] : 0) + count;
1919
+ }
1920
+ return {
1921
+ submissionCount,
1922
+ submissionTotalKnown,
1923
+ setCount,
1924
+ distinctUsers: users.size,
1925
+ distinctUsersOverflowed: overflowed,
1926
+ histogram
1927
+ };
1928
+ }
1929
+ function topHistogramEntry(histogram) {
1930
+ let top = null;
1931
+ for (const [key, count] of Object.entries(histogram)) if (!top || count > top.count) top = {
1932
+ key,
1933
+ count
1934
+ };
1935
+ return top;
1936
+ }
1937
+ function sameProposedValue(a, b) {
1938
+ if (a === b) return true;
1939
+ try {
1940
+ return JSON.stringify(a) === JSON.stringify(b);
1941
+ } catch {
1942
+ return false;
1943
+ }
1944
+ }
1945
+ async function createUnlessSuppressed(suggestions, candidate) {
1946
+ if ((await suggestions.list({ where: {
1947
+ tenantId: candidate.tenantId,
1948
+ objectRef: candidate.objectRef,
1949
+ fieldName: candidate.fieldName,
1950
+ kind: candidate.kind
1951
+ } })).some((row) => row.activeKey === "active" || row.status === "dismissed" && isWithinCoolDown(row, candidate.now))) return false;
1952
+ await suggestions.create({
1953
+ tenantId: candidate.tenantId,
1954
+ objectRef: candidate.objectRef,
1955
+ fieldName: candidate.fieldName,
1956
+ kind: candidate.kind,
1957
+ proposedValue: candidate.proposedValue,
1958
+ evidence: JSON.stringify(candidate.evidence),
1959
+ status: "pending"
1960
+ });
1961
+ return true;
1962
+ }
1963
+ function isWithinCoolDown(row, now) {
1964
+ const raw = row.cooldownUntil;
1965
+ if (raw === null || raw === void 0) return false;
1966
+ const until = raw instanceof Date ? raw.getTime() : Date.parse(String(raw));
1967
+ return Number.isFinite(until) && until > now.getTime();
1968
+ }
1969
+ function restrictingTenantId() {
1970
+ const context = getCurrentTenant();
1971
+ if (!context || isSuperAdminBypass()) return null;
1972
+ return context.tenantId;
1973
+ }
1974
+ function normalizeMaintenanceConfig(options) {
1975
+ return {
1976
+ counterMaxAgeMs: nonNegativeOrDefault(options.counterMaxAgeMs, FIELD_USAGE_MAINTENANCE_DEFAULTS.counterMaxAgeMs),
1977
+ counterMaxRows: nonNegativeOrDefault(options.counterMaxRows, FIELD_USAGE_MAINTENANCE_DEFAULTS.counterMaxRows),
1978
+ suggestionAcceptedMaxAgeMs: nonNegativeOrDefault(options.suggestionAcceptedMaxAgeMs, FIELD_USAGE_MAINTENANCE_DEFAULTS.suggestionAcceptedMaxAgeMs)
1979
+ };
1980
+ }
1981
+ function normalizeSuggestionConfig(options) {
1982
+ const ratio = positiveOrDefault(options.defaultDominanceRatio, FIELD_USAGE_SUGGESTION_DEFAULTS.defaultDominanceRatio);
1983
+ return {
1984
+ windowDays: Math.max(1, Math.floor(positiveOrDefault(options.windowDays, FIELD_USAGE_SUGGESTION_DEFAULTS.windowDays))),
1985
+ minDistinctUsers: Math.max(1, Math.floor(positiveOrDefault(options.minDistinctUsers, FIELD_USAGE_SUGGESTION_DEFAULTS.minDistinctUsers))),
1986
+ minSetCount: Math.max(1, Math.floor(positiveOrDefault(options.minSetCount, FIELD_USAGE_SUGGESTION_DEFAULTS.minSetCount))),
1987
+ defaultDominanceRatio: Math.min(Math.max(ratio, .01), 1)
1988
+ };
1989
+ }
1990
+ function positiveOrDefault(value, fallback) {
1991
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1992
+ }
1993
+ function nonNegativeOrDefault(value, fallback) {
1994
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
1995
+ }
1996
+ function pickFiniteNumbers(args, keys) {
1997
+ const picked = {};
1998
+ if (!args || typeof args !== "object") return picked;
1999
+ for (const key of keys) {
2000
+ const value = args[key];
2001
+ if (typeof value === "number" && Number.isFinite(value)) picked[key] = value;
2002
+ }
2003
+ return picked;
2004
+ }
2005
+ async function deleteCounted(db, condition, params, table = "_smrt_field_usage_counters") {
2006
+ const total = numberFromRow(getQueryRows(await db.query(`SELECT COUNT(*) AS total FROM ${table} WHERE ${condition}`, ...params))[0] ?? {}, "total");
2007
+ if (total > 0) await db.query(`DELETE FROM ${table} WHERE ${condition}`, ...params);
2008
+ return total;
2009
+ }
2010
+ function getQueryRows(result) {
2011
+ return Array.isArray(result) ? result : result?.rows ?? [];
2012
+ }
2013
+ function numberFromRow(row, key) {
2014
+ const value = row[key];
2015
+ if (typeof value === "number") return value;
2016
+ if (typeof value === "bigint") return Number(value);
2017
+ if (typeof value === "string") return Number.parseFloat(value) || 0;
2018
+ return 0;
2019
+ }
2020
+ //#endregion
2021
+ //#region src/users-module.ts
2022
+ var MISSING_MODULE_TARGET_PATTERN = /Cannot find (?:package|module) '([^']+)'/;
2023
+ function isPackageSpecifier(target, packageName) {
2024
+ return target === packageName || target.startsWith(`${packageName}/`);
2025
+ }
2026
+ function isMissingWorkspaceDependency(error, packageName) {
2027
+ let current = error;
2028
+ const seen = /* @__PURE__ */ new Set();
2029
+ while (current instanceof Error && !seen.has(current)) {
2030
+ seen.add(current);
2031
+ const match = current.message.match(MISSING_MODULE_TARGET_PATTERN);
2032
+ if (match && isPackageSpecifier(match[1], packageName)) return true;
2033
+ if (current.message.includes(`Failed to load ${packageName} for`)) return true;
2034
+ current = current.cause;
2035
+ }
2036
+ return false;
2037
+ }
2038
+ //#endregion
2039
+ //#region src/usage-schedules.ts
2040
+ var FIELD_USAGE_LEARNING_AGENT_TYPE = "@happyvertical/smrt-fields:FieldUsageLearningAgent";
2041
+ var FIELD_USAGE_MAINTENANCE_METHOD = "runUsageMaintenance";
2042
+ var FIELD_USAGE_SUGGESTION_METHOD = "runSuggestionGeneration";
2043
+ function fieldUsageScheduleId(method) {
2044
+ return deterministicFieldsUuid([
2045
+ "field-usage-learning-schedule",
2046
+ FIELD_USAGE_LEARNING_AGENT_TYPE,
2047
+ method
2048
+ ]);
2049
+ }
2050
+ async function ensureFieldUsageLearningSchedules(options) {
2051
+ const context = getCurrentTenant();
2052
+ if (context && !isSuperAdminBypass()) throw new TenantIsolationError("ensureFieldUsageLearningSchedules installs GLOBAL schedules and must run from trusted execution (no ambient tenant context, or super-admin bypass)", { tenantId: context.tenantId });
2053
+ let agentsModule = options.agentsModule;
2054
+ if (!agentsModule) try {
2055
+ agentsModule = await importWorkspaceModule({
2056
+ packageName: "@happyvertical/smrt-agents",
2057
+ sourceEntry: "packages/agents/src/index.ts",
2058
+ purpose: "field usage learning schedule installation"
2059
+ });
2060
+ } catch (error) {
2061
+ if (isMissingWorkspaceDependency(error, "@happyvertical/smrt-agents")) return {
2062
+ installed: false,
2063
+ created: 0
2064
+ };
2065
+ throw error;
2066
+ }
2067
+ const schedules = await agentsModule.AgentScheduleCollection.create({ db: options.db });
2068
+ const enabled = options.enabled ?? false;
2069
+ let created = 0;
2070
+ const definitions = [{
2071
+ method: FIELD_USAGE_MAINTENANCE_METHOD,
2072
+ cron: options.maintenanceCron ?? "30 2 * * *",
2073
+ methodArgs: options.maintenanceArgs ?? {}
2074
+ }, {
2075
+ method: FIELD_USAGE_SUGGESTION_METHOD,
2076
+ cron: options.suggestionCron ?? "0 3 * * 1",
2077
+ methodArgs: options.suggestionArgs ?? {}
2078
+ }];
2079
+ for (const definition of definitions) {
2080
+ if ((await schedules.list({ where: {
2081
+ agentType: "@happyvertical/smrt-fields:FieldUsageLearningAgent",
2082
+ method: definition.method
2083
+ } })).some(isGlobalScheduleRow)) continue;
2084
+ const id = await fieldUsageScheduleId(definition.method);
2085
+ let row;
2086
+ try {
2087
+ row = await schedules.create({
2088
+ id,
2089
+ tenantId: null,
2090
+ agentType: FIELD_USAGE_LEARNING_AGENT_TYPE,
2091
+ agentId: null,
2092
+ cron: definition.cron,
2093
+ method: definition.method,
2094
+ agentConfig: {},
2095
+ methodArgs: definition.methodArgs,
2096
+ enabled,
2097
+ status: enabled ? "active" : "disabled",
2098
+ _insertOnly: true
2099
+ });
2100
+ } catch (error) {
2101
+ if ((await schedules.list({ where: { id } })).length === 0) throw error;
2102
+ continue;
2103
+ }
2104
+ await row.save?.();
2105
+ created += 1;
2106
+ }
2107
+ return {
2108
+ installed: true,
2109
+ created
2110
+ };
2111
+ }
2112
+ function isGlobalScheduleRow(row) {
2113
+ return row.tenantId === null || row.tenantId === void 0;
2114
+ }
2115
+ //#endregion
1628
2116
  //#region src/index.ts
1629
2117
  ensureFieldPolicyPermissionsRegistered();
1630
2118
  //#endregion
1631
- export { APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_PERMISSION_DEFINITIONS, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES, FieldPolicy, FieldPolicyCollection, MANAGE_FIELD_POLICY_PERMISSION, PERSONALIZE_FIELD_POLICY_PERMISSION, assertDefaultValueMatchesFieldType, buildCodeSeedDelta, buildCodeSeedVisibility, buildFieldPolicySettingsCatalog, clearFieldPolicyCache, ensureFieldPolicyPermissionsRegistered, fieldPolicyCatalogItemId, getCodeDefault, getCodeSeedGroup, getFieldPolicyCacheTtlMs, getFieldReadPermission, getObjectFieldMap, invalidateFieldPolicyCache, isPolicyAddressableField, isRequiredField, isSensitiveField, isTransientField, isUsableRequiredDefault, parseFieldPolicyCatalogQuery, requireRegisteredObject, resolveFieldPolicy, resolveFieldPolicyExplained, resolveSurvivingTenantChainIds, sanitizeFieldUIHints };
2119
+ export { ACTIVE_SUGGESTION_KEY, APP_FIELD_POLICY_SCOPE_KEY, FIELD_POLICY_PERMISSION_DEFINITIONS, FIELD_POLICY_SCOPE_TYPES, FIELD_POLICY_VISIBILITIES, FIELD_USAGE_LEARNING_AGENT_TYPE, FieldPolicy, FieldPolicyCollection, FieldPolicySuggestion, FieldPolicySuggestionCollection, FieldPolicySuggestionConflictError, FieldUsageCounter, FieldUsageCounterCollection, FieldUsageLearningAgent, MANAGE_FIELD_POLICY_PERMISSION, MAX_DISTINCT_USERS_PER_BUCKET, MAX_USAGE_REPORT_ENTRIES, MAX_VALUE_HISTOGRAM_BUCKETS, PERSONALIZE_FIELD_POLICY_PERMISSION, assertDefaultValueMatchesFieldType, buildCodeSeedDelta, buildCodeSeedVisibility, buildFieldPolicySettingsCatalog, clearFieldPolicyCache, decodeHistogramKey, ensureFieldPolicyPermissionsRegistered, ensureFieldUsageLearningSchedules, fieldPolicyCatalogItemId, fieldUsagePeriodForDate, getCodeDefault, getCodeSeedGroup, getFieldPolicyCacheTtlMs, getFieldReadPermission, getObjectFieldMap, invalidateFieldPolicyCache, isHistogramEligibleField, isPolicyAddressableField, isRequiredField, isSensitiveField, isStorableReferenceId, isTransientField, isUsableRequiredDefault, parseFieldPolicyCatalogQuery, pruneFieldPolicySuggestions, pruneFieldUsageCounters, requireRegisteredObject, resolveFieldPolicy, resolveFieldPolicyExplained, resolveSurvivingTenantChainIds, runFieldPolicySuggestionGeneration, runFieldUsageMaintenance, sanitizeFieldUIHints, serializeHistogramSample, field_policy_resolver_exports as t };
1632
2120
 
1633
2121
  //# sourceMappingURL=index.js.map