@hyperscale0/udl 1.0.0 → 2.0.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +19 -4
  2. package/README.md +24 -36
  3. package/conformance/invalid/invalid-journeys.expected.json +26 -0
  4. package/conformance/invalid/invalid-journeys.udl +79 -0
  5. package/conformance/valid/commerce-escrow.expected.json +1 -1
  6. package/conformance/valid/commerce-escrow.udl +1 -1
  7. package/conformance/valid/hand-edited.expected.json +1 -1
  8. package/conformance/valid/hand-edited.udl +1 -1
  9. package/conformance/valid/minimal.expected.json +1 -1
  10. package/conformance/valid/minimal.udl +23 -0
  11. package/dist/check-profiles.d.ts.map +1 -1
  12. package/dist/diagnostics.d.ts +31 -1
  13. package/dist/diagnostics.d.ts.map +1 -1
  14. package/dist/diagnostics.js +30 -0
  15. package/dist/diagnostics.js.map +1 -1
  16. package/dist/effects.d.ts.map +1 -1
  17. package/dist/evolution.js +30 -9
  18. package/dist/evolution.js.map +1 -1
  19. package/dist/finance.d.ts.map +1 -1
  20. package/dist/finance.js +37 -18
  21. package/dist/finance.js.map +1 -1
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/limits.d.ts +1 -1
  27. package/dist/limits.d.ts.map +1 -1
  28. package/dist/limits.js +7 -4
  29. package/dist/limits.js.map +1 -1
  30. package/dist/schema.d.ts +258 -147
  31. package/dist/schema.d.ts.map +1 -1
  32. package/dist/schema.js +33 -10
  33. package/dist/schema.js.map +1 -1
  34. package/dist/validation.d.ts +5 -1
  35. package/dist/validation.d.ts.map +1 -1
  36. package/dist/validation.js +176 -7
  37. package/dist/validation.js.map +1 -1
  38. package/docs/assets/udl.svg +18 -0
  39. package/docs/guide/03-laws.md +1 -1
  40. package/docs/guide/08-implementing.md +1 -1
  41. package/docs/llms-full.txt +62 -29
  42. package/docs/llms.txt +1 -1
  43. package/docs/reference/clauses.md +29 -1
  44. package/docs/reference/cli.md +1 -1
  45. package/docs/reference/diagnostics.md +32 -27
  46. package/package.json +6 -5
  47. package/spec/README.md +8 -16
  48. package/spec/udl.schema.json +83 -15
  49. package/src/diagnostics.ts +32 -0
  50. package/src/evolution.ts +31 -13
  51. package/src/finance.ts +53 -20
  52. package/src/index.ts +6 -0
  53. package/src/limits.ts +7 -4
  54. package/src/schema.ts +48 -10
  55. package/src/validation.ts +252 -6
package/src/validation.ts CHANGED
@@ -54,7 +54,15 @@ export class UdlError extends Error {
54
54
  }
55
55
  }
56
56
 
57
- export function validateUdl(value: unknown): UdlValidationResult {
57
+ export interface UdlValidationOptions {
58
+ /** New compilers enforce bindings without rejecting previously frozen UDL. */
59
+ readonly requireDecisionPartyBindings?: boolean;
60
+ }
61
+
62
+ export function validateUdl(
63
+ value: unknown,
64
+ options: UdlValidationOptions = {},
65
+ ): UdlValidationResult {
58
66
  const resourceIssue = structuralBudgetIssue(value);
59
67
  if (resourceIssue) return { issues: [resourceIssue], ok: false };
60
68
 
@@ -73,7 +81,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
73
81
  }
74
82
 
75
83
  const references = openReferenceShapeBudget();
76
- const issues = semanticIssues(parsed.data, references);
84
+ const issues = semanticIssues(parsed.data, references, options);
77
85
  if (references.exhausted) {
78
86
  return {
79
87
  issues: [
@@ -254,6 +262,7 @@ const jsonSchemaKeywords = new Set([
254
262
  "required",
255
263
  "title",
256
264
  "type",
265
+ "x-hyperscale-currency",
257
266
  "x-hyperscale-fee-collection-port",
258
267
  "x-hyperscale-reference-filter",
259
268
  ]);
@@ -261,6 +270,7 @@ const jsonSchemaKeywords = new Set([
261
270
  function semanticIssues(
262
271
  document: UdlDocument,
263
272
  references: ReferenceShapeBudget,
273
+ options: UdlValidationOptions,
264
274
  ): UdlIssue[] {
265
275
  const issues: UdlIssue[] = [];
266
276
  const add: AddIssue = (path, message, code) => {
@@ -297,6 +307,7 @@ function semanticIssues(
297
307
  );
298
308
 
299
309
  validateCompositionDials(document, add);
310
+ validateInstrumentJourneys(document, add);
300
311
 
301
312
  const subjects = new Map(
302
313
  document.subjects.map((subject) => [subject.kind, subject] as const),
@@ -319,6 +330,27 @@ function semanticIssues(
319
330
 
320
331
  for (const [instrumentIndex, instrument] of document.instruments.entries()) {
321
332
  for (const [actionName, action] of Object.entries(instrument.actions)) {
333
+ if (options.requireDecisionPartyBindings) {
334
+ for (const [index, role] of (
335
+ action.port?.allowedParties ?? []
336
+ ).entries()) {
337
+ if (!Object.hasOwn(instrument.parties ?? {}, role)) {
338
+ add(
339
+ [
340
+ "instruments",
341
+ instrumentIndex,
342
+ "actions",
343
+ actionName,
344
+ "port",
345
+ "allowedParties",
346
+ index,
347
+ ],
348
+ `decision port allows party role ${role}, which the instrument does not declare`,
349
+ "UDL5008",
350
+ );
351
+ }
352
+ }
353
+ }
322
354
  if (!action.effects) continue;
323
355
  const expected = deriveUdlActionEffects(action, udlClauseVocabulary);
324
356
  for (const kind of udlEffectKinds) {
@@ -366,6 +398,162 @@ function semanticIssues(
366
398
  return issues;
367
399
  }
368
400
 
401
+ const scopedIdPattern =
402
+ /^\^([a-z]{2,8})_\(sandbox\|live\)_\[a-z0-9\]\{8,64\}\$$/;
403
+
404
+ function camel(value: string): string {
405
+ return value.replace(/_([a-z0-9])/g, (_match, character: string) =>
406
+ character.toUpperCase(),
407
+ );
408
+ }
409
+
410
+ function journeyReferenceFields(
411
+ instrument: UdlInstrument,
412
+ actionName: string,
413
+ ): ReadonlyMap<string, string> {
414
+ const action = instrument.actions[actionName];
415
+ if (!action) return new Map();
416
+ const fields = new Map<string, string>();
417
+ const addSchema = (name: string, schema: unknown) => {
418
+ const pattern = recordValue(schema).pattern;
419
+ if (typeof pattern !== "string") return;
420
+ const prefix = scopedIdPattern.exec(pattern)?.[1];
421
+ if (prefix) fields.set(name, prefix);
422
+ };
423
+ if (actionName === "create") {
424
+ const derivedFields = new Set(
425
+ (action.requiresRefs ?? []).flatMap((gate) =>
426
+ Object.keys(gate.bind ?? {}),
427
+ ),
428
+ );
429
+ for (const name of instrument.required) {
430
+ if (!derivedFields.has(name)) addSchema(name, instrument.fields[name]);
431
+ }
432
+ } else {
433
+ fields.set(`${camel(instrument.id)}Id`, instrument.idPrefix);
434
+ }
435
+ const input = recordValue(action.input);
436
+ const required = Array.isArray(input.required)
437
+ ? input.required.filter((name): name is string => typeof name === "string")
438
+ : [];
439
+ const properties = recordValue(input.properties);
440
+ for (const name of required) addSchema(name, properties[name]);
441
+ return fields;
442
+ }
443
+
444
+ /**
445
+ * Validate the part of authored journeys that canonical UDL can prove alone.
446
+ * A caller with the operation catalog validates root-operation examples and
447
+ * cross-kind bindings. UDL owns local examples, lifecycle order, and local
448
+ * reference completeness so raw UDL cannot bypass those laws.
449
+ */
450
+ function validateInstrumentJourneys(
451
+ document: UdlDocument,
452
+ add: AddIssue,
453
+ ): void {
454
+ const instruments = new Map(
455
+ document.instruments.map(
456
+ (instrument) => [instrument.id, instrument] as const,
457
+ ),
458
+ );
459
+ for (const [instrumentIndex, owner] of document.instruments.entries()) {
460
+ for (const [journeyIndex, journey] of (owner.journeys ?? []).entries()) {
461
+ const base = [
462
+ "instruments",
463
+ instrumentIndex,
464
+ "journeys",
465
+ journeyIndex,
466
+ ] as const;
467
+ const seen = new Set<string>();
468
+ const createdKindByStep = new Map<string, string>();
469
+ const stateByStep = new Map<string, string>();
470
+ for (const [stepIndex, step] of journey.steps.entries()) {
471
+ const stepBase = [...base, "steps", stepIndex] as const;
472
+ if (step.id) {
473
+ if (seen.has(step.id)) {
474
+ add(
475
+ [...stepBase, "id"],
476
+ `journey ${journey.id} declares step id ${step.id} more than once`,
477
+ "journey_duplicate_step_id",
478
+ );
479
+ }
480
+ }
481
+ const [instrumentId, actionName] = step.operation.split(".");
482
+ const instrument = instrumentId
483
+ ? instruments.get(instrumentId)
484
+ : undefined;
485
+ const action =
486
+ instrument && actionName ? instrument.actions[actionName] : undefined;
487
+ if (!instrument) {
488
+ if (step.id) seen.add(step.id);
489
+ continue;
490
+ }
491
+ if (!action || !actionName) {
492
+ add(
493
+ [...stepBase, "operation"],
494
+ `journey ${journey.id} names unknown operation ${step.operation}`,
495
+ "journey_unknown_operation",
496
+ );
497
+ continue;
498
+ }
499
+ if (
500
+ !action.examples?.some((example) => example.name === step.example)
501
+ ) {
502
+ add(
503
+ [...stepBase, "example"],
504
+ `journey ${journey.id} names unknown example ${step.example} on ${step.operation}`,
505
+ "journey_unknown_example",
506
+ );
507
+ }
508
+ for (const [field, prefix] of journeyReferenceFields(
509
+ instrument,
510
+ actionName,
511
+ )) {
512
+ const producer = step.bind[field];
513
+ if (!producer || !seen.has(producer)) {
514
+ add(
515
+ [...stepBase, "bind", field],
516
+ `journey ${journey.id} must bind ${step.operation}.${field} to an earlier step`,
517
+ "journey_unbound_reference",
518
+ );
519
+ continue;
520
+ }
521
+ const producedPrefix = createdKindByStep.get(producer);
522
+ if (producedPrefix && producedPrefix !== prefix) {
523
+ add(
524
+ [...stepBase, "bind", field],
525
+ `journey ${journey.id} binds ${step.operation}.${field} to ${producer}, which creates ${producedPrefix} instead of ${prefix}`,
526
+ "journey_unbound_reference",
527
+ );
528
+ }
529
+ }
530
+ if (actionName === "create") {
531
+ if (step.id) {
532
+ createdKindByStep.set(step.id, instrument.idPrefix);
533
+ stateByStep.set(step.id, instrument.lifecycle.initial);
534
+ seen.add(step.id);
535
+ }
536
+ continue;
537
+ }
538
+ const transition = instrument.lifecycle.transitions[actionName];
539
+ if (!transition) continue;
540
+ const ownProducer = step.bind[`${camel(instrument.id)}Id`];
541
+ const state = ownProducer ? stateByStep.get(ownProducer) : undefined;
542
+ if (state && !transition.from.includes(state)) {
543
+ add(
544
+ [...stepBase, "operation"],
545
+ `journey ${journey.id} runs ${step.operation} from ${state}; allowed states are ${transition.from.join(", ")}`,
546
+ "journey_invalid_transition",
547
+ );
548
+ } else if (ownProducer) {
549
+ stateByStep.set(ownProducer, transition.to);
550
+ }
551
+ if (step.id) seen.add(step.id);
552
+ }
553
+ }
554
+ }
555
+ }
556
+
369
557
  function structuralBudgetIssue(value: unknown): UdlIssue | undefined {
370
558
  let discovered = 1;
371
559
  let nodes = 0;
@@ -737,6 +925,18 @@ function validateJsonSchema(
737
925
  );
738
926
  }
739
927
  }
928
+ if (
929
+ Object.hasOwn(schema, "x-hyperscale-currency") &&
930
+ (schema.type !== "string" ||
931
+ typeof schema["x-hyperscale-currency"] !== "string" ||
932
+ !/^[A-Z]{3}$/.test(schema["x-hyperscale-currency"]))
933
+ ) {
934
+ add(
935
+ [...path, "x-hyperscale-currency"],
936
+ "x-hyperscale-currency must be a three-letter uppercase code on a string schema",
937
+ "UDL6001",
938
+ );
939
+ }
740
940
  if (
741
941
  Object.hasOwn(schema, "x-hyperscale-fee-collection-port") &&
742
942
  (schema.type !== "string" ||
@@ -3702,6 +3902,47 @@ function validateGateShape(
3702
3902
  );
3703
3903
  }
3704
3904
  }
3905
+ if (gate.dateComparison) {
3906
+ const { localPath, referencedPath } = gate.dateComparison;
3907
+ const localFieldName = localPath.startsWith("fields.")
3908
+ ? localPath.slice("fields.".length)
3909
+ : undefined;
3910
+ const localField = localFieldName
3911
+ ? instrument.fields[localFieldName]
3912
+ : undefined;
3913
+ const isLocalDate =
3914
+ localField?.type === "string" &&
3915
+ typeof localField.format === "string" &&
3916
+ ["hyperscale-date-time", "hyperscale-date", "date-time", "date"].includes(
3917
+ localField.format,
3918
+ );
3919
+ if (!isLocalDate) {
3920
+ add(
3921
+ [...base, "dateComparison", "localPath"],
3922
+ `dateComparison localPath ${localPath} must target a declared date or date-time field`,
3923
+ "UDL5001",
3924
+ );
3925
+ }
3926
+ const referencedFieldName = referencedPath.startsWith("fields.")
3927
+ ? referencedPath.slice("fields.".length)
3928
+ : undefined;
3929
+ const referencedField = referencedFieldName
3930
+ ? target.fields[referencedFieldName]
3931
+ : undefined;
3932
+ const isReferencedDate =
3933
+ referencedField?.type === "string" &&
3934
+ typeof referencedField.format === "string" &&
3935
+ ["hyperscale-date-time", "hyperscale-date", "date-time", "date"].includes(
3936
+ referencedField.format,
3937
+ );
3938
+ if (!isReferencedDate) {
3939
+ add(
3940
+ [...base, "dateComparison", "referencedPath"],
3941
+ `dateComparison referencedPath ${referencedPath} must target a declared date or date-time field on ${target.id}`,
3942
+ "UDL5001",
3943
+ );
3944
+ }
3945
+ }
3705
3946
  }
3706
3947
 
3707
3948
  /** Every `refs.<key>` a instrument's instances can legitimately carry. */
@@ -4130,13 +4371,20 @@ function validateAggregates(
4130
4371
  ] as const;
4131
4372
  const sum = "childField" in aggregate ? aggregate : undefined;
4132
4373
  const parentField = instrument.fields[aggregate.parentField];
4374
+ const child = instruments.get(aggregate.childInstrumentId);
4375
+ const childField = sum && child ? child.fields[sum.childField] : undefined;
4376
+ const isIntegerSum =
4377
+ sum !== undefined &&
4378
+ parentField?.type === "integer" &&
4379
+ instrument.required.includes(aggregate.parentField) &&
4380
+ childField?.type === "integer";
4133
4381
  if (!parentField) {
4134
4382
  add(
4135
4383
  [...aggregateBase, "parentField"],
4136
4384
  `aggregate references unknown parent field ${aggregate.parentField}`,
4137
4385
  "UDL5004",
4138
4386
  );
4139
- } else if (sum && !isMoneySchema(parentField)) {
4387
+ } else if (sum && !isMoneySchema(parentField) && !isIntegerSum) {
4140
4388
  add(
4141
4389
  [...aggregateBase, "parentField"],
4142
4390
  `${instrument.id}.${aggregate.parentField} must be a money field`,
@@ -4159,7 +4407,6 @@ function validateAggregates(
4159
4407
  "UDL5004",
4160
4408
  );
4161
4409
  }
4162
- const child = instruments.get(aggregate.childInstrumentId);
4163
4410
  if (!child) {
4164
4411
  add(
4165
4412
  [...aggregateBase, "childInstrumentId"],
@@ -4169,14 +4416,13 @@ function validateAggregates(
4169
4416
  return;
4170
4417
  }
4171
4418
  if (sum) {
4172
- const childField = child.fields[sum.childField];
4173
4419
  if (!childField) {
4174
4420
  add(
4175
4421
  [...aggregateBase, "childField"],
4176
4422
  `aggregate references unknown child field ${sum.childField}`,
4177
4423
  "UDL5004",
4178
4424
  );
4179
- } else if (!isMoneySchema(childField)) {
4425
+ } else if (!isMoneySchema(childField) && !isIntegerSum) {
4180
4426
  add(
4181
4427
  [...aggregateBase, "childField"],
4182
4428
  `${child.id}.${sum.childField} must be a money field`,