@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,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 {};