@dlovans/tenet-core 0.2.1 → 0.4.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 +4 -1
- package/dist/core/engine.js +117 -47
- package/dist/core/lint.d.ts +20 -0
- package/dist/core/lint.js +622 -0
- package/dist/core/operators.js +9 -3
- package/dist/core/temporal.js +2 -2
- 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 +35 -36
- package/dist/index.d.ts +12 -85
- package/dist/index.js +11 -1
- package/dist/lint.test.d.ts +5 -0
- package/dist/lint.test.js +570 -0
- package/package.json +1 -1
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,6 +200,7 @@ 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);
|
|
@@ -227,25 +238,29 @@ function getVisibleEditableFields(schema) {
|
|
|
227
238
|
return result;
|
|
228
239
|
}
|
|
229
240
|
/**
|
|
230
|
-
*
|
|
241
|
+
* Get a sorted, comma-joined string of visible field IDs for convergence detection.
|
|
231
242
|
*/
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
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(',');
|
|
240
249
|
}
|
|
241
250
|
/**
|
|
242
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.
|
|
243
253
|
*/
|
|
244
254
|
function validateFinalState(newSchema, resultSchema) {
|
|
255
|
+
const issues = [];
|
|
245
256
|
// Check for unknown/injected fields in newSchema that don't exist in result
|
|
246
257
|
for (const id of Object.keys(newSchema.definitions)) {
|
|
247
258
|
if (!(id in resultSchema.definitions)) {
|
|
248
|
-
|
|
259
|
+
issues.push({
|
|
260
|
+
code: 'unknown_field',
|
|
261
|
+
field_id: id,
|
|
262
|
+
message: `Field '${id}' does not exist in the schema`,
|
|
263
|
+
});
|
|
249
264
|
}
|
|
250
265
|
}
|
|
251
266
|
// Compare computed (readonly) values
|
|
@@ -255,49 +270,85 @@ function validateFinalState(newSchema, resultSchema) {
|
|
|
255
270
|
}
|
|
256
271
|
const newDef = newSchema.definitions[id];
|
|
257
272
|
if (!newDef) {
|
|
258
|
-
|
|
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;
|
|
259
280
|
}
|
|
260
281
|
if (!compareEqual(newDef.value, resultDef.value)) {
|
|
261
|
-
|
|
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
|
+
});
|
|
262
289
|
}
|
|
263
290
|
}
|
|
264
291
|
// Verify attestations are fulfilled
|
|
265
292
|
if (resultSchema.attestations) {
|
|
266
293
|
for (const [id, resultAtt] of Object.entries(resultSchema.attestations)) {
|
|
267
|
-
if (!resultAtt) {
|
|
294
|
+
if (!resultAtt?.required) {
|
|
268
295
|
continue;
|
|
269
296
|
}
|
|
270
297
|
const newAtt = newSchema.attestations?.[id];
|
|
271
298
|
if (!newAtt) {
|
|
272
299
|
continue;
|
|
273
300
|
}
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
});
|
|
284
322
|
}
|
|
285
323
|
}
|
|
286
324
|
}
|
|
287
325
|
// Verify status matches
|
|
288
326
|
if (newSchema.status !== resultSchema.status) {
|
|
289
|
-
|
|
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
|
+
});
|
|
290
333
|
}
|
|
291
|
-
return
|
|
334
|
+
return {
|
|
335
|
+
valid: issues.length === 0,
|
|
336
|
+
status: resultSchema.status,
|
|
337
|
+
issues: issues.length > 0 ? issues : undefined,
|
|
338
|
+
schema: resultSchema,
|
|
339
|
+
};
|
|
292
340
|
}
|
|
293
341
|
/**
|
|
294
342
|
* Verify that a completed document was correctly derived from a base schema.
|
|
295
343
|
* Simulates the user's journey by iteratively copying visible field values and re-running.
|
|
296
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
|
+
*
|
|
297
348
|
* @param newSchema - The completed/submitted schema
|
|
298
349
|
* @param oldSchema - The original base schema
|
|
299
350
|
* @param maxIterations - Maximum replay iterations (default: 100)
|
|
300
|
-
* @returns
|
|
351
|
+
* @returns Structured verification result
|
|
301
352
|
*/
|
|
302
353
|
export function verify(newSchema, oldSchema, maxIterations = 100) {
|
|
303
354
|
try {
|
|
@@ -318,7 +369,7 @@ export function verify(newSchema, oldSchema, maxIterations = 100) {
|
|
|
318
369
|
}
|
|
319
370
|
// Start with base schema
|
|
320
371
|
let currentSchema = parsedOldSchema;
|
|
321
|
-
let
|
|
372
|
+
let previousVisibleIds = '';
|
|
322
373
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
323
374
|
// Count visible editable fields before copying
|
|
324
375
|
const visibleEditable = getVisibleEditableFields(currentSchema);
|
|
@@ -345,23 +396,42 @@ export function verify(newSchema, oldSchema, maxIterations = 100) {
|
|
|
345
396
|
// Run the schema
|
|
346
397
|
const runResult = run(currentSchema, effectiveDate);
|
|
347
398
|
if (runResult.error) {
|
|
348
|
-
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
|
+
};
|
|
349
407
|
}
|
|
350
408
|
const resultSchema = runResult.result;
|
|
351
|
-
//
|
|
352
|
-
const
|
|
353
|
-
// Check for convergence
|
|
354
|
-
if (
|
|
355
|
-
// Converged - now validate the final state
|
|
356
|
-
|
|
357
|
-
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);
|
|
358
415
|
}
|
|
359
|
-
|
|
416
|
+
previousVisibleIds = currentVisibleIds;
|
|
360
417
|
currentSchema = resultSchema;
|
|
361
418
|
}
|
|
362
|
-
return {
|
|
419
|
+
return {
|
|
420
|
+
valid: false,
|
|
421
|
+
issues: [{
|
|
422
|
+
code: 'convergence_failed',
|
|
423
|
+
message: `Document did not converge after ${maxIterations} iterations`,
|
|
424
|
+
}],
|
|
425
|
+
};
|
|
363
426
|
}
|
|
364
427
|
catch (error) {
|
|
365
|
-
return {
|
|
428
|
+
return {
|
|
429
|
+
valid: false,
|
|
430
|
+
issues: [{
|
|
431
|
+
code: 'internal_error',
|
|
432
|
+
message: `Unexpected error during verification`,
|
|
433
|
+
}],
|
|
434
|
+
error: String(error),
|
|
435
|
+
};
|
|
366
436
|
}
|
|
367
437
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static schema linter for Tenet schemas.
|
|
3
|
+
* Analyzes schemas without executing them, catching structural errors,
|
|
4
|
+
* type mismatches, undefined references, and circular dependencies.
|
|
5
|
+
*/
|
|
6
|
+
import type { TenetSchema } from './types.js';
|
|
7
|
+
export type LintSeverity = 'error' | 'warning';
|
|
8
|
+
export interface LintIssue {
|
|
9
|
+
severity: LintSeverity;
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
path?: string;
|
|
13
|
+
field_id?: string;
|
|
14
|
+
rule_id?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LintResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
issues: LintIssue[];
|
|
19
|
+
}
|
|
20
|
+
export declare function lint(schema: TenetSchema | string): LintResult;
|