@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.
@@ -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 Whether the transformation was valid
24
+ * @returns Structured verification result
22
25
  */
23
26
  export declare function verify(newSchema: TenetSchema | string, oldSchema: TenetSchema | string, maxIterations?: number): TenetVerifyResult;
@@ -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
- // 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
- };
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
- * Count visible fields in a schema.
241
+ * Get a sorted, comma-joined string of visible field IDs for convergence detection.
231
242
  */
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;
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
- return [false, `Unknown field '${id}' not in schema`];
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
- return [false, `Computed field '${id}' missing in submitted document`];
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
- return [false, `Computed field '${id}' mismatch: claimed ${JSON.stringify(newDef.value)}, expected ${JSON.stringify(resultDef.value)}`];
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 (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
- }
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
- return [false, `Status mismatch: claimed ${newSchema.status}, expected ${resultSchema.status}`];
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 [true, undefined];
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 Whether the transformation was valid
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 previousVisibleCount = -1;
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 { valid: false, error: `Run failed (iteration ${iteration}): ${runResult.error}` };
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
- // 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 };
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
- previousVisibleCount = currentVisibleCount;
416
+ previousVisibleIds = currentVisibleIds;
360
417
  currentSchema = resultSchema;
361
418
  }
362
- return { valid: false, error: `Verification did not converge after ${maxIterations} iterations` };
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 { valid: false, error: String(error) };
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
  }
@@ -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
- // Evaluate the derived expression on-demand
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);
@@ -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
  }
@@ -1,8 +1,112 @@
1
1
  /**
2
- * Internal types for the Tenet VM core.
3
- * Public types are re-exported from index.ts
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 type { TenetSchema, TenetResult, TenetVerifyResult, Definition, Rule, Action, TemporalBranch, StateModel, DerivedDef, ValidationError, Attestation, Evidence, } from '../index.js';
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: import('../index.js').TenetSchema;
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: import('../index.js').ValidationError[];
132
+ errors: ValidationError[];
133
+ /** Cycle detection for derived fields */
134
+ derivedInProgress: Set<string>;
29
135
  }
30
136
  /**
31
137
  * Document status values
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal types for the Tenet VM core.
3
- * Public types are re-exported from index.ts
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
5
  export {};
@@ -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: any, ruleId: string, lawRef: string) => void): void;
17
+ export declare function checkAttestations(state: EvalState, applyAction: (action: Action, ruleId: string, lawRef: string) => void): void;
18
18
  /**
19
- * Determine document status based on validation errors.
19
+ * Determine document status based on ErrorKind.
20
20
  */
21
21
  export declare function determineStatus(state: EvalState): DocStatus;
@@ -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 validation errors.
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
- const msg = err.message;
183
- if (msg.includes('must be a')) {
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
- if (hasTypeErrors) {
194
- return 'INVALID';
189
+ for (const err of state.errors) {
190
+ if (err.kind === 'missing_required' || err.kind === 'attestation_incomplete') {
191
+ return 'INCOMPLETE';
192
+ }
195
193
  }
196
- if (hasMissingRequired || hasMissingAttestations) {
197
- return 'INCOMPLETE';
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 interface TenetResult {
8
- result?: TenetSchema;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dlovans/tenet-core",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Declarative logic VM for JSON schemas - reactive validation, temporal routing, and computed state",
6
6
  "main": "dist/index.js",