@dlovans/tenet-core 0.1.4 → 0.2.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/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 { init, run } from '@dlovans/tenet-core';
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 { init, run, verify } from '@dlovans/tenet-core';
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
- ### `lint(schema): LintResult` *(No WASM required)*
75
- Static analysis - find issues without executing the schema.
72
+ Returns `{ valid: boolean, error?: string }`.
76
73
 
77
- ```javascript
78
- import { lint } from '@dlovans/tenet-core';
74
+ ### `isReady(): boolean`
75
+ Always returns `true`. Kept for backwards compatibility.
79
76
 
80
- const result = lint(schema);
81
- // No init() needed - pure TypeScript!
77
+ ### `init(): Promise<void>` *(deprecated)*
78
+ No-op. Kept for backwards compatibility with v0.1.x.
82
79
 
83
- if (!result.valid) {
84
- for (const issue of result.issues) {
85
- console.log(`${issue.severity}: ${issue.message}`);
86
- }
87
- }
88
- ```
80
+ ## Runtime Validation
89
81
 
90
- ### `isTenetSchema(obj): boolean`
91
- Check if an object is a Tenet schema.
82
+ The VM automatically detects and reports:
92
83
 
93
- ### `isReady(): boolean`
94
- Check if WASM is initialized.
84
+ - **Undefined variables** — `{"var": "unknown_field"}`
85
+ - **Unknown operators** `{"invalid_op": [...]}`
86
+ - **Temporal conflicts** — Overlapping date ranges, same start/end dates
95
87
 
96
- ## JSON Schema (IDE Support)
88
+ All errors are returned in `result.errors` without failing execution.
97
89
 
98
- Add to your schema files for autocompletion:
90
+ ## TypeScript
99
91
 
100
- ```json
101
- {
102
- "$schema": "https://tenet.dev/schema/v1.json",
103
- "definitions": { ... }
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
- ## TypeScript
98
+ ## Migration from v0.1.x
108
99
 
109
- Full type definitions are included. See `TenetSchema`, `Definition`, `Rule`, `LintResult`, etc.
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
 
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Main Tenet VM engine.
3
+ * Provides run() and verify() functions for schema evaluation.
4
+ */
5
+ import type { TenetSchema, TenetResult, TenetVerifyResult } from './types.js';
6
+ /**
7
+ * Run the Tenet VM on a schema.
8
+ *
9
+ * @param schema - The schema object
10
+ * @param effectiveDate - Effective date for temporal routing (defaults to now)
11
+ * @returns The transformed schema with computed state, errors, and status
12
+ */
13
+ export declare function run(schema: TenetSchema | string, effectiveDate?: Date | string): TenetResult;
14
+ /**
15
+ * Verify that a completed document was correctly derived from a base schema.
16
+ * Simulates the user's journey by iteratively copying visible field values and re-running.
17
+ *
18
+ * @param newSchema - The completed/submitted schema
19
+ * @param oldSchema - The original base schema
20
+ * @param maxIterations - Maximum replay iterations (default: 100)
21
+ * @returns Whether the transformation was valid
22
+ */
23
+ export declare function verify(newSchema: TenetSchema | string, oldSchema: TenetSchema | string, maxIterations?: number): TenetVerifyResult;
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Main Tenet VM engine.
3
+ * Provides run() and verify() functions for schema evaluation.
4
+ */
5
+ import { resolve } from './resolver.js';
6
+ import { isTruthy, toFloat, compareEqual } from './operators.js';
7
+ import { applyTemporalRouting } from './temporal.js';
8
+ import { validateDefinitions, checkAttestations, determineStatus, addError } from './validate.js';
9
+ /**
10
+ * Infer type string from a value.
11
+ */
12
+ function inferType(value) {
13
+ if (typeof value === 'string')
14
+ return 'string';
15
+ if (typeof value === 'number')
16
+ return 'number';
17
+ if (typeof value === 'boolean')
18
+ return 'boolean';
19
+ return 'string'; // default
20
+ }
21
+ /**
22
+ * Deep clone an object (for immutability).
23
+ */
24
+ function deepClone(obj) {
25
+ return JSON.parse(JSON.stringify(obj));
26
+ }
27
+ /**
28
+ * Apply UI modifications to a definition.
29
+ */
30
+ function applyUIModify(state, key, mods) {
31
+ const def = state.schema.definitions[key];
32
+ if (!def) {
33
+ return;
34
+ }
35
+ if (typeof mods !== 'object' || mods === null) {
36
+ return;
37
+ }
38
+ const modMap = mods;
39
+ // Apply visibility and metadata modifications
40
+ if (typeof modMap['visible'] === 'boolean') {
41
+ def.visible = modMap['visible'];
42
+ }
43
+ if (typeof modMap['ui_class'] === 'string') {
44
+ def.ui_class = modMap['ui_class'];
45
+ }
46
+ if (typeof modMap['ui_message'] === 'string') {
47
+ def.ui_message = modMap['ui_message'];
48
+ }
49
+ if (typeof modMap['required'] === 'boolean') {
50
+ def.required = modMap['required'];
51
+ }
52
+ // Apply numeric constraints
53
+ const [minVal, minOk] = toFloat(modMap['min']);
54
+ if (minOk) {
55
+ def.min = minVal;
56
+ }
57
+ const [maxVal, maxOk] = toFloat(modMap['max']);
58
+ if (maxOk) {
59
+ def.max = maxVal;
60
+ }
61
+ const [stepVal, stepOk] = toFloat(modMap['step']);
62
+ if (stepOk) {
63
+ def.step = stepVal;
64
+ }
65
+ // Apply string constraints
66
+ if (typeof modMap['min_length'] === 'number') {
67
+ def.min_length = modMap['min_length'];
68
+ }
69
+ if (typeof modMap['max_length'] === 'number') {
70
+ def.max_length = modMap['max_length'];
71
+ }
72
+ if (typeof modMap['pattern'] === 'string') {
73
+ def.pattern = modMap['pattern'];
74
+ }
75
+ }
76
+ /**
77
+ * Set a definition value, with cycle detection.
78
+ */
79
+ function setDefinitionValue(state, key, value, ruleId) {
80
+ // Cycle detection
81
+ const prevRule = state.fieldsSet.get(key);
82
+ if (prevRule && prevRule !== ruleId) {
83
+ addError(state, key, ruleId, `Potential cycle: field '${key}' set by rule '${prevRule}' and again by rule '${ruleId}'`);
84
+ }
85
+ state.fieldsSet.set(key, ruleId);
86
+ const def = state.schema.definitions[key];
87
+ if (!def) {
88
+ // Create new definition if it doesn't exist
89
+ state.schema.definitions[key] = {
90
+ type: inferType(value),
91
+ value,
92
+ visible: true,
93
+ };
94
+ return;
95
+ }
96
+ def.value = value;
97
+ }
98
+ /**
99
+ * Apply a rule's action: setting values, modifying UI, or emitting errors.
100
+ */
101
+ function applyAction(state, action, ruleId, lawRef) {
102
+ if (!action) {
103
+ return;
104
+ }
105
+ // Apply value mutations
106
+ if (action.set) {
107
+ for (const [key, value] of Object.entries(action.set)) {
108
+ // Resolve the value in case it's an expression
109
+ const resolvedValue = resolve(value, state);
110
+ setDefinitionValue(state, key, resolvedValue, ruleId);
111
+ }
112
+ }
113
+ // Apply UI modifications
114
+ if (action.ui_modify) {
115
+ for (const [key, mods] of Object.entries(action.ui_modify)) {
116
+ applyUIModify(state, key, mods);
117
+ }
118
+ }
119
+ // Emit error if specified
120
+ if (action.error_msg) {
121
+ addError(state, '', ruleId, action.error_msg, lawRef);
122
+ }
123
+ }
124
+ /**
125
+ * Evaluate the logic tree (all active rules in order).
126
+ */
127
+ function evaluateLogicTree(state) {
128
+ if (!state.schema.logic_tree) {
129
+ return;
130
+ }
131
+ for (const rule of state.schema.logic_tree) {
132
+ if (!rule || rule.disabled) {
133
+ continue;
134
+ }
135
+ // Evaluate the condition
136
+ const condition = resolve(rule.when, state);
137
+ if (isTruthy(condition)) {
138
+ applyAction(state, rule.then, rule.id, rule.law_ref || '');
139
+ }
140
+ }
141
+ }
142
+ /**
143
+ * Compute derived state values.
144
+ */
145
+ function computeDerived(state) {
146
+ if (!state.schema.state_model?.derived) {
147
+ return;
148
+ }
149
+ for (const [name, derivedDef] of Object.entries(state.schema.state_model.derived)) {
150
+ if (!derivedDef?.eval) {
151
+ continue;
152
+ }
153
+ // Evaluate the expression
154
+ const value = resolve(derivedDef.eval, state);
155
+ // Store the computed value as a definition (readonly)
156
+ state.schema.definitions[name] = {
157
+ type: inferType(value),
158
+ value,
159
+ readonly: true,
160
+ visible: true,
161
+ };
162
+ }
163
+ }
164
+ /**
165
+ * Run the Tenet VM on a schema.
166
+ *
167
+ * @param schema - The schema object
168
+ * @param effectiveDate - Effective date for temporal routing (defaults to now)
169
+ * @returns The transformed schema with computed state, errors, and status
170
+ */
171
+ export function run(schema, effectiveDate = new Date()) {
172
+ try {
173
+ // Parse if string
174
+ const parsedSchema = typeof schema === 'string'
175
+ ? JSON.parse(schema)
176
+ : deepClone(schema);
177
+ // Parse date
178
+ const date = effectiveDate instanceof Date
179
+ ? effectiveDate
180
+ : new Date(effectiveDate);
181
+ // Initialize default visibility for definitions
182
+ for (const def of Object.values(parsedSchema.definitions)) {
183
+ if (def && def.visible === undefined) {
184
+ def.visible = true;
185
+ }
186
+ }
187
+ // Create evaluation state
188
+ const state = {
189
+ schema: parsedSchema,
190
+ effectiveDate: date,
191
+ fieldsSet: new Map(),
192
+ errors: [],
193
+ };
194
+ // 1. Select temporal branch and prune inactive rules
195
+ applyTemporalRouting(state);
196
+ // 2. Compute derived state (so logic tree can use derived values)
197
+ computeDerived(state);
198
+ // 3. Evaluate logic tree
199
+ evaluateLogicTree(state);
200
+ // 4. Re-compute derived state (in case logic modified inputs)
201
+ computeDerived(state);
202
+ // 5. Validate definitions
203
+ validateDefinitions(state);
204
+ // 6. Check attestations
205
+ checkAttestations(state, (action, ruleId, lawRef) => {
206
+ applyAction(state, action, ruleId, lawRef);
207
+ });
208
+ // 7. Determine status and attach errors
209
+ state.schema.errors = state.errors.length > 0 ? state.errors : undefined;
210
+ state.schema.status = determineStatus(state);
211
+ return { result: state.schema };
212
+ }
213
+ catch (error) {
214
+ return { error: String(error) };
215
+ }
216
+ }
217
+ /**
218
+ * Get visible, editable fields from a schema.
219
+ */
220
+ function getVisibleEditableFields(schema) {
221
+ const result = new Set();
222
+ for (const [id, def] of Object.entries(schema.definitions)) {
223
+ if (def && def.visible && !def.readonly) {
224
+ result.add(id);
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ /**
230
+ * Count visible fields in a schema.
231
+ */
232
+ function countVisibleFields(schema) {
233
+ let count = 0;
234
+ for (const def of Object.values(schema.definitions)) {
235
+ if (def?.visible) {
236
+ count++;
237
+ }
238
+ }
239
+ return count;
240
+ }
241
+ /**
242
+ * Validate that the final state matches expected values.
243
+ */
244
+ function validateFinalState(newSchema, resultSchema) {
245
+ // Check for unknown/injected fields in newSchema that don't exist in result
246
+ for (const id of Object.keys(newSchema.definitions)) {
247
+ if (!(id in resultSchema.definitions)) {
248
+ return [false, `Unknown field '${id}' not in schema`];
249
+ }
250
+ }
251
+ // Compare computed (readonly) values
252
+ for (const [id, resultDef] of Object.entries(resultSchema.definitions)) {
253
+ if (!resultDef?.readonly) {
254
+ continue;
255
+ }
256
+ const newDef = newSchema.definitions[id];
257
+ if (!newDef) {
258
+ return [false, `Computed field '${id}' missing in submitted document`];
259
+ }
260
+ if (!compareEqual(newDef.value, resultDef.value)) {
261
+ return [false, `Computed field '${id}' mismatch: claimed ${JSON.stringify(newDef.value)}, expected ${JSON.stringify(resultDef.value)}`];
262
+ }
263
+ }
264
+ // Verify attestations are fulfilled
265
+ if (resultSchema.attestations) {
266
+ for (const [id, resultAtt] of Object.entries(resultSchema.attestations)) {
267
+ if (!resultAtt) {
268
+ continue;
269
+ }
270
+ const newAtt = newSchema.attestations?.[id];
271
+ if (!newAtt) {
272
+ continue;
273
+ }
274
+ if (resultAtt.required) {
275
+ if (!newAtt.signed) {
276
+ return [false, `Required attestation '${id}' not signed`];
277
+ }
278
+ if (!newAtt.evidence?.provider_audit_id) {
279
+ return [false, `Attestation '${id}' missing evidence`];
280
+ }
281
+ if (!newAtt.evidence?.timestamp) {
282
+ return [false, `Attestation '${id}' missing timestamp`];
283
+ }
284
+ }
285
+ }
286
+ }
287
+ // Verify status matches
288
+ if (newSchema.status !== resultSchema.status) {
289
+ return [false, `Status mismatch: claimed ${newSchema.status}, expected ${resultSchema.status}`];
290
+ }
291
+ return [true, undefined];
292
+ }
293
+ /**
294
+ * Verify that a completed document was correctly derived from a base schema.
295
+ * Simulates the user's journey by iteratively copying visible field values and re-running.
296
+ *
297
+ * @param newSchema - The completed/submitted schema
298
+ * @param oldSchema - The original base schema
299
+ * @param maxIterations - Maximum replay iterations (default: 100)
300
+ * @returns Whether the transformation was valid
301
+ */
302
+ export function verify(newSchema, oldSchema, maxIterations = 100) {
303
+ try {
304
+ // Parse schemas
305
+ const parsedNewSchema = typeof newSchema === 'string'
306
+ ? JSON.parse(newSchema)
307
+ : deepClone(newSchema);
308
+ const parsedOldSchema = typeof oldSchema === 'string'
309
+ ? JSON.parse(oldSchema)
310
+ : deepClone(oldSchema);
311
+ // Extract effective date from newSchema
312
+ let effectiveDate = new Date();
313
+ if (parsedNewSchema.valid_from) {
314
+ const parsed = new Date(parsedNewSchema.valid_from);
315
+ if (!isNaN(parsed.getTime())) {
316
+ effectiveDate = parsed;
317
+ }
318
+ }
319
+ // Start with base schema
320
+ let currentSchema = parsedOldSchema;
321
+ let previousVisibleCount = -1;
322
+ for (let iteration = 0; iteration < maxIterations; iteration++) {
323
+ // Count visible editable fields before copying
324
+ const visibleEditable = getVisibleEditableFields(currentSchema);
325
+ // Copy values from newSchema for visible, editable fields
326
+ for (const fieldId of visibleEditable) {
327
+ const newDef = parsedNewSchema.definitions[fieldId];
328
+ const currentDef = currentSchema.definitions[fieldId];
329
+ if (newDef && currentDef) {
330
+ currentDef.value = newDef.value;
331
+ }
332
+ }
333
+ // Copy attestation states
334
+ if (currentSchema.attestations) {
335
+ for (const [attId, currentAtt] of Object.entries(currentSchema.attestations)) {
336
+ if (!currentAtt)
337
+ continue;
338
+ const newAtt = parsedNewSchema.attestations?.[attId];
339
+ if (newAtt) {
340
+ currentAtt.signed = newAtt.signed;
341
+ currentAtt.evidence = newAtt.evidence;
342
+ }
343
+ }
344
+ }
345
+ // Run the schema
346
+ const runResult = run(currentSchema, effectiveDate);
347
+ if (runResult.error) {
348
+ return { valid: false, error: `Run failed (iteration ${iteration}): ${runResult.error}` };
349
+ }
350
+ const resultSchema = runResult.result;
351
+ // Count visible fields after run
352
+ const currentVisibleCount = countVisibleFields(resultSchema);
353
+ // Check for convergence
354
+ if (currentVisibleCount === previousVisibleCount) {
355
+ // Converged - now validate the final state
356
+ const [valid, error] = validateFinalState(parsedNewSchema, resultSchema);
357
+ return { valid, error };
358
+ }
359
+ previousVisibleCount = currentVisibleCount;
360
+ currentSchema = resultSchema;
361
+ }
362
+ return { valid: false, error: `Verification did not converge after ${maxIterations} iterations` };
363
+ }
364
+ catch (error) {
365
+ return { valid: false, error: String(error) };
366
+ }
367
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * JSON-logic operators for the Tenet VM.
3
+ * All operators are nil-safe: operations on nil/undefined return appropriate defaults.
4
+ */
5
+ import type { EvalState } from './types.js';
6
+ type ResolveFn = (node: unknown, state: EvalState) => unknown;
7
+ /**
8
+ * Convert a value to a number if possible.
9
+ */
10
+ export declare function toFloat(v: unknown): [number, boolean];
11
+ /**
12
+ * Parse a date value (string or Date).
13
+ * Supports ISO 8601 formats.
14
+ */
15
+ export declare function parseDate(v: unknown): [Date, boolean];
16
+ /**
17
+ * Determine if a value is "truthy" in JSON-logic terms.
18
+ * nil, false, 0, and "" are falsy. Everything else is truthy.
19
+ */
20
+ export declare function isTruthy(value: unknown): boolean;
21
+ /**
22
+ * Compare two values for equality with type coercion.
23
+ * nil == nil is true, nil == anything_else is false.
24
+ */
25
+ export declare function compareEqual(a: unknown, b: unknown): boolean;
26
+ /**
27
+ * Apply an operator to its arguments.
28
+ */
29
+ export declare function applyOperator(op: string, args: unknown, state: EvalState, resolve: ResolveFn): unknown;
30
+ export {};