@timeax/digital-service-engine 0.0.3 → 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;
@@ -273,10 +79,7 @@ type OrderSnapshot = {
273
79
  max: number;
274
80
  services: Array<string | number>;
275
81
  serviceMap: Record<string, Array<string | number>>;
276
- fallbacks?: {
277
- nodes?: Record<string, Array<string | number>>;
278
- global?: Record<string | number, Array<string | number>>;
279
- };
82
+ fallbacks?: ServiceFallbacks;
280
83
  utilities?: UtilityLineItem[];
281
84
  warnings?: {
282
85
  utility?: Array<{
@@ -295,6 +98,46 @@ type OrderSnapshot = {
295
98
  };
296
99
  };
297
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
+
298
141
  type NodeRef = {
299
142
  kind: "tag";
300
143
  id: string;
@@ -386,6 +229,246 @@ type FallbackSettings = {
386
229
  mode?: "strict" | "dev";
387
230
  };
388
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
+
389
472
  type NormaliseOptions = {
390
473
  /** default pricing role for fields/options when missing */
391
474
  defaultPricingRole?: PricingRole;
@@ -495,6 +578,10 @@ declare function getEligibleFallbacks(params: {
495
578
  unique?: boolean;
496
579
  limit?: number;
497
580
  }): ServiceIdRef[];
581
+ declare function getFallbackRegistrationInfo(props: ServiceProps, nodeId: NodeIdRef): {
582
+ primary?: ServiceIdRef;
583
+ tagContexts: string[];
584
+ };
498
585
 
499
586
  type BaseCandidate = {
500
587
  kind: "field" | "option";
@@ -644,4 +731,38 @@ type BuildOrderSelection = {
644
731
  };
645
732
  declare function buildOrderSnapshot(props: ServiceProps, builder: Builder, selection: BuildOrderSelection, services: DgpServiceMap, settings?: BuildOrderSnapshotSettings): OrderSnapshot;
646
733
 
647
- 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 };