@timeax/digital-service-engine 0.0.2 → 0.0.4

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.
@@ -1,198 +1,3 @@
1
- type PricingRole = "base" | "utility";
2
- type FieldType = "custom" | (string & {});
3
- /** ── Marker types (live inside meta; non-breaking) ───────────────────── */
4
- type QuantityMark = {
5
- quantity?: {
6
- valueBy: "value" | "length" | "eval";
7
- code?: string;
8
- multiply?: number;
9
- clamp?: {
10
- min?: number;
11
- max?: number;
12
- };
13
- fallback?: number;
14
- };
15
- };
16
- type UtilityMark = {
17
- utility?: {
18
- rate: number;
19
- mode: "flat" | "per_quantity" | "per_value" | "percent";
20
- valueBy?: "value" | "length";
21
- percentBase?: "service_total" | "base_service" | "all";
22
- label?: string;
23
- };
24
- };
25
- type WithQuantityDefault = {
26
- quantityDefault?: number;
27
- };
28
- /** ---------------- Core schema (as you designed) ---------------- */
29
- interface BaseFieldUI {
30
- name?: string;
31
- label: string;
32
- required?: boolean;
33
- /** Host-defined prop names → runtime default values (untyped base) */
34
- defaults?: Record<string, unknown>;
35
- }
36
- type FieldOption = {
37
- id: string;
38
- label: string;
39
- value?: string | number;
40
- service_id?: number;
41
- pricing_role?: PricingRole;
42
- meta?: Record<string, unknown> & UtilityMark & WithQuantityDefault;
43
- };
44
- type Field = BaseFieldUI & {
45
- id: string;
46
- type: FieldType;
47
- bind_id?: string | string[];
48
- name?: string;
49
- options?: FieldOption[];
50
- description?: string;
51
- component?: string;
52
- pricing_role?: PricingRole;
53
- meta?: Record<string, unknown> & QuantityMark & UtilityMark & {
54
- multi?: boolean;
55
- };
56
- } & ({
57
- button?: false;
58
- service_id?: undefined;
59
- } | ({
60
- button: true;
61
- service_id?: number;
62
- } & WithQuantityDefault));
63
- type ConstraintKey = string;
64
- type Tag = {
65
- id: string;
66
- label: string;
67
- bind_id?: string;
68
- service_id?: number;
69
- includes?: string[];
70
- excludes?: string[];
71
- meta?: Record<string, unknown> & WithQuantityDefault;
72
- /**
73
- * Which flags are set for this tag. If a flag is not set, it's inherited from the nearest ancestor with a value set.
74
- */
75
- constraints?: Partial<Record<ConstraintKey, boolean>>;
76
- /** Which ancestor defined the *effective* value for each flag (nearest source). */
77
- constraints_origin?: Partial<Record<ConstraintKey, string>>;
78
- /**
79
- * Present only when a child explicitly set a different value but was overridden
80
- * by an ancestor during normalisation.
81
- */
82
- constraints_overrides?: Partial<Record<ConstraintKey, {
83
- from: boolean;
84
- to: boolean;
85
- origin: string;
86
- }>>;
87
- };
88
- type ServiceProps = {
89
- order_for_tags?: Record<string, string[]>;
90
- filters: Tag[];
91
- fields: Field[];
92
- includes_for_buttons?: Record<string, string[]>;
93
- excludes_for_buttons?: Record<string, string[]>;
94
- schema_version?: string;
95
- fallbacks?: ServiceFallback;
96
- name?: string;
97
- notices?: ServicePropsNotice[];
98
- };
99
- type ServiceIdRef = number | string;
100
- type NodeIdRef = string;
101
- type ServiceFallback = {
102
- /** Node-scoped fallbacks: prefer these when that node’s primary service fails */
103
- nodes?: Record<NodeIdRef, ServiceIdRef[]>;
104
- /** Primary→fallback list used when no node-scoped entry is present */
105
- global?: Record<ServiceIdRef, ServiceIdRef[]>;
106
- };
107
- type NoticeType = "public" | "private";
108
- type NoticeSeverity = "info" | "warning" | "error";
109
- /**
110
- * “label” is lightweight + UI-friendly (best, sale, hot, etc).
111
- * Others remain semantic / governance oriented.
112
- */
113
- type NoticeKind = "label" | "warning" | "deprecation" | "compat" | "migration" | "policy";
114
- type NoticeTarget = {
115
- scope: "global";
116
- } | {
117
- scope: "node";
118
- node_kind: "tag" | "field" | "option";
119
- node_id: string;
120
- };
121
- interface ServicePropsNotice {
122
- id: string;
123
- type: NoticeType;
124
- kind: NoticeKind;
125
- severity: NoticeSeverity;
126
- target: NoticeTarget;
127
- title: string;
128
- description?: string;
129
- reason?: string;
130
- marked_at?: string;
131
- icon?: string;
132
- color?: string;
133
- meta?: Record<string, unknown>;
134
- }
135
-
136
- type NodeKind$1 = "tag" | "field" | "comment" | "option";
137
- type EdgeKind = "child" | "bind" | "include" | "exclude" | "error" | "anchor";
138
- type GraphNode = {
139
- id: string;
140
- kind: NodeKind$1;
141
- bind_type?: "bound" | "utility" | null;
142
- errors?: string[];
143
- label: string;
144
- };
145
- type GraphEdge = {
146
- from: string;
147
- to: string;
148
- kind: EdgeKind;
149
- meta?: Record<string, unknown>;
150
- };
151
- type GraphSnapshot = {
152
- nodes: GraphNode[];
153
- edges: GraphEdge[];
154
- };
155
-
156
- type TimeRangeEstimate = {
157
- min_seconds?: number;
158
- max_seconds?: number;
159
- label?: string;
160
- meta?: Record<string, unknown>;
161
- };
162
- type SpeedEstimate = {
163
- amount?: number;
164
- per?: "minute" | "hour" | "day" | "week" | "month";
165
- unit?: string;
166
- label?: string;
167
- meta?: Record<string, unknown>;
168
- };
169
- type ServiceEstimates = {
170
- start?: TimeRangeEstimate;
171
- speed?: SpeedEstimate;
172
- average?: TimeRangeEstimate;
173
- meta?: Record<string, unknown>;
174
- };
175
- type ServiceFlag = {
176
- enabled: boolean;
177
- description: string;
178
- meta?: Record<string, unknown>;
179
- };
180
- type IdType = string | number;
181
- type ServiceFlags = Record<string, ServiceFlag>;
182
- type DgpServiceCapability = {
183
- id: IdType;
184
- name?: string;
185
- rate: number;
186
- min?: number;
187
- max?: number;
188
- category?: string;
189
- flags?: ServiceFlags;
190
- estimates?: ServiceEstimates;
191
- meta?: Record<string, unknown>;
192
- [x: string]: any;
193
- };
194
- type DgpServiceMap = Record<string, DgpServiceCapability> & Record<number, DgpServiceCapability>;
195
-
196
1
  interface ButtonValue {
197
2
  id: string;
198
3
  value: string | number;
@@ -217,6 +22,7 @@ type UtilityLineItem = {
217
22
  evalCodeUsed?: boolean;
218
23
  };
219
24
  };
25
+ type ServiceFallbacks = ServiceFallback;
220
26
  type FallbackDiagnostics = {
221
27
  scope: "node" | "global";
222
28
  nodeId?: string;
@@ -251,6 +57,7 @@ type OrderSnapshot = {
251
57
  builtAt: string;
252
58
  selection: {
253
59
  tag: string;
60
+ buttons: string[];
254
61
  fields: Array<{
255
62
  id: string;
256
63
  type: string;
@@ -272,10 +79,7 @@ type OrderSnapshot = {
272
79
  max: number;
273
80
  services: Array<string | number>;
274
81
  serviceMap: Record<string, Array<string | number>>;
275
- fallbacks?: {
276
- nodes?: Record<string, Array<string | number>>;
277
- global?: Record<string | number, Array<string | number>>;
278
- };
82
+ fallbacks?: ServiceFallbacks;
279
83
  utilities?: UtilityLineItem[];
280
84
  warnings?: {
281
85
  utility?: Array<{
@@ -294,6 +98,46 @@ type OrderSnapshot = {
294
98
  };
295
99
  };
296
100
 
101
+ type TimeRangeEstimate = {
102
+ min_seconds?: number;
103
+ max_seconds?: number;
104
+ label?: string;
105
+ meta?: Record<string, unknown>;
106
+ };
107
+ type SpeedEstimate = {
108
+ amount?: number;
109
+ per?: "minute" | "hour" | "day" | "week" | "month";
110
+ unit?: string;
111
+ label?: string;
112
+ meta?: Record<string, unknown>;
113
+ };
114
+ type ServiceEstimates = {
115
+ start?: TimeRangeEstimate;
116
+ speed?: SpeedEstimate;
117
+ average?: TimeRangeEstimate;
118
+ meta?: Record<string, unknown>;
119
+ };
120
+ type ServiceFlag = {
121
+ enabled: boolean;
122
+ description: string;
123
+ meta?: Record<string, unknown>;
124
+ };
125
+ type IdType = string | number;
126
+ type ServiceFlags = Record<string, ServiceFlag>;
127
+ type DgpServiceCapability = {
128
+ id: IdType;
129
+ name?: string;
130
+ rate: number;
131
+ min?: number;
132
+ max?: number;
133
+ category?: string;
134
+ flags?: ServiceFlags;
135
+ estimates?: ServiceEstimates;
136
+ meta?: Record<string, unknown>;
137
+ [x: string]: any;
138
+ };
139
+ type DgpServiceMap = Record<string, DgpServiceCapability> & Record<number, DgpServiceCapability>;
140
+
297
141
  type NodeRef = {
298
142
  kind: "tag";
299
143
  id: string;
@@ -385,6 +229,246 @@ type FallbackSettings = {
385
229
  mode?: "strict" | "dev";
386
230
  };
387
231
 
232
+ type ServiceIdRef = number | string;
233
+ type NodeIdRef = string;
234
+ type ServiceFallback = {
235
+ /** Node-scoped fallbacks: prefer these when that node’s primary service fails */
236
+ nodes?: Record<NodeIdRef, ServiceIdRef[]>;
237
+ /** Primary→fallback list used when no node-scoped entry is present */
238
+ global?: Record<ServiceIdRef, ServiceIdRef[]>;
239
+ };
240
+ type FallbackEditorServiceMap = DgpServiceMap;
241
+ type FallbackRegistrationScope = "global" | "node";
242
+ type FallbackScopeRef = {
243
+ scope: "global";
244
+ primary: ServiceIdRef;
245
+ } | {
246
+ scope: "node";
247
+ nodeId: NodeIdRef;
248
+ };
249
+ type FallbackRegistration = {
250
+ scope: FallbackRegistrationScope;
251
+ /**
252
+ * For node scope => node id
253
+ * For global scope => omitted
254
+ */
255
+ scopeId?: NodeIdRef;
256
+ /**
257
+ * The primary DGP service this registration belongs to.
258
+ * For global scope, this is the global key.
259
+ * For node scope, this is resolved from ServiceProps/snapshot context.
260
+ */
261
+ primary: ServiceIdRef;
262
+ /** Registered fallback services */
263
+ services: ServiceIdRef[];
264
+ };
265
+ type FallbackCheckReason = "duplicate" | "self_reference" | "unknown_primary" | "unknown_candidate" | "missing_snapshot" | "node_scope_not_supported" | "node_primary_unresolved" | "ambiguous_context" | "invalid_candidate" | "unknown_service" | "no_primary" | "rate_violation" | "constraint_mismatch" | "cycle" | "no_tag_context" | "missing_service_props" | "node_not_found";
266
+ type FallbackCandidateCheck = {
267
+ candidate: ServiceIdRef;
268
+ ok: boolean;
269
+ reasons: FallbackCheckReason[];
270
+ };
271
+ type FallbackCheckResult = {
272
+ context: FallbackScopeRef;
273
+ /**
274
+ * Resolved primary when known.
275
+ * For global scope this should normally equal context.primary.
276
+ */
277
+ primary?: ServiceIdRef;
278
+ allowed: ServiceIdRef[];
279
+ rejected: FallbackCandidateCheck[];
280
+ warnings: FallbackCheckReason[];
281
+ };
282
+ type FallbackEditorState = {
283
+ original: ServiceFallback;
284
+ current: ServiceFallback;
285
+ changed: boolean;
286
+ };
287
+ type FallbackEditorOptions = {
288
+ /**
289
+ * The editable payload.
290
+ * The editor clones this and never mutates the caller’s object directly.
291
+ */
292
+ fallbacks?: ServiceFallback;
293
+ /**
294
+ * Optional read-only source used to resolve node→service ownership
295
+ * and validate node-scoped registrations.
296
+ */
297
+ props?: ServiceProps;
298
+ /**
299
+ * Optional runtime context enhancer.
300
+ * Useful for ambiguous node contexts / diagnostics.
301
+ */
302
+ snapshot?: OrderSnapshot;
303
+ /**
304
+ * Optional service map used for rate / existence validation.
305
+ */
306
+ services?: FallbackEditorServiceMap;
307
+ /**
308
+ * Optional fallback policy.
309
+ */
310
+ settings?: FallbackSettings;
311
+ };
312
+ type FallbackMutationOptions = {
313
+ /**
314
+ * When true, reject candidates failing validation.
315
+ * When false, keep structurally valid values and return warnings.
316
+ */
317
+ strict?: boolean;
318
+ /**
319
+ * Optional insert position for add/addMany.
320
+ * Omit to append.
321
+ */
322
+ index?: number;
323
+ };
324
+
325
+ type PricingRole = "base" | "utility";
326
+ type FieldType = "custom" | (string & {});
327
+ /** ── Marker types (live inside meta; non-breaking) ───────────────────── */
328
+ type QuantityMark = {
329
+ quantity?: {
330
+ valueBy: "value" | "length" | "eval";
331
+ code?: string;
332
+ multiply?: number;
333
+ clamp?: {
334
+ min?: number;
335
+ max?: number;
336
+ };
337
+ fallback?: number;
338
+ };
339
+ };
340
+ type UtilityMark = {
341
+ utility?: {
342
+ rate: number;
343
+ mode: "flat" | "per_quantity" | "per_value" | "percent";
344
+ valueBy?: "value" | "length";
345
+ percentBase?: "service_total" | "base_service" | "all";
346
+ label?: string;
347
+ };
348
+ };
349
+ type WithQuantityDefault = {
350
+ quantityDefault?: number;
351
+ };
352
+ /** ---------------- Core schema (as you designed) ---------------- */
353
+ interface BaseFieldUI {
354
+ name?: string;
355
+ label: string;
356
+ required?: boolean;
357
+ /** Host-defined prop names → runtime default values (untyped base) */
358
+ defaults?: Record<string, unknown>;
359
+ }
360
+ type FieldOption = {
361
+ id: string;
362
+ label: string;
363
+ value?: string | number;
364
+ service_id?: number;
365
+ pricing_role?: PricingRole;
366
+ meta?: Record<string, unknown> & UtilityMark & WithQuantityDefault;
367
+ };
368
+ type Field = BaseFieldUI & {
369
+ id: string;
370
+ type: FieldType;
371
+ bind_id?: string | string[];
372
+ name?: string;
373
+ options?: FieldOption[];
374
+ description?: string;
375
+ component?: string;
376
+ pricing_role?: PricingRole;
377
+ meta?: Record<string, unknown> & QuantityMark & UtilityMark & {
378
+ multi?: boolean;
379
+ };
380
+ } & ({
381
+ button?: false;
382
+ service_id?: undefined;
383
+ } | ({
384
+ button: true;
385
+ service_id?: number;
386
+ } & WithQuantityDefault));
387
+ type ConstraintKey = string;
388
+ type Tag = {
389
+ id: string;
390
+ label: string;
391
+ bind_id?: string;
392
+ service_id?: number;
393
+ includes?: string[];
394
+ excludes?: string[];
395
+ meta?: Record<string, unknown> & WithQuantityDefault;
396
+ /**
397
+ * Which flags are set for this tag. If a flag is not set, it's inherited from the nearest ancestor with a value set.
398
+ */
399
+ constraints?: Partial<Record<ConstraintKey, boolean>>;
400
+ /** Which ancestor defined the *effective* value for each flag (nearest source). */
401
+ constraints_origin?: Partial<Record<ConstraintKey, string>>;
402
+ /**
403
+ * Present only when a child explicitly set a different value but was overridden
404
+ * by an ancestor during normalisation.
405
+ */
406
+ constraints_overrides?: Partial<Record<ConstraintKey, {
407
+ from: boolean;
408
+ to: boolean;
409
+ origin: string;
410
+ }>>;
411
+ };
412
+ type ServiceProps = {
413
+ order_for_tags?: Record<string, string[]>;
414
+ filters: Tag[];
415
+ fields: Field[];
416
+ includes_for_buttons?: Record<string, string[]>;
417
+ excludes_for_buttons?: Record<string, string[]>;
418
+ schema_version?: string;
419
+ fallbacks?: ServiceFallback;
420
+ name?: string;
421
+ notices?: ServicePropsNotice[];
422
+ };
423
+ type NoticeType = "public" | "private";
424
+ type NoticeSeverity = "info" | "warning" | "error";
425
+ /**
426
+ * “label” is lightweight + UI-friendly (best, sale, hot, etc).
427
+ * Others remain semantic / governance oriented.
428
+ */
429
+ type NoticeKind = "label" | "warning" | "deprecation" | "compat" | "migration" | "policy";
430
+ type NoticeTarget = {
431
+ scope: "global";
432
+ } | {
433
+ scope: "node";
434
+ node_kind: "tag" | "field" | "option";
435
+ node_id: string;
436
+ };
437
+ interface ServicePropsNotice {
438
+ id: string;
439
+ type: NoticeType;
440
+ kind: NoticeKind;
441
+ severity: NoticeSeverity;
442
+ target: NoticeTarget;
443
+ title: string;
444
+ description?: string;
445
+ reason?: string;
446
+ marked_at?: string;
447
+ icon?: string;
448
+ color?: string;
449
+ meta?: Record<string, unknown>;
450
+ }
451
+
452
+ type NodeKind$1 = "tag" | "field" | "comment" | "option";
453
+ type EdgeKind = "child" | "bind" | "include" | "exclude" | "error" | "anchor";
454
+ type GraphNode = {
455
+ id: string;
456
+ kind: NodeKind$1;
457
+ bind_type?: "bound" | "utility" | null;
458
+ errors?: string[];
459
+ label: string;
460
+ };
461
+ type GraphEdge = {
462
+ from: string;
463
+ to: string;
464
+ kind: EdgeKind;
465
+ meta?: Record<string, unknown>;
466
+ };
467
+ type GraphSnapshot = {
468
+ nodes: GraphNode[];
469
+ edges: GraphEdge[];
470
+ };
471
+
388
472
  type NormaliseOptions = {
389
473
  /** default pricing role for fields/options when missing */
390
474
  defaultPricingRole?: PricingRole;
@@ -494,6 +578,10 @@ declare function getEligibleFallbacks(params: {
494
578
  unique?: boolean;
495
579
  limit?: number;
496
580
  }): ServiceIdRef[];
581
+ declare function getFallbackRegistrationInfo(props: ServiceProps, nodeId: NodeIdRef): {
582
+ primary?: ServiceIdRef;
583
+ tagContexts: string[];
584
+ };
497
585
 
498
586
  type BaseCandidate = {
499
587
  kind: "field" | "option";
@@ -631,6 +719,7 @@ type BuildOrderSelection = {
631
719
  formValuesByFieldId: Record<string, Scalar | Scalar[]>;
632
720
  /** Option selections, keyed by fieldId → optionId[] */
633
721
  optionSelectionsByFieldId: Record<string, string[]>;
722
+ selectedKeys?: string[];
634
723
  /**
635
724
  * Selection visit order for options (optional, improves "first option wins primary" determinism).
636
725
  * If omitted, iteration order falls back to Object.entries(optionSelectionsByFieldId).
@@ -642,4 +731,38 @@ type BuildOrderSelection = {
642
731
  };
643
732
  declare function buildOrderSnapshot(props: ServiceProps, builder: Builder, selection: BuildOrderSelection, services: DgpServiceMap, settings?: BuildOrderSnapshotSettings): OrderSnapshot;
644
733
 
645
- export { type AncestryHit, type AnyNode, type Builder, type BuilderOptions, type FailedFallbackContext, type FieldNode, type NodeIndex, type NodeKind, type NormaliseOptions, type OptionNode, type RateCoherenceDiagnostic, type RelKind, type TagNode, type UnknownNode, type WithAncestry, buildOrderSnapshot, collectFailedFallbacks, createBuilder, createNodeIndex, getEligibleFallbacks, normalise, resolveServiceFallback, validate, validateAsync, validateRateCoherenceDeep };
734
+ /**
735
+ * Keep the editor contract exactly as discussed:
736
+ * - mutates only ServiceFallback (internal clone)
737
+ * - props are read-only context
738
+ * - get(serviceId) is service-centric
739
+ * - getScope(context) is raw scope access
740
+ */
741
+ interface FallbackEditor {
742
+ state(): FallbackEditorState;
743
+ value(): ServiceFallback;
744
+ reset(): FallbackEditorState;
745
+ /** Service-centric: all registrations that belong to this primary service */
746
+ get(serviceId: ServiceIdRef): FallbackRegistration[];
747
+ /** Exact/raw scope access */
748
+ getScope(context: FallbackScopeRef): ServiceIdRef[];
749
+ /** Validation preview */
750
+ check(context: FallbackScopeRef, candidates?: ServiceIdRef[]): FallbackCheckResult;
751
+ add(context: FallbackScopeRef, candidate: ServiceIdRef, options?: FallbackMutationOptions): FallbackEditorState;
752
+ addMany(context: FallbackScopeRef, candidates: ServiceIdRef[], options?: FallbackMutationOptions): FallbackEditorState;
753
+ remove(context: FallbackScopeRef, candidate: ServiceIdRef): FallbackEditorState;
754
+ replace(context: FallbackScopeRef, candidates: ServiceIdRef[], options?: FallbackMutationOptions): FallbackEditorState;
755
+ clear(context: FallbackScopeRef): FallbackEditorState;
756
+ /**
757
+ * Optional helper for picker UIs:
758
+ * shows candidates that the core fallback resolver would currently accept.
759
+ */
760
+ eligible(context: FallbackScopeRef, options?: {
761
+ exclude?: ServiceIdRef[];
762
+ unique?: boolean;
763
+ limit?: number;
764
+ }): ServiceIdRef[];
765
+ }
766
+ declare function createFallbackEditor(options?: FallbackEditorOptions): FallbackEditor;
767
+
768
+ export { type AncestryHit, type AnyNode, type Builder, type BuilderOptions, type FailedFallbackContext, type FallbackEditor, type FieldNode, type NodeIndex, type NodeKind, type NormaliseOptions, type OptionNode, type RateCoherenceDiagnostic, type RelKind, type TagNode, type UnknownNode, type WithAncestry, buildOrderSnapshot, collectFailedFallbacks, createBuilder, createFallbackEditor, createNodeIndex, getEligibleFallbacks, getFallbackRegistrationInfo, normalise, resolveServiceFallback, validate, validateAsync, validateRateCoherenceDeep };