@hyperscale0/udl 1.0.0 → 2.0.1
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/CHANGELOG.md +23 -4
- package/README.md +24 -36
- package/conformance/invalid/invalid-journeys.expected.json +26 -0
- package/conformance/invalid/invalid-journeys.udl +79 -0
- package/conformance/valid/commerce-escrow.expected.json +1 -1
- package/conformance/valid/commerce-escrow.udl +1 -1
- package/conformance/valid/hand-edited.expected.json +1 -1
- package/conformance/valid/hand-edited.udl +1 -1
- package/conformance/valid/minimal.expected.json +1 -1
- package/conformance/valid/minimal.udl +23 -0
- package/dist/check-profiles.d.ts.map +1 -1
- package/dist/diagnostics.d.ts +31 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +30 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/effects.d.ts.map +1 -1
- package/dist/evolution.js +30 -9
- package/dist/evolution.js.map +1 -1
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +37 -18
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +1 -1
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +7 -4
- package/dist/limits.js.map +1 -1
- package/dist/schema.d.ts +258 -147
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +33 -10
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +5 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +176 -7
- package/dist/validation.js.map +1 -1
- package/docs/assets/udl.svg +18 -0
- package/docs/guide/03-laws.md +1 -1
- package/docs/guide/08-implementing.md +1 -1
- package/docs/llms-full.txt +62 -29
- package/docs/llms.txt +1 -1
- package/docs/reference/clauses.md +29 -1
- package/docs/reference/cli.md +1 -1
- package/docs/reference/diagnostics.md +32 -27
- package/package.json +6 -5
- package/spec/README.md +8 -16
- package/spec/udl.schema.json +83 -15
- package/src/diagnostics.ts +32 -0
- package/src/evolution.ts +31 -13
- package/src/finance.ts +53 -20
- package/src/index.ts +6 -0
- package/src/limits.ts +7 -4
- package/src/schema.ts +48 -10
- 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
|
|
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`,
|