@dlovans/tenet-core 0.2.1 → 0.3.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/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 +2 -85
- package/dist/index.js +0 -1
- 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
|
}
|
package/dist/core/operators.js
CHANGED
|
@@ -127,15 +127,21 @@ function getVar(path, state, resolve) {
|
|
|
127
127
|
const rootVar = parts[0];
|
|
128
128
|
// Check if variable is defined (only for root-level vars, not nested access)
|
|
129
129
|
if (!isVariableDefined(rootVar, state) && state.currentElement === undefined) {
|
|
130
|
-
addError(state, '', '', `Undefined variable '${rootVar}' in logic expression`);
|
|
130
|
+
addError(state, '', '', 'runtime_warning', `Undefined variable '${rootVar}' in logic expression`);
|
|
131
131
|
return null;
|
|
132
132
|
}
|
|
133
133
|
// First, check derived state (derived values take precedence)
|
|
134
134
|
if (state.schema.state_model?.derived && resolve) {
|
|
135
135
|
const derived = state.schema.state_model.derived[rootVar];
|
|
136
136
|
if (derived?.eval) {
|
|
137
|
-
//
|
|
137
|
+
// Cycle detection for derived fields
|
|
138
|
+
if (state.derivedInProgress.has(rootVar)) {
|
|
139
|
+
addError(state, '', '', 'cycle_detected', `Circular dependency detected in derived field '${rootVar}'`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
state.derivedInProgress.add(rootVar);
|
|
138
143
|
const result = resolve(derived.eval, state);
|
|
144
|
+
state.derivedInProgress.delete(rootVar);
|
|
139
145
|
if (parts.length === 1) {
|
|
140
146
|
return result;
|
|
141
147
|
}
|
|
@@ -442,7 +448,7 @@ const operators = {
|
|
|
442
448
|
export function applyOperator(op, args, state, resolve) {
|
|
443
449
|
const fn = operators[op];
|
|
444
450
|
if (!fn) {
|
|
445
|
-
addError(state, '', '', `Unknown operator '${op}' in logic expression`);
|
|
451
|
+
addError(state, '', '', 'runtime_warning', `Unknown operator '${op}' in logic expression`);
|
|
446
452
|
return null;
|
|
447
453
|
}
|
|
448
454
|
return fn(args, state, resolve);
|
package/dist/core/temporal.js
CHANGED
|
@@ -21,7 +21,7 @@ function validateTemporalMap(state) {
|
|
|
21
21
|
const end = branch.valid_range[1];
|
|
22
22
|
// Check for same start/end date (invalid zero-length range)
|
|
23
23
|
if (start && end && start === end) {
|
|
24
|
-
addError(state, '', '', `Temporal branch ${i} has same start and end date '${start}' (invalid range)`);
|
|
24
|
+
addError(state, '', '', 'runtime_warning', `Temporal branch ${i} has same start and end date '${start}' (invalid range)`);
|
|
25
25
|
}
|
|
26
26
|
// Check for overlapping with previous branch
|
|
27
27
|
if (i > 0) {
|
|
@@ -34,7 +34,7 @@ function validateTemporalMap(state) {
|
|
|
34
34
|
? new Date(start).getTime()
|
|
35
35
|
: -Infinity;
|
|
36
36
|
if (currStart <= prevEnd) {
|
|
37
|
-
addError(state, '', '', `Temporal branch ${i} overlaps with branch ${i - 1} (ranges must not overlap)`);
|
|
37
|
+
addError(state, '', '', 'runtime_warning', `Temporal branch ${i} overlaps with branch ${i - 1} (ranges must not overlap)`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,8 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* All type definitions for the Tenet VM.
|
|
3
|
+
* This is the single source of truth — other modules re-export from here.
|
|
4
4
|
*/
|
|
5
|
-
export
|
|
5
|
+
export interface TenetResult {
|
|
6
|
+
result?: TenetSchema;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Machine-parseable codes for verification issues.
|
|
11
|
+
* UI layers map these to customer-friendly messages; the VM never decides presentation.
|
|
12
|
+
*/
|
|
13
|
+
export type VerifyIssueCode = 'unknown_field' | 'computed_mismatch' | 'attestation_unsigned' | 'attestation_no_evidence' | 'attestation_no_timestamp' | 'status_mismatch' | 'convergence_failed' | 'internal_error';
|
|
14
|
+
/**
|
|
15
|
+
* A single structured problem found during verification.
|
|
16
|
+
*/
|
|
17
|
+
export interface VerifyIssue {
|
|
18
|
+
code: VerifyIssueCode;
|
|
19
|
+
field_id?: string;
|
|
20
|
+
message: string;
|
|
21
|
+
expected?: unknown;
|
|
22
|
+
claimed?: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface TenetVerifyResult {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
status?: string;
|
|
27
|
+
issues?: VerifyIssue[];
|
|
28
|
+
schema?: TenetSchema;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface Evidence {
|
|
32
|
+
provider_audit_id?: string;
|
|
33
|
+
timestamp?: string;
|
|
34
|
+
signer_id?: string;
|
|
35
|
+
logic_version?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface Attestation {
|
|
38
|
+
statement: string;
|
|
39
|
+
law_ref?: string;
|
|
40
|
+
required_role?: string;
|
|
41
|
+
provider?: string;
|
|
42
|
+
required?: boolean;
|
|
43
|
+
signed?: boolean;
|
|
44
|
+
evidence?: Evidence;
|
|
45
|
+
on_sign?: Action;
|
|
46
|
+
}
|
|
47
|
+
export interface TenetSchema {
|
|
48
|
+
protocol?: string;
|
|
49
|
+
schema_id?: string;
|
|
50
|
+
version?: string;
|
|
51
|
+
valid_from?: string;
|
|
52
|
+
definitions: Record<string, Definition>;
|
|
53
|
+
logic_tree?: Rule[];
|
|
54
|
+
temporal_map?: TemporalBranch[];
|
|
55
|
+
state_model?: StateModel;
|
|
56
|
+
errors?: ValidationError[];
|
|
57
|
+
status?: 'READY' | 'INCOMPLETE' | 'INVALID';
|
|
58
|
+
attestations?: Record<string, Attestation>;
|
|
59
|
+
}
|
|
60
|
+
export interface Definition {
|
|
61
|
+
type: 'string' | 'number' | 'boolean' | 'select' | 'date' | 'attestation' | 'currency';
|
|
62
|
+
value?: unknown;
|
|
63
|
+
options?: string[];
|
|
64
|
+
label?: string;
|
|
65
|
+
required?: boolean;
|
|
66
|
+
readonly?: boolean;
|
|
67
|
+
visible?: boolean;
|
|
68
|
+
min?: number;
|
|
69
|
+
max?: number;
|
|
70
|
+
step?: number;
|
|
71
|
+
min_length?: number;
|
|
72
|
+
max_length?: number;
|
|
73
|
+
pattern?: string;
|
|
74
|
+
ui_class?: string;
|
|
75
|
+
ui_message?: string;
|
|
76
|
+
}
|
|
77
|
+
export interface Rule {
|
|
78
|
+
id: string;
|
|
79
|
+
law_ref?: string;
|
|
80
|
+
logic_version?: string;
|
|
81
|
+
when: Record<string, unknown>;
|
|
82
|
+
then: Action;
|
|
83
|
+
disabled?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface Action {
|
|
86
|
+
set?: Record<string, unknown>;
|
|
87
|
+
ui_modify?: Record<string, unknown>;
|
|
88
|
+
error_msg?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface TemporalBranch {
|
|
91
|
+
valid_range: [string | null, string | null];
|
|
92
|
+
logic_version: string;
|
|
93
|
+
status: 'ACTIVE' | 'ARCHIVED';
|
|
94
|
+
}
|
|
95
|
+
export interface StateModel {
|
|
96
|
+
inputs: string[];
|
|
97
|
+
derived: Record<string, DerivedDef>;
|
|
98
|
+
}
|
|
99
|
+
export interface DerivedDef {
|
|
100
|
+
eval: Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
export type ErrorKind = 'type_mismatch' | 'missing_required' | 'constraint_violation' | 'attestation_incomplete' | 'runtime_warning' | 'cycle_detected';
|
|
103
|
+
export interface ValidationError {
|
|
104
|
+
field_id?: string;
|
|
105
|
+
rule_id?: string;
|
|
106
|
+
kind: ErrorKind;
|
|
107
|
+
message: string;
|
|
108
|
+
law_ref?: string;
|
|
109
|
+
}
|
|
6
110
|
/**
|
|
7
111
|
* Evaluation context for collection operators (some/all/none).
|
|
8
112
|
* When iterating over an array, provides access to the current element.
|
|
@@ -17,7 +121,7 @@ export interface EvalContext {
|
|
|
17
121
|
*/
|
|
18
122
|
export interface EvalState {
|
|
19
123
|
/** The schema being evaluated (mutable copy) */
|
|
20
|
-
schema:
|
|
124
|
+
schema: TenetSchema;
|
|
21
125
|
/** Effective date for temporal routing */
|
|
22
126
|
effectiveDate: Date;
|
|
23
127
|
/** Tracks which fields were set by which rule (cycle detection) */
|
|
@@ -25,7 +129,9 @@ export interface EvalState {
|
|
|
25
129
|
/** Current element context for some/all/none operators */
|
|
26
130
|
currentElement?: unknown;
|
|
27
131
|
/** Accumulated validation errors */
|
|
28
|
-
errors:
|
|
132
|
+
errors: ValidationError[];
|
|
133
|
+
/** Cycle detection for derived fields */
|
|
134
|
+
derivedInProgress: Set<string>;
|
|
29
135
|
}
|
|
30
136
|
/**
|
|
31
137
|
* Document status values
|
package/dist/core/types.js
CHANGED
package/dist/core/validate.d.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Definition validation and status determination.
|
|
3
3
|
* Validates types, constraints, and required fields.
|
|
4
4
|
*/
|
|
5
|
-
import type { EvalState, DocStatus } from './types.js';
|
|
5
|
+
import type { EvalState, ErrorKind, DocStatus, Action } from './types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Add an error to the state's error list.
|
|
8
8
|
*/
|
|
9
|
-
export declare function addError(state: EvalState, fieldId: string, ruleId: string, message: string, lawRef?: string): void;
|
|
9
|
+
export declare function addError(state: EvalState, fieldId: string, ruleId: string, kind: ErrorKind, message: string, lawRef?: string): void;
|
|
10
10
|
/**
|
|
11
11
|
* Validate all definitions for type correctness and required fields.
|
|
12
12
|
*/
|
|
@@ -14,8 +14,8 @@ export declare function validateDefinitions(state: EvalState): void;
|
|
|
14
14
|
/**
|
|
15
15
|
* Check attestations for required signatures.
|
|
16
16
|
*/
|
|
17
|
-
export declare function checkAttestations(state: EvalState, applyAction: (action:
|
|
17
|
+
export declare function checkAttestations(state: EvalState, applyAction: (action: Action, ruleId: string, lawRef: string) => void): void;
|
|
18
18
|
/**
|
|
19
|
-
* Determine document status based on
|
|
19
|
+
* Determine document status based on ErrorKind.
|
|
20
20
|
*/
|
|
21
21
|
export declare function determineStatus(state: EvalState): DocStatus;
|
package/dist/core/validate.js
CHANGED
|
@@ -6,10 +6,11 @@ import { toFloat, parseDate } from './operators.js';
|
|
|
6
6
|
/**
|
|
7
7
|
* Add an error to the state's error list.
|
|
8
8
|
*/
|
|
9
|
-
export function addError(state, fieldId, ruleId, message, lawRef) {
|
|
9
|
+
export function addError(state, fieldId, ruleId, kind, message, lawRef) {
|
|
10
10
|
state.errors.push({
|
|
11
11
|
field_id: fieldId || undefined,
|
|
12
12
|
rule_id: ruleId || undefined,
|
|
13
|
+
kind,
|
|
13
14
|
message,
|
|
14
15
|
law_ref: lawRef || undefined,
|
|
15
16
|
});
|
|
@@ -28,10 +29,10 @@ function isValidOption(value, options) {
|
|
|
28
29
|
*/
|
|
29
30
|
function validateNumericConstraints(state, id, value, def) {
|
|
30
31
|
if (def.min !== undefined && value < def.min) {
|
|
31
|
-
addError(state, id, '', `Field '${id}' value ${value.toFixed(2)} is below minimum ${def.min.toFixed(2)}`);
|
|
32
|
+
addError(state, id, '', 'constraint_violation', `Field '${id}' value ${value.toFixed(2)} is below minimum ${def.min.toFixed(2)}`);
|
|
32
33
|
}
|
|
33
34
|
if (def.max !== undefined && value > def.max) {
|
|
34
|
-
addError(state, id, '', `Field '${id}' value ${value.toFixed(2)} exceeds maximum ${def.max.toFixed(2)}`);
|
|
35
|
+
addError(state, id, '', 'constraint_violation', `Field '${id}' value ${value.toFixed(2)} exceeds maximum ${def.max.toFixed(2)}`);
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
@@ -39,16 +40,16 @@ function validateNumericConstraints(state, id, value, def) {
|
|
|
39
40
|
*/
|
|
40
41
|
function validateStringConstraints(state, id, value, def) {
|
|
41
42
|
if (def.min_length !== undefined && value.length < def.min_length) {
|
|
42
|
-
addError(state, id, '', `Field '${id}' is too short (minimum ${def.min_length} characters)`);
|
|
43
|
+
addError(state, id, '', 'constraint_violation', `Field '${id}' is too short (minimum ${def.min_length} characters)`);
|
|
43
44
|
}
|
|
44
45
|
if (def.max_length !== undefined && value.length > def.max_length) {
|
|
45
|
-
addError(state, id, '', `Field '${id}' is too long (maximum ${def.max_length} characters)`);
|
|
46
|
+
addError(state, id, '', 'constraint_violation', `Field '${id}' is too long (maximum ${def.max_length} characters)`);
|
|
46
47
|
}
|
|
47
48
|
if (def.pattern) {
|
|
48
49
|
try {
|
|
49
50
|
const regex = new RegExp(def.pattern);
|
|
50
51
|
if (!regex.test(value)) {
|
|
51
|
-
addError(state, id, '', `Field '${id}' does not match required pattern`);
|
|
52
|
+
addError(state, id, '', 'constraint_violation', `Field '${id}' does not match required pattern`);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
catch {
|
|
@@ -58,13 +59,19 @@ function validateStringConstraints(state, id, value, def) {
|
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
60
61
|
* Validate a single definition's type and constraints.
|
|
62
|
+
* Array values are allowed — the declared type describes the element type,
|
|
63
|
+
* used by collection operators (some/all/none). Scalar validation is skipped for arrays.
|
|
61
64
|
*/
|
|
62
65
|
function validateType(state, id, def) {
|
|
63
66
|
const value = def.value;
|
|
67
|
+
// Skip scalar validation for array values (used with some/all/none operators)
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
64
71
|
switch (def.type) {
|
|
65
72
|
case 'string': {
|
|
66
73
|
if (typeof value !== 'string') {
|
|
67
|
-
addError(state, id, '', `Field '${id}' must be a string`);
|
|
74
|
+
addError(state, id, '', 'type_mismatch', `Field '${id}' must be a string`);
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
70
77
|
validateStringConstraints(state, id, value, def);
|
|
@@ -74,7 +81,7 @@ function validateType(state, id, def) {
|
|
|
74
81
|
case 'currency': {
|
|
75
82
|
const [numVal, ok] = toFloat(value);
|
|
76
83
|
if (!ok) {
|
|
77
|
-
addError(state, id, '', `Field '${id}' must be a number`);
|
|
84
|
+
addError(state, id, '', 'type_mismatch', `Field '${id}' must be a number`);
|
|
78
85
|
return;
|
|
79
86
|
}
|
|
80
87
|
validateNumericConstraints(state, id, numVal, def);
|
|
@@ -82,30 +89,30 @@ function validateType(state, id, def) {
|
|
|
82
89
|
}
|
|
83
90
|
case 'boolean': {
|
|
84
91
|
if (typeof value !== 'boolean') {
|
|
85
|
-
addError(state, id, '', `Field '${id}' must be a boolean`);
|
|
92
|
+
addError(state, id, '', 'type_mismatch', `Field '${id}' must be a boolean`);
|
|
86
93
|
}
|
|
87
94
|
break;
|
|
88
95
|
}
|
|
89
96
|
case 'select': {
|
|
90
97
|
if (typeof value !== 'string') {
|
|
91
|
-
addError(state, id, '', `Field '${id}' must be a string`);
|
|
98
|
+
addError(state, id, '', 'type_mismatch', `Field '${id}' must be a string`);
|
|
92
99
|
return;
|
|
93
100
|
}
|
|
94
101
|
if (!isValidOption(value, def.options)) {
|
|
95
|
-
addError(state, id, '', `Field '${id}' value '${value}' is not a valid option`);
|
|
102
|
+
addError(state, id, '', 'constraint_violation', `Field '${id}' value '${value}' is not a valid option`);
|
|
96
103
|
}
|
|
97
104
|
break;
|
|
98
105
|
}
|
|
99
106
|
case 'attestation': {
|
|
100
107
|
if (typeof value !== 'boolean') {
|
|
101
|
-
addError(state, id, '', `Attestation '${id}' must be a boolean`);
|
|
108
|
+
addError(state, id, '', 'type_mismatch', `Attestation '${id}' must be a boolean`);
|
|
102
109
|
}
|
|
103
110
|
break;
|
|
104
111
|
}
|
|
105
112
|
case 'date': {
|
|
106
113
|
const [, ok] = parseDate(value);
|
|
107
114
|
if (!ok) {
|
|
108
|
-
addError(state, id, '', `Field '${id}' must be a valid date`);
|
|
115
|
+
addError(state, id, '', 'type_mismatch', `Field '${id}' must be a valid date`);
|
|
109
116
|
}
|
|
110
117
|
break;
|
|
111
118
|
}
|
|
@@ -122,11 +129,11 @@ export function validateDefinitions(state) {
|
|
|
122
129
|
// Check required fields
|
|
123
130
|
if (def.required) {
|
|
124
131
|
if (def.value === undefined || def.value === null) {
|
|
125
|
-
addError(state, id, '', `Required field '${id}' is missing`);
|
|
132
|
+
addError(state, id, '', 'missing_required', `Required field '${id}' is missing`);
|
|
126
133
|
}
|
|
127
134
|
else if ((def.type === 'string' || def.type === 'select') && def.value === '') {
|
|
128
135
|
// Empty string is also considered "missing" for required string/select fields
|
|
129
|
-
addError(state, id, '', `Required field '${id}' is missing`);
|
|
136
|
+
addError(state, id, '', 'missing_required', `Required field '${id}' is missing`);
|
|
130
137
|
}
|
|
131
138
|
}
|
|
132
139
|
// Validate type if value is present
|
|
@@ -145,7 +152,7 @@ export function checkAttestations(state, applyAction) {
|
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
147
154
|
if (def.required && def.value !== true) {
|
|
148
|
-
addError(state, id, '', `Required attestation '${id}' not confirmed`);
|
|
155
|
+
addError(state, id, '', 'attestation_incomplete', `Required attestation '${id}' not confirmed`);
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
// Check rich attestations
|
|
@@ -163,38 +170,30 @@ export function checkAttestations(state, applyAction) {
|
|
|
163
170
|
// Validate required attestations
|
|
164
171
|
if (att.required) {
|
|
165
172
|
if (!att.signed) {
|
|
166
|
-
addError(state, id, '', `Required attestation '${id}' not signed`, att.law_ref);
|
|
173
|
+
addError(state, id, '', 'attestation_incomplete', `Required attestation '${id}' not signed`, att.law_ref);
|
|
167
174
|
}
|
|
168
175
|
else if (!att.evidence || !att.evidence.provider_audit_id) {
|
|
169
|
-
addError(state, id, '', `Attestation '${id}' signed but missing evidence`, att.law_ref);
|
|
176
|
+
addError(state, id, '', 'attestation_incomplete', `Attestation '${id}' signed but missing evidence`, att.law_ref);
|
|
170
177
|
}
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
}
|
|
174
181
|
/**
|
|
175
|
-
* Determine document status based on
|
|
182
|
+
* Determine document status based on ErrorKind.
|
|
176
183
|
*/
|
|
177
184
|
export function determineStatus(state) {
|
|
178
|
-
let hasTypeErrors = false;
|
|
179
|
-
let hasMissingRequired = false;
|
|
180
|
-
let hasMissingAttestations = false;
|
|
181
185
|
for (const err of state.errors) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
hasTypeErrors = true;
|
|
185
|
-
}
|
|
186
|
-
else if (msg.includes('missing') || msg.includes('Required field')) {
|
|
187
|
-
hasMissingRequired = true;
|
|
188
|
-
}
|
|
189
|
-
else if (msg.includes('attestation')) {
|
|
190
|
-
hasMissingAttestations = true;
|
|
191
|
-
}
|
|
186
|
+
if (err.kind === 'type_mismatch')
|
|
187
|
+
return 'INVALID';
|
|
192
188
|
}
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
for (const err of state.errors) {
|
|
190
|
+
if (err.kind === 'missing_required' || err.kind === 'attestation_incomplete') {
|
|
191
|
+
return 'INCOMPLETE';
|
|
192
|
+
}
|
|
195
193
|
}
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
for (const err of state.errors) {
|
|
195
|
+
if (err.kind === 'constraint_violation')
|
|
196
|
+
return 'INVALID';
|
|
198
197
|
}
|
|
199
198
|
return 'READY';
|
|
200
199
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,91 +4,8 @@
|
|
|
4
4
|
* This module provides a pure TypeScript implementation of the Tenet VM.
|
|
5
5
|
* Works in both browser and Node.js environments with no WASM dependencies.
|
|
6
6
|
*/
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
error?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface TenetVerifyResult {
|
|
12
|
-
valid: boolean;
|
|
13
|
-
error?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface Evidence {
|
|
16
|
-
provider_audit_id?: string;
|
|
17
|
-
timestamp?: string;
|
|
18
|
-
signer_id?: string;
|
|
19
|
-
logic_version?: string;
|
|
20
|
-
}
|
|
21
|
-
export interface Attestation {
|
|
22
|
-
statement: string;
|
|
23
|
-
law_ref?: string;
|
|
24
|
-
required_role?: string;
|
|
25
|
-
provider?: string;
|
|
26
|
-
required?: boolean;
|
|
27
|
-
signed?: boolean;
|
|
28
|
-
evidence?: Evidence;
|
|
29
|
-
on_sign?: Action;
|
|
30
|
-
}
|
|
31
|
-
export interface TenetSchema {
|
|
32
|
-
protocol?: string;
|
|
33
|
-
schema_id?: string;
|
|
34
|
-
version?: string;
|
|
35
|
-
valid_from?: string;
|
|
36
|
-
definitions: Record<string, Definition>;
|
|
37
|
-
logic_tree?: Rule[];
|
|
38
|
-
temporal_map?: TemporalBranch[];
|
|
39
|
-
state_model?: StateModel;
|
|
40
|
-
errors?: ValidationError[];
|
|
41
|
-
status?: 'READY' | 'INCOMPLETE' | 'INVALID';
|
|
42
|
-
attestations?: Record<string, Attestation>;
|
|
43
|
-
}
|
|
44
|
-
export interface Definition {
|
|
45
|
-
type: 'string' | 'number' | 'boolean' | 'select' | 'date' | 'attestation' | 'currency';
|
|
46
|
-
value?: unknown;
|
|
47
|
-
options?: string[];
|
|
48
|
-
label?: string;
|
|
49
|
-
required?: boolean;
|
|
50
|
-
readonly?: boolean;
|
|
51
|
-
visible?: boolean;
|
|
52
|
-
min?: number;
|
|
53
|
-
max?: number;
|
|
54
|
-
step?: number;
|
|
55
|
-
min_length?: number;
|
|
56
|
-
max_length?: number;
|
|
57
|
-
pattern?: string;
|
|
58
|
-
ui_class?: string;
|
|
59
|
-
ui_message?: string;
|
|
60
|
-
}
|
|
61
|
-
export interface Rule {
|
|
62
|
-
id: string;
|
|
63
|
-
law_ref?: string;
|
|
64
|
-
logic_version?: string;
|
|
65
|
-
when: Record<string, unknown>;
|
|
66
|
-
then: Action;
|
|
67
|
-
disabled?: boolean;
|
|
68
|
-
}
|
|
69
|
-
export interface Action {
|
|
70
|
-
set?: Record<string, unknown>;
|
|
71
|
-
ui_modify?: Record<string, unknown>;
|
|
72
|
-
error_msg?: string;
|
|
73
|
-
}
|
|
74
|
-
export interface TemporalBranch {
|
|
75
|
-
valid_range: [string | null, string | null];
|
|
76
|
-
logic_version: string;
|
|
77
|
-
status: 'ACTIVE' | 'ARCHIVED';
|
|
78
|
-
}
|
|
79
|
-
export interface StateModel {
|
|
80
|
-
inputs: string[];
|
|
81
|
-
derived: Record<string, DerivedDef>;
|
|
82
|
-
}
|
|
83
|
-
export interface DerivedDef {
|
|
84
|
-
eval: Record<string, unknown>;
|
|
85
|
-
}
|
|
86
|
-
export interface ValidationError {
|
|
87
|
-
field_id?: string;
|
|
88
|
-
rule_id?: string;
|
|
89
|
-
message: string;
|
|
90
|
-
law_ref?: string;
|
|
91
|
-
}
|
|
7
|
+
export type { TenetSchema, TenetResult, TenetVerifyResult, VerifyIssue, VerifyIssueCode, Definition, Rule, Action, TemporalBranch, StateModel, DerivedDef, ValidationError, Evidence, Attestation, ErrorKind, } from './core/types.js';
|
|
8
|
+
import type { TenetSchema, TenetResult, TenetVerifyResult } from './core/types.js';
|
|
92
9
|
/**
|
|
93
10
|
* Initialize the Tenet VM.
|
|
94
11
|
* This is a no-op in the pure TypeScript implementation (kept for backwards compatibility).
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* This module provides a pure TypeScript implementation of the Tenet VM.
|
|
5
5
|
* Works in both browser and Node.js environments with no WASM dependencies.
|
|
6
6
|
*/
|
|
7
|
-
// Import core engine functions
|
|
8
7
|
import { run as coreRun, verify as coreVerify } from './core/engine.js';
|
|
9
8
|
/**
|
|
10
9
|
* Initialize the Tenet VM.
|
package/package.json
CHANGED