@contractspec/lib.personalization 6.0.26 → 6.1.1
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/README.md +78 -2
- package/dist/adapter.d.ts +1 -0
- package/dist/adapter.js +1 -1
- package/dist/analyzer.js +1 -1
- package/dist/authorization.test.d.ts +1 -0
- package/dist/data-view-preferences.d.ts +43 -0
- package/dist/data-view-preferences.js +2 -0
- package/dist/data-view-preferences.test.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -4
- package/dist/node/adapter.js +1 -1
- package/dist/node/analyzer.js +1 -1
- package/dist/node/data-view-preferences.js +1 -0
- package/dist/node/index.js +4 -4
- package/dist/node/store.js +1 -1
- package/dist/node/tracker.js +1 -1
- package/dist/store.js +1 -1
- package/dist/tracker.d.ts +19 -0
- package/dist/tracker.js +1 -1
- package/dist/types.d.ts +40 -2
- package/package.json +18 -30
- package/dist/browser/adapter.js +0 -1
- package/dist/browser/analyzer.js +0 -1
- package/dist/browser/docs/behavior-tracking.docblock.js +0 -80
- package/dist/browser/docs/index.js +0 -212
- package/dist/browser/docs/overlay-engine.docblock.js +0 -81
- package/dist/browser/docs/workflow-composition.docblock.js +0 -53
- package/dist/browser/index.js +0 -212
- package/dist/browser/personalization.feature.js +0 -1
- package/dist/browser/preference-dimensions.js +0 -0
- package/dist/browser/store.js +0 -1
- package/dist/browser/tracker.js +0 -1
- package/dist/browser/types.js +0 -0
package/README.md
CHANGED
|
@@ -39,6 +39,15 @@ const tracker = createBehaviorTracker({
|
|
|
39
39
|
tenantId: "acme",
|
|
40
40
|
userId: "user-123",
|
|
41
41
|
role: "manager",
|
|
42
|
+
roles: ["manager"],
|
|
43
|
+
permissions: ["billing.order.read"],
|
|
44
|
+
policyDecisions: {
|
|
45
|
+
internalNotes: {
|
|
46
|
+
effect: "deny",
|
|
47
|
+
fields: ["internalNotes"],
|
|
48
|
+
missing: { permissions: ["billing.notes.read"] },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
42
51
|
},
|
|
43
52
|
autoFlushIntervalMs: 5000,
|
|
44
53
|
});
|
|
@@ -88,6 +97,68 @@ Typical flow:
|
|
|
88
97
|
3. Analyze the summary with `BehaviorAnalyzer`.
|
|
89
98
|
4. Convert insights into an `OverlaySpec` suggestion or workflow adaptation hints.
|
|
90
99
|
|
|
100
|
+
## DataView preferences
|
|
101
|
+
|
|
102
|
+
Use the data-view preference helpers when a collection `DataViewSpec` should
|
|
103
|
+
honor preferred list/grid/table mode, density, or data depth without coupling
|
|
104
|
+
the design-system renderer to personalization:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { DataViewRenderer } from "@contractspec/lib.design-system";
|
|
108
|
+
import { resolveDataViewPreferences } from "@contractspec/lib.personalization/data-view-preferences";
|
|
109
|
+
|
|
110
|
+
const resolved = resolveDataViewPreferences({
|
|
111
|
+
spec: AccountsDataView,
|
|
112
|
+
preferences: profile.canonical,
|
|
113
|
+
insights,
|
|
114
|
+
record: savedDataViewPreference,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
<DataViewRenderer
|
|
118
|
+
spec={AccountsDataView}
|
|
119
|
+
items={rows}
|
|
120
|
+
defaultViewMode={resolved.viewMode}
|
|
121
|
+
defaultDensity={resolved.density}
|
|
122
|
+
defaultDataDepth={resolved.dataDepth}
|
|
123
|
+
/>;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The helper returns plain data and never imports React or the design-system
|
|
127
|
+
package. Stored data-view preference records win over behavior insights,
|
|
128
|
+
preference dimensions, and authored contract defaults. Disallowed inferred view
|
|
129
|
+
modes are ignored so the renderer only receives modes allowed by the spec.
|
|
130
|
+
|
|
131
|
+
Record renderer interactions with the behavior tracker so later analysis can
|
|
132
|
+
derive scoped preferred modes:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
tracker.trackDataViewInteraction({
|
|
136
|
+
dataViewKey: AccountsDataView.meta.key,
|
|
137
|
+
dataViewVersion: AccountsDataView.meta.version,
|
|
138
|
+
action: "view_mode_changed",
|
|
139
|
+
viewMode: "grid",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
tracker.trackDataViewInteraction({
|
|
143
|
+
dataViewKey: AccountsDataView.meta.key,
|
|
144
|
+
action: "data_depth_changed",
|
|
145
|
+
dataDepth: "detailed",
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Agent prompt for a DataView preference integration:
|
|
150
|
+
|
|
151
|
+
```md
|
|
152
|
+
Add DataView personalization for <screen>.
|
|
153
|
+
|
|
154
|
+
- Resolve viewMode, density, dataDepth, and pageSize with resolveDataViewPreferences.
|
|
155
|
+
- Apply resolved values to DataViewRenderer as default or controlled props.
|
|
156
|
+
- Track opened, view_mode_changed, density_changed, data_depth_changed, search_changed, filter_changed, sort_changed, and page_changed actions with trackDataViewInteraction.
|
|
157
|
+
- Persist only the dimensions enabled by view.collection.personalization.persist.
|
|
158
|
+
- Ignore behavior-derived modes not allowed by view.collection.viewModes.allowedModes.
|
|
159
|
+
- Keep @contractspec/lib.personalization free of React and design-system imports.
|
|
160
|
+
```
|
|
161
|
+
|
|
91
162
|
## API map
|
|
92
163
|
|
|
93
164
|
### Main runtime APIs
|
|
@@ -96,18 +167,21 @@ Typical flow:
|
|
|
96
167
|
- `InMemoryBehaviorStore`: simple in-memory implementation for tests, demos, and local composition.
|
|
97
168
|
- `BehaviorTracker` and `createBehaviorTracker`: buffered event capture with tenant/user context and OpenTelemetry instrumentation.
|
|
98
169
|
- `BehaviorAnalyzer`: converts `BehaviorSummary` data into `BehaviorInsights`.
|
|
170
|
+
- Authorization context: events may carry `roles`, `permissions`, and policy decision summaries so analyzers can suppress denied fields/actions without granting access.
|
|
99
171
|
- `insightsToOverlaySuggestion`: turns analysis output into an overlay-engine `OverlaySpec`.
|
|
100
172
|
- `insightsToWorkflowAdaptations`: turns workflow bottlenecks into lightweight adaptation notes.
|
|
101
173
|
|
|
102
174
|
### Core data contracts
|
|
103
175
|
|
|
104
|
-
- `BehaviorEvent`: discriminated union with
|
|
176
|
+
- `BehaviorEvent`: discriminated union with four event kinds: `field_access`, `feature_usage`, `workflow_step`, and `data_view_interaction`.
|
|
105
177
|
- `BehaviorQuery`: filter shape used by `BehaviorStore.query()` and `BehaviorStore.summarize()`.
|
|
106
178
|
- `BehaviorSummary`: aggregated counts returned by store summarization.
|
|
107
179
|
- `BehaviorInsights`: analyzer output including hidden-field candidates, bottlenecks, and layout preference hints.
|
|
180
|
+
When authorization decisions are supplied, denied fields/actions are surfaced for suppression and are not promoted in overlay reorder suggestions.
|
|
108
181
|
- `BehaviorAnalyzerOptions`: tuning knobs for inactivity threshold and minimum workflow sample size.
|
|
109
182
|
- `OverlaySuggestionOptions`: metadata required to build an overlay suggestion.
|
|
110
183
|
- `WorkflowAdaptation`: workflow, step, and note triple derived from bottlenecks.
|
|
184
|
+
- DataView preference helpers: `resolveDataViewPreferences`, `DataViewPreferenceRecord`, `DataViewPreferencePatch`, and mapping helpers for density and view-mode patches.
|
|
111
185
|
|
|
112
186
|
### Preference model contracts
|
|
113
187
|
|
|
@@ -123,6 +197,7 @@ The root barrel at `src/index.ts` re-exports public symbols from:
|
|
|
123
197
|
|
|
124
198
|
- `adapter`
|
|
125
199
|
- `analyzer`
|
|
200
|
+
- `data-view-preferences`
|
|
126
201
|
- `preference-dimensions`
|
|
127
202
|
- `store`
|
|
128
203
|
- `tracker`
|
|
@@ -133,6 +208,7 @@ Published subpaths from `package.json`:
|
|
|
133
208
|
- `.`
|
|
134
209
|
- `./adapter`
|
|
135
210
|
- `./analyzer`
|
|
211
|
+
- `./data-view-preferences`
|
|
136
212
|
- `./docs`
|
|
137
213
|
- `./docs/behavior-tracking.docblock`
|
|
138
214
|
- `./docs/overlay-engine.docblock`
|
|
@@ -152,7 +228,7 @@ For application code, prefer `.` or the focused subpaths above. The `./docs*` su
|
|
|
152
228
|
- Each enqueue also emits OpenTelemetry metrics and tracing through `@opentelemetry/api`.
|
|
153
229
|
- `BehaviorAnalyzer` uses deterministic threshold heuristics. It does not do ranking, learning, or probabilistic inference.
|
|
154
230
|
- `insightsToOverlaySuggestion()` currently emits only `hideField` and `reorderFields` modifications.
|
|
155
|
-
- `layoutPreference` is inferred from field-count thresholds
|
|
231
|
+
- `layoutPreference` is still inferred from field-count thresholds. Data-view-specific preferred modes are derived from `data_view_interaction` events when those events include a valid collection `viewMode`.
|
|
156
232
|
- `PreferenceDimensions` and related types are contracts. Durable persistence and runtime resolution live elsewhere.
|
|
157
233
|
|
|
158
234
|
## When not to use this package
|
package/dist/adapter.d.ts
CHANGED
package/dist/adapter.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
function
|
|
2
|
+
function a(t,e){let r=[];t.suggestedHiddenFields.forEach((i)=>{r.push({type:"hideField",field:i,reason:"Automatically hidden because usage is near zero"})});let o=new Set([...t.deniedFields??[],...e.deniedFields??[]]),n=t.frequentlyUsedFields.filter((i)=>!o.has(i));if(n.length)r.push({type:"reorderFields",fields:n});if(!r.length)return null;return{overlayId:e.overlayId,version:e.version??"1.0.0",appliesTo:{tenantId:e.tenantId,capability:e.capability,userId:e.userId,role:e.role},modifications:r,metadata:{generatedAt:new Date().toISOString(),automated:!0}}}function s(t){return t.workflowBottlenecks.map((e)=>({workflow:e.workflow,step:e.step,note:`High drop rate (${Math.round(e.dropRate*100)}%) detected`}))}export{s as insightsToWorkflowAdaptations,a as insightsToOverlaySuggestion};
|
package/dist/analyzer.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
class
|
|
2
|
+
class p{store;options;constructor(e,t={}){this.store=e;this.options=t}async analyze(e){let t={tenantId:e.tenantId,userId:e.userId,role:e.role};if(e.windowMs)t.since=Date.now()-e.windowMs;let n=await this.store.summarize(t);return v(n,this.options)}}function v(e,t){let n=t.fieldInactivityThreshold??3,d=t.minSamples??10,r=[],a=[];for(let[i,o]of Object.entries(e.fieldCounts)){if(o<=n)r.push(i);if(o>=n*4)a.push(i)}let l=Object.entries(e.workflowStepCounts).flatMap(([i,o])=>{let c=Object.values(o).reduce((s,u)=>s+u,0);if(!c||c<d)return[];return Object.entries(o).filter(([,s])=>s/c<0.4).map(([s,u])=>({workflow:i,step:s,dropRate:1-u/c}))}),f=Object.keys(e.deniedFieldCounts??{}),h=Object.keys(e.deniedActionCounts??{}),y=g(e);return{unusedFields:r,suggestedHiddenFields:r.slice(0,5),frequentlyUsedFields:a.filter((i)=>!f.includes(i)).slice(0,10),deniedFields:f,deniedActions:h,workflowBottlenecks:l,layoutPreference:y,dataViewPreferences:w(e)}}function w(e){let t=Object.entries(e.dataViewViewModeCounts??{}).flatMap(([n,d])=>{let r=Object.entries(d).sort(([,a],[,l])=>(l??0)-(a??0))[0]?.[0];if(!m(r))return[];return[[n,{preferredViewMode:r}]]});return t.length?Object.fromEntries(t):void 0}function m(e){return e==="list"||e==="grid"||e==="table"}function g(e){let t=Object.keys(e.fieldCounts).length;if(!t)return;if(t>=15)return"table";if(t>=8)return"grid";return"form"}export{p as BehaviorAnalyzer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { DataViewCollectionMode, DataViewDataDepth, DataViewDensity, DataViewSpec } from '@contractspec/lib.contracts-spec/data-views';
|
|
2
|
+
import type { PreferenceDimensions } from './preference-dimensions';
|
|
3
|
+
import type { BehaviorInsights } from './types';
|
|
4
|
+
export interface DataViewPreferenceRecord {
|
|
5
|
+
dataViewKey: string;
|
|
6
|
+
dataViewVersion?: string;
|
|
7
|
+
viewMode?: DataViewCollectionMode;
|
|
8
|
+
density?: DataViewDensity;
|
|
9
|
+
dataDepth?: DataViewDataDepth;
|
|
10
|
+
pageSize?: number;
|
|
11
|
+
updatedAt?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DataViewPreferencePatch {
|
|
14
|
+
dataViewKey: string;
|
|
15
|
+
dataViewVersion?: string;
|
|
16
|
+
viewMode?: DataViewCollectionMode;
|
|
17
|
+
density?: DataViewDensity;
|
|
18
|
+
dataDepth?: DataViewDataDepth;
|
|
19
|
+
pageSize?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedDataViewPreferences {
|
|
22
|
+
viewMode: DataViewCollectionMode;
|
|
23
|
+
density: DataViewDensity;
|
|
24
|
+
dataDepth: DataViewDataDepth;
|
|
25
|
+
pageSize?: number;
|
|
26
|
+
source: {
|
|
27
|
+
viewMode: DataViewPreferenceSource;
|
|
28
|
+
density: DataViewPreferenceSource;
|
|
29
|
+
dataDepth: DataViewPreferenceSource;
|
|
30
|
+
pageSize?: DataViewPreferenceSource;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export type DataViewPreferenceSource = 'record' | 'insights' | 'preferences' | 'contract' | 'fallback';
|
|
34
|
+
export interface ResolveDataViewPreferencesInput {
|
|
35
|
+
spec: DataViewSpec;
|
|
36
|
+
preferences?: PreferenceDimensions;
|
|
37
|
+
insights?: BehaviorInsights;
|
|
38
|
+
record?: DataViewPreferenceRecord;
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveDataViewPreferences({ spec, preferences, insights, record, }: ResolveDataViewPreferencesInput): ResolvedDataViewPreferences;
|
|
41
|
+
export declare function preferenceDimensionsToDataViewDensity(preferences?: PreferenceDimensions): DataViewDensity | undefined;
|
|
42
|
+
export declare function dataViewDensityToPreferencePatch(density: DataViewDensity): Partial<PreferenceDimensions>;
|
|
43
|
+
export declare function dataViewModeToPreferencePatch(spec: DataViewSpec, viewMode: DataViewCollectionMode): DataViewPreferencePatch;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var G=["list","grid","table"];function N({spec:b,preferences:j,insights:k,record:x}){let B=V(b),J=Z(b,B),P=Y(b,k,B),Q=F(x?.viewMode,B),R=$(b),U=q(b);return{viewMode:Q??P??J??"list",density:x?.density??X(j)??R??"comfortable",dataDepth:x?.dataDepth??j?.dataDepth??U??"standard",pageSize:x?.pageSize??z(b)?.pagination?.pageSize,source:{viewMode:Q?"record":P?"insights":J?"contract":"fallback",density:x?.density?"record":j?"preferences":R?"contract":"fallback",dataDepth:x?.dataDepth?"record":j?.dataDepth?"preferences":U?"contract":"fallback",pageSize:x?.pageSize?"record":z(b)?.pagination?.pageSize?"contract":void 0}}}function X(b){if(!b)return;if(b.density==="minimal"||b.density==="compact"||b.density==="dense")return"compact";return"comfortable"}function S(b){return{density:b==="compact"?"compact":"standard"}}function A(b,j){return{dataViewKey:b.meta.key,dataViewVersion:b.meta.version,viewMode:j}}function Y(b,j,k){let x=j?.dataViewPreferences?.[b.meta.key]?.preferredViewMode,B=j?.layoutPreference;return F(x,k)??F(B,k)}function Z(b,j){if(!H(b.view.kind))return;let k=z(b)?.viewModes?.defaultMode;return F(k,j)??F(b.view.kind,j)}function $(b){if(b.view.kind==="table"&&b.view.density)return b.view.density;return z(b)?.density}function q(b){return z(b)?.dataDepth}function V(b){if(!H(b.view.kind))return[];let j=z(b),k=j?.viewModes?.allowedModes?.filter(W);if(k?.length)return K(k);if(j?.toolbar?.viewMode===!0)return G;return j?.viewModes?G:[b.view.kind]}function z(b){if(b.view.kind==="detail")return;return b.view.collection}function F(b,j){return W(b)&&j.includes(b)?b:void 0}function K(b){return G.filter((j)=>b.includes(j))}function H(b){return b==="list"||b==="grid"||b==="table"}function W(b){return H(b)}export{N as resolveDataViewPreferences,X as preferenceDimensionsToDataViewDensity,A as dataViewModeToPreferencePatch,S as dataViewDensityToPreferencePatch};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
function
|
|
2
|
+
function G(e,t){let i=[];e.suggestedHiddenFields.forEach((n)=>{i.push({type:"hideField",field:n,reason:"Automatically hidden because usage is near zero"})});let r=new Set([...e.deniedFields??[],...t.deniedFields??[]]),a=e.frequentlyUsedFields.filter((n)=>!r.has(n));if(a.length)i.push({type:"reorderFields",fields:a});if(!i.length)return null;return{overlayId:t.overlayId,version:t.version??"1.0.0",appliesTo:{tenantId:t.tenantId,capability:t.capability,userId:t.userId,role:t.role},modifications:i,metadata:{generatedAt:new Date().toISOString(),automated:!0}}}function J(e){return e.workflowBottlenecks.map((t)=>({workflow:t.workflow,step:t.step,note:`High drop rate (${Math.round(t.dropRate*100)}%) detected`}))}class I{store;options;constructor(e,t={}){this.store=e;this.options=t}async analyze(e){let t={tenantId:e.tenantId,userId:e.userId,role:e.role};if(e.windowMs)t.since=Date.now()-e.windowMs;let i=await this.store.summarize(t);return k(i,this.options)}}function k(e,t){let i=t.fieldInactivityThreshold??3,r=t.minSamples??10,a=[],n=[];for(let[d,c]of Object.entries(e.fieldCounts)){if(c<=i)a.push(d);if(c>=i*4)n.push(d)}let o=Object.entries(e.workflowStepCounts).flatMap(([d,c])=>{let w=Object.values(c).reduce((f,p)=>f+p,0);if(!w||w<r)return[];return Object.entries(c).filter(([,f])=>f/w<0.4).map(([f,p])=>({workflow:d,step:f,dropRate:1-p/w}))}),s=Object.keys(e.deniedFieldCounts??{}),h=Object.keys(e.deniedActionCounts??{}),m=x(e);return{unusedFields:a,suggestedHiddenFields:a.slice(0,5),frequentlyUsedFields:n.filter((d)=>!s.includes(d)).slice(0,10),deniedFields:s,deniedActions:h,workflowBottlenecks:o,layoutPreference:m,dataViewPreferences:V(e)}}function V(e){let t=Object.entries(e.dataViewViewModeCounts??{}).flatMap(([i,r])=>{let a=Object.entries(r).sort(([,n],[,o])=>(o??0)-(n??0))[0]?.[0];if(!b(a))return[];return[[i,{preferredViewMode:a}]]});return t.length?Object.fromEntries(t):void 0}function b(e){return e==="list"||e==="grid"||e==="table"}function x(e){let t=Object.keys(e.fieldCounts).length;if(!t)return;if(t>=15)return"table";if(t>=8)return"grid";return"form"}var g=["list","grid","table"];function Y({spec:e,preferences:t,insights:i,record:r}){let a=P(e),n=F(e,a),o=D(e,i,a),s=l(r?.viewMode,a),h=E(e),m=A(e);return{viewMode:s??o??n??"list",density:r?.density??S(t)??h??"comfortable",dataDepth:r?.dataDepth??t?.dataDepth??m??"standard",pageSize:r?.pageSize??u(e)?.pagination?.pageSize,source:{viewMode:s?"record":o?"insights":n?"contract":"fallback",density:r?.density?"record":t?"preferences":h?"contract":"fallback",dataDepth:r?.dataDepth?"record":t?.dataDepth?"preferences":m?"contract":"fallback",pageSize:r?.pageSize?"record":u(e)?.pagination?.pageSize?"contract":void 0}}}function S(e){if(!e)return;if(e.density==="minimal"||e.density==="compact"||e.density==="dense")return"compact";return"comfortable"}function N(e){return{density:e==="compact"?"compact":"standard"}}function L(e,t){return{dataViewKey:e.meta.key,dataViewVersion:e.meta.version,viewMode:t}}function D(e,t,i){let r=t?.dataViewPreferences?.[e.meta.key]?.preferredViewMode,a=t?.layoutPreference;return l(r,i)??l(a,i)}function F(e,t){if(!v(e.view.kind))return;let i=u(e)?.viewModes?.defaultMode;return l(i,t)??l(e.view.kind,t)}function E(e){if(e.view.kind==="table"&&e.view.density)return e.view.density;return u(e)?.density}function A(e){return u(e)?.dataDepth}function P(e){if(!v(e.view.kind))return[];let t=u(e),i=t?.viewModes?.allowedModes?.filter(y);if(i?.length)return C(i);if(t?.toolbar?.viewMode===!0)return g;return t?.viewModes?g:[e.view.kind]}function u(e){if(e.view.kind==="detail")return;return e.view.collection}function l(e,t){return y(e)&&t.includes(e)?e:void 0}function C(e){return g.filter((t)=>e.includes(t))}function v(e){return e==="list"||e==="grid"||e==="table"}function y(e){return v(e)}import{registerDocBlocks as R}from"@contractspec/lib.contracts-spec/docs";var T=[{id:"docs.personalization.behavior-tracking",title:"Behavior Tracking",summary:"`@contractspec/lib.personalization` provides primitives to observe how tenants/users interact with specs and turn that telemetry into personalization insights.",kind:"reference",visibility:"public",route:"/docs/personalization/behavior-tracking",tags:["personalization","behavior-tracking"],body:`# Behavior Tracking
|
|
3
3
|
|
|
4
4
|
\`@contractspec/lib.personalization\` provides primitives to observe how tenants/users interact with specs and turn that telemetry into personalization insights.
|
|
5
5
|
|
|
@@ -78,7 +78,7 @@ When the adapter returns an overlay spec, pass it to the overlay engine to regis
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
`}];
|
|
81
|
+
`}];R(T);import{registerDocBlocks as z}from"@contractspec/lib.contracts-spec/docs";var _=[{id:"docs.personalization.overlay-engine",title:"Overlay Engine",summary:"`@contractspec/lib.overlay-engine` is the canonical runtime for OverlaySpecs. It validates specs, tracks scope precedence, and exposes hooks for React renderers.",kind:"reference",visibility:"public",route:"/docs/personalization/overlay-engine",tags:["personalization","overlay-engine"],body:`# Overlay Engine
|
|
82
82
|
|
|
83
83
|
\`@contractspec/lib.overlay-engine\` is the canonical runtime for OverlaySpecs. It validates specs, tracks scope precedence, and exposes hooks for React renderers.
|
|
84
84
|
|
|
@@ -158,7 +158,7 @@ const result = engine.apply({
|
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
`}];
|
|
161
|
+
`}];z(_);import{registerDocBlocks as O}from"@contractspec/lib.contracts-spec/docs";var U=[{id:"docs.personalization.workflow-composition",title:"Workflow Composition",summary:"`@contractspec/lib.workflow-composer` composes base WorkflowSpecs with tenant/role/device-specific extensions, strict validation, deterministic merge ordering, metadata/annotation overlays, and orphan-graph protection for hidden-step rewrites.",kind:"reference",visibility:"public",route:"/docs/personalization/workflow-composition",tags:["personalization","workflow-composition"],body:`# Workflow Composition
|
|
162
162
|
|
|
163
163
|
\`@contractspec/lib.workflow-composer\` composes base WorkflowSpecs with tenant/role/device-specific extensions.
|
|
164
164
|
|
|
@@ -210,4 +210,4 @@ workflowRunner.execute(runtimeSpec, ctx);
|
|
|
210
210
|
- \`metadata\` and \`annotations\` overlays are merged into the composed runtime workflow for downstream observability and rollout tracing.
|
|
211
211
|
|
|
212
212
|
This keeps tenant overlays additive, auditable, and replay-safe.
|
|
213
|
-
`}];
|
|
213
|
+
`}];O(U);class j{events=[];async record(e){this.events.push(e)}async bulkRecord(e){this.events.push(...e)}async query(e){return K(this.events,e)}async summarize(e){let t=await this.query(e),i={fieldCounts:{},featureCounts:{},workflowStepCounts:{},dataViewInteractionCounts:{},dataViewViewModeCounts:{},totalEvents:t.length,deniedFieldCounts:{},deniedActionCounts:{}};return t.forEach((r)=>{switch(Q(i,r),r.type){case"field_access":i.fieldCounts[r.field]=(i.fieldCounts[r.field]??0)+1;break;case"feature_usage":i.featureCounts[r.feature]=(i.featureCounts[r.feature]??0)+1;break;case"workflow_step":{let a=i.workflowStepCounts[r.workflow]??={};a[r.step]=(a[r.step]??0)+1;break}case"data_view_interaction":{let a=i.dataViewInteractionCounts??={},n=a[r.dataViewKey]??={};if(n[r.action]=(n[r.action]??0)+1,r.viewMode){let o=i.dataViewViewModeCounts??={},s=o[r.dataViewKey]??={};s[r.viewMode]=(s[r.viewMode]??0)+1}break}default:break}}),i}async clear(){this.events=[]}}function K(e,t){return e.filter((i)=>{if(t.tenantId&&i.tenantId!==t.tenantId)return!1;if(t.userId&&i.userId!==t.userId)return!1;if(t.role&&i.role!==t.role&&!i.roles?.includes(t.role))return!1;if(t.roles?.length&&!t.roles.some((r)=>i.roles?.includes(r)||i.role===r))return!1;if(t.permission&&!i.permissions?.includes(t.permission))return!1;if(t.permissions?.length&&!t.permissions.every((r)=>i.permissions?.includes(r)))return!1;if(t.since&&i.timestamp<t.since)return!1;if(t.until&&i.timestamp>t.until)return!1;if(t.operation&&i.type==="field_access"&&i.operation!==t.operation)return!1;if(t.feature&&i.type==="feature_usage"&&i.feature!==t.feature)return!1;if(t.workflow&&i.type==="workflow_step"&&i.workflow!==t.workflow)return!1;if(t.dataViewKey&&i.type==="data_view_interaction"&&i.dataViewKey!==t.dataViewKey)return!1;if(t.dataViewAction&&i.type==="data_view_interaction"&&i.action!==t.dataViewAction)return!1;return!0})}function Q(e,t){Object.values(t.policyDecisions??{}).forEach((i)=>{if(i.effect!=="deny")return;i.fields?.forEach((r)=>{let a=e.deniedFieldCounts??={};a[r]=(a[r]??0)+1}),i.actions?.forEach((r)=>{let a=e.deniedActionCounts??={};a[r]=(a[r]??0)+1})})}import{metrics as H,trace as W}from"@opentelemetry/api";var $=25;class B{store;context;tracer=W.getTracer("lssm.personalization","1.0.0");counter=H.getMeter("lssm.personalization","1.0.0").createCounter("lssm.personalization.events",{description:"Behavior events tracked for personalization"});buffer=[];bufferSize;flushTimer;constructor(e){if(this.store=e.store,this.context=e.context,this.bufferSize=e.bufferSize??$,e.autoFlushIntervalMs)this.flushTimer=setInterval(()=>{this.flush()},e.autoFlushIntervalMs)}trackFieldAccess(e){let t={type:"field_access",operation:e.operation,field:e.field,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackFeatureUsage(e){let t={type:"feature_usage",feature:e.feature,action:e.action,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackWorkflowStep(e){let t={type:"workflow_step",workflow:e.workflow,step:e.step,status:e.status,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackDataViewInteraction(e){let t={type:"data_view_interaction",dataViewKey:e.dataViewKey,dataViewVersion:e.dataViewVersion,action:e.action,viewMode:e.viewMode,density:e.density,dataDepth:e.dataDepth,filterKey:e.filterKey,sortField:e.sortField,page:e.page,pageSize:e.pageSize,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}async flush(){if(!this.buffer.length)return;let e=this.buffer;this.buffer=[],await this.store.bulkRecord(e)}async dispose(){if(this.flushTimer)clearInterval(this.flushTimer);await this.flush()}enqueue(e){if(this.buffer.push(e),this.counter.add(1,{tenantId:this.context.tenantId,type:e.type}),this.tracer.startActiveSpan(`personalization.${e.type}`,(t)=>{if(t.setAttribute("tenant.id",this.context.tenantId),this.context.userId)t.setAttribute("user.id",this.context.userId);t.setAttribute("personalization.event_type",e.type),t.end()}),this.buffer.length>=this.bufferSize)this.flush()}}var fe=(e)=>new B(e);import{defineFeature as Z}from"@contractspec/lib.contracts-spec/features";var me=Z({meta:{key:"libs.personalization",version:"1.0.0",title:"Personalization",description:"Behavior tracking, analysis, and adaptation helpers for ContractSpec personalization.",domain:"personalization",owners:["@contractspec-core"],tags:["package","libs","personalization"],stability:"experimental"}});export{Y as resolveDataViewPreferences,S as preferenceDimensionsToDataViewDensity,J as insightsToWorkflowAdaptations,G as insightsToOverlaySuggestion,L as dataViewModeToPreferencePatch,N as dataViewDensityToPreferencePatch,fe as createBehaviorTracker,me as PersonalizationFeature,j as InMemoryBehaviorStore,B as BehaviorTracker,I as BehaviorAnalyzer};
|
package/dist/node/adapter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function a(t,e){let r=[];t.suggestedHiddenFields.forEach((i)=>{r.push({type:"hideField",field:i,reason:"Automatically hidden because usage is near zero"})});let o=new Set([...t.deniedFields??[],...e.deniedFields??[]]),n=t.frequentlyUsedFields.filter((i)=>!o.has(i));if(n.length)r.push({type:"reorderFields",fields:n});if(!r.length)return null;return{overlayId:e.overlayId,version:e.version??"1.0.0",appliesTo:{tenantId:e.tenantId,capability:e.capability,userId:e.userId,role:e.role},modifications:r,metadata:{generatedAt:new Date().toISOString(),automated:!0}}}function s(t){return t.workflowBottlenecks.map((e)=>({workflow:e.workflow,step:e.step,note:`High drop rate (${Math.round(e.dropRate*100)}%) detected`}))}export{s as insightsToWorkflowAdaptations,a as insightsToOverlaySuggestion};
|
package/dist/node/analyzer.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class
|
|
1
|
+
class p{store;options;constructor(e,t={}){this.store=e;this.options=t}async analyze(e){let t={tenantId:e.tenantId,userId:e.userId,role:e.role};if(e.windowMs)t.since=Date.now()-e.windowMs;let n=await this.store.summarize(t);return v(n,this.options)}}function v(e,t){let n=t.fieldInactivityThreshold??3,d=t.minSamples??10,r=[],a=[];for(let[i,o]of Object.entries(e.fieldCounts)){if(o<=n)r.push(i);if(o>=n*4)a.push(i)}let l=Object.entries(e.workflowStepCounts).flatMap(([i,o])=>{let c=Object.values(o).reduce((s,u)=>s+u,0);if(!c||c<d)return[];return Object.entries(o).filter(([,s])=>s/c<0.4).map(([s,u])=>({workflow:i,step:s,dropRate:1-u/c}))}),f=Object.keys(e.deniedFieldCounts??{}),h=Object.keys(e.deniedActionCounts??{}),y=g(e);return{unusedFields:r,suggestedHiddenFields:r.slice(0,5),frequentlyUsedFields:a.filter((i)=>!f.includes(i)).slice(0,10),deniedFields:f,deniedActions:h,workflowBottlenecks:l,layoutPreference:y,dataViewPreferences:w(e)}}function w(e){let t=Object.entries(e.dataViewViewModeCounts??{}).flatMap(([n,d])=>{let r=Object.entries(d).sort(([,a],[,l])=>(l??0)-(a??0))[0]?.[0];if(!m(r))return[];return[[n,{preferredViewMode:r}]]});return t.length?Object.fromEntries(t):void 0}function m(e){return e==="list"||e==="grid"||e==="table"}function g(e){let t=Object.keys(e.fieldCounts).length;if(!t)return;if(t>=15)return"table";if(t>=8)return"grid";return"form"}export{p as BehaviorAnalyzer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var G=["list","grid","table"];function N({spec:b,preferences:j,insights:k,record:x}){let B=V(b),J=Z(b,B),P=Y(b,k,B),Q=F(x?.viewMode,B),R=$(b),U=q(b);return{viewMode:Q??P??J??"list",density:x?.density??X(j)??R??"comfortable",dataDepth:x?.dataDepth??j?.dataDepth??U??"standard",pageSize:x?.pageSize??z(b)?.pagination?.pageSize,source:{viewMode:Q?"record":P?"insights":J?"contract":"fallback",density:x?.density?"record":j?"preferences":R?"contract":"fallback",dataDepth:x?.dataDepth?"record":j?.dataDepth?"preferences":U?"contract":"fallback",pageSize:x?.pageSize?"record":z(b)?.pagination?.pageSize?"contract":void 0}}}function X(b){if(!b)return;if(b.density==="minimal"||b.density==="compact"||b.density==="dense")return"compact";return"comfortable"}function S(b){return{density:b==="compact"?"compact":"standard"}}function A(b,j){return{dataViewKey:b.meta.key,dataViewVersion:b.meta.version,viewMode:j}}function Y(b,j,k){let x=j?.dataViewPreferences?.[b.meta.key]?.preferredViewMode,B=j?.layoutPreference;return F(x,k)??F(B,k)}function Z(b,j){if(!H(b.view.kind))return;let k=z(b)?.viewModes?.defaultMode;return F(k,j)??F(b.view.kind,j)}function $(b){if(b.view.kind==="table"&&b.view.density)return b.view.density;return z(b)?.density}function q(b){return z(b)?.dataDepth}function V(b){if(!H(b.view.kind))return[];let j=z(b),k=j?.viewModes?.allowedModes?.filter(W);if(k?.length)return K(k);if(j?.toolbar?.viewMode===!0)return G;return j?.viewModes?G:[b.view.kind]}function z(b){if(b.view.kind==="detail")return;return b.view.collection}function F(b,j){return W(b)&&j.includes(b)?b:void 0}function K(b){return G.filter((j)=>b.includes(j))}function H(b){return b==="list"||b==="grid"||b==="table"}function W(b){return H(b)}export{N as resolveDataViewPreferences,X as preferenceDimensionsToDataViewDensity,A as dataViewModeToPreferencePatch,S as dataViewDensityToPreferencePatch};
|
package/dist/node/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
function G(e,t){let i=[];e.suggestedHiddenFields.forEach((n)=>{i.push({type:"hideField",field:n,reason:"Automatically hidden because usage is near zero"})});let r=new Set([...e.deniedFields??[],...t.deniedFields??[]]),a=e.frequentlyUsedFields.filter((n)=>!r.has(n));if(a.length)i.push({type:"reorderFields",fields:a});if(!i.length)return null;return{overlayId:t.overlayId,version:t.version??"1.0.0",appliesTo:{tenantId:t.tenantId,capability:t.capability,userId:t.userId,role:t.role},modifications:i,metadata:{generatedAt:new Date().toISOString(),automated:!0}}}function J(e){return e.workflowBottlenecks.map((t)=>({workflow:t.workflow,step:t.step,note:`High drop rate (${Math.round(t.dropRate*100)}%) detected`}))}class I{store;options;constructor(e,t={}){this.store=e;this.options=t}async analyze(e){let t={tenantId:e.tenantId,userId:e.userId,role:e.role};if(e.windowMs)t.since=Date.now()-e.windowMs;let i=await this.store.summarize(t);return k(i,this.options)}}function k(e,t){let i=t.fieldInactivityThreshold??3,r=t.minSamples??10,a=[],n=[];for(let[d,c]of Object.entries(e.fieldCounts)){if(c<=i)a.push(d);if(c>=i*4)n.push(d)}let o=Object.entries(e.workflowStepCounts).flatMap(([d,c])=>{let w=Object.values(c).reduce((f,p)=>f+p,0);if(!w||w<r)return[];return Object.entries(c).filter(([,f])=>f/w<0.4).map(([f,p])=>({workflow:d,step:f,dropRate:1-p/w}))}),s=Object.keys(e.deniedFieldCounts??{}),h=Object.keys(e.deniedActionCounts??{}),m=x(e);return{unusedFields:a,suggestedHiddenFields:a.slice(0,5),frequentlyUsedFields:n.filter((d)=>!s.includes(d)).slice(0,10),deniedFields:s,deniedActions:h,workflowBottlenecks:o,layoutPreference:m,dataViewPreferences:V(e)}}function V(e){let t=Object.entries(e.dataViewViewModeCounts??{}).flatMap(([i,r])=>{let a=Object.entries(r).sort(([,n],[,o])=>(o??0)-(n??0))[0]?.[0];if(!b(a))return[];return[[i,{preferredViewMode:a}]]});return t.length?Object.fromEntries(t):void 0}function b(e){return e==="list"||e==="grid"||e==="table"}function x(e){let t=Object.keys(e.fieldCounts).length;if(!t)return;if(t>=15)return"table";if(t>=8)return"grid";return"form"}var g=["list","grid","table"];function Y({spec:e,preferences:t,insights:i,record:r}){let a=P(e),n=F(e,a),o=D(e,i,a),s=l(r?.viewMode,a),h=E(e),m=A(e);return{viewMode:s??o??n??"list",density:r?.density??S(t)??h??"comfortable",dataDepth:r?.dataDepth??t?.dataDepth??m??"standard",pageSize:r?.pageSize??u(e)?.pagination?.pageSize,source:{viewMode:s?"record":o?"insights":n?"contract":"fallback",density:r?.density?"record":t?"preferences":h?"contract":"fallback",dataDepth:r?.dataDepth?"record":t?.dataDepth?"preferences":m?"contract":"fallback",pageSize:r?.pageSize?"record":u(e)?.pagination?.pageSize?"contract":void 0}}}function S(e){if(!e)return;if(e.density==="minimal"||e.density==="compact"||e.density==="dense")return"compact";return"comfortable"}function N(e){return{density:e==="compact"?"compact":"standard"}}function L(e,t){return{dataViewKey:e.meta.key,dataViewVersion:e.meta.version,viewMode:t}}function D(e,t,i){let r=t?.dataViewPreferences?.[e.meta.key]?.preferredViewMode,a=t?.layoutPreference;return l(r,i)??l(a,i)}function F(e,t){if(!v(e.view.kind))return;let i=u(e)?.viewModes?.defaultMode;return l(i,t)??l(e.view.kind,t)}function E(e){if(e.view.kind==="table"&&e.view.density)return e.view.density;return u(e)?.density}function A(e){return u(e)?.dataDepth}function P(e){if(!v(e.view.kind))return[];let t=u(e),i=t?.viewModes?.allowedModes?.filter(y);if(i?.length)return C(i);if(t?.toolbar?.viewMode===!0)return g;return t?.viewModes?g:[e.view.kind]}function u(e){if(e.view.kind==="detail")return;return e.view.collection}function l(e,t){return y(e)&&t.includes(e)?e:void 0}function C(e){return g.filter((t)=>e.includes(t))}function v(e){return e==="list"||e==="grid"||e==="table"}function y(e){return v(e)}import{registerDocBlocks as R}from"@contractspec/lib.contracts-spec/docs";var T=[{id:"docs.personalization.behavior-tracking",title:"Behavior Tracking",summary:"`@contractspec/lib.personalization` provides primitives to observe how tenants/users interact with specs and turn that telemetry into personalization insights.",kind:"reference",visibility:"public",route:"/docs/personalization/behavior-tracking",tags:["personalization","behavior-tracking"],body:`# Behavior Tracking
|
|
2
2
|
|
|
3
3
|
\`@contractspec/lib.personalization\` provides primitives to observe how tenants/users interact with specs and turn that telemetry into personalization insights.
|
|
4
4
|
|
|
@@ -77,7 +77,7 @@ When the adapter returns an overlay spec, pass it to the overlay engine to regis
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
`}];
|
|
80
|
+
`}];R(T);import{registerDocBlocks as z}from"@contractspec/lib.contracts-spec/docs";var _=[{id:"docs.personalization.overlay-engine",title:"Overlay Engine",summary:"`@contractspec/lib.overlay-engine` is the canonical runtime for OverlaySpecs. It validates specs, tracks scope precedence, and exposes hooks for React renderers.",kind:"reference",visibility:"public",route:"/docs/personalization/overlay-engine",tags:["personalization","overlay-engine"],body:`# Overlay Engine
|
|
81
81
|
|
|
82
82
|
\`@contractspec/lib.overlay-engine\` is the canonical runtime for OverlaySpecs. It validates specs, tracks scope precedence, and exposes hooks for React renderers.
|
|
83
83
|
|
|
@@ -157,7 +157,7 @@ const result = engine.apply({
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
`}];
|
|
160
|
+
`}];z(_);import{registerDocBlocks as O}from"@contractspec/lib.contracts-spec/docs";var U=[{id:"docs.personalization.workflow-composition",title:"Workflow Composition",summary:"`@contractspec/lib.workflow-composer` composes base WorkflowSpecs with tenant/role/device-specific extensions, strict validation, deterministic merge ordering, metadata/annotation overlays, and orphan-graph protection for hidden-step rewrites.",kind:"reference",visibility:"public",route:"/docs/personalization/workflow-composition",tags:["personalization","workflow-composition"],body:`# Workflow Composition
|
|
161
161
|
|
|
162
162
|
\`@contractspec/lib.workflow-composer\` composes base WorkflowSpecs with tenant/role/device-specific extensions.
|
|
163
163
|
|
|
@@ -209,4 +209,4 @@ workflowRunner.execute(runtimeSpec, ctx);
|
|
|
209
209
|
- \`metadata\` and \`annotations\` overlays are merged into the composed runtime workflow for downstream observability and rollout tracing.
|
|
210
210
|
|
|
211
211
|
This keeps tenant overlays additive, auditable, and replay-safe.
|
|
212
|
-
`}];
|
|
212
|
+
`}];O(U);class j{events=[];async record(e){this.events.push(e)}async bulkRecord(e){this.events.push(...e)}async query(e){return K(this.events,e)}async summarize(e){let t=await this.query(e),i={fieldCounts:{},featureCounts:{},workflowStepCounts:{},dataViewInteractionCounts:{},dataViewViewModeCounts:{},totalEvents:t.length,deniedFieldCounts:{},deniedActionCounts:{}};return t.forEach((r)=>{switch(Q(i,r),r.type){case"field_access":i.fieldCounts[r.field]=(i.fieldCounts[r.field]??0)+1;break;case"feature_usage":i.featureCounts[r.feature]=(i.featureCounts[r.feature]??0)+1;break;case"workflow_step":{let a=i.workflowStepCounts[r.workflow]??={};a[r.step]=(a[r.step]??0)+1;break}case"data_view_interaction":{let a=i.dataViewInteractionCounts??={},n=a[r.dataViewKey]??={};if(n[r.action]=(n[r.action]??0)+1,r.viewMode){let o=i.dataViewViewModeCounts??={},s=o[r.dataViewKey]??={};s[r.viewMode]=(s[r.viewMode]??0)+1}break}default:break}}),i}async clear(){this.events=[]}}function K(e,t){return e.filter((i)=>{if(t.tenantId&&i.tenantId!==t.tenantId)return!1;if(t.userId&&i.userId!==t.userId)return!1;if(t.role&&i.role!==t.role&&!i.roles?.includes(t.role))return!1;if(t.roles?.length&&!t.roles.some((r)=>i.roles?.includes(r)||i.role===r))return!1;if(t.permission&&!i.permissions?.includes(t.permission))return!1;if(t.permissions?.length&&!t.permissions.every((r)=>i.permissions?.includes(r)))return!1;if(t.since&&i.timestamp<t.since)return!1;if(t.until&&i.timestamp>t.until)return!1;if(t.operation&&i.type==="field_access"&&i.operation!==t.operation)return!1;if(t.feature&&i.type==="feature_usage"&&i.feature!==t.feature)return!1;if(t.workflow&&i.type==="workflow_step"&&i.workflow!==t.workflow)return!1;if(t.dataViewKey&&i.type==="data_view_interaction"&&i.dataViewKey!==t.dataViewKey)return!1;if(t.dataViewAction&&i.type==="data_view_interaction"&&i.action!==t.dataViewAction)return!1;return!0})}function Q(e,t){Object.values(t.policyDecisions??{}).forEach((i)=>{if(i.effect!=="deny")return;i.fields?.forEach((r)=>{let a=e.deniedFieldCounts??={};a[r]=(a[r]??0)+1}),i.actions?.forEach((r)=>{let a=e.deniedActionCounts??={};a[r]=(a[r]??0)+1})})}import{metrics as H,trace as W}from"@opentelemetry/api";var $=25;class B{store;context;tracer=W.getTracer("lssm.personalization","1.0.0");counter=H.getMeter("lssm.personalization","1.0.0").createCounter("lssm.personalization.events",{description:"Behavior events tracked for personalization"});buffer=[];bufferSize;flushTimer;constructor(e){if(this.store=e.store,this.context=e.context,this.bufferSize=e.bufferSize??$,e.autoFlushIntervalMs)this.flushTimer=setInterval(()=>{this.flush()},e.autoFlushIntervalMs)}trackFieldAccess(e){let t={type:"field_access",operation:e.operation,field:e.field,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackFeatureUsage(e){let t={type:"feature_usage",feature:e.feature,action:e.action,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackWorkflowStep(e){let t={type:"workflow_step",workflow:e.workflow,step:e.step,status:e.status,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackDataViewInteraction(e){let t={type:"data_view_interaction",dataViewKey:e.dataViewKey,dataViewVersion:e.dataViewVersion,action:e.action,viewMode:e.viewMode,density:e.density,dataDepth:e.dataDepth,filterKey:e.filterKey,sortField:e.sortField,page:e.page,pageSize:e.pageSize,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}async flush(){if(!this.buffer.length)return;let e=this.buffer;this.buffer=[],await this.store.bulkRecord(e)}async dispose(){if(this.flushTimer)clearInterval(this.flushTimer);await this.flush()}enqueue(e){if(this.buffer.push(e),this.counter.add(1,{tenantId:this.context.tenantId,type:e.type}),this.tracer.startActiveSpan(`personalization.${e.type}`,(t)=>{if(t.setAttribute("tenant.id",this.context.tenantId),this.context.userId)t.setAttribute("user.id",this.context.userId);t.setAttribute("personalization.event_type",e.type),t.end()}),this.buffer.length>=this.bufferSize)this.flush()}}var fe=(e)=>new B(e);import{defineFeature as Z}from"@contractspec/lib.contracts-spec/features";var me=Z({meta:{key:"libs.personalization",version:"1.0.0",title:"Personalization",description:"Behavior tracking, analysis, and adaptation helpers for ContractSpec personalization.",domain:"personalization",owners:["@contractspec-core"],tags:["package","libs","personalization"],stability:"experimental"}});export{Y as resolveDataViewPreferences,S as preferenceDimensionsToDataViewDensity,J as insightsToWorkflowAdaptations,G as insightsToOverlaySuggestion,L as dataViewModeToPreferencePatch,N as dataViewDensityToPreferencePatch,fe as createBehaviorTracker,me as PersonalizationFeature,j as InMemoryBehaviorStore,B as BehaviorTracker,I as BehaviorAnalyzer};
|
package/dist/node/store.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class
|
|
1
|
+
class f{events=[];async record(r){this.events.push(r)}async bulkRecord(r){this.events.push(...r)}async query(r){return c(this.events,r)}async summarize(r){let o=await this.query(r),e={fieldCounts:{},featureCounts:{},workflowStepCounts:{},dataViewInteractionCounts:{},dataViewViewModeCounts:{},totalEvents:o.length,deniedFieldCounts:{},deniedActionCounts:{}};return o.forEach((i)=>{switch(d(e,i),i.type){case"field_access":e.fieldCounts[i.field]=(e.fieldCounts[i.field]??0)+1;break;case"feature_usage":e.featureCounts[i.feature]=(e.featureCounts[i.feature]??0)+1;break;case"workflow_step":{let t=e.workflowStepCounts[i.workflow]??={};t[i.step]=(t[i.step]??0)+1;break}case"data_view_interaction":{let t=e.dataViewInteractionCounts??={},a=t[i.dataViewKey]??={};if(a[i.action]=(a[i.action]??0)+1,i.viewMode){let n=e.dataViewViewModeCounts??={},s=n[i.dataViewKey]??={};s[i.viewMode]=(s[i.viewMode]??0)+1}break}default:break}}),e}async clear(){this.events=[]}}function c(r,o){return r.filter((e)=>{if(o.tenantId&&e.tenantId!==o.tenantId)return!1;if(o.userId&&e.userId!==o.userId)return!1;if(o.role&&e.role!==o.role&&!e.roles?.includes(o.role))return!1;if(o.roles?.length&&!o.roles.some((i)=>e.roles?.includes(i)||e.role===i))return!1;if(o.permission&&!e.permissions?.includes(o.permission))return!1;if(o.permissions?.length&&!o.permissions.every((i)=>e.permissions?.includes(i)))return!1;if(o.since&&e.timestamp<o.since)return!1;if(o.until&&e.timestamp>o.until)return!1;if(o.operation&&e.type==="field_access"&&e.operation!==o.operation)return!1;if(o.feature&&e.type==="feature_usage"&&e.feature!==o.feature)return!1;if(o.workflow&&e.type==="workflow_step"&&e.workflow!==o.workflow)return!1;if(o.dataViewKey&&e.type==="data_view_interaction"&&e.dataViewKey!==o.dataViewKey)return!1;if(o.dataViewAction&&e.type==="data_view_interaction"&&e.action!==o.dataViewAction)return!1;return!0})}function d(r,o){Object.values(o.policyDecisions??{}).forEach((e)=>{if(e.effect!=="deny")return;e.fields?.forEach((i)=>{let t=r.deniedFieldCounts??={};t[i]=(t[i]??0)+1}),e.actions?.forEach((i)=>{let t=r.deniedActionCounts??={};t[i]=(t[i]??0)+1})})}export{f as InMemoryBehaviorStore};
|
package/dist/node/tracker.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{metrics as
|
|
1
|
+
import{metrics as r,trace as i}from"@opentelemetry/api";var o=25;class a{store;context;tracer=i.getTracer("lssm.personalization","1.0.0");counter=r.getMeter("lssm.personalization","1.0.0").createCounter("lssm.personalization.events",{description:"Behavior events tracked for personalization"});buffer=[];bufferSize;flushTimer;constructor(e){if(this.store=e.store,this.context=e.context,this.bufferSize=e.bufferSize??o,e.autoFlushIntervalMs)this.flushTimer=setInterval(()=>{this.flush()},e.autoFlushIntervalMs)}trackFieldAccess(e){let t={type:"field_access",operation:e.operation,field:e.field,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackFeatureUsage(e){let t={type:"feature_usage",feature:e.feature,action:e.action,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackWorkflowStep(e){let t={type:"workflow_step",workflow:e.workflow,step:e.step,status:e.status,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackDataViewInteraction(e){let t={type:"data_view_interaction",dataViewKey:e.dataViewKey,dataViewVersion:e.dataViewVersion,action:e.action,viewMode:e.viewMode,density:e.density,dataDepth:e.dataDepth,filterKey:e.filterKey,sortField:e.sortField,page:e.page,pageSize:e.pageSize,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}async flush(){if(!this.buffer.length)return;let e=this.buffer;this.buffer=[],await this.store.bulkRecord(e)}async dispose(){if(this.flushTimer)clearInterval(this.flushTimer);await this.flush()}enqueue(e){if(this.buffer.push(e),this.counter.add(1,{tenantId:this.context.tenantId,type:e.type}),this.tracer.startActiveSpan(`personalization.${e.type}`,(t)=>{if(t.setAttribute("tenant.id",this.context.tenantId),this.context.userId)t.setAttribute("user.id",this.context.userId);t.setAttribute("personalization.event_type",e.type),t.end()}),this.buffer.length>=this.bufferSize)this.flush()}}var n=(e)=>new a(e);export{n as createBehaviorTracker,a as BehaviorTracker};
|
package/dist/store.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
class
|
|
2
|
+
class f{events=[];async record(r){this.events.push(r)}async bulkRecord(r){this.events.push(...r)}async query(r){return c(this.events,r)}async summarize(r){let o=await this.query(r),e={fieldCounts:{},featureCounts:{},workflowStepCounts:{},dataViewInteractionCounts:{},dataViewViewModeCounts:{},totalEvents:o.length,deniedFieldCounts:{},deniedActionCounts:{}};return o.forEach((i)=>{switch(d(e,i),i.type){case"field_access":e.fieldCounts[i.field]=(e.fieldCounts[i.field]??0)+1;break;case"feature_usage":e.featureCounts[i.feature]=(e.featureCounts[i.feature]??0)+1;break;case"workflow_step":{let t=e.workflowStepCounts[i.workflow]??={};t[i.step]=(t[i.step]??0)+1;break}case"data_view_interaction":{let t=e.dataViewInteractionCounts??={},a=t[i.dataViewKey]??={};if(a[i.action]=(a[i.action]??0)+1,i.viewMode){let n=e.dataViewViewModeCounts??={},s=n[i.dataViewKey]??={};s[i.viewMode]=(s[i.viewMode]??0)+1}break}default:break}}),e}async clear(){this.events=[]}}function c(r,o){return r.filter((e)=>{if(o.tenantId&&e.tenantId!==o.tenantId)return!1;if(o.userId&&e.userId!==o.userId)return!1;if(o.role&&e.role!==o.role&&!e.roles?.includes(o.role))return!1;if(o.roles?.length&&!o.roles.some((i)=>e.roles?.includes(i)||e.role===i))return!1;if(o.permission&&!e.permissions?.includes(o.permission))return!1;if(o.permissions?.length&&!o.permissions.every((i)=>e.permissions?.includes(i)))return!1;if(o.since&&e.timestamp<o.since)return!1;if(o.until&&e.timestamp>o.until)return!1;if(o.operation&&e.type==="field_access"&&e.operation!==o.operation)return!1;if(o.feature&&e.type==="feature_usage"&&e.feature!==o.feature)return!1;if(o.workflow&&e.type==="workflow_step"&&e.workflow!==o.workflow)return!1;if(o.dataViewKey&&e.type==="data_view_interaction"&&e.dataViewKey!==o.dataViewKey)return!1;if(o.dataViewAction&&e.type==="data_view_interaction"&&e.action!==o.dataViewAction)return!1;return!0})}function d(r,o){Object.values(o.policyDecisions??{}).forEach((e)=>{if(e.effect!=="deny")return;e.fields?.forEach((i)=>{let t=r.deniedFieldCounts??={};t[i]=(t[i]??0)+1}),e.actions?.forEach((i)=>{let t=r.deniedActionCounts??={};t[i]=(t[i]??0)+1})})}export{f as InMemoryBehaviorStore};
|
package/dist/tracker.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import type { DataViewCollectionMode, DataViewDataDepth, DataViewDensity } from '@contractspec/lib.contracts-spec/data-views';
|
|
1
2
|
import type { BehaviorStore } from './store';
|
|
3
|
+
import type { AuthorizationDecisionSummary, DataViewInteractionAction } from './types';
|
|
2
4
|
export interface BehaviorTrackerContext {
|
|
3
5
|
tenantId: string;
|
|
4
6
|
userId?: string;
|
|
5
7
|
role?: string;
|
|
8
|
+
roles?: string[];
|
|
9
|
+
permissions?: string[];
|
|
10
|
+
policyDecisions?: Record<string, AuthorizationDecisionSummary>;
|
|
6
11
|
device?: string;
|
|
7
12
|
metadata?: Record<string, unknown>;
|
|
8
13
|
}
|
|
@@ -29,6 +34,19 @@ export interface TrackWorkflowStepInput {
|
|
|
29
34
|
status: 'entered' | 'completed' | 'skipped' | 'errored';
|
|
30
35
|
metadata?: Record<string, unknown>;
|
|
31
36
|
}
|
|
37
|
+
export interface TrackDataViewInteractionInput {
|
|
38
|
+
dataViewKey: string;
|
|
39
|
+
dataViewVersion?: string;
|
|
40
|
+
action: DataViewInteractionAction;
|
|
41
|
+
viewMode?: DataViewCollectionMode;
|
|
42
|
+
density?: DataViewDensity;
|
|
43
|
+
dataDepth?: DataViewDataDepth;
|
|
44
|
+
filterKey?: string;
|
|
45
|
+
sortField?: string;
|
|
46
|
+
page?: number;
|
|
47
|
+
pageSize?: number;
|
|
48
|
+
metadata?: Record<string, unknown>;
|
|
49
|
+
}
|
|
32
50
|
export declare class BehaviorTracker {
|
|
33
51
|
private readonly store;
|
|
34
52
|
private readonly context;
|
|
@@ -41,6 +59,7 @@ export declare class BehaviorTracker {
|
|
|
41
59
|
trackFieldAccess(input: TrackFieldAccessInput): void;
|
|
42
60
|
trackFeatureUsage(input: TrackFeatureUsageInput): void;
|
|
43
61
|
trackWorkflowStep(input: TrackWorkflowStepInput): void;
|
|
62
|
+
trackDataViewInteraction(input: TrackDataViewInteractionInput): void;
|
|
44
63
|
flush(): Promise<void>;
|
|
45
64
|
dispose(): Promise<void>;
|
|
46
65
|
private enqueue;
|
package/dist/tracker.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{metrics as
|
|
2
|
+
import{metrics as r,trace as i}from"@opentelemetry/api";var o=25;class a{store;context;tracer=i.getTracer("lssm.personalization","1.0.0");counter=r.getMeter("lssm.personalization","1.0.0").createCounter("lssm.personalization.events",{description:"Behavior events tracked for personalization"});buffer=[];bufferSize;flushTimer;constructor(e){if(this.store=e.store,this.context=e.context,this.bufferSize=e.bufferSize??o,e.autoFlushIntervalMs)this.flushTimer=setInterval(()=>{this.flush()},e.autoFlushIntervalMs)}trackFieldAccess(e){let t={type:"field_access",operation:e.operation,field:e.field,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackFeatureUsage(e){let t={type:"feature_usage",feature:e.feature,action:e.action,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackWorkflowStep(e){let t={type:"workflow_step",workflow:e.workflow,step:e.step,status:e.status,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}trackDataViewInteraction(e){let t={type:"data_view_interaction",dataViewKey:e.dataViewKey,dataViewVersion:e.dataViewVersion,action:e.action,viewMode:e.viewMode,density:e.density,dataDepth:e.dataDepth,filterKey:e.filterKey,sortField:e.sortField,page:e.page,pageSize:e.pageSize,timestamp:Date.now(),...this.context,metadata:{...this.context.metadata,...e.metadata}};this.enqueue(t)}async flush(){if(!this.buffer.length)return;let e=this.buffer;this.buffer=[],await this.store.bulkRecord(e)}async dispose(){if(this.flushTimer)clearInterval(this.flushTimer);await this.flush()}enqueue(e){if(this.buffer.push(e),this.counter.add(1,{tenantId:this.context.tenantId,type:e.type}),this.tracer.startActiveSpan(`personalization.${e.type}`,(t)=>{if(t.setAttribute("tenant.id",this.context.tenantId),this.context.userId)t.setAttribute("user.id",this.context.userId);t.setAttribute("personalization.event_type",e.type),t.end()}),this.buffer.length>=this.bufferSize)this.flush()}}var n=(e)=>new a(e);export{n as createBehaviorTracker,a as BehaviorTracker};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import type { PolicyDecision } from '@contractspec/lib.contracts-spec';
|
|
2
|
+
import type { DataViewCollectionMode, DataViewDataDepth, DataViewDensity } from '@contractspec/lib.contracts-spec/data-views';
|
|
3
|
+
export type BehaviorEventType = 'field_access' | 'feature_usage' | 'workflow_step' | 'data_view_interaction';
|
|
4
|
+
export interface AuthorizationDecisionSummary extends Pick<PolicyDecision, 'effect' | 'reason' | 'missing' | 'matched' | 'source'> {
|
|
5
|
+
fields?: string[];
|
|
6
|
+
actions?: string[];
|
|
7
|
+
}
|
|
2
8
|
export interface BehaviorEventBase {
|
|
3
9
|
id?: string;
|
|
4
10
|
tenantId: string;
|
|
5
11
|
userId?: string;
|
|
6
12
|
role?: string;
|
|
13
|
+
roles?: string[];
|
|
14
|
+
permissions?: string[];
|
|
15
|
+
policyDecisions?: Record<string, AuthorizationDecisionSummary>;
|
|
7
16
|
device?: string;
|
|
8
17
|
timestamp: number;
|
|
9
18
|
metadata?: Record<string, unknown>;
|
|
@@ -24,14 +33,33 @@ export interface WorkflowStepEvent extends BehaviorEventBase {
|
|
|
24
33
|
step: string;
|
|
25
34
|
status: 'entered' | 'completed' | 'skipped' | 'errored';
|
|
26
35
|
}
|
|
27
|
-
export type
|
|
36
|
+
export type DataViewInteractionAction = 'opened' | 'view_mode_changed' | 'density_changed' | 'data_depth_changed' | 'search_changed' | 'filter_changed' | 'sort_changed' | 'page_changed';
|
|
37
|
+
export interface DataViewInteractionEvent extends BehaviorEventBase {
|
|
38
|
+
type: 'data_view_interaction';
|
|
39
|
+
dataViewKey: string;
|
|
40
|
+
dataViewVersion?: string;
|
|
41
|
+
action: DataViewInteractionAction;
|
|
42
|
+
viewMode?: DataViewCollectionMode;
|
|
43
|
+
density?: DataViewDensity;
|
|
44
|
+
dataDepth?: DataViewDataDepth;
|
|
45
|
+
filterKey?: string;
|
|
46
|
+
sortField?: string;
|
|
47
|
+
page?: number;
|
|
48
|
+
pageSize?: number;
|
|
49
|
+
}
|
|
50
|
+
export type BehaviorEvent = FieldAccessEvent | FeatureUsageEvent | WorkflowStepEvent | DataViewInteractionEvent;
|
|
28
51
|
export interface BehaviorQuery {
|
|
29
52
|
tenantId?: string;
|
|
30
53
|
userId?: string;
|
|
31
54
|
role?: string;
|
|
55
|
+
roles?: string[];
|
|
56
|
+
permission?: string;
|
|
57
|
+
permissions?: string[];
|
|
32
58
|
feature?: string;
|
|
33
59
|
operation?: string;
|
|
34
60
|
workflow?: string;
|
|
61
|
+
dataViewKey?: string;
|
|
62
|
+
dataViewAction?: DataViewInteractionAction;
|
|
35
63
|
since?: number;
|
|
36
64
|
until?: number;
|
|
37
65
|
limit?: number;
|
|
@@ -40,16 +68,26 @@ export interface BehaviorSummary {
|
|
|
40
68
|
fieldCounts: Record<string, number>;
|
|
41
69
|
featureCounts: Record<string, number>;
|
|
42
70
|
workflowStepCounts: Record<string, Record<string, number>>;
|
|
71
|
+
dataViewInteractionCounts?: Record<string, Partial<Record<DataViewInteractionAction, number>>>;
|
|
72
|
+
dataViewViewModeCounts?: Record<string, Partial<Record<DataViewCollectionMode, number>>>;
|
|
43
73
|
totalEvents: number;
|
|
74
|
+
deniedFieldCounts?: Record<string, number>;
|
|
75
|
+
deniedActionCounts?: Record<string, number>;
|
|
76
|
+
}
|
|
77
|
+
export interface DataViewPreferenceInsights {
|
|
78
|
+
preferredViewMode?: DataViewCollectionMode;
|
|
44
79
|
}
|
|
45
80
|
export interface BehaviorInsights {
|
|
46
81
|
unusedFields: string[];
|
|
47
82
|
frequentlyUsedFields: string[];
|
|
48
83
|
suggestedHiddenFields: string[];
|
|
84
|
+
deniedFields?: string[];
|
|
85
|
+
deniedActions?: string[];
|
|
49
86
|
workflowBottlenecks: {
|
|
50
87
|
workflow: string;
|
|
51
88
|
step: string;
|
|
52
89
|
dropRate: number;
|
|
53
90
|
}[];
|
|
54
91
|
layoutPreference?: 'form' | 'grid' | 'list' | 'table';
|
|
92
|
+
dataViewPreferences?: Record<string, DataViewPreferenceInsights>;
|
|
55
93
|
}
|