@dlovans/tenet-core 0.1.3 → 0.2.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.
@@ -0,0 +1,424 @@
1
+ /**
2
+ * JSON-logic operators for the Tenet VM.
3
+ * All operators are nil-safe: operations on nil/undefined return appropriate defaults.
4
+ */
5
+ /**
6
+ * Convert a value to a number if possible.
7
+ */
8
+ export function toFloat(v) {
9
+ if (v === null || v === undefined) {
10
+ return [0, false];
11
+ }
12
+ if (typeof v === 'number') {
13
+ return [v, true];
14
+ }
15
+ if (typeof v === 'string') {
16
+ // Don't auto-convert strings to numbers
17
+ return [0, false];
18
+ }
19
+ return [0, false];
20
+ }
21
+ /**
22
+ * Parse a date value (string or Date).
23
+ * Supports ISO 8601 formats.
24
+ */
25
+ export function parseDate(v) {
26
+ if (v === null || v === undefined) {
27
+ return [new Date(0), false];
28
+ }
29
+ if (v instanceof Date) {
30
+ return [v, true];
31
+ }
32
+ if (typeof v === 'string') {
33
+ const d = new Date(v);
34
+ if (!isNaN(d.getTime())) {
35
+ return [d, true];
36
+ }
37
+ }
38
+ return [new Date(0), false];
39
+ }
40
+ /**
41
+ * Determine if a value is "truthy" in JSON-logic terms.
42
+ * nil, false, 0, and "" are falsy. Everything else is truthy.
43
+ */
44
+ export function isTruthy(value) {
45
+ if (value === null || value === undefined) {
46
+ return false;
47
+ }
48
+ if (typeof value === 'boolean') {
49
+ return value;
50
+ }
51
+ if (typeof value === 'number') {
52
+ return value !== 0;
53
+ }
54
+ if (typeof value === 'string') {
55
+ return value !== '';
56
+ }
57
+ if (Array.isArray(value)) {
58
+ return value.length > 0;
59
+ }
60
+ if (typeof value === 'object') {
61
+ return Object.keys(value).length > 0;
62
+ }
63
+ return true;
64
+ }
65
+ /**
66
+ * Compare two values for equality with type coercion.
67
+ * nil == nil is true, nil == anything_else is false.
68
+ */
69
+ export function compareEqual(a, b) {
70
+ if (a === null || a === undefined) {
71
+ return b === null || b === undefined;
72
+ }
73
+ if (b === null || b === undefined) {
74
+ return false;
75
+ }
76
+ // Try numeric comparison if both can be numbers
77
+ const [aNum, aOk] = toFloat(a);
78
+ const [bNum, bOk] = toFloat(b);
79
+ if (aOk && bOk) {
80
+ return aNum === bNum;
81
+ }
82
+ // String comparison
83
+ return String(a) === String(b);
84
+ }
85
+ /**
86
+ * Resolve arguments array, handling single values and missing args.
87
+ */
88
+ function resolveArgs(args, expected, resolve, state) {
89
+ const result = new Array(expected).fill(null);
90
+ if (!Array.isArray(args)) {
91
+ // Single value case (e.g., {"not": true})
92
+ if (expected > 0) {
93
+ result[0] = resolve(args, state);
94
+ }
95
+ return result;
96
+ }
97
+ for (let i = 0; i < expected && i < args.length; i++) {
98
+ result[i] = resolve(args[i], state);
99
+ }
100
+ return result;
101
+ }
102
+ /**
103
+ * Get variable value from schema definitions or current element context.
104
+ */
105
+ function getVar(path, state) {
106
+ // Empty path returns current element context (for some/all/none)
107
+ if (path === '') {
108
+ return state.currentElement;
109
+ }
110
+ const parts = path.split('.');
111
+ // Check definitions first
112
+ const def = state.schema.definitions[parts[0]];
113
+ if (def) {
114
+ if (parts.length === 1) {
115
+ return def.value;
116
+ }
117
+ // Nested access into the value
118
+ return accessPath(def.value, parts.slice(1));
119
+ }
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
+ return undefined;
130
+ }
131
+ /**
132
+ * Access nested path in an object.
133
+ */
134
+ function accessPath(value, parts) {
135
+ if (parts.length === 0 || value === null || value === undefined) {
136
+ return value;
137
+ }
138
+ if (typeof value === 'object' && !Array.isArray(value)) {
139
+ const obj = value;
140
+ const next = obj[parts[0]];
141
+ return accessPath(next, parts.slice(1));
142
+ }
143
+ return undefined;
144
+ }
145
+ // ============================================================
146
+ // Operator implementations
147
+ // ============================================================
148
+ const operators = {
149
+ // === Variable Access ===
150
+ 'var': (args, state) => {
151
+ const path = typeof args === 'string' ? args : '';
152
+ return getVar(path, state);
153
+ },
154
+ // === Comparison Operators ===
155
+ '==': (args, state, resolve) => {
156
+ const a = resolveArgs(args, 2, resolve, state);
157
+ return compareEqual(a[0], a[1]);
158
+ },
159
+ '!=': (args, state, resolve) => {
160
+ const a = resolveArgs(args, 2, resolve, state);
161
+ return !compareEqual(a[0], a[1]);
162
+ },
163
+ '>': (args, state, resolve) => {
164
+ const a = resolveArgs(args, 2, resolve, state);
165
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
166
+ return false;
167
+ }
168
+ const [aNum, aOk] = toFloat(a[0]);
169
+ const [bNum, bOk] = toFloat(a[1]);
170
+ return aOk && bOk && aNum > bNum;
171
+ },
172
+ '<': (args, state, resolve) => {
173
+ const a = resolveArgs(args, 2, resolve, state);
174
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
175
+ return false;
176
+ }
177
+ const [aNum, aOk] = toFloat(a[0]);
178
+ const [bNum, bOk] = toFloat(a[1]);
179
+ return aOk && bOk && aNum < bNum;
180
+ },
181
+ '>=': (args, state, resolve) => {
182
+ const a = resolveArgs(args, 2, resolve, state);
183
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
184
+ return false;
185
+ }
186
+ const [aNum, aOk] = toFloat(a[0]);
187
+ const [bNum, bOk] = toFloat(a[1]);
188
+ return aOk && bOk && aNum >= bNum;
189
+ },
190
+ '<=': (args, state, resolve) => {
191
+ const a = resolveArgs(args, 2, resolve, state);
192
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
193
+ return false;
194
+ }
195
+ const [aNum, aOk] = toFloat(a[0]);
196
+ const [bNum, bOk] = toFloat(a[1]);
197
+ return aOk && bOk && aNum <= bNum;
198
+ },
199
+ // === Logical Operators ===
200
+ 'and': (args, state, resolve) => {
201
+ if (!Array.isArray(args)) {
202
+ return isTruthy(resolve(args, state));
203
+ }
204
+ for (const arg of args) {
205
+ if (!isTruthy(resolve(arg, state))) {
206
+ return false;
207
+ }
208
+ }
209
+ return true;
210
+ },
211
+ 'or': (args, state, resolve) => {
212
+ if (!Array.isArray(args)) {
213
+ return isTruthy(resolve(args, state));
214
+ }
215
+ for (const arg of args) {
216
+ if (isTruthy(resolve(arg, state))) {
217
+ return true;
218
+ }
219
+ }
220
+ return false;
221
+ },
222
+ 'not': (args, state, resolve) => {
223
+ const a = resolveArgs(args, 1, resolve, state);
224
+ return !isTruthy(a[0]);
225
+ },
226
+ '!': (args, state, resolve) => {
227
+ const a = resolveArgs(args, 1, resolve, state);
228
+ return !isTruthy(a[0]);
229
+ },
230
+ 'if': (args, state, resolve) => {
231
+ if (!Array.isArray(args) || args.length < 2) {
232
+ return null;
233
+ }
234
+ // Process condition-then pairs
235
+ for (let i = 0; i + 1 < args.length; i += 2) {
236
+ const condition = resolve(args[i], state);
237
+ if (isTruthy(condition)) {
238
+ return resolve(args[i + 1], state);
239
+ }
240
+ }
241
+ // Else clause (odd number of elements = has else)
242
+ if (args.length % 2 === 1) {
243
+ return resolve(args[args.length - 1], state);
244
+ }
245
+ return null;
246
+ },
247
+ // === Arithmetic Operators ===
248
+ '+': (args, state, resolve) => {
249
+ const a = resolveArgs(args, 2, resolve, state);
250
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
251
+ return null;
252
+ }
253
+ const [aNum, aOk] = toFloat(a[0]);
254
+ const [bNum, bOk] = toFloat(a[1]);
255
+ if (!aOk || !bOk) {
256
+ return null;
257
+ }
258
+ return aNum + bNum;
259
+ },
260
+ '-': (args, state, resolve) => {
261
+ const a = resolveArgs(args, 2, resolve, state);
262
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
263
+ return null;
264
+ }
265
+ const [aNum, aOk] = toFloat(a[0]);
266
+ const [bNum, bOk] = toFloat(a[1]);
267
+ if (!aOk || !bOk) {
268
+ return null;
269
+ }
270
+ return aNum - bNum;
271
+ },
272
+ '*': (args, state, resolve) => {
273
+ const a = resolveArgs(args, 2, resolve, state);
274
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
275
+ return null;
276
+ }
277
+ const [aNum, aOk] = toFloat(a[0]);
278
+ const [bNum, bOk] = toFloat(a[1]);
279
+ if (!aOk || !bOk) {
280
+ return null;
281
+ }
282
+ return aNum * bNum;
283
+ },
284
+ '/': (args, state, resolve) => {
285
+ const a = resolveArgs(args, 2, resolve, state);
286
+ if (a[0] === null || a[0] === undefined || a[1] === null || a[1] === undefined) {
287
+ return null;
288
+ }
289
+ const [aNum, aOk] = toFloat(a[0]);
290
+ const [bNum, bOk] = toFloat(a[1]);
291
+ if (!aOk || !bOk || bNum === 0) {
292
+ return null;
293
+ }
294
+ return aNum / bNum;
295
+ },
296
+ // === Date Operators ===
297
+ 'before': (args, state, resolve) => {
298
+ const a = resolveArgs(args, 2, resolve, state);
299
+ const [aDate, aOk] = parseDate(a[0]);
300
+ const [bDate, bOk] = parseDate(a[1]);
301
+ if (!aOk || !bOk) {
302
+ return false;
303
+ }
304
+ return aDate.getTime() < bDate.getTime();
305
+ },
306
+ 'after': (args, state, resolve) => {
307
+ const a = resolveArgs(args, 2, resolve, state);
308
+ const [aDate, aOk] = parseDate(a[0]);
309
+ const [bDate, bOk] = parseDate(a[1]);
310
+ if (!aOk || !bOk) {
311
+ return false;
312
+ }
313
+ return aDate.getTime() > bDate.getTime();
314
+ },
315
+ // === Collection Operators ===
316
+ 'in': (args, state, resolve) => {
317
+ const a = resolveArgs(args, 2, resolve, state);
318
+ const needle = a[0];
319
+ const haystack = a[1];
320
+ if (needle === null || needle === undefined || haystack === null || haystack === undefined) {
321
+ return false;
322
+ }
323
+ if (Array.isArray(haystack)) {
324
+ for (const item of haystack) {
325
+ if (compareEqual(needle, item)) {
326
+ return true;
327
+ }
328
+ }
329
+ return false;
330
+ }
331
+ if (typeof haystack === 'string' && typeof needle === 'string') {
332
+ return haystack.includes(needle);
333
+ }
334
+ return false;
335
+ },
336
+ 'some': (args, state, resolve) => {
337
+ if (!Array.isArray(args) || args.length < 2) {
338
+ return false;
339
+ }
340
+ // First arg is the array (resolve it)
341
+ const collection = resolve(args[0], state);
342
+ if (!Array.isArray(collection) || collection.length === 0) {
343
+ return false;
344
+ }
345
+ // Second arg is the condition
346
+ const condition = args[1];
347
+ // Check if any element satisfies the condition
348
+ for (const item of collection) {
349
+ const oldContext = state.currentElement;
350
+ state.currentElement = item;
351
+ const result = isTruthy(resolve(condition, state));
352
+ state.currentElement = oldContext;
353
+ if (result) {
354
+ return true;
355
+ }
356
+ }
357
+ return false;
358
+ },
359
+ 'all': (args, state, resolve) => {
360
+ if (!Array.isArray(args) || args.length < 2) {
361
+ return false;
362
+ }
363
+ // First arg is the array (resolve it)
364
+ const collection = resolve(args[0], state);
365
+ if (!Array.isArray(collection)) {
366
+ return false;
367
+ }
368
+ // Empty array returns true for "all" (vacuous truth)
369
+ if (collection.length === 0) {
370
+ return true;
371
+ }
372
+ // Second arg is the condition
373
+ const condition = args[1];
374
+ // Check if all elements satisfy the condition
375
+ for (const item of collection) {
376
+ const oldContext = state.currentElement;
377
+ state.currentElement = item;
378
+ const result = isTruthy(resolve(condition, state));
379
+ state.currentElement = oldContext;
380
+ if (!result) {
381
+ return false;
382
+ }
383
+ }
384
+ return true;
385
+ },
386
+ 'none': (args, state, resolve) => {
387
+ if (!Array.isArray(args) || args.length < 2) {
388
+ return false;
389
+ }
390
+ // First arg is the array (resolve it)
391
+ const collection = resolve(args[0], state);
392
+ if (!Array.isArray(collection)) {
393
+ return false;
394
+ }
395
+ // Empty array returns true for "none"
396
+ if (collection.length === 0) {
397
+ return true;
398
+ }
399
+ // Second arg is the condition
400
+ const condition = args[1];
401
+ // Check that no element satisfies the condition
402
+ for (const item of collection) {
403
+ const oldContext = state.currentElement;
404
+ state.currentElement = item;
405
+ const result = isTruthy(resolve(condition, state));
406
+ state.currentElement = oldContext;
407
+ if (result) {
408
+ return false;
409
+ }
410
+ }
411
+ return true;
412
+ },
413
+ };
414
+ /**
415
+ * Apply an operator to its arguments.
416
+ */
417
+ export function applyOperator(op, args, state, resolve) {
418
+ const fn = operators[op];
419
+ if (!fn) {
420
+ // Unknown operator - return null
421
+ return null;
422
+ }
423
+ return fn(args, state, resolve);
424
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * JSON-logic expression resolver.
3
+ * Recursively evaluates JSON-logic expressions and returns their values.
4
+ */
5
+ import type { EvalState } from './types.js';
6
+ /**
7
+ * Resolve any JSON-logic node and return its value.
8
+ * This is the recursive core of the VM.
9
+ * It is nil-safe: operations on nil values return appropriate defaults.
10
+ */
11
+ export declare function resolve(node: unknown, state: EvalState): unknown;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * JSON-logic expression resolver.
3
+ * Recursively evaluates JSON-logic expressions and returns their values.
4
+ */
5
+ import { applyOperator } from './operators.js';
6
+ /**
7
+ * Resolve any JSON-logic node and return its value.
8
+ * This is the recursive core of the VM.
9
+ * It is nil-safe: operations on nil values return appropriate defaults.
10
+ */
11
+ export function resolve(node, state) {
12
+ if (node === null || node === undefined) {
13
+ return null;
14
+ }
15
+ // Object - could be an operator or a literal map
16
+ if (typeof node === 'object' && !Array.isArray(node)) {
17
+ const obj = node;
18
+ const keys = Object.keys(obj);
19
+ // Single key = operator: {"==": [a, b]} or {"var": "field_name"}
20
+ if (keys.length === 1) {
21
+ const op = keys[0];
22
+ const args = obj[op];
23
+ return applyOperator(op, args, state, resolve);
24
+ }
25
+ // Multi-key object is treated as a literal (return as-is)
26
+ return node;
27
+ }
28
+ // Array literal - resolve each element
29
+ if (Array.isArray(node)) {
30
+ return node.map(elem => resolve(elem, state));
31
+ }
32
+ // Primitives (string, number, boolean) - return as-is
33
+ return node;
34
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Temporal branch selection and rule pruning.
3
+ * Routes logic based on effective dates for bitemporal support.
4
+ */
5
+ import type { EvalState, TemporalBranch } from './types.js';
6
+ /**
7
+ * Find the active temporal branch for a given effective date.
8
+ * Returns undefined if no branch matches (uses default/unversioned logic).
9
+ */
10
+ export declare function selectBranch(state: EvalState): TemporalBranch | undefined;
11
+ /**
12
+ * Mark rules as disabled if they don't belong to the active branch.
13
+ * Rules without a logic_version are always active (unversioned rules).
14
+ */
15
+ export declare function pruneRules(state: EvalState, activeBranch: TemporalBranch | undefined): void;
16
+ /**
17
+ * Select temporal branch and prune inactive rules.
18
+ * Call this at the start of Run().
19
+ */
20
+ export declare function applyTemporalRouting(state: EvalState): void;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Temporal branch selection and rule pruning.
3
+ * Routes logic based on effective dates for bitemporal support.
4
+ */
5
+ /**
6
+ * Find the active temporal branch for a given effective date.
7
+ * Returns undefined if no branch matches (uses default/unversioned logic).
8
+ */
9
+ export function selectBranch(state) {
10
+ const { schema, effectiveDate } = state;
11
+ if (!schema.temporal_map || schema.temporal_map.length === 0) {
12
+ return undefined;
13
+ }
14
+ for (const branch of schema.temporal_map) {
15
+ if (!branch || !branch.valid_range[0]) {
16
+ continue;
17
+ }
18
+ const start = new Date(branch.valid_range[0]);
19
+ if (isNaN(start.getTime())) {
20
+ continue;
21
+ }
22
+ // Check if effectiveDate is at or after start
23
+ if (effectiveDate < start) {
24
+ continue;
25
+ }
26
+ // Check end date (null = open-ended)
27
+ if (branch.valid_range[1]) {
28
+ const end = new Date(branch.valid_range[1]);
29
+ if (!isNaN(end.getTime()) && effectiveDate > end) {
30
+ continue;
31
+ }
32
+ }
33
+ return branch;
34
+ }
35
+ return undefined;
36
+ }
37
+ /**
38
+ * Mark rules as disabled if they don't belong to the active branch.
39
+ * Rules without a logic_version are always active (unversioned rules).
40
+ */
41
+ export function pruneRules(state, activeBranch) {
42
+ if (!activeBranch || !state.schema.logic_tree) {
43
+ return;
44
+ }
45
+ const activeVersion = activeBranch.logic_version;
46
+ for (const rule of state.schema.logic_tree) {
47
+ if (!rule) {
48
+ continue;
49
+ }
50
+ // Rules without a version are always active
51
+ if (!rule.logic_version) {
52
+ continue;
53
+ }
54
+ // Disable rules that don't match the active version
55
+ if (rule.logic_version !== activeVersion) {
56
+ rule.disabled = true;
57
+ }
58
+ }
59
+ }
60
+ /**
61
+ * Select temporal branch and prune inactive rules.
62
+ * Call this at the start of Run().
63
+ */
64
+ export function applyTemporalRouting(state) {
65
+ const branch = selectBranch(state);
66
+ if (branch) {
67
+ pruneRules(state, branch);
68
+ }
69
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Internal types for the Tenet VM core.
3
+ * Public types are re-exported from index.ts
4
+ */
5
+ export type { TenetSchema, TenetResult, TenetVerifyResult, Definition, Rule, Action, TemporalBranch, StateModel, DerivedDef, ValidationError, Attestation, Evidence, } from '../index.js';
6
+ /**
7
+ * Evaluation context for collection operators (some/all/none).
8
+ * When iterating over an array, provides access to the current element.
9
+ */
10
+ export interface EvalContext {
11
+ /** Current element being evaluated */
12
+ item: unknown;
13
+ }
14
+ /**
15
+ * Internal state during rule evaluation.
16
+ * Passed through the resolver and operators.
17
+ */
18
+ export interface EvalState {
19
+ /** The schema being evaluated (mutable copy) */
20
+ schema: import('../index.js').TenetSchema;
21
+ /** Effective date for temporal routing */
22
+ effectiveDate: Date;
23
+ /** Tracks which fields were set by which rule (cycle detection) */
24
+ fieldsSet: Map<string, string>;
25
+ /** Current element context for some/all/none operators */
26
+ currentElement?: unknown;
27
+ /** Accumulated validation errors */
28
+ errors: import('../index.js').ValidationError[];
29
+ }
30
+ /**
31
+ * Document status values
32
+ */
33
+ export type DocStatus = 'READY' | 'INCOMPLETE' | 'INVALID';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Internal types for the Tenet VM core.
3
+ * Public types are re-exported from index.ts
4
+ */
5
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Definition validation and status determination.
3
+ * Validates types, constraints, and required fields.
4
+ */
5
+ import type { EvalState, DocStatus } from './types.js';
6
+ /**
7
+ * Add an error to the state's error list.
8
+ */
9
+ export declare function addError(state: EvalState, fieldId: string, ruleId: string, message: string, lawRef?: string): void;
10
+ /**
11
+ * Validate all definitions for type correctness and required fields.
12
+ */
13
+ export declare function validateDefinitions(state: EvalState): void;
14
+ /**
15
+ * Check attestations for required signatures.
16
+ */
17
+ export declare function checkAttestations(state: EvalState, applyAction: (action: any, ruleId: string, lawRef: string) => void): void;
18
+ /**
19
+ * Determine document status based on validation errors.
20
+ */
21
+ export declare function determineStatus(state: EvalState): DocStatus;