@hyperscale0/udl 1.0.265 → 1.0.284

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 (62) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/SECURITY.md +15 -14
  4. package/dist/diagnostics.d.ts +7 -6
  5. package/dist/diagnostics.d.ts.map +1 -1
  6. package/dist/diagnostics.js.map +1 -1
  7. package/dist/field-value.d.ts.map +1 -1
  8. package/dist/field-value.js +5 -6
  9. package/dist/field-value.js.map +1 -1
  10. package/dist/finance.d.ts +2 -6
  11. package/dist/finance.d.ts.map +1 -1
  12. package/dist/finance.js +20 -14
  13. package/dist/finance.js.map +1 -1
  14. package/dist/limits.d.ts +0 -1
  15. package/dist/limits.d.ts.map +1 -1
  16. package/dist/limits.js +0 -1
  17. package/dist/limits.js.map +1 -1
  18. package/dist/object-contract.d.ts +2 -2
  19. package/dist/object-contract.d.ts.map +1 -1
  20. package/dist/object-contract.js +4 -4
  21. package/dist/object-contract.js.map +1 -1
  22. package/dist/object-semantics-schema.d.ts +0 -10
  23. package/dist/object-semantics-schema.d.ts.map +1 -1
  24. package/dist/object-semantics.d.ts.map +1 -1
  25. package/dist/object-semantics.js +9 -9
  26. package/dist/object-semantics.js.map +1 -1
  27. package/dist/object-structures.d.ts.map +1 -1
  28. package/dist/object-structures.js +2 -3
  29. package/dist/object-structures.js.map +1 -1
  30. package/dist/primitives.d.ts +12 -0
  31. package/dist/primitives.d.ts.map +1 -0
  32. package/dist/primitives.js +21 -0
  33. package/dist/primitives.js.map +1 -0
  34. package/dist/reporting-validation.d.ts.map +1 -1
  35. package/dist/reporting-validation.js +2 -1
  36. package/dist/reporting-validation.js.map +1 -1
  37. package/dist/reporting.d.ts +0 -5
  38. package/dist/reporting.d.ts.map +1 -1
  39. package/dist/reporting.js +0 -5
  40. package/dist/reporting.js.map +1 -1
  41. package/dist/schema.d.ts +1 -11
  42. package/dist/schema.d.ts.map +1 -1
  43. package/dist/schema.js +2 -1
  44. package/dist/schema.js.map +1 -1
  45. package/dist/validation.d.ts.map +1 -1
  46. package/dist/validation.js +20 -43
  47. package/dist/validation.js.map +1 -1
  48. package/package.json +2 -2
  49. package/spec/README.md +24 -23
  50. package/spec/udl.schema.json +1 -36
  51. package/src/diagnostics.ts +7 -6
  52. package/src/field-value.ts +5 -6
  53. package/src/finance.ts +20 -22
  54. package/src/limits.ts +0 -1
  55. package/src/object-contract.ts +4 -10
  56. package/src/object-semantics.ts +14 -13
  57. package/src/object-structures.ts +2 -4
  58. package/src/primitives.ts +29 -0
  59. package/src/reporting-validation.ts +2 -1
  60. package/src/reporting.ts +0 -5
  61. package/src/schema.ts +2 -1
  62. package/src/validation.ts +22 -65
package/spec/README.md CHANGED
@@ -24,22 +24,21 @@ A Build freezes this contract and its bindings for the executor.
24
24
  Business instructions are actions composed from typed requirements and effects.
25
25
  The four `internal_transfer` instructions are their money effects. The contract
26
26
  expresses valid conditions; execution can still refuse for the current state,
27
- authority, evidence or balance. Parsing and validation execute nothing.
27
+ authority, evidence or balance. Validation executes nothing.
28
28
 
29
29
  ## JSON admission and canonical bytes
30
30
 
31
- The parser refuses duplicate JSON member names, including escaped spellings of
32
- one name. Decoded documents must contain finite JSON data. Accessors, cycles,
33
- sparse arrays, symbol keys, hidden properties and `__proto__` members refuse
34
- before schema parsing. Shared values count once per occurrence toward the budget.
35
- Admission permits at most 100,000 visited values, 32 nesting levels, 240 code units
36
- per key and 1 MiB of UTF-8 key and string data. Source text has a separate 1 MiB
37
- byte limit. These bounds live in `src/limits.ts`.
31
+ `validateUdl` takes a decoded JSON value. The host reads and decodes document
32
+ text and owns its byte limit and duplicate member policy. Numbers must be safe
33
+ integers. Accessors, cycles, sparse arrays, symbol keys, hidden properties,
34
+ non-plain prototypes and `__proto__` members refuse before schema parsing. Shared
35
+ values count once per occurrence toward the budget. Admission permits at most
36
+ 100,000 visited values, 32 nesting levels, 240 code units per key and 1 MiB of
37
+ UTF-8 key and string data. These bounds live in `src/limits.ts`.
38
38
 
39
39
  Canonical JSON sorts object keys recursively, preserves array order and ends
40
40
  with a newline. Schema defaults are part of the admitted document. Equivalent
41
- key insertion orders and explicit defaults have the same digest. Object field
42
- compatibility and evolution comparisons use the same key ordering.
41
+ key insertion orders and explicit defaults have the same digest.
43
42
 
44
43
  ## Objects and subjects
45
44
 
@@ -66,12 +65,10 @@ Attachment parties resolve from authenticated authority, never from party accoun
66
65
  IDs supplied in a caller body.
67
66
 
68
67
  `object-contract.ts` projects `ObjectDiscovery` for
69
- `GET /v1/products/{productId}/objects`. It exports the discovery, instance, list
70
- and action response types. Discovery preserves Build identity, authored field
71
- names, the full optional create schema and each exposed action's requirements.
72
- Object instances carry `objectId`, `kind`, `revision`, optional `externalId`,
73
- `fields` and `productBuildId`. Action responses carry the object and either a
74
- financial instrument outcome or null.
68
+ `GET /v1/products/{productId}/objects` and exports the discovery and action
69
+ state types. Discovery preserves Build identity, authored field names, the full
70
+ optional create schema and each exposed action's requirements. The host defines
71
+ the object instance, list and action response types.
75
72
 
76
73
  An object may declare `entryActions`, a list of exposed attachment creation
77
74
  names. The host admits an active enrolled customer only to an entry action whose
@@ -82,8 +79,7 @@ participation never changes the owner.
82
79
  An instrument may declare `scope: "product"`. Object creation can resolve a
83
80
  reference to that instrument on another record in the same Product when every
84
81
  retained party account matches the current bindings. Otherwise references stay
85
- on the current record. Create and approve shared limits once per Product or
86
- borrower; referencing them does not create a new limit.
82
+ on the current record.
87
83
 
88
84
  ## Object operations
89
85
 
@@ -144,7 +140,8 @@ These are ordinary paired moves in HSX, not executor loan rules.
144
140
  A value is `{literal: value}` or `{field: path}`. Paths start with `self`, `input`, `subject`
145
141
  or `party`. Reference fields allow typed traversal. Account paths expose locked,
146
142
  read-only `.balance` and `.reserved` money values. `self.id`, `self.status`,
147
- `self.createdAt` and `self.now` are sealed executor values. Callers cannot set
143
+ `self.createdAt`, `self.now` and `self.productRevision` are sealed executor
144
+ values. Callers cannot set
148
145
  constants, calculated fields, account bindings or capture fields. Create supplies
149
146
  declared typed references; later actions cannot replace them. A ref declares `targetKind: "object" | "instrument"` and one target id or
150
147
  a list of 1 to 16 distinct target ids in that namespace. Its stored value is an
@@ -161,9 +158,10 @@ Calculations form a finite dependency graph. `sum`, `subtract` and `minimum`
161
158
  operate on money or integer fields, with operands of the same type as the target.
162
159
  `rate`, `multiply`, `divide` and `shift` retain their typed operands. A shift's
163
160
  `milliseconds` operand is a duration field or a non-negative safe integer literal.
164
- Zero leaves the date unchanged; duration fields still require positive values. Rate and
165
- division round down. Weighted shares also round down; residual minor units go
166
- to the declared residual account. Cash and loss use this one rounding rule.
161
+ Zero leaves the date unchanged; duration fields still require positive values. Rate,
162
+ ratio and division round down. UDL has no residual account. A program that
163
+ splits an amount computes its last share with `subtract`, so that share carries
164
+ the remainder. Cash and loss use this one rounding rule.
167
165
  There is no largest-remainder allocation. Subtraction refuses a negative result. Integer results must
168
166
  be safe integers. No calculation evaluates source text.
169
167
 
@@ -232,7 +230,10 @@ balances. An invariant holds before and after every affected transaction.
232
230
 
233
231
  `hours` converts a date path to its literal IANA timezone and accepts `[start,end)`.
234
232
  A start greater than end wraps midnight; equal endpoints admit no time.
235
- `evidence` declares subject, family, check, result and maxAge. The subject is an
233
+ `evidence` declares subject, family, check, result and maxAge. Optional `instruction`
234
+ is a path to a captured boundary instruction (HSX: `instruction self.receipt`).
235
+ Only instruction-bound requirements consume settlement evidence once; ordinary
236
+ identity evidence remains reusable. The subject is an
236
237
  account or text id. The executor selects the newest matching subject binding,
237
238
  checks its retained Build, protected input digest and adapter declaration, then
238
239
  requires the declared result and age. A newer matching refusal or pending result
@@ -1282,36 +1282,6 @@
1282
1282
  "items": {
1283
1283
  "$ref": "#/$defs/__schema40"
1284
1284
  }
1285
- },
1286
- "lockTimeoutMs": {
1287
- "type": "integer",
1288
- "minimum": 1,
1289
- "maximum": 2000,
1290
- "$ref": "#/$defs/__schema29"
1291
- },
1292
- "captureTimeoutMs": {
1293
- "type": "integer",
1294
- "minimum": 1,
1295
- "maximum": 10000,
1296
- "$ref": "#/$defs/__schema29"
1297
- },
1298
- "maxRows": {
1299
- "type": "integer",
1300
- "minimum": 1,
1301
- "maximum": 100000,
1302
- "$ref": "#/$defs/__schema29"
1303
- },
1304
- "maxJoinRows": {
1305
- "type": "integer",
1306
- "minimum": 1,
1307
- "maximum": 1000000,
1308
- "$ref": "#/$defs/__schema29"
1309
- },
1310
- "maxBytes": {
1311
- "type": "integer",
1312
- "minimum": 1024,
1313
- "maximum": 16777216,
1314
- "$ref": "#/$defs/__schema29"
1315
1285
  }
1316
1286
  },
1317
1287
  "required": [
@@ -1319,12 +1289,7 @@
1319
1289
  "required",
1320
1290
  "rowReconcile",
1321
1291
  "reconcile",
1322
- "unavailableFacts",
1323
- "lockTimeoutMs",
1324
- "captureTimeoutMs",
1325
- "maxRows",
1326
- "maxJoinRows",
1327
- "maxBytes"
1292
+ "unavailableFacts"
1328
1293
  ],
1329
1294
  "additionalProperties": false
1330
1295
  },
@@ -25,12 +25,13 @@ export interface UdlIssue {
25
25
  message: string;
26
26
  fix: string;
27
27
  category: string;
28
- stranded?: {
29
- state: string;
30
- accounts: string[];
31
- actions: string[];
32
- paths: Record<string, string[]>;
33
- };
28
+ stranded?: Stranded;
29
+ }
30
+ export interface Stranded {
31
+ state: string;
32
+ accounts: string[];
33
+ actions: string[];
34
+ paths: Record<string, string[]>;
34
35
  }
35
36
  const fixes: Record<UdlIssueCode, string> = {
36
37
  economic_party_unbound:
@@ -1,8 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { udlObjectIdSchema, type UdlField } from "./schema.js";
3
+ import { amount, integer, isMinorUnits, text } from "./primitives.js";
3
4
 
4
- const amount = z.string().regex(/^(0|[1-9][0-9]{0,17})$/);
5
- const text = z.string().min(1).max(2048);
6
5
  // HSX dates are instants; say so when a caller sends a calendar date.
7
6
  const dateTime = z.iso.datetime({
8
7
  offset: true,
@@ -18,7 +17,7 @@ export function udlFieldValueSchema(field: UdlField): z.ZodType {
18
17
  schema = amount
19
18
  .refine(
20
19
  (value) =>
21
- /^(0|[1-9][0-9]{0,17})$/.test(value) &&
20
+ isMinorUnits(value) &&
22
21
  (field.minimum === undefined ||
23
22
  BigInt(value) >= BigInt(field.minimum)) &&
24
23
  (field.maximum === undefined ||
@@ -44,7 +43,7 @@ export function udlFieldValueSchema(field: UdlField): z.ZodType {
44
43
  schema = dateTime;
45
44
  break;
46
45
  case "duration":
47
- schema = z.number().int().safe().positive();
46
+ schema = integer.positive();
48
47
  break;
49
48
  case "text": {
50
49
  let value = z
@@ -56,7 +55,7 @@ export function udlFieldValueSchema(field: UdlField): z.ZodType {
56
55
  break;
57
56
  }
58
57
  case "integer": {
59
- let value = z.number().int().safe();
58
+ let value = integer;
60
59
  if (field.minimum !== undefined) value = value.min(field.minimum);
61
60
  if (field.maximum !== undefined) value = value.max(field.maximum);
62
61
  schema = value;
@@ -78,7 +77,7 @@ export function udlFieldValueSchema(field: UdlField): z.ZodType {
78
77
  : field.item === "date"
79
78
  ? dateTime
80
79
  : field.item === "integer"
81
- ? z.number().int().safe()
80
+ ? integer
82
81
  : field.item === "ref" && field.targetKind === "object"
83
82
  ? udlObjectIdSchema
84
83
  : text;
package/src/finance.ts CHANGED
@@ -4,16 +4,13 @@ import type {
4
4
  UdlInstrument,
5
5
  UdlValue,
6
6
  } from "./schema.js";
7
+ import { targetIds } from "./primitives.js";
8
+ import type { Stranded } from "./diagnostics.js";
7
9
 
8
10
  export interface FinanceIssue {
9
11
  path: string;
10
12
  message: string;
11
- stranded?: {
12
- state: string;
13
- accounts: string[];
14
- actions: string[];
15
- paths: Record<string, string[]>;
16
- };
13
+ stranded?: Stranded;
17
14
  }
18
15
  type Sum = Map<string, bigint>;
19
16
  type Balance = Sum | null;
@@ -59,6 +56,12 @@ export function analyzeInstrumentFinance(
59
56
  .filter((f) => f.type === "account" && f.owner === "self")
60
57
  .map((f) => `self.${f.name}`),
61
58
  );
59
+ /** The owned account a `.balance` read names; this pass tracks no other account. */
60
+ const ownedAccount = (field: string): string | undefined => {
61
+ if (!field.endsWith(".balance")) return undefined;
62
+ const account = field.slice(0, -".balance".length);
63
+ return owned.has(account) ? account : undefined;
64
+ };
62
65
  const shared = new Set<string>();
63
66
  for (const owner of document?.instruments ?? []) {
64
67
  if (owner.id === instrument.id) continue;
@@ -76,10 +79,7 @@ export function analyzeInstrumentFinance(
76
79
  (field) => field.name === key,
77
80
  );
78
81
  if (reference?.type !== "ref") return [];
79
- const ids =
80
- typeof reference.target === "string"
81
- ? [reference.target]
82
- : reference.target;
82
+ const ids = targetIds(reference.target);
83
83
  return (
84
84
  document?.instruments.filter((candidate) =>
85
85
  ids.includes(candidate.id),
@@ -219,19 +219,21 @@ export function analyzeInstrumentFinance(
219
219
  ...downstream((field) => declared.has(field)),
220
220
  ]);
221
221
  let reading = 0;
222
+ // The pass keeps no reserved total per account, so a reserved read is external even on an owned account.
222
223
  const external = (field: string) =>
223
- /\.(balance|reserved)$/.test(field) && !owned.has(field.slice(0, -8));
224
+ field.endsWith(".reserved") ||
225
+ (field.endsWith(".balance") && ownedAccount(field) === undefined);
224
226
  const resolve = (sum: Sum): Balance => {
225
227
  let result: Balance = new Map();
226
228
  for (const [key, coefficient] of sum) {
227
- const account = key.slice(0, -8);
229
+ const account = ownedAccount(key);
228
230
  const scoped =
229
231
  balanceBound.has(key) ||
230
232
  fresh.has(key) ||
231
233
  live.has(key) ||
232
234
  external(key);
233
235
  const term: Balance =
234
- key.endsWith(".balance") && owned.has(account)
236
+ account !== undefined
235
237
  ? (start.get(account) ?? null)
236
238
  : new Map([[scoped ? `${scope}:${key}` : key, 1n]]);
237
239
  result = add(result, term, coefficient);
@@ -245,11 +247,8 @@ export function analyzeInstrumentFinance(
245
247
  const amount = (value: UdlValue, seen = new Set<string>()): Balance => {
246
248
  if ("field" in value) {
247
249
  if (calculated.has(value.field)) return calculated.get(value.field)!;
248
- if (
249
- value.field.endsWith(".balance") &&
250
- owned.has(value.field.slice(0, -8))
251
- )
252
- return state.balances.get(value.field.slice(0, -8)) ?? null;
250
+ const account = ownedAccount(value.field);
251
+ if (account !== undefined) return state.balances.get(account) ?? null;
253
252
  // Another account's balance moves between reads, so each read is its own symbol.
254
253
  if (external(value.field))
255
254
  return new Map([[`${scope}:${reading++}:${value.field}`, 1n]]);
@@ -277,10 +276,9 @@ export function analyzeInstrumentFinance(
277
276
  [requirement.left, requirement.right],
278
277
  [requirement.right, requirement.left],
279
278
  ] as const) {
280
- if ("field" in left && left.field.endsWith(".balance")) {
281
- const account = left.field.slice(0, -8);
282
- if (owned.has(account)) state.balances.set(account, amount(right));
283
- }
279
+ const account =
280
+ "field" in left ? ownedAccount(left.field) : undefined;
281
+ if (account !== undefined) state.balances.set(account, amount(right));
284
282
  }
285
283
  }
286
284
  for (const key of local.keys()) calculated.set(key, amount({ field: key }));
package/src/limits.ts CHANGED
@@ -2,6 +2,5 @@ export const UDL_LIMITS = Object.freeze({
2
2
  maxDepth: 32,
3
3
  maxKeyLength: 240,
4
4
  maxNodes: 100_000,
5
- maxSourceBytes: 1_024 * 1_024,
6
5
  maxTotalStringLength: 1_024 * 1_024,
7
6
  });
@@ -461,10 +461,9 @@ function requirementPossible(
461
461
  export function objectActionState(
462
462
  action: ObjectActionDiscovery,
463
463
  fields: Readonly<Record<string, unknown>>,
464
- options?: ObjectActionStateOptions | boolean,
464
+ options: ObjectActionStateOptions = {},
465
465
  ): ObjectActionState {
466
- const attached =
467
- typeof options === "boolean" ? options : (options?.attached ?? false);
466
+ const attached = options.attached ?? false;
468
467
  const creationFilter = new Set(action.creationOnlyNames ?? []);
469
468
  return {
470
469
  ...action,
@@ -477,12 +476,7 @@ export function objectActionState(
477
476
  : undefined;
478
477
  if (
479
478
  conditions &&
480
- !requirementPossible(
481
- conditions,
482
- action,
483
- fields,
484
- typeof options === "object" ? options : {},
485
- )
479
+ !requirementPossible(conditions, action, fields, options)
486
480
  )
487
481
  return false;
488
482
  const value = fields[field.name];
@@ -512,7 +506,7 @@ export function validateObjectActionSubject(
512
506
  action: ObjectActionDiscovery,
513
507
  storedFields: Readonly<Record<string, unknown>>,
514
508
  submittedFields: Readonly<Record<string, unknown>>,
515
- options?: ObjectActionStateOptions | boolean,
509
+ options: ObjectActionStateOptions = {},
516
510
  ): readonly SubjectRequirementIssue[] {
517
511
  const origin = `${action.instrument}.${action.action}.subject`;
518
512
  const issues: SubjectRequirementIssue[] = [];
@@ -6,6 +6,7 @@ import {
6
6
  type UdlAction,
7
7
  } from "./schema.js";
8
8
  import { resolveField } from "./validation.js";
9
+ import { own, targetIds } from "./primitives.js";
9
10
  import type {
10
11
  DocumentSemantics,
11
12
  SemanticAccount,
@@ -40,8 +41,6 @@ const labelled = <T extends UdlField>(field: T): T => ({
40
41
  ...field,
41
42
  label: field.label ?? presentationLabel(field.name),
42
43
  });
43
- const targets = (target: string | string[]) =>
44
- typeof target === "string" ? [target] : target;
45
44
 
46
45
  function owner(
47
46
  document: UdlDocument,
@@ -52,11 +51,12 @@ function owner(
52
51
  return { kind: "adapter", adapter: value.adapter };
53
52
  if (value === "self")
54
53
  return { kind: "instrument", instrument: instrument.id };
55
- const binding = document.objects
56
- .flatMap((object) => object.attachments)
57
- .find((attachment) => attachment.instrument === instrument.id)?.parties[
58
- value
59
- ];
54
+ const binding = own(
55
+ document.objects
56
+ .flatMap((object) => object.attachments)
57
+ .find((attachment) => attachment.instrument === instrument.id)?.parties,
58
+ value,
59
+ );
60
60
  if (binding)
61
61
  return "role" in binding
62
62
  ? { kind: "role", role: binding.role }
@@ -82,7 +82,7 @@ function scopes(
82
82
  field.targetKind === "instrument"
83
83
  )
84
84
  return document.instruments
85
- .filter((candidate) => targets(field.target).includes(candidate.id))
85
+ .filter((candidate) => targetIds(field.target).includes(candidate.id))
86
86
  .flatMap((candidate) =>
87
87
  scopes(document, candidate, `self.${rest.join(".")}`),
88
88
  );
@@ -200,7 +200,7 @@ function relationships(
200
200
  via: "field" as const,
201
201
  name: field.name,
202
202
  targetKind: field.targetKind,
203
- targets: targets(field.target),
203
+ targets: targetIds(field.target),
204
204
  cardinality: { min: field.optional ? 0 : 1, max: 1 },
205
205
  },
206
206
  ];
@@ -252,7 +252,7 @@ function relationships(
252
252
  (action) =>
253
253
  typeof action.actor === "object" &&
254
254
  "parent" in action.actor &&
255
- targets(action.actor.parent).includes(instrument.id),
255
+ targetIds(action.actor.parent).includes(instrument.id),
256
256
  );
257
257
  if (
258
258
  parentActor &&
@@ -260,7 +260,7 @@ function relationships(
260
260
  (field) =>
261
261
  field.type === "ref" &&
262
262
  field.targetKind === "instrument" &&
263
- targets(field.target).includes(instrument.id),
263
+ targetIds(field.target).includes(instrument.id),
264
264
  )
265
265
  )
266
266
  references.push({
@@ -292,7 +292,7 @@ function projectAction(
292
292
  ? { kind: actor }
293
293
  : "party" in actor
294
294
  ? owner(document, instrument, actor.party)
295
- : { kind: "parent", instruments: targets(actor.parent) },
295
+ : { kind: "parent", instruments: targetIds(actor.parent) },
296
296
  ],
297
297
  from: transition?.from ?? [],
298
298
  to:
@@ -360,7 +360,8 @@ export function projectDocumentSemantics(
360
360
  initial: instrument.lifecycle.initial,
361
361
  states: instrument.lifecycle.states.map((name) => ({
362
362
  name,
363
- label: instrument.lifecycle.labels?.[name] ?? presentationLabel(name),
363
+ label:
364
+ own(instrument.lifecycle.labels, name) ?? presentationLabel(name),
364
365
  terminal: isTerminalState(instrument, name),
365
366
  })),
366
367
  cancellationActions: [],
@@ -3,6 +3,7 @@ import type {
3
3
  SemanticAction,
4
4
  SemanticInstrument,
5
5
  } from "./object-semantics-schema.js";
6
+ import { targetIds } from "./primitives.js";
6
7
 
7
8
  const hasMoney = (instrument: SemanticInstrument) =>
8
9
  instrument.actions.some((action) => action.moneyEffects.length > 0);
@@ -149,10 +150,7 @@ export function projectStructures(
149
150
  );
150
151
  const funding = contributions.some((selection) => {
151
152
  if (selection.anchor !== "self.id") return false;
152
- const ids =
153
- typeof selection.instrument === "string"
154
- ? [selection.instrument]
155
- : selection.instrument;
153
+ const ids = targetIds(selection.instrument);
156
154
  const children = instruments.filter(
157
155
  (candidate) =>
158
156
  ids.includes(candidate.id) &&
@@ -0,0 +1,29 @@
1
+ import * as z from "zod";
2
+
3
+ const minorUnits = /^(0|[1-9][0-9]{0,17})$/;
4
+
5
+ export const name = z
6
+ .string()
7
+ .regex(/^[a-z][a-zA-Z0-9_]*$/)
8
+ .max(80);
9
+ export const text = z.string().min(1).max(2048);
10
+ export const amount = z.string().regex(minorUnits);
11
+ export const integer = z.number().int().safe();
12
+ export const path = z
13
+ .string()
14
+ .regex(/^[a-z][a-zA-Z0-9_]*(?:\.[a-z][a-zA-Z0-9_]*)*$/)
15
+ .max(240);
16
+
17
+ /** True for a money amount in minor units. Guard every BigInt() of an unchecked amount with it. */
18
+ export const isMinorUnits = (value: string): boolean => minorUnits.test(value);
19
+
20
+ /** Reads only the record's own keys, so names like `constructor` never reach the prototype. */
21
+ export function own<T>(
22
+ record: Record<string, T> | undefined,
23
+ key: string,
24
+ ): T | undefined {
25
+ return record && Object.hasOwn(record, key) ? record[key] : undefined;
26
+ }
27
+
28
+ export const targetIds = (target: string | string[]): string[] =>
29
+ typeof target === "string" ? [target] : target;
@@ -4,6 +4,7 @@ import type {
4
4
  ReportExpression,
5
5
  ReportType,
6
6
  } from "./reporting.js";
7
+ import { isMinorUnits } from "./primitives.js";
7
8
 
8
9
  type Types = Map<string, ReportType>;
9
10
  const sealedFields = new Map([
@@ -49,7 +50,7 @@ export function reportExpressionTypes(
49
50
  const value = expression.value;
50
51
  requireRule(
51
52
  result.kind === "money"
52
- ? typeof value === "string" && /^(0|[1-9][0-9]{0,17})$/.test(value)
53
+ ? typeof value === "string" && isMinorUnits(value)
53
54
  : result.kind === "integer"
54
55
  ? typeof value === "number" && Number.isSafeInteger(value)
55
56
  : result.kind === "boolean"
package/src/reporting.ts CHANGED
@@ -144,11 +144,6 @@ export const reportDefinitionSchema = z.strictObject({
144
144
  rowReconcile: z.array(z.strictObject({ left: name, right: name })).max(16),
145
145
  reconcile: z.array(z.strictObject({ left: name, right: name })).max(16),
146
146
  unavailableFacts: z.array(z.string().min(1).max(240)).max(32),
147
- lockTimeoutMs: integer.min(1).max(2000),
148
- captureTimeoutMs: integer.min(1).max(10000),
149
- maxRows: integer.min(1).max(100000),
150
- maxJoinRows: integer.min(1).max(1000000),
151
- maxBytes: integer.min(1024).max(16777216),
152
147
  }),
153
148
  output: z.strictObject({
154
149
  profile: z.literal("internal.v1"),
package/src/schema.ts CHANGED
@@ -5,7 +5,7 @@ import { writeJson } from "./json.js";
5
5
 
6
6
  export const UDL_FORMAT_VERSION = 1 as const;
7
7
  /** Counts the root and every nested invocation, including selected and ranged children. */
8
- export const MAX_ACTION_EXPANSION = 4096;
8
+ export const MAX_ACTION_EXPANSION = 8192;
9
9
  const name = z
10
10
  .string()
11
11
  .regex(/^[a-z][a-zA-Z0-9_]*$/)
@@ -454,6 +454,7 @@ export const udlActionSchema = z.strictObject({
454
454
  summary: text,
455
455
  title: text.optional(),
456
456
  publicAction: name.optional(),
457
+ // Retained Builds keep this key; execution uses MAX_ACTION_EXPANSION.
457
458
  expansionLimit: z.literal(8192).optional(),
458
459
  reminder: z
459
460
  .strictObject({