@spaceteams/weft 0.0.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/dist/index.mjs ADDED
@@ -0,0 +1,956 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region src/draft/index.ts
3
+ function createDraft(draftId, base, overlay) {
4
+ return {
5
+ draftId,
6
+ base,
7
+ overlay
8
+ };
9
+ }
10
+ function isEmptyDraft(draft) {
11
+ return Object.keys(draft.overlay).length === 0;
12
+ }
13
+ //#endregion
14
+ //#region src/evaluate/index.ts
15
+ const defaultEvaluateMode = "strict";
16
+ function evaluate(model, provided, mode = defaultEvaluateMode) {
17
+ const values = /* @__PURE__ */ new Map();
18
+ const missing = /* @__PURE__ */ new Map();
19
+ const trace = [];
20
+ for (const input of model.inputs) {
21
+ if (!(input.key.id in provided)) if (mode === "strict") throw new Error(`Missing input: ${input.key.id}`);
22
+ else {
23
+ missing.set(input.key.id, {
24
+ kind: "missing-input",
25
+ key: input.key.id
26
+ });
27
+ continue;
28
+ }
29
+ values.set(input.key.id, provided[input.key.id]);
30
+ }
31
+ for (const target of model.orderedRuleTargets) {
32
+ const rule = model.ruleByTarget.get(target);
33
+ if (!rule) throw new Error(`Missing compiled rule: ${target}`);
34
+ const inputs = {};
35
+ const missingDeps = [];
36
+ for (const dep of rule.deps) {
37
+ if (!values.has(dep.id)) if (mode === "strict") throw new Error(`Missing value: ${dep.id}`);
38
+ else {
39
+ missingDeps.push(dep);
40
+ continue;
41
+ }
42
+ inputs[dep.id] = values.get(dep.id);
43
+ }
44
+ if (missingDeps.length > 0) {
45
+ missing.set(target, {
46
+ kind: "rule-not-run",
47
+ key: target,
48
+ because: missingDeps.map((d) => d.id)
49
+ });
50
+ continue;
51
+ }
52
+ const get = (key) => {
53
+ if (!values.has(key.id)) throw new Error(`Invariant violation: missing dependency reached rule eval: ${key.id}`);
54
+ return values.get(key.id);
55
+ };
56
+ const { output, detail } = rule.eval(get);
57
+ values.set(target, output);
58
+ const deps = model.depsByTarget.get(target) ?? [];
59
+ const ruleMeta = model.ruleMeta.get(target);
60
+ const keyMeta = model.keyMeta.get(target);
61
+ trace.push({
62
+ target,
63
+ deps,
64
+ ruleSpec: rule.spec,
65
+ ruleMeta,
66
+ keyMeta,
67
+ detail: detail ?? {},
68
+ inputs,
69
+ output
70
+ });
71
+ }
72
+ return {
73
+ values,
74
+ missing,
75
+ order: model.orderedRuleTargets,
76
+ trace
77
+ };
78
+ }
79
+ //#endregion
80
+ //#region src/facts.ts
81
+ function mapToFactBag(map) {
82
+ return Object.fromEntries([...map.entries()].sort(([a], [b]) => a.localeCompare(b)));
83
+ }
84
+ //#endregion
85
+ //#region src/model/snapshot-model.ts
86
+ function snapshotModel(model) {
87
+ return {
88
+ inputKeys: [...model.inputKeys],
89
+ rules: model.rules.map((r) => ({
90
+ target: r.target.id,
91
+ spec: r.spec
92
+ }))
93
+ };
94
+ }
95
+ //#endregion
96
+ //#region src/overlay/diff-group.ts
97
+ function groupDiffByOrigin(result, deltas) {
98
+ const overlayInputs = [];
99
+ const derivedValues = [];
100
+ const other = [];
101
+ for (const delta of deltas) {
102
+ const origin = result.origins.get(delta.key);
103
+ if (origin?.kind === "overlay") overlayInputs.push(delta);
104
+ else if (origin?.kind === "derived") derivedValues.push(delta);
105
+ else other.push(delta);
106
+ }
107
+ const groups = [];
108
+ if (overlayInputs.length) groups.push({
109
+ label: "Overlay inputs",
110
+ deltas: overlayInputs
111
+ });
112
+ if (derivedValues.length) groups.push({
113
+ label: "Derived values",
114
+ deltas: derivedValues
115
+ });
116
+ if (other.length) groups.push({
117
+ label: "Other",
118
+ deltas: other
119
+ });
120
+ return groups;
121
+ }
122
+ //#endregion
123
+ //#region src/overlay/explain-diff.ts
124
+ function explainDiffs(result, deltas) {
125
+ const changedKeys = new Set(deltas.map((d) => d.key));
126
+ const traceByTarget = new Map(result.trace.map((step) => [step.target, step]));
127
+ return deltas.map((delta) => {
128
+ const step = traceByTarget.get(delta.key);
129
+ if (!step) return { delta };
130
+ return {
131
+ delta,
132
+ dependencies: step.deps.map((depKey) => ({
133
+ key: depKey,
134
+ changed: changedKeys.has(depKey)
135
+ }))
136
+ };
137
+ });
138
+ }
139
+ //#endregion
140
+ //#region src/snapshot/canonicalize.ts
141
+ function canonicalize(value) {
142
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
143
+ if (Array.isArray(value)) return value.map(canonicalize);
144
+ if (typeof value === "object" && value !== null) {
145
+ if (value instanceof Date) return value.toISOString();
146
+ const sortedKeys = Object.keys(value).sort();
147
+ const obj = value;
148
+ const out = {};
149
+ for (const key of sortedKeys) out[key] = canonicalize(obj[key]);
150
+ return out;
151
+ }
152
+ throw new Error(`Unsupported value for canonicalization: ${JSON.stringify(value)}`);
153
+ }
154
+ //#endregion
155
+ //#region src/snapshot/canonicalizeValue.ts
156
+ function canonicalizeValue(model, key, value) {
157
+ const semantics = model.semantics.get(key);
158
+ if (semantics?.encode) return semantics.encode(value);
159
+ else return canonicalize(value);
160
+ }
161
+ //#endregion
162
+ //#region src/snapshot/canonicalizeDelta.ts
163
+ function canonicalizeDelta(model, delta) {
164
+ switch (delta.kind) {
165
+ case "added": return {
166
+ key: delta.key,
167
+ kind: "added",
168
+ after: canonicalizeValue(model, delta.key, delta.after)
169
+ };
170
+ case "removed": return {
171
+ key: delta.key,
172
+ kind: "removed",
173
+ before: canonicalizeValue(model, delta.key, delta.before)
174
+ };
175
+ case "changed": return {
176
+ key: delta.key,
177
+ kind: "changed",
178
+ before: canonicalizeValue(model, delta.key, delta.before),
179
+ after: canonicalizeValue(model, delta.key, delta.after)
180
+ };
181
+ }
182
+ }
183
+ //#endregion
184
+ //#region src/snapshot/canonicalizeFacts.ts
185
+ function canonicalizeFacts(model, facts) {
186
+ const result = {};
187
+ for (const [key, value] of Object.entries(facts)) result[key] = canonicalizeValue(model, key, value);
188
+ return result;
189
+ }
190
+ //#endregion
191
+ //#region src/snapshot/fingerprint.ts
192
+ function fingerprintCanonical(value) {
193
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
194
+ }
195
+ function fingerprintValue(value) {
196
+ return fingerprintCanonical(canonicalize(value));
197
+ }
198
+ //#endregion
199
+ //#region src/model/model.ts
200
+ function getDependencies(model, key) {
201
+ return model.depsByTarget.get(key.id) ?? [];
202
+ }
203
+ function getDependents(model, key) {
204
+ return model.dependentsByKey.get(key.id) ?? [];
205
+ }
206
+ function getDeclaredKeys(model) {
207
+ return [...model.inputKeys, ...model.orderedRuleTargets];
208
+ }
209
+ function upstreamOf(model, key) {
210
+ const visited = /* @__PURE__ */ new Set();
211
+ const stack = [...model.depsByTarget.get(key.id) ?? []];
212
+ while (stack.length > 0) {
213
+ const current = stack.pop();
214
+ if (visited.has(current)) continue;
215
+ visited.add(current);
216
+ const deps = model.depsByTarget.get(current);
217
+ if (deps) {
218
+ for (const dep of deps) if (!visited.has(dep)) stack.push(dep);
219
+ }
220
+ }
221
+ return sortKeysByModelOrder(model, visited);
222
+ }
223
+ function downstreamOf(model, key) {
224
+ const visited = /* @__PURE__ */ new Set();
225
+ const stack = [...model.dependentsByKey.get(key.id) ?? []];
226
+ while (stack.length > 0) {
227
+ const current = stack.pop();
228
+ if (visited.has(current)) continue;
229
+ visited.add(current);
230
+ const dependents = model.dependentsByKey.get(current);
231
+ if (dependents) {
232
+ for (const dependent of dependents) if (!visited.has(dependent)) stack.push(dependent);
233
+ }
234
+ }
235
+ return sortKeysByModelOrder(model, visited);
236
+ }
237
+ function sortKeysByModelOrder(model, keys) {
238
+ const keySet = new Set(keys);
239
+ return [...model.inputKeys.filter((k) => keySet.has(k)), ...model.orderedRuleTargets.filter((k) => keySet.has(k))];
240
+ }
241
+ //#endregion
242
+ //#region src/overlay/diff-results.ts
243
+ function diffResults(model, before, after) {
244
+ const deltas = [];
245
+ for (const key of getDeclaredKeys(model)) {
246
+ const hasBefore = before.values.has(key);
247
+ const hasAfter = after.values.has(key);
248
+ if (!hasBefore && !hasAfter) continue;
249
+ if (hasBefore !== hasAfter) {
250
+ deltas.push(hasBefore ? {
251
+ key,
252
+ kind: "removed",
253
+ before: before.values.get(key)
254
+ } : {
255
+ key,
256
+ kind: "added",
257
+ after: after.values.get(key)
258
+ });
259
+ continue;
260
+ }
261
+ const beforeValue = before.values.get(key);
262
+ const afterValue = after.values.get(key);
263
+ if (!(model.semantics.get(key)?.eq ?? Object.is)(beforeValue, afterValue)) deltas.push({
264
+ key,
265
+ kind: "changed",
266
+ before: beforeValue,
267
+ after: afterValue
268
+ });
269
+ }
270
+ return { deltas };
271
+ }
272
+ //#endregion
273
+ //#region src/overlay/apply-overlay.ts
274
+ function applyOverlay(base, overlay) {
275
+ return {
276
+ base,
277
+ overlay,
278
+ effective: {
279
+ ...base,
280
+ ...overlay
281
+ }
282
+ };
283
+ }
284
+ //#endregion
285
+ //#region src/overlay/evaluate-overlay.ts
286
+ function evaluateOverlay(model, base, overlay, mode = defaultEvaluateMode) {
287
+ const facts = applyOverlay(base, overlay);
288
+ const evaluated = evaluate(model, facts.effective, mode);
289
+ const origins = /* @__PURE__ */ new Map();
290
+ for (const input of model.inputs) {
291
+ const key = input.key.id;
292
+ if (key in overlay) origins.set(key, { kind: "overlay" });
293
+ else if (key in base) origins.set(key, { kind: "base" });
294
+ }
295
+ for (const rule of model.rules) origins.set(rule.target.id, { kind: "derived" });
296
+ return {
297
+ ...evaluated,
298
+ overlayedFacts: facts,
299
+ origins
300
+ };
301
+ }
302
+ //#endregion
303
+ //#region src/draft/evaluate-draft.ts
304
+ function evaluateDraft(model, draft, mode = defaultEvaluateMode) {
305
+ const baseResult = evaluate(model, draft.base, mode);
306
+ const result = evaluateOverlay(model, draft.base, draft.overlay, mode);
307
+ const { deltas } = diffResults(model, baseResult, result);
308
+ return {
309
+ draft,
310
+ result,
311
+ deltas
312
+ };
313
+ }
314
+ //#endregion
315
+ //#region src/draft/analyze-draft.ts
316
+ function analyzeDraft(model, draft, mode = defaultEvaluateMode) {
317
+ const normalized = normalizeDraft(model, draft);
318
+ const evaluated = evaluateDraft(model, normalized.draft, mode);
319
+ const impact = analyzeImpact(model, evaluated);
320
+ return {
321
+ evaluated,
322
+ groupedDiffs: groupDiffByOrigin(evaluated.result, evaluated.deltas),
323
+ changes: explainDiffs(evaluated.result, evaluated.deltas),
324
+ impact,
325
+ normalizationIssues: normalized.issues
326
+ };
327
+ }
328
+ function normalizeDraft(model, draft) {
329
+ const normalized = {
330
+ draftId: draft.draftId,
331
+ base: draft.base,
332
+ overlay: {},
333
+ meta: draft.meta
334
+ };
335
+ const issues = [];
336
+ for (const key of Object.keys(draft.overlay)) {
337
+ if (!model.inputKeys.includes(key)) {
338
+ issues.push({
339
+ level: "warning",
340
+ key,
341
+ message: `Key "${key}" is not an input key and will be ignored`
342
+ });
343
+ continue;
344
+ }
345
+ const base = draft.base[key];
346
+ let overlay = draft.overlay[key];
347
+ const semantics = model.semantics.get(key);
348
+ const normalize = semantics?.normalize;
349
+ if (normalize) overlay = normalize(overlay);
350
+ if ((semantics?.eq ?? Object.is)(base, overlay)) continue;
351
+ normalized.overlay[key] = overlay;
352
+ }
353
+ return {
354
+ draft: normalized,
355
+ issues
356
+ };
357
+ }
358
+ function analyzeImpact(model, evaluated) {
359
+ const changed = new Set(evaluated.deltas.map((d) => d.key));
360
+ const direct = [];
361
+ const affected = [];
362
+ const terminal = [];
363
+ for (const key of changed) {
364
+ const origin = evaluated.result.origins.get(key);
365
+ if (origin?.kind === "overlay") direct.push(key);
366
+ else if (origin?.kind === "derived") affected.push(key);
367
+ }
368
+ for (const key of changed) if (!model.dependentsByKey.get(key)?.some((d) => changed.has(d))) terminal.push(key);
369
+ return {
370
+ direct,
371
+ affected,
372
+ terminal
373
+ };
374
+ }
375
+ function freezeDraftAnalysis(model, analysis) {
376
+ const modelShape = snapshotModel(model);
377
+ const draft = analysis.evaluated.draft;
378
+ const now = (/* @__PURE__ */ new Date()).toISOString();
379
+ const snapshot = {
380
+ modelFingerprint: fingerprintValue(modelShape),
381
+ baseFingerprint: fingerprintValue(draft.base),
382
+ overlayFingerprint: fingerprintValue(draft.overlay),
383
+ analysisFingerprint: fingerprintValue({
384
+ model: modelShape,
385
+ base: draft.base,
386
+ overlay: draft.overlay
387
+ }),
388
+ createdAt: now
389
+ };
390
+ return {
391
+ draftId: draft.draftId,
392
+ snapshot,
393
+ base: canonicalizeFacts(model, draft.base),
394
+ overlay: canonicalizeFacts(model, draft.overlay),
395
+ effective: canonicalizeFacts(model, analysis.evaluated.result.overlayedFacts.effective),
396
+ values: canonicalizeFacts(model, mapToFactBag(analysis.evaluated.result.values)),
397
+ deltas: analysis.evaluated.deltas.map((delta) => canonicalizeDelta(model, delta)),
398
+ groupedDiffs: analysis.groupedDiffs.map((group) => ({
399
+ label: group.label,
400
+ deltas: group.deltas.map((delta) => canonicalizeDelta(model, delta))
401
+ })),
402
+ changes: analysis.changes.map((change) => ({
403
+ delta: canonicalizeDelta(model, change.delta),
404
+ dependencies: change.dependencies
405
+ })),
406
+ impact: analysis.impact,
407
+ normalizationIssues: analysis.normalizationIssues,
408
+ frozenAt: now
409
+ };
410
+ }
411
+ //#endregion
412
+ //#region src/inspect/inspect-diff-target.ts
413
+ function inspectDiffTarget(model, result, changes, target) {
414
+ const changeByKey = new Map(changes.map((change) => [change.delta.key, change]));
415
+ const stepByTarget = new Map(result.trace.map((step) => [step.target, step]));
416
+ function build(key, parentStep) {
417
+ const step = stepByTarget.get(key);
418
+ const change = changeByKey.get(key);
419
+ if (!step) {
420
+ const keyMeta = model.keyMeta.get(key);
421
+ return {
422
+ key,
423
+ kind: "input",
424
+ meta: { key: keyMeta },
425
+ execution: { value: parentStep?.inputs[key] },
426
+ change,
427
+ label: keyMeta?.label ?? key,
428
+ children: []
429
+ };
430
+ }
431
+ return {
432
+ key,
433
+ kind: step?.ruleSpec?.op ?? "rule",
434
+ meta: {
435
+ key: step?.keyMeta,
436
+ rule: step?.ruleMeta
437
+ },
438
+ structure: { ruleSpec: step?.ruleSpec },
439
+ execution: {
440
+ value: step?.output,
441
+ trace: step
442
+ },
443
+ change,
444
+ label: step?.keyMeta?.label ?? key,
445
+ children: step?.deps.map((dep) => build(dep, step)) ?? []
446
+ };
447
+ }
448
+ return build(target);
449
+ }
450
+ //#endregion
451
+ //#region src/inspect/inspect-model-target.ts
452
+ function inspectModelTarget(model, target) {
453
+ function build(key) {
454
+ const rule = model.ruleByTarget.get(key);
455
+ const keyMeta = model.keyMeta.get(key);
456
+ const ruleMeta = model.ruleMeta.get(key);
457
+ return {
458
+ key,
459
+ kind: rule ? rule?.spec?.op ?? "rule" : "input",
460
+ meta: {
461
+ key: keyMeta,
462
+ rule: ruleMeta
463
+ },
464
+ structure: { ruleSpec: rule?.spec },
465
+ label: keyMeta?.label ?? key,
466
+ children: rule?.deps.map((dep) => build(dep.id)) ?? []
467
+ };
468
+ }
469
+ return build(target);
470
+ }
471
+ //#endregion
472
+ //#region src/inspect/inspect-trace-target.ts
473
+ function inspectTraceTarget(model, trace, target) {
474
+ const stepByTarget = new Map(trace.map((s) => [s.target, s]));
475
+ function build(key, parentStep) {
476
+ const step = stepByTarget.get(key);
477
+ if (!step) {
478
+ const keyMeta = model.keyMeta.get(key);
479
+ return {
480
+ key,
481
+ kind: "input",
482
+ meta: { key: keyMeta },
483
+ execution: { value: parentStep?.inputs[key] },
484
+ label: keyMeta?.label ?? key,
485
+ children: []
486
+ };
487
+ }
488
+ return {
489
+ key,
490
+ kind: step?.ruleSpec?.op ?? "rule",
491
+ meta: {
492
+ key: step?.keyMeta,
493
+ rule: step?.ruleMeta
494
+ },
495
+ structure: { ruleSpec: step?.ruleSpec },
496
+ execution: {
497
+ value: step?.output,
498
+ trace: step
499
+ },
500
+ label: step?.keyMeta?.label ?? key,
501
+ children: step?.deps.map((dep) => build(dep, step)) ?? []
502
+ };
503
+ }
504
+ return build(target);
505
+ }
506
+ //#endregion
507
+ //#region src/inspect/inspection-node-to-ascii.ts
508
+ const formatLabel = (node, { showMeta, showChange }) => {
509
+ let label = node.meta?.rule?.label ?? node.label;
510
+ if (showMeta) label += ` [${node.kind}]`;
511
+ if (node.change && showChange) {
512
+ const delta = node.change.delta;
513
+ switch (delta.kind) {
514
+ case "added":
515
+ label += ` = ${String(delta.after)} (addded)`;
516
+ break;
517
+ case "changed":
518
+ label += ` = ${String(delta.before)} -> ${delta.after} (changed)`;
519
+ break;
520
+ case "removed":
521
+ label += ` = ${String(delta.before)} (removed)`;
522
+ break;
523
+ }
524
+ } else if (node.execution?.value !== void 0) label += ` = ${String(node.execution?.value)}`;
525
+ const detail = node.execution?.trace?.detail;
526
+ if (detail) switch (detail.op) {
527
+ case "decision": {
528
+ const tableDetail = detail;
529
+ label += tableDetail.usedDefault ? " :: default" : ` :: ${tableDetail.matchedRowLabel ?? tableDetail.matchedRowId}`;
530
+ }
531
+ }
532
+ return label;
533
+ };
534
+ const inspectionNodeToAscii = (root, options) => {
535
+ const toAscii = (node, prefix = "", isLast = true) => {
536
+ const lines = [`${prefix}${isLast ? "└── " : "├── "}${formatLabel(node, options)}`];
537
+ const childPrefix = prefix + (isLast ? " " : "│ ");
538
+ const children = node.children.map((dep, index) => toAscii(dep, childPrefix, index === node.children.length - 1));
539
+ return [...lines, ...children].join("\n");
540
+ };
541
+ return toAscii(root);
542
+ };
543
+ //#endregion
544
+ //#region src/key.ts
545
+ function key(id) {
546
+ return {
547
+ __kind: "key",
548
+ id
549
+ };
550
+ }
551
+ //#endregion
552
+ //#region src/model/compile-model.ts
553
+ function compileModel(model) {
554
+ const issues = [];
555
+ const inputKeys = /* @__PURE__ */ new Set();
556
+ for (const input of model.inputs) {
557
+ const id = input.key.id;
558
+ if (inputKeys.has(id)) issues.push({
559
+ level: "error",
560
+ code: "DUPLICATE_INPUT",
561
+ message: `Input "${id}" is declared more than once.`,
562
+ keys: [id]
563
+ });
564
+ inputKeys.add(id);
565
+ }
566
+ const ruleByTarget = /* @__PURE__ */ new Map();
567
+ for (const rule of model.rules) {
568
+ const targetId = rule.target.id;
569
+ if (ruleByTarget.has(targetId)) {
570
+ issues.push({
571
+ level: "error",
572
+ code: "DUPLICATE_RULE_TARGET",
573
+ message: `Rule target "${targetId}" is declared more than once.`,
574
+ keys: [targetId]
575
+ });
576
+ continue;
577
+ }
578
+ ruleByTarget.set(targetId, rule);
579
+ }
580
+ for (const inputId of inputKeys) if (ruleByTarget.has(inputId)) issues.push({
581
+ level: "error",
582
+ code: "INPUT_RULE_CONFLICT",
583
+ message: `Key "${inputId}" is declared as both input and rule target.`,
584
+ keys: [inputId]
585
+ });
586
+ const knownKeys = new Set([...inputKeys, ...ruleByTarget.keys()]);
587
+ const depsByTarget = /* @__PURE__ */ new Map();
588
+ const dependentsByKeyMutable = /* @__PURE__ */ new Map();
589
+ for (const rule of model.rules) {
590
+ const targetId = rule.target.id;
591
+ const depIds = rule.deps.map((dep) => dep.id);
592
+ depsByTarget.set(targetId, depIds);
593
+ for (const depId of depIds) {
594
+ if (!knownKeys.has(depId)) {
595
+ issues.push({
596
+ level: "error",
597
+ code: "MISSING_DEPENDENCY",
598
+ message: `Rule "${targetId}" depends on unkown key "${depId}".`,
599
+ keys: [targetId, depId]
600
+ });
601
+ continue;
602
+ }
603
+ const dependents = dependentsByKeyMutable.get(depId);
604
+ if (dependents) dependents.push(targetId);
605
+ else dependentsByKeyMutable.set(depId, [targetId]);
606
+ }
607
+ }
608
+ if (issues.some((issue) => issue.level === "error")) return {
609
+ ok: false,
610
+ issues
611
+ };
612
+ const inDegree = /* @__PURE__ */ new Map();
613
+ for (const targetId of ruleByTarget.keys()) inDegree.set(targetId, 0);
614
+ for (const [targetId, depIds] of depsByTarget) {
615
+ let count = 0;
616
+ for (const depId of depIds) if (ruleByTarget.has(depId)) count += 1;
617
+ inDegree.set(targetId, count);
618
+ }
619
+ const queue = [];
620
+ for (const [targetId, degree] of inDegree) if (degree === 0) queue.push(targetId);
621
+ const orderedRuleTargets = [];
622
+ while (queue.length > 0) {
623
+ const current = queue.shift();
624
+ orderedRuleTargets.push(current);
625
+ const dependents = dependentsByKeyMutable.get(current) ?? [];
626
+ for (const dependentTarget of dependents) {
627
+ if (!inDegree.has(dependentTarget)) continue;
628
+ const nextDegree = inDegree.get(dependentTarget) - 1;
629
+ inDegree.set(dependentTarget, nextDegree);
630
+ if (nextDegree === 0) queue.push(dependentTarget);
631
+ }
632
+ }
633
+ if (orderedRuleTargets.length !== ruleByTarget.size) {
634
+ const unresolved = [...ruleByTarget.keys()].filter((targetId) => !orderedRuleTargets.includes(targetId));
635
+ issues.push({
636
+ level: "error",
637
+ code: "CYCLE",
638
+ message: `Cycle detected among rule targets "${unresolved.join(", ")}"`,
639
+ keys: unresolved
640
+ });
641
+ return {
642
+ ok: false,
643
+ issues
644
+ };
645
+ }
646
+ const dependentsByKey = new Map([...dependentsByKeyMutable.entries()].map(([k, v]) => [k, [...v]]));
647
+ const declaredKeys = [...model.inputs.map((i) => [i.key.id, i.key]), ...model.rules.map((r) => [r.target.id, r.target])];
648
+ return {
649
+ ok: true,
650
+ issues,
651
+ model: {
652
+ keys: new Map(declaredKeys),
653
+ semantics: model.semantics,
654
+ inputs: model.inputs,
655
+ rules: model.rules,
656
+ keyMeta: model.keyMeta,
657
+ ruleMeta: model.ruleMeta,
658
+ inputKeys: [...inputKeys],
659
+ orderedRuleTargets,
660
+ ruleByTarget,
661
+ depsByTarget,
662
+ dependentsByKey
663
+ }
664
+ };
665
+ }
666
+ //#endregion
667
+ //#region src/input.ts
668
+ function input(key) {
669
+ return {
670
+ kind: "input",
671
+ key
672
+ };
673
+ }
674
+ //#endregion
675
+ //#region src/model/create-model.ts
676
+ function createModel() {
677
+ const inputs = [];
678
+ const rules = [];
679
+ const keyMeta = /* @__PURE__ */ new Map();
680
+ const ruleMeta = /* @__PURE__ */ new Map();
681
+ const semanticsMap = /* @__PURE__ */ new Map();
682
+ return {
683
+ input(k, meta = {}, semantics) {
684
+ inputs.push(input(k));
685
+ keyMeta.set(k.id, meta);
686
+ if (semantics) semanticsMap.set(k.id, semantics);
687
+ return k;
688
+ },
689
+ rule(r, meta = {}, semantics) {
690
+ rules.push(r);
691
+ ruleMeta.set(r.target.id, meta);
692
+ if (semantics) semanticsMap.set(r.target.id, semantics);
693
+ return r.target;
694
+ },
695
+ build() {
696
+ return {
697
+ inputs,
698
+ rules,
699
+ semantics: semanticsMap,
700
+ keyMeta,
701
+ ruleMeta
702
+ };
703
+ }
704
+ };
705
+ }
706
+ //#endregion
707
+ //#region src/model/model-graph.ts
708
+ function toGraph(model) {
709
+ return {
710
+ nodes: [...model.inputKeys, ...model.orderedRuleTargets],
711
+ edges: model.orderedRuleTargets.flatMap((to) => (model.depsByTarget.get(to) ?? []).map((from) => ({
712
+ from,
713
+ to
714
+ })))
715
+ };
716
+ }
717
+ function subgraph(model, includedKeys) {
718
+ const included = new Set(includedKeys);
719
+ const nodes = [...model.inputKeys.filter((k) => included.has(k)), ...model.orderedRuleTargets.filter((k) => included.has(k))];
720
+ const edges = [];
721
+ for (const target of model.orderedRuleTargets) {
722
+ if (!included.has(target)) continue;
723
+ const deps = model.depsByTarget.get(target) ?? [];
724
+ for (const dep of deps) if (included.has(dep)) edges.push({
725
+ from: dep,
726
+ to: target
727
+ });
728
+ }
729
+ return {
730
+ nodes,
731
+ edges
732
+ };
733
+ }
734
+ function upstreamGraphOf(model, key, options) {
735
+ const keys = new Set(upstreamOf(model, key));
736
+ if (options?.includeTarget ?? true) keys.add(key.id);
737
+ return subgraph(model, [...keys]);
738
+ }
739
+ function downstreamGraphOf(model, key, options) {
740
+ const keys = new Set(downstreamOf(model, key));
741
+ if (options?.includeTarget ?? true) keys.add(key.id);
742
+ return subgraph(model, [...keys]);
743
+ }
744
+ //#endregion
745
+ //#region src/rule/index.ts
746
+ function rule(def) {
747
+ return {
748
+ __kind: "rule",
749
+ spec: def.spec,
750
+ target: def.target,
751
+ deps: def.deps,
752
+ eval: def.eval
753
+ };
754
+ }
755
+ //#endregion
756
+ //#region src/rule/operand.ts
757
+ function resolveOperand(operand, get) {
758
+ return operand.__kind === "value" ? operand.value : get(operand);
759
+ }
760
+ //#endregion
761
+ //#region src/rule/decision.ts
762
+ function evalPredicate(p, ops, get) {
763
+ if (p.op === "in") return p.values.some((v) => ops.eq(get(p.source), resolveOperand(v, get)));
764
+ else {
765
+ const left = get(p.source);
766
+ const right = resolveOperand(p.right, get);
767
+ switch (p.op) {
768
+ case "eq": return ops.eq(get(p.source), right);
769
+ case "gt": return ops.compare(left, right) > 0;
770
+ case "gte": return ops.compare(left, right) >= 0;
771
+ case "lt": return ops.compare(left, right) < 0;
772
+ case "lte": return ops.compare(left, right) <= 0;
773
+ }
774
+ }
775
+ }
776
+ function predicateDependencies(p) {
777
+ if ("right" in p && p.right.__kind === "key") return [p.source, p.right];
778
+ return [p.source];
779
+ }
780
+ function decisionDependencies(table) {
781
+ const deps = /* @__PURE__ */ new Map();
782
+ for (const row of table.rows) for (const predicate of row.when) for (const dep of predicateDependencies(predicate)) deps.set(dep.id, dep);
783
+ if (table.default?.__kind === "key") deps.set(table.default.id, table.default);
784
+ return [...deps.values()];
785
+ }
786
+ function toSpec(ops, table) {
787
+ const spec = {
788
+ op: "decision",
789
+ opsDescriptor: {
790
+ family: ops.family,
791
+ version: ops.version
792
+ },
793
+ table: {
794
+ name: table.name,
795
+ rows: table.rows
796
+ }
797
+ };
798
+ if (table.default) spec.table.default = table.default;
799
+ return spec;
800
+ }
801
+ function decision(ops, target, table) {
802
+ return rule({
803
+ target,
804
+ spec: toSpec(ops, table),
805
+ deps: decisionDependencies(table),
806
+ eval: (get) => {
807
+ for (const row of table.rows) if (row.when.every((p) => evalPredicate(p, ops, get))) return {
808
+ output: resolveOperand(row.output, get),
809
+ detail: {
810
+ op: "decision",
811
+ tableName: table.name,
812
+ matchedRowId: row.id,
813
+ matchedRowLabel: row.label,
814
+ usedDefault: false
815
+ }
816
+ };
817
+ if (table.default === void 0) throw new Error(`No matching row found for decision table ${table.name}`);
818
+ return {
819
+ output: resolveOperand(table.default, get),
820
+ detail: {
821
+ op: "decision",
822
+ tableName: table.name,
823
+ usedDefault: true
824
+ }
825
+ };
826
+ }
827
+ });
828
+ }
829
+ //#endregion
830
+ //#region src/rule/projection.ts
831
+ function projection(target, source, field) {
832
+ return rule({
833
+ target,
834
+ spec: {
835
+ op: "project",
836
+ source: source.id,
837
+ field: String(field)
838
+ },
839
+ deps: [source],
840
+ eval: (get) => {
841
+ return { output: get(source)[field] };
842
+ }
843
+ });
844
+ }
845
+ //#endregion
846
+ //#region src/rule/ratio.ts
847
+ function ratio(ops, target, numerator, denominator) {
848
+ return rule({
849
+ target,
850
+ spec: {
851
+ op: "ratio",
852
+ opsDescriptor: {
853
+ family: ops.family,
854
+ version: ops.version
855
+ },
856
+ numerator: numerator.id,
857
+ denominator: denominator.id
858
+ },
859
+ deps: [numerator, denominator],
860
+ eval: (get) => {
861
+ return { output: ops.div(get(numerator), get(denominator)) };
862
+ }
863
+ });
864
+ }
865
+ //#endregion
866
+ //#region src/rule/scale.ts
867
+ function scale(ops, target, input, factor) {
868
+ return rule({
869
+ target,
870
+ spec: {
871
+ op: "scale",
872
+ opsDescriptor: {
873
+ family: ops.family,
874
+ version: ops.version
875
+ },
876
+ input: input.id,
877
+ factor: factor.id
878
+ },
879
+ deps: [input, factor],
880
+ eval: (get) => {
881
+ return { output: ops.scale(get(input), get(factor)) };
882
+ }
883
+ });
884
+ }
885
+ //#endregion
886
+ //#region src/rule/sum.ts
887
+ function sum(ops, target, deps) {
888
+ return rule({
889
+ target,
890
+ spec: {
891
+ op: "sum",
892
+ opsDescriptor: {
893
+ family: ops.family,
894
+ version: ops.version
895
+ },
896
+ deps: deps.map((d) => d.id)
897
+ },
898
+ deps,
899
+ eval: (get) => {
900
+ return { output: deps.reduce((acc, b) => ops.add(acc, get(b)), ops.zero()) };
901
+ }
902
+ });
903
+ }
904
+ //#endregion
905
+ //#region src/rule/weighted-sum.ts
906
+ function weightedSum(ops, target, deps) {
907
+ return rule({
908
+ target,
909
+ spec: {
910
+ op: "weighted-sum",
911
+ opsDescriptor: {
912
+ family: ops.family,
913
+ version: ops.version
914
+ },
915
+ deps: deps.map((d) => d.key.id),
916
+ weights: deps.map((d) => d.weight)
917
+ },
918
+ deps: deps.map((d) => d.key),
919
+ eval: (get) => {
920
+ return { output: deps.reduce((acc, { key, weight }) => {
921
+ const factor = resolveOperand(weight, get);
922
+ const value = get(key);
923
+ return ops.add(acc, ops.scale(value, factor));
924
+ }, ops.zero()) };
925
+ }
926
+ });
927
+ }
928
+ //#endregion
929
+ //#region src/semantics/algebra.ts
930
+ const defaultOps = {
931
+ family: "default/unknown",
932
+ version: "1",
933
+ eq: Object.is
934
+ };
935
+ const defaultNumberOps = {
936
+ family: "default/number",
937
+ version: "1",
938
+ eq: (a, b) => a === b,
939
+ compare: (a, b) => a < b ? -1 : a > b ? 1 : 0,
940
+ zero: () => 0,
941
+ add: (a, b) => a + b,
942
+ sub: (a, b) => a - b,
943
+ one: () => 1,
944
+ scale: (value, factor) => value * factor,
945
+ div: (a, b) => a / b
946
+ };
947
+ //#endregion
948
+ //#region src/value.ts
949
+ function value(value) {
950
+ return {
951
+ __kind: "value",
952
+ value
953
+ };
954
+ }
955
+ //#endregion
956
+ export { analyzeDraft, compileModel, createDraft, createModel, decision, defaultEvaluateMode, defaultNumberOps, defaultOps, diffResults, downstreamGraphOf, downstreamOf, evaluate, evaluateDraft, evaluateOverlay, explainDiffs, freezeDraftAnalysis, getDeclaredKeys, getDependencies, getDependents, inspectDiffTarget, inspectModelTarget, inspectTraceTarget, inspectionNodeToAscii, isEmptyDraft, key, projection, ratio, resolveOperand, rule, scale, sortKeysByModelOrder, subgraph, sum, toGraph, upstreamGraphOf, upstreamOf, value, weightedSum };