@happyvertical/smrt-fields 0.40.60 → 0.40.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +58 -12
- package/README.md +143 -42
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/cache.d.ts +18 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/chunks/FieldPolicyCollection--RxukfCX.js +1218 -0
- package/dist/chunks/FieldPolicyCollection--RxukfCX.js.map +1 -0
- package/dist/collections/FieldPolicyCollection.d.ts +95 -0
- package/dist/collections/FieldPolicyCollection.d.ts.map +1 -0
- package/dist/collections/FieldPolicySuggestionCollection.d.ts +160 -0
- package/dist/collections/FieldPolicySuggestionCollection.d.ts.map +1 -0
- package/dist/collections/FieldUsageCounterCollection.d.ts +143 -0
- package/dist/collections/FieldUsageCounterCollection.d.ts.map +1 -0
- package/dist/deterministic-id.d.ts +18 -0
- package/dist/deterministic-id.d.ts.map +1 -0
- package/dist/field-definitions.d.ts +85 -0
- package/dist/field-definitions.d.ts.map +1 -0
- package/dist/field-policy-resolver.d.ts +46 -0
- package/dist/field-policy-resolver.d.ts.map +1 -0
- package/dist/index.d.ts +15 -532
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1813 -628
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1487 -191
- package/dist/models/FieldPolicy.d.ts +163 -0
- package/dist/models/FieldPolicy.d.ts.map +1 -0
- package/dist/models/FieldPolicySuggestion.d.ts +132 -0
- package/dist/models/FieldPolicySuggestion.d.ts.map +1 -0
- package/dist/models/FieldUsageCounter.d.ts +178 -0
- package/dist/models/FieldUsageCounter.d.ts.map +1 -0
- package/dist/models/FieldUsageReportReceipt.d.ts +25 -0
- package/dist/models/FieldUsageReportReceipt.d.ts.map +1 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +39 -0
- package/dist/playground.js.map +1 -0
- package/dist/settings-catalog.d.ts +73 -0
- package/dist/settings-catalog.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +449 -9
- package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +277 -0
- package/dist/svelte/__tests__/FieldPolicyGear.test.js +503 -0
- package/dist/svelte/__tests__/FormHelp.test.js +138 -0
- package/dist/svelte/__tests__/ModeUX.test.js +109 -0
- package/dist/svelte/__tests__/ObjectForm.test.js +687 -0
- package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
- package/dist/svelte/__tests__/PolicyField.test.js +373 -0
- package/dist/svelte/__tests__/UsageLearning.test.js +140 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte +30 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte +19 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte +11 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +37 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +15 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte +17 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte +34 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte +40 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte +26 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte +39 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts +17 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte +15 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts +5 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte +28 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte +23 -0
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte +14 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/settings-catalog.test.js +161 -0
- package/dist/svelte/components/AdvancedFields.svelte +49 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts +17 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldInput.svelte +159 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts +5 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte +288 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +29 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte +417 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts +18 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte +24 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts +8 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte +169 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +20 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte +89 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts +35 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicySuggestionQueue.svelte +135 -0
- package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts +11 -0
- package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FormHelp.svelte +157 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts +15 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ModeSwitch.svelte +47 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts +10 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectForm.svelte +440 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts +50 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte +18 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts +11 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PolicyField.svelte +338 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts +80 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.d.ts +54 -0
- package/dist/svelte/context.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.js +55 -0
- package/dist/svelte/data-table.d.ts +17 -0
- package/dist/svelte/data-table.d.ts.map +1 -0
- package/dist/svelte/data-table.js +19 -0
- package/dist/svelte/field-policy-editor.d.ts +81 -0
- package/dist/svelte/field-policy-editor.d.ts.map +1 -0
- package/dist/svelte/field-policy-editor.js +120 -0
- package/dist/svelte/gear-context.svelte.d.ts +16 -0
- package/dist/svelte/gear-context.svelte.d.ts.map +1 -0
- package/dist/svelte/gear-context.svelte.js +15 -0
- package/dist/svelte/index.d.ts +63 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +41 -0
- package/dist/svelte/input-registry.d.ts +21 -0
- package/dist/svelte/input-registry.d.ts.map +1 -0
- package/dist/svelte/input-registry.js +26 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts +4 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source-context.svelte.js +8 -0
- package/dist/svelte/object-form-source.svelte.d.ts +18 -0
- package/dist/svelte/object-form-source.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source.svelte.js +99 -0
- package/dist/svelte/object-form.d.ts +6 -0
- package/dist/svelte/object-form.d.ts.map +1 -0
- package/dist/svelte/object-form.js +38 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte +212 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte +129 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +20 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +38 -0
- package/dist/svelte/settings-catalog.d.ts +54 -0
- package/dist/svelte/settings-catalog.d.ts.map +1 -0
- package/dist/svelte/settings-catalog.js +116 -0
- package/dist/svelte/suggestions.d.ts +38 -0
- package/dist/svelte/suggestions.d.ts.map +1 -0
- package/dist/svelte/suggestions.js +68 -0
- package/dist/svelte/types.d.ts +101 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +4 -0
- package/dist/svelte/usage-capture.d.ts +63 -0
- package/dist/svelte/usage-capture.d.ts.map +1 -0
- package/dist/svelte/usage-capture.js +61 -0
- package/dist/types.d.ts +316 -203
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/dist/usage-learning.d.ts +201 -0
- package/dist/usage-learning.d.ts.map +1 -0
- package/dist/usage-schedules.d.ts +110 -0
- package/dist/usage-schedules.d.ts.map +1 -0
- package/dist/users-module.d.ts +30 -0
- package/dist/users-module.d.ts.map +1 -0
- package/package.json +33 -8
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { SmrtObject } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FieldPolicyOptions, FieldPolicyScopeType, FieldPolicyVisibility } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Sparse, layered field policy override (epic #2045, issue #2047).
|
|
5
|
+
*
|
|
6
|
+
* One row personalizes a subset of `{defaultValue, visibility, help, label,
|
|
7
|
+
* displayOrder, locked}` for a single `(objectRef, fieldName)` at one scope
|
|
8
|
+
* tier. A NULL column means "inherit from the lower layer" (code seed → app →
|
|
9
|
+
* tenant → user), so resetting a customization is a row DELETE — later
|
|
10
|
+
* lower-layer changes then flow through (sparse-delta rationale, #1770).
|
|
11
|
+
*
|
|
12
|
+
* Rows are validated at write time against the live `ObjectRegistry` (the
|
|
13
|
+
* manifest is the definition registry): unknown objects/fields are rejected,
|
|
14
|
+
* defaults are type-checked, and the security rail (`sensitive` /
|
|
15
|
+
* `readPermission` / `transient`) refuses stored defaults outright.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FieldPolicy extends SmrtObject {
|
|
18
|
+
/** Qualified class name of the target object (`@package/name:ClassName`). */
|
|
19
|
+
objectRef: string;
|
|
20
|
+
/** Field name on the target object (validated against the registry). */
|
|
21
|
+
fieldName: string;
|
|
22
|
+
/** Scope tier this row belongs to ('app' | 'tenant' | 'user'). */
|
|
23
|
+
scopeType: FieldPolicyScopeType;
|
|
24
|
+
/** Owning tenant for tenant-scope rows; NULL otherwise (native UUID on PG). */
|
|
25
|
+
tenantId: string | null;
|
|
26
|
+
/** Owning user for user-scope rows; NULL otherwise. */
|
|
27
|
+
userId: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Computed uniqueness key (`userId ?? tenantId ?? '__app__'`), set in
|
|
30
|
+
* `save()`. Used ONLY by `conflictColumns` so the unique index stays total
|
|
31
|
+
* while `tenantId`/`userId` are nullable — mirrors `PromptOverride.context`.
|
|
32
|
+
* Never read it for scoping logic; `scopeType` + the typed columns own that.
|
|
33
|
+
*/
|
|
34
|
+
scopeKey: string;
|
|
35
|
+
/** JSON-encoded default value; NULL = inherit. JSON `null` = "default to null". */
|
|
36
|
+
defaultValue: string | null;
|
|
37
|
+
/** Visibility override ('basic' | 'advanced' | 'hidden'); NULL = inherit. */
|
|
38
|
+
visibility: FieldPolicyVisibility | null;
|
|
39
|
+
/** Help text override; NULL = inherit (code seed: field description). */
|
|
40
|
+
help: string | null;
|
|
41
|
+
/** Label override; NULL = inherit (consumers derive from the field name). */
|
|
42
|
+
label: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Sort-order override; NULL = inherit (code seed: `ui.order`). Named
|
|
45
|
+
* `displayOrder` because a column literally named `order` is an SQL keyword
|
|
46
|
+
* the runtime INSERT path does not quote; resolved output exposes `order`.
|
|
47
|
+
*/
|
|
48
|
+
displayOrder: number | null;
|
|
49
|
+
/**
|
|
50
|
+
* Org lock (app/tenant rows only): when the effective lock is true, the
|
|
51
|
+
* user tier may not override this field. NULL = inherit (code seed:
|
|
52
|
+
* `ui.locked`); org rows may set `false` to explicitly unlock.
|
|
53
|
+
*/
|
|
54
|
+
locked: boolean | null;
|
|
55
|
+
/** Audit attribution for #2050 ("who changed what"); not validated. */
|
|
56
|
+
updatedBy: string | null;
|
|
57
|
+
constructor(options?: FieldPolicyOptions);
|
|
58
|
+
/** Parse the stored JSON default. `undefined` = no stored default (inherit). */
|
|
59
|
+
getDefaultValue(): unknown;
|
|
60
|
+
/** Serialize a default value; `undefined` clears the override (inherit). */
|
|
61
|
+
setDefaultValue(value: unknown): void;
|
|
62
|
+
save(): Promise<this>;
|
|
63
|
+
private saveAfterIdentityChange;
|
|
64
|
+
private saveAfterIdentityChangeInTransaction;
|
|
65
|
+
private saveAfterIdentityChangeWithDeferredDelete;
|
|
66
|
+
delete(): Promise<void>;
|
|
67
|
+
private validateFieldPolicy;
|
|
68
|
+
/**
|
|
69
|
+
* Effective org-tier (code → app → tenant hierarchy) policy for this row's
|
|
70
|
+
* field, computed by the RESOLVER so write-time checks share the one
|
|
71
|
+
* precedence implementation — including ancestor-tenant cascades via the
|
|
72
|
+
* default hierarchy loader. The chain tenant is the row's own tenant for
|
|
73
|
+
* tenant-scope rows, the ambient context tenant for user-scope rows, and
|
|
74
|
+
* none for app-scope rows (their only lower layer is the code seed).
|
|
75
|
+
*
|
|
76
|
+
* During an update, the persisted row is excluded from the lower-layer
|
|
77
|
+
* projection. That makes a clear-default + required-field demotion validate
|
|
78
|
+
* the post-write policy rather than accidentally counting the row being
|
|
79
|
+
* replaced.
|
|
80
|
+
*/
|
|
81
|
+
private resolveOrgTierFieldPolicy;
|
|
82
|
+
/**
|
|
83
|
+
* Exactly-one-owner scope shape: app rows carry neither id, tenant rows
|
|
84
|
+
* carry only `tenantId`, user rows carry only `userId` (the user tier is
|
|
85
|
+
* keyed by user alone so preferences follow the user across tenants).
|
|
86
|
+
*/
|
|
87
|
+
private validateScopeConsistency;
|
|
88
|
+
/** `userId ?? tenantId ?? '__app__'` — the `conflictColumns` scope key. */
|
|
89
|
+
private computeScopeKey;
|
|
90
|
+
/**
|
|
91
|
+
* Derive a scope-owner column the transport could not carry (#2047).
|
|
92
|
+
*
|
|
93
|
+
* Core's mass-assignment guard treats `tenantId` as server-managed and
|
|
94
|
+
* strips it from EVERY generated create/update body, and `FieldPolicy` is
|
|
95
|
+
* deliberately not `@TenantScoped`, so the tenancy interceptor never
|
|
96
|
+
* repopulates it. A `POST {scopeType:'tenant', tenantId}` therefore always
|
|
97
|
+
* reached `validateScopeConsistency` with `tenantId === null`, making the
|
|
98
|
+
* org tier write-dead over every generated surface — the model, as the
|
|
99
|
+
* single validation authority for these rows, fills it in instead.
|
|
100
|
+
*
|
|
101
|
+
* This grants nothing: `assertScopeOwnedByAmbientContext` already requires
|
|
102
|
+
* a non-bypass caller's tenant/user rows to name exactly the ambient
|
|
103
|
+
* tenant/user, so the derived value is the ONLY value that could ever have
|
|
104
|
+
* been accepted. An explicit value is never overwritten (a super-admin
|
|
105
|
+
* bypass caller keeps naming other scopes), and with neither tenancy nor
|
|
106
|
+
* authenticated-session context nothing is derived — scope-shape validation
|
|
107
|
+
* rejects the row as before.
|
|
108
|
+
* Consequence: a bypass caller writing ANOTHER tenant's row must pass
|
|
109
|
+
* `tenantId` through a server-side model call, because the generated routes
|
|
110
|
+
* still strip it.
|
|
111
|
+
*/
|
|
112
|
+
private attributeScopeToAmbientContext;
|
|
113
|
+
/**
|
|
114
|
+
* Fail-closed write boundary against the ambient tenant context, applied to
|
|
115
|
+
* the NEW scope on save and to the PERSISTED scope on save/delete of an
|
|
116
|
+
* existing row.
|
|
117
|
+
*
|
|
118
|
+
* With NO ambient identity at all (neither tenant ALS nor authenticated
|
|
119
|
+
* session-permission ALS), only app-scope rows are accepted: tenant- and
|
|
120
|
+
* user-scope rows are unattributable without a context, so they are
|
|
121
|
+
* rejected outright rather than allowed by default. Inside a non-bypass
|
|
122
|
+
* context, a caller may only touch rows for its own tenant, and user rows
|
|
123
|
+
* only for its own user id — a context that carries NO user id may not
|
|
124
|
+
* touch the user tier at all.
|
|
125
|
+
* App-wide rows then require super-admin bypass (or a context-less/system
|
|
126
|
+
* caller).
|
|
127
|
+
*
|
|
128
|
+
* Package rule: a missing identity component DENIES, it never skips.
|
|
129
|
+
*/
|
|
130
|
+
private assertScopeOwnedByAmbientContext;
|
|
131
|
+
private validateTenantContextBoundary;
|
|
132
|
+
/**
|
|
133
|
+
* Permission boundary for policy mutations. Scope ownership is checked
|
|
134
|
+
* separately (and first) because permission never grants cross-tenant or
|
|
135
|
+
* cross-user access. Existing rows are checked against their persisted
|
|
136
|
+
* scope before an identity-changing save can mutate or delete them.
|
|
137
|
+
*/
|
|
138
|
+
private assertScopePermission;
|
|
139
|
+
private validateDefaultAgainstSecurityRail;
|
|
140
|
+
private parseDefaultValueOrThrow;
|
|
141
|
+
/**
|
|
142
|
+
* The persisted row this save/delete would replace, looked up by primary
|
|
143
|
+
* key and — when that misses — by the NATURAL key.
|
|
144
|
+
*
|
|
145
|
+
* The natural-key fallback is load-bearing for authorization: every
|
|
146
|
+
* generated create arrives with a freshly minted UUID, so a primary-key
|
|
147
|
+
* lookup always misses, yet the `conflictColumns` upsert still overwrites
|
|
148
|
+
* whatever row already occupies `(objectRef, fieldName, scopeType,
|
|
149
|
+
* scopeKey)`. Authorizing on the primary key alone therefore skipped the
|
|
150
|
+
* persisted-scope guard on exactly the path that can replace an existing
|
|
151
|
+
* row's contents.
|
|
152
|
+
*/
|
|
153
|
+
private getPersistedIdentity;
|
|
154
|
+
private getPersistedRowById;
|
|
155
|
+
/**
|
|
156
|
+
* The row occupying this row's `conflictColumns` tuple, if any. Column
|
|
157
|
+
* names are the physical snake_case ones: this reads the driver directly
|
|
158
|
+
* rather than through a collection (the model has no collection handle).
|
|
159
|
+
*/
|
|
160
|
+
private getPersistedRowByNaturalKey;
|
|
161
|
+
private normalizeDefaultValueForPersistence;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=FieldPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldPolicy.d.ts","sourceRoot":"","sources":["../../src/models/FieldPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEX,MAAM,0BAA0B,CAAC;AA4BlC,OAAO,EAIL,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAE3B,MAAM,aAAa,CAAC;AA6GrB;;;;;;;;;;;;;GAaG;AASH,qBAgBa,WAAY,SAAQ,UAAU;IACzC,6EAA6E;IAE7E,SAAS,EAAE,MAAM,CAAM;IAEvB,wEAAwE;IAExE,SAAS,EAAE,MAAM,CAAM;IAEvB,kEAAkE;IAElE,SAAS,EAAE,oBAAoB,CAAS;IAExC,+EAA+E;IAE/E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B,uDAAuD;IAEvD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE7B;;;;;OAKG;IAEH,QAAQ,EAAE,MAAM,CAAM;IAEtB,mFAAmF;IAEnF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC,6EAA6E;IAE7E,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAAQ;IAEhD,yEAAyE;IAEzE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE3B,6EAA6E;IAE7E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE5B;;;;OAIG;IAEH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEnC;;;;OAIG;IAEH,MAAM,EAAE,OAAO,GAAG,IAAI,CAAQ;IAE9B,uEAAuE;IAEvE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAEpB,OAAO,GAAE,kBAAuB;IAsC5C,gFAAgF;IAChF,eAAe,IAAI,OAAO;IAW1B,4EAA4E;IAC5E,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAkDtB,uBAAuB;YAQvB,oCAAoC;YA+BpC,yCAAyC;IA6BxC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;YAiBxB,mBAAmB;IA8HjC;;;;;;;;;;;;OAYG;YACW,yBAAyB;IAuBvC;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAwBhC,2EAA2E;IAC3E,OAAO,CAAC,eAAe;IAIvB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,CAAC,8BAA8B;IAatC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,gCAAgC;IA0ExC,OAAO,CAAC,6BAA6B;IAWrC;;;;;OAKG;YACW,qBAAqB;IAoBnC,OAAO,CAAC,kCAAkC;IAwB1C,OAAO,CAAC,wBAAwB;IAkBhC;;;;;;;;;;;OAWG;YACW,oBAAoB;YAkCpB,mBAAmB;IAQjC;;;;OAIG;YACW,2BAA2B;IAgBzC,OAAO,CAAC,mCAAmC;CAiB5C"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SmrtObject, SmrtObjectOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
import { FieldPolicySuggestionData, FieldPolicySuggestionKind, FieldPolicySuggestionStatus } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* `activeKey` sentinel for the single ACTIVE (pending) suggestion per
|
|
5
|
+
* `(objectRef, fieldName, tenantId, kind)`. Settled rows key themselves by id,
|
|
6
|
+
* so history never competes for the slot.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ACTIVE_SUGGESTION_KEY = "active";
|
|
9
|
+
export interface FieldPolicySuggestionOptions extends SmrtObjectOptions {
|
|
10
|
+
objectRef?: string;
|
|
11
|
+
fieldName?: string;
|
|
12
|
+
tenantId?: string;
|
|
13
|
+
kind?: FieldPolicySuggestionKind;
|
|
14
|
+
proposedValue?: string | null;
|
|
15
|
+
evidence?: string;
|
|
16
|
+
status?: FieldPolicySuggestionStatus;
|
|
17
|
+
cooldownUntil?: Date | null;
|
|
18
|
+
decidedBy?: string | null;
|
|
19
|
+
decidedAt?: Date | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A pending, human-reviewable field-policy improvement proposed from real
|
|
23
|
+
* usage (epic #2045, issue #2051): promote a field to the `basic` tier, or
|
|
24
|
+
* seed an org default with the dominant observed value.
|
|
25
|
+
*
|
|
26
|
+
* Suggestion-first by design — a row DOES NOTHING until a
|
|
27
|
+
* `fields.policy.manage` holder accepts it, and acceptance writes the
|
|
28
|
+
* org-scope {@link ../models/FieldPolicy.FieldPolicy} row through NORMAL
|
|
29
|
+
* validation (registry check, type check, security rail, required-field
|
|
30
|
+
* invariant, ownership + permission split). Dismissing sets a cool-down that
|
|
31
|
+
* suppresses regeneration of the same suggestion.
|
|
32
|
+
*
|
|
33
|
+
* **One ACTIVE suggestion per identity, structurally** (not merely by a
|
|
34
|
+
* check-then-insert): {@link activeKey} is a computed column holding the
|
|
35
|
+
* sentinel {@link ACTIVE_SUGGESTION_KEY} while the row is `pending` and the
|
|
36
|
+
* row's own id once it settles, and it participates in `conflictColumns`. The
|
|
37
|
+
* unique index therefore admits at most ONE pending row per
|
|
38
|
+
* `(objectRef, fieldName, tenantId, kind)` while every settled row keys
|
|
39
|
+
* itself — so two overlapping generation runs (e.g. a global and a
|
|
40
|
+
* tenant-specific schedule) UPSERT onto the same row instead of duplicating,
|
|
41
|
+
* with no transaction spanning their reads. It is the `FieldPolicy.scopeKey`
|
|
42
|
+
* trick applied to a lifecycle slot. On settle the column flips to the row's
|
|
43
|
+
* id, which frees the slot for a post-cool-down regeneration while keeping the
|
|
44
|
+
* dismissed/accepted history (and its id) intact — core conflicts a persisted
|
|
45
|
+
* row on its primary key (#1472), so the flip is a plain UPDATE.
|
|
46
|
+
*
|
|
47
|
+
* #1885 seam: this substrate is fully independent of personas'
|
|
48
|
+
* `DirectiveProposal` review queue (no shared producer discriminator —
|
|
49
|
+
* deliberately out of scope). Tenant learning agents MAY create
|
|
50
|
+
* FieldPolicySuggestion rows through this model's normal validation; the
|
|
51
|
+
* reviewed `fields.policy.manage` acceptance gate is unchanged by who
|
|
52
|
+
* proposed.
|
|
53
|
+
*/
|
|
54
|
+
export declare class FieldPolicySuggestion extends SmrtObject {
|
|
55
|
+
/** Qualified class name of the target object (`@package/name:ClassName`). */
|
|
56
|
+
objectRef: string;
|
|
57
|
+
/** Field name on the target object (validated against the registry). */
|
|
58
|
+
fieldName: string;
|
|
59
|
+
/** Owning tenant (required — suggestions always target one org). */
|
|
60
|
+
tenantId?: string;
|
|
61
|
+
/** What the suggestion proposes ('promote' | 'default'). */
|
|
62
|
+
kind: FieldPolicySuggestionKind;
|
|
63
|
+
/**
|
|
64
|
+
* JSON-encoded proposed default (`kind: 'default'` only) — the exact
|
|
65
|
+
* encoding `FieldPolicy.defaultValue` stores, so acceptance passes it
|
|
66
|
+
* through unchanged. NULL for `promote`.
|
|
67
|
+
*/
|
|
68
|
+
proposedValue: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Human-readable evidence as a JSON string: a `summary` sentence plus the
|
|
71
|
+
* structured window/threshold numbers behind it (see
|
|
72
|
+
* `buildFieldUsageEvidence`).
|
|
73
|
+
*/
|
|
74
|
+
evidence: string;
|
|
75
|
+
/** Lifecycle status ('pending' | 'accepted' | 'dismissed'). */
|
|
76
|
+
status: FieldPolicySuggestionStatus;
|
|
77
|
+
/**
|
|
78
|
+
* Computed lifecycle-slot key, set in `save()`: {@link ACTIVE_SUGGESTION_KEY}
|
|
79
|
+
* while `pending`, else the row's own id. It exists ONLY to make the
|
|
80
|
+
* `conflictColumns` unique index express "at most one ACTIVE suggestion per
|
|
81
|
+
* identity, unlimited settled history" (the `FieldPolicy.scopeKey`
|
|
82
|
+
* precedent) — never read it for logic; `status` owns that.
|
|
83
|
+
*/
|
|
84
|
+
activeKey: string;
|
|
85
|
+
/**
|
|
86
|
+
* Until this instant, a dismissed suggestion suppresses regeneration of the
|
|
87
|
+
* same `(objectRef, fieldName, tenantId, kind)` suggestion. NULL until
|
|
88
|
+
* dismissed.
|
|
89
|
+
*/
|
|
90
|
+
cooldownUntil: Date | null;
|
|
91
|
+
/** Who accepted/dismissed (audit attribution, #2050); not validated. */
|
|
92
|
+
decidedBy: string | null;
|
|
93
|
+
/** When the suggestion was accepted/dismissed. */
|
|
94
|
+
decidedAt: Date | null;
|
|
95
|
+
constructor(options?: FieldPolicySuggestionOptions);
|
|
96
|
+
/** Parse the stored evidence object (guarded; junk parses as empty). */
|
|
97
|
+
getEvidence(): Record<string, unknown>;
|
|
98
|
+
/** Serialize an evidence object into the stored JSON string. */
|
|
99
|
+
setEvidence(evidence: Record<string, unknown>): void;
|
|
100
|
+
/** Parse the proposed value; `undefined` when none is stored. */
|
|
101
|
+
getProposedValue(): unknown;
|
|
102
|
+
/** Serialized row shape for the collection actions. */
|
|
103
|
+
toSuggestionData(): FieldPolicySuggestionData;
|
|
104
|
+
save(): Promise<this>;
|
|
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
|
+
private applyActiveKey;
|
|
113
|
+
delete(): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* The "normal validation" the #1885 seam promises producers: registry-known
|
|
116
|
+
* field, policy-addressable, never sensitive/read-permission-gated/transient
|
|
117
|
+
* (those fields are count-only in usage data and get no suggestions), valid
|
|
118
|
+
* kind/status, and for `default` suggestions a JSON proposed value that
|
|
119
|
+
* type-checks against the manifest field type.
|
|
120
|
+
*/
|
|
121
|
+
private validateFieldPolicySuggestion;
|
|
122
|
+
/**
|
|
123
|
+
* Tenant write boundary (the FieldPolicy posture): inside a non-bypass
|
|
124
|
+
* tenant context a caller may only touch its own tenant's suggestions —
|
|
125
|
+
* checked against BOTH the in-memory scope and, for persisted rows, the
|
|
126
|
+
* PERSISTED tenant (a foreign row cannot be re-scoped into the caller's
|
|
127
|
+
* tenant). Trusted execution (no context / bypass) is exempt — that is what
|
|
128
|
+
* lets the scheduled generation job and platform flows operate.
|
|
129
|
+
*/
|
|
130
|
+
private assertRowOwnedByAmbientContext;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=FieldPolicySuggestion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldPolicySuggestion.d.ts","sourceRoot":"","sources":["../../src/models/FieldPolicySuggestion.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAclC,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EACjC,MAAM,aAAa,CAAC;AAErB;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAE9C,MAAM,WAAW,4BAA6B,SAAQ,iBAAiB;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,yBAAyB,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,aAAa,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAOH,qBAaa,qBAAsB,SAAQ,UAAU;IACnD,6EAA6E;IAE7E,SAAS,EAAE,MAAM,CAAM;IAEvB,wEAAwE;IAExE,SAAS,EAAE,MAAM,CAAM;IAEvB,oEAAoE;IAEpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,4DAA4D;IAE5D,IAAI,EAAE,yBAAyB,CAAa;IAE5C;;;;OAIG;IAEH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEpC;;;;OAIG;IAEH,QAAQ,EAAE,MAAM,CAAQ;IAExB,+DAA+D;IAE/D,MAAM,EAAE,2BAA2B,CAAa;IAEhD;;;;;;OAMG;IAEH,SAAS,EAAE,MAAM,CAAyB;IAE1C;;;;OAIG;IAEH,aAAa,EAAE,IAAI,GAAG,IAAI,CAAQ;IAElC,wEAAwE;IAExE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEhC,kDAAkD;IAElD,SAAS,EAAE,IAAI,GAAG,IAAI,CAAQ;gBAElB,OAAO,GAAE,4BAAiC;IAkBtD,wEAAwE;IACxE,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAWtC,gEAAgE;IAChE,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIpD,iEAAiE;IACjE,gBAAgB,IAAI,OAAO;IAW3B,uDAAuD;IACvD,gBAAgB,IAAI,yBAAyB;IAgB9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAWP,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtC;;;;;;OAMG;YACW,6BAA6B;IAiF3C;;;;;;;OAOG;YACW,8BAA8B;CAyC7C"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { SmrtObject, SmrtObjectOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
/**
|
|
3
|
+
* Cap on distinct-user ids stored per bucket. For threshold questions
|
|
4
|
+
* ("did at least N distinct users set this field?") the capped set is EXACT up
|
|
5
|
+
* to the cap; once overflowed, {@link FieldUsageCounter.distinctUserCount} is
|
|
6
|
+
* an honest LOWER BOUND that trivially satisfies any threshold ≤ the cap.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_DISTINCT_USERS_PER_BUCKET = 100;
|
|
9
|
+
/** Cap on histogram buckets per counter row (bounded storage). */
|
|
10
|
+
export declare const MAX_VALUE_HISTOGRAM_BUCKETS = 25;
|
|
11
|
+
/** Longest histogram key recorded; longer samples are skipped (count-only). */
|
|
12
|
+
export declare const MAX_VALUE_HISTOGRAM_KEY_LENGTH = 64;
|
|
13
|
+
/** `period` bucket format: UTC calendar day. */
|
|
14
|
+
export declare const FIELD_USAGE_PERIOD_PATTERN: RegExp;
|
|
15
|
+
/** The UTC day bucket for a timestamp (`YYYY-MM-DD`). */
|
|
16
|
+
export declare function fieldUsagePeriodForDate(date: Date): string;
|
|
17
|
+
/**
|
|
18
|
+
* A prototype-free histogram map.
|
|
19
|
+
*
|
|
20
|
+
* Histogram keys are user-supplied values (an `idType: 'text'` reference id may
|
|
21
|
+
* legitimately be `constructor`, `toString`, or `__proto__`). On a plain object
|
|
22
|
+
* those either resolve to inherited members — making an absent bucket look
|
|
23
|
+
* present and corrupting its count — or, for `__proto__`, invoke the prototype
|
|
24
|
+
* setter instead of creating an own key. A null-prototype object has no such
|
|
25
|
+
* members, so every key behaves like data. Use this everywhere histogram counts
|
|
26
|
+
* are accumulated (storage AND merge paths).
|
|
27
|
+
*/
|
|
28
|
+
export declare function emptyHistogram(): Record<string, number>;
|
|
29
|
+
export interface FieldUsageCounterOptions extends SmrtObjectOptions {
|
|
30
|
+
objectRef?: string;
|
|
31
|
+
fieldName?: string;
|
|
32
|
+
tenantId?: string;
|
|
33
|
+
period?: string;
|
|
34
|
+
submissionCount?: number;
|
|
35
|
+
setCount?: number;
|
|
36
|
+
distinctUserCount?: number;
|
|
37
|
+
distinctUserIds?: string;
|
|
38
|
+
distinctUsersOverflowed?: boolean;
|
|
39
|
+
valueHistogram?: string | null;
|
|
40
|
+
valueHistogramOverflowed?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Period-bucketed field usage counter (epic #2045, issue #2051).
|
|
44
|
+
*
|
|
45
|
+
* One row aggregates field submissions for a single
|
|
46
|
+
* `(objectRef, fieldName, tenantId, period)` — the substrate the
|
|
47
|
+
* suggestion-generation job reads. Counters are deliberately APPROXIMATE:
|
|
48
|
+
* ingestion is fire-and-forget and concurrent bucket merges may lose an
|
|
49
|
+
* increment (read-modify-write), which is acceptable for usage statistics and
|
|
50
|
+
* documented here rather than papered over.
|
|
51
|
+
*
|
|
52
|
+
* TWO counters, because they answer different questions:
|
|
53
|
+
* - {@link submissionCount} — EVERY observed submission of the field
|
|
54
|
+
* (default-matching or not). It is the denominator for value dominance:
|
|
55
|
+
* "N% of submissions used value V". Without it, a value seen only in
|
|
56
|
+
* deviations would look 100% dominant even against thousands of
|
|
57
|
+
* default-valued submissions.
|
|
58
|
+
* - {@link setCount} — submissions whose value DIFFERED from the resolved
|
|
59
|
+
* default (server-derived). It plus {@link distinctUserIds} is the
|
|
60
|
+
* promote signal ("real users are actively filling this in").
|
|
61
|
+
*
|
|
62
|
+
* Content rails (enforced by the ingestion action, which derives everything
|
|
63
|
+
* from the live registry and never trusts the client):
|
|
64
|
+
* - Sensitive and read-permission-gated fields are COUNT-ONLY: their raw
|
|
65
|
+
* values are never recorded anywhere in usage data — not even for
|
|
66
|
+
* default-matching submissions.
|
|
67
|
+
* - Value histograms exist only for low-cardinality field types (`boolean`,
|
|
68
|
+
* `foreignKey`, `crossPackageRef`) — never free text, even non-sensitive
|
|
69
|
+
* text (PII risk) — with a bounded bucket count and key length. They cover
|
|
70
|
+
* ALL submissions (not just deviations) so the dominance ratio is a true
|
|
71
|
+
* fraction of {@link submissionCount}.
|
|
72
|
+
* - `distinctUserIds` is a capped set with an overflow marker (see
|
|
73
|
+
* {@link MAX_DISTINCT_USERS_PER_BUCKET} for the honesty contract).
|
|
74
|
+
*/
|
|
75
|
+
export declare class FieldUsageCounter extends SmrtObject {
|
|
76
|
+
/** Qualified class name of the target object (`@package/name:ClassName`). */
|
|
77
|
+
objectRef: string;
|
|
78
|
+
/** Field name on the target object. */
|
|
79
|
+
fieldName: string;
|
|
80
|
+
/**
|
|
81
|
+
* Owning tenant. REQUIRED: ingestion fails closed without an ambient tenant
|
|
82
|
+
* context, so every row is attributable (and the conflict-column tuple
|
|
83
|
+
* stays total). Native UUID on PostgreSQL/DuckDB.
|
|
84
|
+
*/
|
|
85
|
+
tenantId?: string;
|
|
86
|
+
/** UTC day bucket (`YYYY-MM-DD`); lexicographic order is time order. */
|
|
87
|
+
period: string;
|
|
88
|
+
/**
|
|
89
|
+
* EVERY observed submission of this field in the bucket, whether or not the
|
|
90
|
+
* value matched the resolved default — the dominance denominator.
|
|
91
|
+
*
|
|
92
|
+
* Rows written before this column existed carry `0` while `setCount > 0`;
|
|
93
|
+
* {@link isLegacyBucket} detects that shape and the generation job then
|
|
94
|
+
* treats the total as UNKNOWN and skips `default` suggestions for the group
|
|
95
|
+
* (promote, which needs no denominator, still works).
|
|
96
|
+
*/
|
|
97
|
+
submissionCount: number;
|
|
98
|
+
/**
|
|
99
|
+
* Submissions whose value DIFFERED from the server-resolved default (the
|
|
100
|
+
* promote signal). Always `<= submissionCount` on rows written by the
|
|
101
|
+
* current ingestion path.
|
|
102
|
+
*/
|
|
103
|
+
setCount: number;
|
|
104
|
+
/**
|
|
105
|
+
* Size of the stored distinct-user set. When
|
|
106
|
+
* {@link distinctUsersOverflowed} is true this is a LOWER BOUND (the set is
|
|
107
|
+
* capped), never an estimate.
|
|
108
|
+
*/
|
|
109
|
+
distinctUserCount: number;
|
|
110
|
+
/** JSON array of distinct user ids, capped (see the class doc). */
|
|
111
|
+
distinctUserIds: string;
|
|
112
|
+
/** True once a distinct user was NOT added because the set is at its cap. */
|
|
113
|
+
distinctUsersOverflowed: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* JSON object `serializedValue -> count` for histogram-eligible fields;
|
|
116
|
+
* NULL when the field is count-only. Keys are bounded in number and length.
|
|
117
|
+
*/
|
|
118
|
+
valueHistogram: string | null;
|
|
119
|
+
/** True once a sample was dropped because the bucket cap was reached. */
|
|
120
|
+
valueHistogramOverflowed: boolean;
|
|
121
|
+
constructor(options?: FieldUsageCounterOptions);
|
|
122
|
+
/** Parse the stored distinct-user set (guarded; junk parses as empty). */
|
|
123
|
+
getDistinctUserIds(): string[];
|
|
124
|
+
/**
|
|
125
|
+
* Add a user to the distinct set, honoring the cap. At the cap the id is
|
|
126
|
+
* NOT added and the overflow marker is set instead, keeping
|
|
127
|
+
* {@link distinctUserCount} an honest lower bound.
|
|
128
|
+
*/
|
|
129
|
+
addDistinctUser(userId: string): void;
|
|
130
|
+
/**
|
|
131
|
+
* Parse the stored histogram (guarded; junk parses as empty).
|
|
132
|
+
*
|
|
133
|
+
* Returns a NULL-PROTOTYPE object. Histogram keys are user-supplied ids —
|
|
134
|
+
* an `idType: 'text'` reference may legitimately be `constructor`,
|
|
135
|
+
* `toString`, or `__proto__` — and on a plain object those inherit truthy
|
|
136
|
+
* prototype values (so a missing bucket reads as present) or, for
|
|
137
|
+
* `__proto__`, hit the prototype setter instead of creating an own key.
|
|
138
|
+
* Both would silently corrupt counts. See {@link emptyHistogram}.
|
|
139
|
+
*/
|
|
140
|
+
getValueHistogram(): Record<string, number>;
|
|
141
|
+
/**
|
|
142
|
+
* Record one histogram sample under an already-serialized key, honoring the
|
|
143
|
+
* bucket cap (a NEW key past the cap is dropped and the overflow marker
|
|
144
|
+
* set; existing keys keep counting).
|
|
145
|
+
*
|
|
146
|
+
* Bucket presence is an OWN-key test, never a truthiness/`undefined` read,
|
|
147
|
+
* so prototype-shaped ids behave like any other key.
|
|
148
|
+
*/
|
|
149
|
+
recordHistogramSample(key: string): void;
|
|
150
|
+
/**
|
|
151
|
+
* Whether this bucket predates the {@link submissionCount} column (or was
|
|
152
|
+
* corrupted): it records deviations without a total, so no honest dominance
|
|
153
|
+
* ratio can be computed from it. The generation job skips `default`
|
|
154
|
+
* suggestions for any group containing such a bucket.
|
|
155
|
+
*/
|
|
156
|
+
isLegacyBucket(): boolean;
|
|
157
|
+
save(): Promise<this>;
|
|
158
|
+
delete(): Promise<void>;
|
|
159
|
+
private validateFieldUsageCounter;
|
|
160
|
+
/**
|
|
161
|
+
* Tenant write boundary (the FieldPolicy posture — no class-level
|
|
162
|
+
* `@TenantScoped`, because the learning jobs legitimately operate
|
|
163
|
+
* cross-tenant in trusted execution): inside a non-bypass tenant context a
|
|
164
|
+
* caller may only touch rows of its own tenant; without a context (system/
|
|
165
|
+
* job execution) writes are trusted.
|
|
166
|
+
*
|
|
167
|
+
* Checked against BOTH the in-memory tenant and — for a row that already
|
|
168
|
+
* exists — the PERSISTED one. The persisted check is what makes the boundary
|
|
169
|
+
* real: bucket ids are deterministic and the deriving helper is exported, so
|
|
170
|
+
* a foreign row is trivially addressable, and an in-memory-only check would
|
|
171
|
+
* let a caller load it, re-stamp `tenantId` with its own, and adopt or delete
|
|
172
|
+
* another tenant's counters (the #2047 FieldPolicy pattern).
|
|
173
|
+
*/
|
|
174
|
+
private assertRowOwnedByAmbientContext;
|
|
175
|
+
/** The stored tenant for this row's id; `null` when it is not persisted. */
|
|
176
|
+
private getPersistedTenantId;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=FieldUsageCounter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldUsageCounter.d.ts","sourceRoot":"","sources":["../../src/models/FieldUsageCounter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAQlC;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD,kEAAkE;AAClE,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,+EAA+E;AAC/E,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD,gDAAgD;AAChD,eAAO,MAAM,0BAA0B,QAAwB,CAAC;AAEhE,yDAAyD;AACzD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEvD;AAED,MAAM,WAAW,wBAAyB,SAAQ,iBAAiB;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAMH,qBAOa,iBAAkB,SAAQ,UAAU;IAC/C,6EAA6E;IAE7E,SAAS,EAAE,MAAM,CAAM;IAEvB,uCAAuC;IAEvC,SAAS,EAAE,MAAM,CAAM;IAEvB;;;;OAIG;IAEH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,wEAAwE;IAExE,MAAM,EAAE,MAAM,CAAM;IAEpB;;;;;;;;OAQG;IAEH,eAAe,EAAE,MAAM,CAAK;IAE5B;;;;OAIG;IAEH,QAAQ,EAAE,MAAM,CAAK;IAErB;;;;OAIG;IAEH,iBAAiB,EAAE,MAAM,CAAK;IAE9B,mEAAmE;IAEnE,eAAe,EAAE,MAAM,CAAQ;IAE/B,6EAA6E;IAE7E,uBAAuB,EAAE,OAAO,CAAS;IAEzC;;;OAGG;IAEH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAQ;IAErC,yEAAyE;IAEzE,wBAAwB,EAAE,OAAO,CAAS;gBAE9B,OAAO,GAAE,wBAA6B;IA2BlD,0EAA0E;IAC1E,kBAAkB,IAAI,MAAM,EAAE;IAW9B;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAcrC;;;;;;;;;OASG;IACH,iBAAiB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAwB3C;;;;;;;OAOG;IACH,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAiBxC;;;;;OAKG;IACH,cAAc,IAAI,OAAO;IAIV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtC,OAAO,CAAC,yBAAyB;IAoCjC;;;;;;;;;;;;;OAaG;YACW,8BAA8B;IAgC5C,4EAA4E;YAC9D,oBAAoB;CAYnC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SmrtObject, SmrtObjectOptions } from '@happyvertical/smrt-core';
|
|
2
|
+
/**
|
|
3
|
+
* Durable daily receipt for one member's contribution to one field.
|
|
4
|
+
*
|
|
5
|
+
* The counter action creates this before incrementing its aggregate. Its
|
|
6
|
+
* natural key makes the anti-inflation rule durable across requests and
|
|
7
|
+
* replicas: one `(tenant, user, object, field, UTC day)` sample may affect
|
|
8
|
+
* usage evidence. Receipts intentionally retain no submitted value.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FieldUsageReportReceipt extends SmrtObject {
|
|
11
|
+
tenantId?: string;
|
|
12
|
+
userId: string;
|
|
13
|
+
objectRef: string;
|
|
14
|
+
fieldName: string;
|
|
15
|
+
period: string;
|
|
16
|
+
constructor(options?: FieldUsageReportReceiptOptions);
|
|
17
|
+
}
|
|
18
|
+
export interface FieldUsageReportReceiptOptions extends SmrtObjectOptions {
|
|
19
|
+
tenantId?: string;
|
|
20
|
+
userId?: string;
|
|
21
|
+
objectRef?: string;
|
|
22
|
+
fieldName?: string;
|
|
23
|
+
period?: string;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=FieldUsageReportReceipt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldUsageReportReceipt.d.ts","sourceRoot":"","sources":["../../src/models/FieldUsageReportReceipt.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAGlC;;;;;;;GAOG;AACH,qBAaa,uBAAwB,SAAQ,UAAU;IAErD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,MAAM,EAAE,MAAM,CAAM;IAGpB,SAAS,EAAE,MAAM,CAAM;IAGvB,SAAS,EAAE,MAAM,CAAM;IAGvB,MAAM,EAAE,MAAM,CAAM;gBAER,OAAO,GAAE,8BAAmC;CAQzD;AAED,MAAM,WAAW,8BAA+B,SAAQ,iBAAiB;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PermissionDefinition } from '@happyvertical/smrt-users';
|
|
2
|
+
/** Authorizes app- and tenant-scope field-policy administration. */
|
|
3
|
+
export declare const MANAGE_FIELD_POLICY_PERMISSION = "fields.policy.manage";
|
|
4
|
+
/** Authorizes a principal to maintain their own user-scope field policy. */
|
|
5
|
+
export declare const PERSONALIZE_FIELD_POLICY_PERMISSION = "fields.policy.personalize";
|
|
6
|
+
export declare const FIELD_POLICY_PERMISSION_DEFINITIONS: PermissionDefinition[];
|
|
7
|
+
/** Register field-policy permissions once when smrt-fields is loaded. */
|
|
8
|
+
export declare function ensureFieldPolicyPermissionsRegistered(): void;
|
|
9
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,2BAA2B,CAAC;AAEnC,oEAAoE;AACpE,eAAO,MAAM,8BAA8B,yBAAyB,CAAC;AAErE,4EAA4E;AAC5E,eAAO,MAAM,mCAAmC,8BAA8B,CAAC;AAE/E,eAAO,MAAM,mCAAmC,EAAE,oBAAoB,EAarE,CAAC;AAIF,yEAAyE;AACzE,wBAAgB,sCAAsC,IAAI,IAAI,CAM7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../src/playground.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/svelte/playground.ts
|
|
2
|
+
var loadFieldPolicyForm = () => import("./svelte/playground/FieldPolicyFormPreview.svelte");
|
|
3
|
+
var loadObjectForm = () => import("./svelte/playground/ObjectFormPreview.svelte");
|
|
4
|
+
var playground_default = {
|
|
5
|
+
packageName: "@happyvertical/smrt-fields",
|
|
6
|
+
displayName: "Field Policies",
|
|
7
|
+
description: "Headless form primitives: FieldPolicyProvider, PolicyField, basic/advanced modes, and manifest-sourced help.",
|
|
8
|
+
entries: [{
|
|
9
|
+
id: "policy-form",
|
|
10
|
+
title: "Policy-Driven Form",
|
|
11
|
+
description: "A hand-written product form adopting PolicyField on a subset of fields — visibility by mode, default prefill, help hints, and the form-level glossary.",
|
|
12
|
+
loadComponent: loadFieldPolicyForm,
|
|
13
|
+
order: 1,
|
|
14
|
+
tags: [
|
|
15
|
+
"forms",
|
|
16
|
+
"fields",
|
|
17
|
+
"policy",
|
|
18
|
+
"defaults"
|
|
19
|
+
],
|
|
20
|
+
modes: { mock: { label: "Mock" } }
|
|
21
|
+
}, {
|
|
22
|
+
id: "object-form",
|
|
23
|
+
title: "Generated ObjectForm",
|
|
24
|
+
description: "A reusable create/edit form from generated web fields and resolved policy, including advanced groups and a per-field custom renderer.",
|
|
25
|
+
loadComponent: loadObjectForm,
|
|
26
|
+
order: 2,
|
|
27
|
+
tags: [
|
|
28
|
+
"forms",
|
|
29
|
+
"fields",
|
|
30
|
+
"object-form",
|
|
31
|
+
"registry"
|
|
32
|
+
],
|
|
33
|
+
modes: { mock: { label: "Mock" } }
|
|
34
|
+
}]
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { playground_default as default };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=playground.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground.js","names":[],"sources":["../src/svelte/playground.ts"],"sourcesContent":["/**\n * Field-policy playground previews (#2048).\n *\n * Registered with the shared playground host via the auto-discovered\n * \"packages/STAR/src/svelte/playground.ts\" convention (smrt-playground vite\n * plugin).\n */\nconst loadFieldPolicyForm = () =>\n import('./playground/FieldPolicyFormPreview.svelte');\nconst loadObjectForm = () => import('./playground/ObjectFormPreview.svelte');\n\nexport default {\n packageName: '@happyvertical/smrt-fields',\n displayName: 'Field Policies',\n description:\n 'Headless form primitives: FieldPolicyProvider, PolicyField, basic/advanced modes, and manifest-sourced help.',\n entries: [\n {\n id: 'policy-form',\n title: 'Policy-Driven Form',\n description:\n 'A hand-written product form adopting PolicyField on a subset of fields — visibility by mode, default prefill, help hints, and the form-level glossary.',\n loadComponent: loadFieldPolicyForm,\n order: 1,\n tags: ['forms', 'fields', 'policy', 'defaults'],\n modes: {\n mock: {\n label: 'Mock',\n },\n },\n },\n {\n id: 'object-form',\n title: 'Generated ObjectForm',\n description:\n 'A reusable create/edit form from generated web fields and resolved policy, including advanced groups and a per-field custom renderer.',\n loadComponent: loadObjectForm,\n order: 2,\n tags: ['forms', 'fields', 'object-form', 'registry'],\n modes: { mock: { label: 'Mock' } },\n },\n ],\n};\n"],"mappings":";AAOA,IAAM,4BACJ,OAAO;AACT,IAAM,uBAAuB,OAAO;AAEpC,IAAA,qBAAe;CACb,aAAa;CACb,aAAa;CACb,aACE;CACF,SAAS,CACP;EACE,IAAI;EACJ,OAAO;EACP,aACE;EACF,eAAe;EACf,OAAO;EACP,MAAM;GAAC;GAAS;GAAU;GAAU;EAAU;EAC9C,OAAO,EACL,MAAM,EACJ,OAAO,OACT,EACF;CACF,GACA;EACE,IAAI;EACJ,OAAO;EACP,aACE;EACF,eAAe;EACf,OAAO;EACP,MAAM;GAAC;GAAS;GAAU;GAAe;EAAU;EACnD,OAAO,EAAE,MAAM,EAAE,OAAO,OAAO,EAAE;CACnC,CACF;AACF"}
|