@dlovans/tenet-core 0.2.0 → 0.3.0
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 +41 -39
- package/dist/core/engine.d.ts +4 -1
- package/dist/core/engine.js +124 -52
- package/dist/core/operators.js +46 -15
- package/dist/core/temporal.js +40 -0
- package/dist/core/types.d.ts +111 -5
- package/dist/core/types.js +2 -2
- package/dist/core/validate.d.ts +4 -4
- package/dist/core/validate.js +41 -36
- package/dist/index.d.ts +2 -87
- package/dist/index.js +0 -3
- package/dist/validation-fixes.test.d.ts +7 -0
- package/dist/validation-fixes.test.js +399 -0
- package/package.json +2 -7
- package/dist/lint.d.ts +0 -31
- package/dist/lint.js +0 -160
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Declarative logic VM for JSON schemas. Reactive validation, temporal routing, and computed state.
|
|
4
4
|
|
|
5
|
+
**Pure TypeScript** — No WASM, no native dependencies. Works in browsers, Node.js, Deno, Bun.
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -13,11 +15,8 @@ npm install @dlovans/tenet-core
|
|
|
13
15
|
### Browser
|
|
14
16
|
|
|
15
17
|
```html
|
|
16
|
-
<script src="https://unpkg.com/@dlovans/tenet-core/wasm/wasm_exec.js"></script>
|
|
17
18
|
<script type="module">
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
await init('/path/to/tenet.wasm');
|
|
19
|
+
import { run } from '@dlovans/tenet-core';
|
|
21
20
|
|
|
22
21
|
const schema = {
|
|
23
22
|
definitions: {
|
|
@@ -33,26 +32,23 @@ const schema = {
|
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
const result = run(schema);
|
|
36
|
-
console.log(result);
|
|
35
|
+
console.log(result.result.status); // 'READY'
|
|
37
36
|
</script>
|
|
38
37
|
```
|
|
39
38
|
|
|
40
39
|
### Node.js
|
|
41
40
|
|
|
42
41
|
```javascript
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
// Initialize WASM
|
|
46
|
-
await init('./node_modules/@dlovans/tenet-core/wasm/tenet.wasm');
|
|
42
|
+
import { run, verify } from '@dlovans/tenet-core';
|
|
47
43
|
|
|
48
|
-
// Run schema logic
|
|
44
|
+
// Run schema logic - no initialization needed
|
|
49
45
|
const result = run(schema, new Date());
|
|
50
46
|
|
|
51
47
|
if (result.error) {
|
|
52
48
|
console.error(result.error);
|
|
53
49
|
} else {
|
|
54
50
|
console.log(result.result.status); // 'READY', 'INCOMPLETE', or 'INVALID'
|
|
55
|
-
console.log(result.result.errors); // Validation errors
|
|
51
|
+
console.log(result.result.errors); // Validation errors (if any)
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
// Verify transformation
|
|
@@ -62,51 +58,57 @@ console.log(verification.valid);
|
|
|
62
58
|
|
|
63
59
|
## API
|
|
64
60
|
|
|
65
|
-
### `init(wasmPath?: string): Promise<void>`
|
|
66
|
-
Initialize the WASM module. Must be called before `run()` or `verify()`.
|
|
67
|
-
|
|
68
61
|
### `run(schema, date?): TenetResult`
|
|
69
62
|
Execute the schema logic for the given effective date.
|
|
70
63
|
|
|
64
|
+
- `schema` — TenetSchema object or JSON string
|
|
65
|
+
- `date` — Effective date for temporal routing (default: now)
|
|
66
|
+
|
|
67
|
+
Returns `{ result: TenetSchema }` or `{ error: string }`.
|
|
68
|
+
|
|
71
69
|
### `verify(newSchema, oldSchema): TenetVerifyResult`
|
|
72
70
|
Verify that a transformation is legal by replaying the logic.
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
Static analysis - find issues without executing the schema.
|
|
72
|
+
Returns `{ valid: boolean, error?: string }`.
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
### `isReady(): boolean`
|
|
75
|
+
Always returns `true`. Kept for backwards compatibility.
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
### `init(): Promise<void>` *(deprecated)*
|
|
78
|
+
No-op. Kept for backwards compatibility with v0.1.x.
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
for (const issue of result.issues) {
|
|
85
|
-
console.log(`${issue.severity}: ${issue.message}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
80
|
+
## Runtime Validation
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
Check if an object is a Tenet schema.
|
|
82
|
+
The VM automatically detects and reports:
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
- **Undefined variables** — `{"var": "unknown_field"}`
|
|
85
|
+
- **Unknown operators** — `{"invalid_op": [...]}`
|
|
86
|
+
- **Temporal conflicts** — Overlapping date ranges, same start/end dates
|
|
95
87
|
|
|
96
|
-
|
|
88
|
+
All errors are returned in `result.errors` without failing execution.
|
|
97
89
|
|
|
98
|
-
|
|
90
|
+
## TypeScript
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
92
|
+
Full type definitions included:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import type { TenetSchema, TenetResult, Definition, Rule } from '@dlovans/tenet-core';
|
|
105
96
|
```
|
|
106
97
|
|
|
107
|
-
##
|
|
98
|
+
## Migration from v0.1.x
|
|
108
99
|
|
|
109
|
-
|
|
100
|
+
```javascript
|
|
101
|
+
// Before (v0.1.x with WASM)
|
|
102
|
+
import { init, run, lint } from '@dlovans/tenet-core';
|
|
103
|
+
await init('./tenet.wasm');
|
|
104
|
+
const result = run(schema);
|
|
105
|
+
const issues = lint(schema);
|
|
106
|
+
|
|
107
|
+
// After (v0.2.x pure TypeScript)
|
|
108
|
+
import { run } from '@dlovans/tenet-core';
|
|
109
|
+
const result = run(schema);
|
|
110
|
+
// Validation errors are now in result.result.errors
|
|
111
|
+
```
|
|
110
112
|
|
|
111
113
|
## License
|
|
112
114
|
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -15,9 +15,12 @@ export declare function run(schema: TenetSchema | string, effectiveDate?: Date |
|
|
|
15
15
|
* Verify that a completed document was correctly derived from a base schema.
|
|
16
16
|
* Simulates the user's journey by iteratively copying visible field values and re-running.
|
|
17
17
|
*
|
|
18
|
+
* Returns a structured result with all issues found (not just the first).
|
|
19
|
+
* Crash-safe: catches any unexpected error and returns it as an internal_error issue.
|
|
20
|
+
*
|
|
18
21
|
* @param newSchema - The completed/submitted schema
|
|
19
22
|
* @param oldSchema - The original base schema
|
|
20
23
|
* @param maxIterations - Maximum replay iterations (default: 100)
|
|
21
|
-
* @returns
|
|
24
|
+
* @returns Structured verification result
|
|
22
25
|
*/
|
|
23
26
|
export declare function verify(newSchema: TenetSchema | string, oldSchema: TenetSchema | string, maxIterations?: number): TenetVerifyResult;
|
package/dist/core/engine.js
CHANGED
|
@@ -80,7 +80,7 @@ function setDefinitionValue(state, key, value, ruleId) {
|
|
|
80
80
|
// Cycle detection
|
|
81
81
|
const prevRule = state.fieldsSet.get(key);
|
|
82
82
|
if (prevRule && prevRule !== ruleId) {
|
|
83
|
-
addError(state, key, ruleId, `Potential cycle: field '${key}' set by rule '${prevRule}' and again by rule '${ruleId}'`);
|
|
83
|
+
addError(state, key, ruleId, 'cycle_detected', `Potential cycle: field '${key}' set by rule '${prevRule}' and again by rule '${ruleId}'`);
|
|
84
84
|
}
|
|
85
85
|
state.fieldsSet.set(key, ruleId);
|
|
86
86
|
const def = state.schema.definitions[key];
|
|
@@ -118,7 +118,7 @@ function applyAction(state, action, ruleId, lawRef) {
|
|
|
118
118
|
}
|
|
119
119
|
// Emit error if specified
|
|
120
120
|
if (action.error_msg) {
|
|
121
|
-
addError(state, '', ruleId, action.error_msg, lawRef);
|
|
121
|
+
addError(state, '', ruleId, 'runtime_warning', action.error_msg, lawRef);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
@@ -152,13 +152,23 @@ function computeDerived(state) {
|
|
|
152
152
|
}
|
|
153
153
|
// Evaluate the expression
|
|
154
154
|
const value = resolve(derivedDef.eval, state);
|
|
155
|
-
//
|
|
156
|
-
state.schema.definitions[name]
|
|
157
|
-
|
|
158
|
-
value
|
|
159
|
-
readonly
|
|
160
|
-
visible
|
|
161
|
-
|
|
155
|
+
// Preserve existing definition metadata if present
|
|
156
|
+
const existing = state.schema.definitions[name];
|
|
157
|
+
if (existing) {
|
|
158
|
+
existing.value = value;
|
|
159
|
+
existing.readonly = true;
|
|
160
|
+
if (existing.visible === undefined) {
|
|
161
|
+
existing.visible = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
state.schema.definitions[name] = {
|
|
166
|
+
type: inferType(value),
|
|
167
|
+
value,
|
|
168
|
+
readonly: true,
|
|
169
|
+
visible: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
162
172
|
}
|
|
163
173
|
}
|
|
164
174
|
/**
|
|
@@ -190,20 +200,23 @@ export function run(schema, effectiveDate = new Date()) {
|
|
|
190
200
|
effectiveDate: date,
|
|
191
201
|
fieldsSet: new Map(),
|
|
192
202
|
errors: [],
|
|
203
|
+
derivedInProgress: new Set(),
|
|
193
204
|
};
|
|
194
205
|
// 1. Select temporal branch and prune inactive rules
|
|
195
206
|
applyTemporalRouting(state);
|
|
196
|
-
// 2.
|
|
207
|
+
// 2. Compute derived state (so logic tree can use derived values)
|
|
208
|
+
computeDerived(state);
|
|
209
|
+
// 3. Evaluate logic tree
|
|
197
210
|
evaluateLogicTree(state);
|
|
198
|
-
//
|
|
211
|
+
// 4. Re-compute derived state (in case logic modified inputs)
|
|
199
212
|
computeDerived(state);
|
|
200
|
-
//
|
|
213
|
+
// 5. Validate definitions
|
|
201
214
|
validateDefinitions(state);
|
|
202
|
-
//
|
|
215
|
+
// 6. Check attestations
|
|
203
216
|
checkAttestations(state, (action, ruleId, lawRef) => {
|
|
204
217
|
applyAction(state, action, ruleId, lawRef);
|
|
205
218
|
});
|
|
206
|
-
//
|
|
219
|
+
// 7. Determine status and attach errors
|
|
207
220
|
state.schema.errors = state.errors.length > 0 ? state.errors : undefined;
|
|
208
221
|
state.schema.status = determineStatus(state);
|
|
209
222
|
return { result: state.schema };
|
|
@@ -225,25 +238,29 @@ function getVisibleEditableFields(schema) {
|
|
|
225
238
|
return result;
|
|
226
239
|
}
|
|
227
240
|
/**
|
|
228
|
-
*
|
|
241
|
+
* Get a sorted, comma-joined string of visible field IDs for convergence detection.
|
|
229
242
|
*/
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
return count;
|
|
243
|
+
function getVisibleFieldIds(schema) {
|
|
244
|
+
return Object.entries(schema.definitions)
|
|
245
|
+
.filter(([, def]) => def?.visible)
|
|
246
|
+
.map(([id]) => id)
|
|
247
|
+
.sort()
|
|
248
|
+
.join(',');
|
|
238
249
|
}
|
|
239
250
|
/**
|
|
240
251
|
* Validate that the final state matches expected values.
|
|
252
|
+
* Collects ALL issues instead of bailing on the first — the UI needs the complete picture.
|
|
241
253
|
*/
|
|
242
254
|
function validateFinalState(newSchema, resultSchema) {
|
|
255
|
+
const issues = [];
|
|
243
256
|
// Check for unknown/injected fields in newSchema that don't exist in result
|
|
244
257
|
for (const id of Object.keys(newSchema.definitions)) {
|
|
245
258
|
if (!(id in resultSchema.definitions)) {
|
|
246
|
-
|
|
259
|
+
issues.push({
|
|
260
|
+
code: 'unknown_field',
|
|
261
|
+
field_id: id,
|
|
262
|
+
message: `Field '${id}' does not exist in the schema`,
|
|
263
|
+
});
|
|
247
264
|
}
|
|
248
265
|
}
|
|
249
266
|
// Compare computed (readonly) values
|
|
@@ -253,49 +270,85 @@ function validateFinalState(newSchema, resultSchema) {
|
|
|
253
270
|
}
|
|
254
271
|
const newDef = newSchema.definitions[id];
|
|
255
272
|
if (!newDef) {
|
|
256
|
-
|
|
273
|
+
issues.push({
|
|
274
|
+
code: 'computed_mismatch',
|
|
275
|
+
field_id: id,
|
|
276
|
+
message: `Computed field '${id}' is missing from the submitted document`,
|
|
277
|
+
expected: resultDef.value,
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
257
280
|
}
|
|
258
281
|
if (!compareEqual(newDef.value, resultDef.value)) {
|
|
259
|
-
|
|
282
|
+
issues.push({
|
|
283
|
+
code: 'computed_mismatch',
|
|
284
|
+
field_id: id,
|
|
285
|
+
message: `Computed field '${id}' was modified`,
|
|
286
|
+
expected: resultDef.value,
|
|
287
|
+
claimed: newDef.value,
|
|
288
|
+
});
|
|
260
289
|
}
|
|
261
290
|
}
|
|
262
291
|
// Verify attestations are fulfilled
|
|
263
292
|
if (resultSchema.attestations) {
|
|
264
293
|
for (const [id, resultAtt] of Object.entries(resultSchema.attestations)) {
|
|
265
|
-
if (!resultAtt) {
|
|
294
|
+
if (!resultAtt?.required) {
|
|
266
295
|
continue;
|
|
267
296
|
}
|
|
268
297
|
const newAtt = newSchema.attestations?.[id];
|
|
269
298
|
if (!newAtt) {
|
|
270
299
|
continue;
|
|
271
300
|
}
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
301
|
+
if (!newAtt.signed) {
|
|
302
|
+
issues.push({
|
|
303
|
+
code: 'attestation_unsigned',
|
|
304
|
+
field_id: id,
|
|
305
|
+
message: `Required attestation '${id}' has not been signed`,
|
|
306
|
+
});
|
|
307
|
+
continue; // No point checking evidence if unsigned
|
|
308
|
+
}
|
|
309
|
+
if (!newAtt.evidence?.provider_audit_id) {
|
|
310
|
+
issues.push({
|
|
311
|
+
code: 'attestation_no_evidence',
|
|
312
|
+
field_id: id,
|
|
313
|
+
message: `Attestation '${id}' is signed but missing proof of signing`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (!newAtt.evidence?.timestamp) {
|
|
317
|
+
issues.push({
|
|
318
|
+
code: 'attestation_no_timestamp',
|
|
319
|
+
field_id: id,
|
|
320
|
+
message: `Attestation '${id}' is signed but missing a timestamp`,
|
|
321
|
+
});
|
|
282
322
|
}
|
|
283
323
|
}
|
|
284
324
|
}
|
|
285
325
|
// Verify status matches
|
|
286
326
|
if (newSchema.status !== resultSchema.status) {
|
|
287
|
-
|
|
327
|
+
issues.push({
|
|
328
|
+
code: 'status_mismatch',
|
|
329
|
+
message: 'The document status does not match what was computed',
|
|
330
|
+
expected: resultSchema.status,
|
|
331
|
+
claimed: newSchema.status,
|
|
332
|
+
});
|
|
288
333
|
}
|
|
289
|
-
return
|
|
334
|
+
return {
|
|
335
|
+
valid: issues.length === 0,
|
|
336
|
+
status: resultSchema.status,
|
|
337
|
+
issues: issues.length > 0 ? issues : undefined,
|
|
338
|
+
schema: resultSchema,
|
|
339
|
+
};
|
|
290
340
|
}
|
|
291
341
|
/**
|
|
292
342
|
* Verify that a completed document was correctly derived from a base schema.
|
|
293
343
|
* Simulates the user's journey by iteratively copying visible field values and re-running.
|
|
294
344
|
*
|
|
345
|
+
* Returns a structured result with all issues found (not just the first).
|
|
346
|
+
* Crash-safe: catches any unexpected error and returns it as an internal_error issue.
|
|
347
|
+
*
|
|
295
348
|
* @param newSchema - The completed/submitted schema
|
|
296
349
|
* @param oldSchema - The original base schema
|
|
297
350
|
* @param maxIterations - Maximum replay iterations (default: 100)
|
|
298
|
-
* @returns
|
|
351
|
+
* @returns Structured verification result
|
|
299
352
|
*/
|
|
300
353
|
export function verify(newSchema, oldSchema, maxIterations = 100) {
|
|
301
354
|
try {
|
|
@@ -316,7 +369,7 @@ export function verify(newSchema, oldSchema, maxIterations = 100) {
|
|
|
316
369
|
}
|
|
317
370
|
// Start with base schema
|
|
318
371
|
let currentSchema = parsedOldSchema;
|
|
319
|
-
let
|
|
372
|
+
let previousVisibleIds = '';
|
|
320
373
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
321
374
|
// Count visible editable fields before copying
|
|
322
375
|
const visibleEditable = getVisibleEditableFields(currentSchema);
|
|
@@ -343,23 +396,42 @@ export function verify(newSchema, oldSchema, maxIterations = 100) {
|
|
|
343
396
|
// Run the schema
|
|
344
397
|
const runResult = run(currentSchema, effectiveDate);
|
|
345
398
|
if (runResult.error) {
|
|
346
|
-
return {
|
|
399
|
+
return {
|
|
400
|
+
valid: false,
|
|
401
|
+
issues: [{
|
|
402
|
+
code: 'internal_error',
|
|
403
|
+
message: `VM run failed at iteration ${iteration}`,
|
|
404
|
+
}],
|
|
405
|
+
error: `Run failed (iteration ${iteration}): ${runResult.error}`,
|
|
406
|
+
};
|
|
347
407
|
}
|
|
348
408
|
const resultSchema = runResult.result;
|
|
349
|
-
//
|
|
350
|
-
const
|
|
351
|
-
// Check for convergence
|
|
352
|
-
if (
|
|
353
|
-
// Converged - now validate the final state
|
|
354
|
-
|
|
355
|
-
return { valid, error };
|
|
409
|
+
// Get visible field IDs after run
|
|
410
|
+
const currentVisibleIds = getVisibleFieldIds(resultSchema);
|
|
411
|
+
// Check for convergence using set comparison
|
|
412
|
+
if (currentVisibleIds === previousVisibleIds) {
|
|
413
|
+
// Converged - now validate the final state and return full result
|
|
414
|
+
return validateFinalState(parsedNewSchema, resultSchema);
|
|
356
415
|
}
|
|
357
|
-
|
|
416
|
+
previousVisibleIds = currentVisibleIds;
|
|
358
417
|
currentSchema = resultSchema;
|
|
359
418
|
}
|
|
360
|
-
return {
|
|
419
|
+
return {
|
|
420
|
+
valid: false,
|
|
421
|
+
issues: [{
|
|
422
|
+
code: 'convergence_failed',
|
|
423
|
+
message: `Document did not converge after ${maxIterations} iterations`,
|
|
424
|
+
}],
|
|
425
|
+
};
|
|
361
426
|
}
|
|
362
427
|
catch (error) {
|
|
363
|
-
return {
|
|
428
|
+
return {
|
|
429
|
+
valid: false,
|
|
430
|
+
issues: [{
|
|
431
|
+
code: 'internal_error',
|
|
432
|
+
message: `Unexpected error during verification`,
|
|
433
|
+
}],
|
|
434
|
+
error: String(error),
|
|
435
|
+
};
|
|
364
436
|
}
|
|
365
437
|
}
|
package/dist/core/operators.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* JSON-logic operators for the Tenet VM.
|
|
3
3
|
* All operators are nil-safe: operations on nil/undefined return appropriate defaults.
|
|
4
4
|
*/
|
|
5
|
+
import { addError } from './validate.js';
|
|
5
6
|
/**
|
|
6
7
|
* Convert a value to a number if possible.
|
|
7
8
|
*/
|
|
@@ -99,17 +100,56 @@ function resolveArgs(args, expected, resolve, state) {
|
|
|
99
100
|
}
|
|
100
101
|
return result;
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if a variable name is defined in the schema.
|
|
105
|
+
*/
|
|
106
|
+
function isVariableDefined(name, state) {
|
|
107
|
+
// Check definitions
|
|
108
|
+
if (state.schema.definitions[name]) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Check derived state
|
|
112
|
+
if (state.schema.state_model?.derived?.[name]) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
102
117
|
/**
|
|
103
118
|
* Get variable value from schema definitions or current element context.
|
|
119
|
+
* Now accepts an optional resolve function to evaluate derived expressions on-demand.
|
|
104
120
|
*/
|
|
105
|
-
function getVar(path, state) {
|
|
121
|
+
function getVar(path, state, resolve) {
|
|
106
122
|
// Empty path returns current element context (for some/all/none)
|
|
107
123
|
if (path === '') {
|
|
108
124
|
return state.currentElement;
|
|
109
125
|
}
|
|
110
126
|
const parts = path.split('.');
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
const rootVar = parts[0];
|
|
128
|
+
// Check if variable is defined (only for root-level vars, not nested access)
|
|
129
|
+
if (!isVariableDefined(rootVar, state) && state.currentElement === undefined) {
|
|
130
|
+
addError(state, '', '', 'runtime_warning', `Undefined variable '${rootVar}' in logic expression`);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// First, check derived state (derived values take precedence)
|
|
134
|
+
if (state.schema.state_model?.derived && resolve) {
|
|
135
|
+
const derived = state.schema.state_model.derived[rootVar];
|
|
136
|
+
if (derived?.eval) {
|
|
137
|
+
// Cycle detection for derived fields
|
|
138
|
+
if (state.derivedInProgress.has(rootVar)) {
|
|
139
|
+
addError(state, '', '', 'cycle_detected', `Circular dependency detected in derived field '${rootVar}'`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
state.derivedInProgress.add(rootVar);
|
|
143
|
+
const result = resolve(derived.eval, state);
|
|
144
|
+
state.derivedInProgress.delete(rootVar);
|
|
145
|
+
if (parts.length === 1) {
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
return accessPath(result, parts.slice(1));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Then, check definitions
|
|
152
|
+
const def = state.schema.definitions[rootVar];
|
|
113
153
|
if (def) {
|
|
114
154
|
if (parts.length === 1) {
|
|
115
155
|
return def.value;
|
|
@@ -117,15 +157,6 @@ function getVar(path, state) {
|
|
|
117
157
|
// Nested access into the value
|
|
118
158
|
return accessPath(def.value, parts.slice(1));
|
|
119
159
|
}
|
|
120
|
-
// Check derived state
|
|
121
|
-
if (state.schema.state_model?.derived) {
|
|
122
|
-
const derived = state.schema.state_model.derived[parts[0]];
|
|
123
|
-
if (derived) {
|
|
124
|
-
// Note: derived values should already be computed by this point
|
|
125
|
-
// This is a fallback for direct access
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
160
|
return undefined;
|
|
130
161
|
}
|
|
131
162
|
/**
|
|
@@ -147,9 +178,9 @@ function accessPath(value, parts) {
|
|
|
147
178
|
// ============================================================
|
|
148
179
|
const operators = {
|
|
149
180
|
// === Variable Access ===
|
|
150
|
-
'var': (args, state) => {
|
|
181
|
+
'var': (args, state, resolve) => {
|
|
151
182
|
const path = typeof args === 'string' ? args : '';
|
|
152
|
-
return getVar(path, state);
|
|
183
|
+
return getVar(path, state, resolve);
|
|
153
184
|
},
|
|
154
185
|
// === Comparison Operators ===
|
|
155
186
|
'==': (args, state, resolve) => {
|
|
@@ -417,7 +448,7 @@ const operators = {
|
|
|
417
448
|
export function applyOperator(op, args, state, resolve) {
|
|
418
449
|
const fn = operators[op];
|
|
419
450
|
if (!fn) {
|
|
420
|
-
|
|
451
|
+
addError(state, '', '', 'runtime_warning', `Unknown operator '${op}' in logic expression`);
|
|
421
452
|
return null;
|
|
422
453
|
}
|
|
423
454
|
return fn(args, state, resolve);
|
package/dist/core/temporal.js
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
* Temporal branch selection and rule pruning.
|
|
3
3
|
* Routes logic based on effective dates for bitemporal support.
|
|
4
4
|
*/
|
|
5
|
+
import { addError } from './validate.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validate temporal_map for configuration errors.
|
|
8
|
+
* Checks for same start/end dates and overlapping ranges.
|
|
9
|
+
*/
|
|
10
|
+
function validateTemporalMap(state) {
|
|
11
|
+
const { schema } = state;
|
|
12
|
+
if (!schema.temporal_map || schema.temporal_map.length === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (let i = 0; i < schema.temporal_map.length; i++) {
|
|
16
|
+
const branch = schema.temporal_map[i];
|
|
17
|
+
if (!branch) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const start = branch.valid_range[0];
|
|
21
|
+
const end = branch.valid_range[1];
|
|
22
|
+
// Check for same start/end date (invalid zero-length range)
|
|
23
|
+
if (start && end && start === end) {
|
|
24
|
+
addError(state, '', '', 'runtime_warning', `Temporal branch ${i} has same start and end date '${start}' (invalid range)`);
|
|
25
|
+
}
|
|
26
|
+
// Check for overlapping with previous branch
|
|
27
|
+
if (i > 0) {
|
|
28
|
+
const prev = schema.temporal_map[i - 1];
|
|
29
|
+
if (prev) {
|
|
30
|
+
const prevEnd = prev.valid_range[1]
|
|
31
|
+
? new Date(prev.valid_range[1]).getTime()
|
|
32
|
+
: Infinity;
|
|
33
|
+
const currStart = start
|
|
34
|
+
? new Date(start).getTime()
|
|
35
|
+
: -Infinity;
|
|
36
|
+
if (currStart <= prevEnd) {
|
|
37
|
+
addError(state, '', '', 'runtime_warning', `Temporal branch ${i} overlaps with branch ${i - 1} (ranges must not overlap)`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
5
43
|
/**
|
|
6
44
|
* Find the active temporal branch for a given effective date.
|
|
7
45
|
* Returns undefined if no branch matches (uses default/unversioned logic).
|
|
@@ -62,6 +100,8 @@ export function pruneRules(state, activeBranch) {
|
|
|
62
100
|
* Call this at the start of Run().
|
|
63
101
|
*/
|
|
64
102
|
export function applyTemporalRouting(state) {
|
|
103
|
+
// Validate temporal_map configuration
|
|
104
|
+
validateTemporalMap(state);
|
|
65
105
|
const branch = selectBranch(state);
|
|
66
106
|
if (branch) {
|
|
67
107
|
pruneRules(state, branch);
|