@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.
- package/dist/core/engine.d.ts +23 -0
- package/dist/core/engine.js +365 -0
- package/dist/core/operators.d.ts +30 -0
- package/dist/core/operators.js +424 -0
- package/dist/core/resolver.d.ts +11 -0
- package/dist/core/resolver.js +34 -0
- package/dist/core/temporal.d.ts +20 -0
- package/dist/core/temporal.js +69 -0
- package/dist/core/types.d.ts +33 -0
- package/dist/core/types.js +5 -0
- package/dist/core/validate.d.ts +21 -0
- package/dist/core/validate.js +194 -0
- package/dist/index.d.ts +9 -16
- package/dist/index.js +16 -62
- package/package.json +4 -8
- package/wasm/tenet.wasm +0 -0
- package/wasm/wasm_exec.js +0 -575
|
@@ -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,365 @@
|
|
|
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. Evaluate logic tree
|
|
197
|
+
evaluateLogicTree(state);
|
|
198
|
+
// 3. Compute derived state
|
|
199
|
+
computeDerived(state);
|
|
200
|
+
// 4. Validate definitions
|
|
201
|
+
validateDefinitions(state);
|
|
202
|
+
// 5. Check attestations
|
|
203
|
+
checkAttestations(state, (action, ruleId, lawRef) => {
|
|
204
|
+
applyAction(state, action, ruleId, lawRef);
|
|
205
|
+
});
|
|
206
|
+
// 6. Determine status and attach errors
|
|
207
|
+
state.schema.errors = state.errors.length > 0 ? state.errors : undefined;
|
|
208
|
+
state.schema.status = determineStatus(state);
|
|
209
|
+
return { result: state.schema };
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return { error: String(error) };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get visible, editable fields from a schema.
|
|
217
|
+
*/
|
|
218
|
+
function getVisibleEditableFields(schema) {
|
|
219
|
+
const result = new Set();
|
|
220
|
+
for (const [id, def] of Object.entries(schema.definitions)) {
|
|
221
|
+
if (def && def.visible && !def.readonly) {
|
|
222
|
+
result.add(id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Count visible fields in a schema.
|
|
229
|
+
*/
|
|
230
|
+
function countVisibleFields(schema) {
|
|
231
|
+
let count = 0;
|
|
232
|
+
for (const def of Object.values(schema.definitions)) {
|
|
233
|
+
if (def?.visible) {
|
|
234
|
+
count++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return count;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Validate that the final state matches expected values.
|
|
241
|
+
*/
|
|
242
|
+
function validateFinalState(newSchema, resultSchema) {
|
|
243
|
+
// Check for unknown/injected fields in newSchema that don't exist in result
|
|
244
|
+
for (const id of Object.keys(newSchema.definitions)) {
|
|
245
|
+
if (!(id in resultSchema.definitions)) {
|
|
246
|
+
return [false, `Unknown field '${id}' not in schema`];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Compare computed (readonly) values
|
|
250
|
+
for (const [id, resultDef] of Object.entries(resultSchema.definitions)) {
|
|
251
|
+
if (!resultDef?.readonly) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const newDef = newSchema.definitions[id];
|
|
255
|
+
if (!newDef) {
|
|
256
|
+
return [false, `Computed field '${id}' missing in submitted document`];
|
|
257
|
+
}
|
|
258
|
+
if (!compareEqual(newDef.value, resultDef.value)) {
|
|
259
|
+
return [false, `Computed field '${id}' mismatch: claimed ${JSON.stringify(newDef.value)}, expected ${JSON.stringify(resultDef.value)}`];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Verify attestations are fulfilled
|
|
263
|
+
if (resultSchema.attestations) {
|
|
264
|
+
for (const [id, resultAtt] of Object.entries(resultSchema.attestations)) {
|
|
265
|
+
if (!resultAtt) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const newAtt = newSchema.attestations?.[id];
|
|
269
|
+
if (!newAtt) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (resultAtt.required) {
|
|
273
|
+
if (!newAtt.signed) {
|
|
274
|
+
return [false, `Required attestation '${id}' not signed`];
|
|
275
|
+
}
|
|
276
|
+
if (!newAtt.evidence?.provider_audit_id) {
|
|
277
|
+
return [false, `Attestation '${id}' missing evidence`];
|
|
278
|
+
}
|
|
279
|
+
if (!newAtt.evidence?.timestamp) {
|
|
280
|
+
return [false, `Attestation '${id}' missing timestamp`];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Verify status matches
|
|
286
|
+
if (newSchema.status !== resultSchema.status) {
|
|
287
|
+
return [false, `Status mismatch: claimed ${newSchema.status}, expected ${resultSchema.status}`];
|
|
288
|
+
}
|
|
289
|
+
return [true, undefined];
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Verify that a completed document was correctly derived from a base schema.
|
|
293
|
+
* Simulates the user's journey by iteratively copying visible field values and re-running.
|
|
294
|
+
*
|
|
295
|
+
* @param newSchema - The completed/submitted schema
|
|
296
|
+
* @param oldSchema - The original base schema
|
|
297
|
+
* @param maxIterations - Maximum replay iterations (default: 100)
|
|
298
|
+
* @returns Whether the transformation was valid
|
|
299
|
+
*/
|
|
300
|
+
export function verify(newSchema, oldSchema, maxIterations = 100) {
|
|
301
|
+
try {
|
|
302
|
+
// Parse schemas
|
|
303
|
+
const parsedNewSchema = typeof newSchema === 'string'
|
|
304
|
+
? JSON.parse(newSchema)
|
|
305
|
+
: deepClone(newSchema);
|
|
306
|
+
const parsedOldSchema = typeof oldSchema === 'string'
|
|
307
|
+
? JSON.parse(oldSchema)
|
|
308
|
+
: deepClone(oldSchema);
|
|
309
|
+
// Extract effective date from newSchema
|
|
310
|
+
let effectiveDate = new Date();
|
|
311
|
+
if (parsedNewSchema.valid_from) {
|
|
312
|
+
const parsed = new Date(parsedNewSchema.valid_from);
|
|
313
|
+
if (!isNaN(parsed.getTime())) {
|
|
314
|
+
effectiveDate = parsed;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Start with base schema
|
|
318
|
+
let currentSchema = parsedOldSchema;
|
|
319
|
+
let previousVisibleCount = -1;
|
|
320
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
321
|
+
// Count visible editable fields before copying
|
|
322
|
+
const visibleEditable = getVisibleEditableFields(currentSchema);
|
|
323
|
+
// Copy values from newSchema for visible, editable fields
|
|
324
|
+
for (const fieldId of visibleEditable) {
|
|
325
|
+
const newDef = parsedNewSchema.definitions[fieldId];
|
|
326
|
+
const currentDef = currentSchema.definitions[fieldId];
|
|
327
|
+
if (newDef && currentDef) {
|
|
328
|
+
currentDef.value = newDef.value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Copy attestation states
|
|
332
|
+
if (currentSchema.attestations) {
|
|
333
|
+
for (const [attId, currentAtt] of Object.entries(currentSchema.attestations)) {
|
|
334
|
+
if (!currentAtt)
|
|
335
|
+
continue;
|
|
336
|
+
const newAtt = parsedNewSchema.attestations?.[attId];
|
|
337
|
+
if (newAtt) {
|
|
338
|
+
currentAtt.signed = newAtt.signed;
|
|
339
|
+
currentAtt.evidence = newAtt.evidence;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Run the schema
|
|
344
|
+
const runResult = run(currentSchema, effectiveDate);
|
|
345
|
+
if (runResult.error) {
|
|
346
|
+
return { valid: false, error: `Run failed (iteration ${iteration}): ${runResult.error}` };
|
|
347
|
+
}
|
|
348
|
+
const resultSchema = runResult.result;
|
|
349
|
+
// Count visible fields after run
|
|
350
|
+
const currentVisibleCount = countVisibleFields(resultSchema);
|
|
351
|
+
// Check for convergence
|
|
352
|
+
if (currentVisibleCount === previousVisibleCount) {
|
|
353
|
+
// Converged - now validate the final state
|
|
354
|
+
const [valid, error] = validateFinalState(parsedNewSchema, resultSchema);
|
|
355
|
+
return { valid, error };
|
|
356
|
+
}
|
|
357
|
+
previousVisibleCount = currentVisibleCount;
|
|
358
|
+
currentSchema = resultSchema;
|
|
359
|
+
}
|
|
360
|
+
return { valid: false, error: `Verification did not converge after ${maxIterations} iterations` };
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return { valid: false, error: String(error) };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -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 {};
|