@hyperscale0/hsx 1.0.334 → 1.0.335

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/src/compile.ts CHANGED
@@ -40,7 +40,10 @@ import {
40
40
  } from "./binding-contract.ts";
41
41
  import { tunableBounds } from "./tunables.ts";
42
42
  import { parseProgram } from "./parse.ts";
43
- import { applyAttachmentEconomics } from "./attachment-economics.ts";
43
+ import {
44
+ applyAttachmentEconomics,
45
+ purposeWarnings,
46
+ } from "./attachment-economics.ts";
44
47
  import { parseHeader } from "./header-source.ts";
45
48
  import {
46
49
  lineColAt,
@@ -79,9 +82,14 @@ export interface CompileOptions {
79
82
  standardLibrary?: StandardLibrary;
80
83
  adapterRegistry?: Readonly<Record<string, AdapterBindingTarget>>;
81
84
  }
85
+ export interface CompileWarning extends Omit<CompileDiagnostic, "severity"> {
86
+ severity: "warning";
87
+ }
82
88
  export interface CompileResult {
83
89
  verdict: "valid" | "invalid";
84
90
  diagnostics: CompileDiagnostic[];
91
+ /** Advice that does not block a freeze, such as a money move with no purpose. */
92
+ warnings: CompileWarning[];
85
93
  artifacts?: {
86
94
  document: UdlDocument;
87
95
  costManifest: UdlCostManifest;
@@ -392,8 +400,10 @@ function literal(expr: Expr, expectedType?: string): string | number | boolean {
392
400
  if (!Number.isSafeInteger(n) || n <= 0)
393
401
  fail(
394
402
  expr,
395
- "invalid duration",
396
- "write a positive fixed duration such as 48h or 3d",
403
+ expr.value.includes(" ")
404
+ ? `${expr.value} is not a fixed duration`
405
+ : "invalid duration",
406
+ "write a positive fixed duration such as 48h, 3d or 30d",
397
407
  );
398
408
  return n;
399
409
  }
@@ -463,6 +473,7 @@ export function compile(
463
473
  return {
464
474
  verdict: "invalid",
465
475
  diagnostics: parsed.diagnostics.map((d) => diagnostic(d, "parse")),
476
+ warnings: [],
466
477
  };
467
478
  const program = parsed.program;
468
479
  const bindingDiagnostics: Diagnostic[] = [];
@@ -489,6 +500,15 @@ export function compile(
489
500
  "write currency SAR or omit currency",
490
501
  );
491
502
  const templates = new Map<string, InstrumentDecl>();
503
+ // Attached instruments, for the warning on a money move with no purpose.
504
+ const attachedSelectors = new Map<
505
+ string,
506
+ { prefix: string; root: string }
507
+ >();
508
+ const attachEntries = new Map<
509
+ string,
510
+ { entry: Entry; name: string; template: InstrumentDecl }
511
+ >();
492
512
  const declarationSources = new Map<InstrumentDecl, string>();
493
513
  const standardOrigins = new Map<
494
514
  InstrumentDecl,
@@ -746,6 +766,11 @@ export function compile(
746
766
  attachments: UdlObjectAttachment[];
747
767
  economics: Entry[];
748
768
  child?: boolean;
769
+ /** How an economics selector names this instrument: "" or "occurrence.". */
770
+ selectorPrefix?: string;
771
+ rootInstrument?: string;
772
+ /** The template as the founder wrote it, such as money.schedule. */
773
+ template?: string;
749
774
  },
750
775
  familyDeclaration?: {
751
776
  module: string;
@@ -933,6 +958,7 @@ export function compile(
933
958
  enums.set(parameter.key, type.args.map(text));
934
959
  }
935
960
  const supplied = new Map<string, Expr>();
961
+ const suppliedEntries = new Map<string, Entry>();
936
962
  for (const entry of arguments_.entries) {
937
963
  if (supplied.has(entry.key))
938
964
  fail(
@@ -941,6 +967,7 @@ export function compile(
941
967
  "supply each parameter once",
942
968
  );
943
969
  supplied.set(entry.key, entry.value);
970
+ suppliedEntries.set(entry.key, entry);
944
971
  }
945
972
  const environment = new Map<string, Expr>(inherited);
946
973
  const parameterErrors: Diagnostic[] = [];
@@ -1082,10 +1109,11 @@ export function compile(
1082
1109
  `Remove \`${key}\`. To use your object's money field, add \`rename { ${field}: ${target} }\`.`,
1083
1110
  );
1084
1111
  }
1112
+ const tunables = decl.parameters.map((p) => p.key);
1085
1113
  fail(
1086
- suppliedValue,
1087
- `unknown tunable ${key}`,
1088
- `choose ${decl.parameters.map((p) => p.key).join(", ")}`,
1114
+ suppliedEntries.get(key) ?? suppliedValue,
1115
+ `${assignments.get(id)?.target ?? attachmentInfo?.template ?? decl.name} has no tunable "${key}".${didYouMean(key, tunables)} Tunables: ${tunables.join(", ")}`,
1116
+ `remove ${key}, or set one of ${tunables.join(", ")}`,
1089
1117
  );
1090
1118
  }
1091
1119
  const isParty = (name: string) =>
@@ -3205,14 +3233,39 @@ export function compile(
3205
3233
  );
3206
3234
  if (receipt) move.transfer = `self.${receipt}`;
3207
3235
  }
3208
- if (attachmentInfo && !attachmentInfo.child)
3236
+ // `economics occurrence.pay` classifies a record's move; an action of
3237
+ // the attached instrument wins over a record of the same name.
3238
+ const recordEconomics = new Map<string, Entry[]>();
3239
+ if (attachmentInfo) {
3240
+ const own: Entry[] = [];
3241
+ for (const declaration of attachmentInfo.economics) {
3242
+ const [first = "", ...rest] = declaration.key
3243
+ .slice("economics ".length)
3244
+ .split(".");
3245
+ if (
3246
+ rest.length &&
3247
+ !Object.hasOwn(inst.actions, first) &&
3248
+ records.has(first)
3249
+ )
3250
+ recordEconomics.set(first, [
3251
+ ...(recordEconomics.get(first) ?? []),
3252
+ { ...declaration, key: `economics ${rest.join(".")}` },
3253
+ ]);
3254
+ else own.push(declaration);
3255
+ }
3209
3256
  applyAttachmentEconomics(
3210
3257
  document,
3211
3258
  inst,
3212
3259
  attachmentInfo.parties,
3213
- attachmentInfo.economics,
3260
+ own,
3214
3261
  data,
3262
+ [...records.keys()],
3215
3263
  );
3264
+ attachedSelectors.set(inst.id, {
3265
+ prefix: attachmentInfo.selectorPrefix ?? "",
3266
+ root: attachmentInfo.rootInstrument ?? inst.id,
3267
+ });
3268
+ }
3216
3269
  for (const key of [
3217
3270
  "invariants",
3218
3271
  "reports",
@@ -3292,6 +3345,9 @@ export function compile(
3292
3345
  attachmentName: `${attachmentInfo.attachmentName}_${key}`,
3293
3346
  child: true,
3294
3347
  exposed: new Map<string, string>(),
3348
+ economics: recordEconomics.get(key) ?? [],
3349
+ selectorPrefix: `${attachmentInfo.selectorPrefix ?? ""}${key}.`,
3350
+ rootInstrument: attachmentInfo.rootInstrument ?? id,
3295
3351
  }
3296
3352
  : undefined;
3297
3353
  if (childAttachment)
@@ -3450,6 +3506,7 @@ export function compile(
3450
3506
 
3451
3507
  const parties: Record<string, AttachmentPartyBinding> = {};
3452
3508
  attachments.push({ name: attachmentName, instrument: instId, parties });
3509
+ attachEntries.set(instId, { entry, name: attachmentName, template });
3453
3510
 
3454
3511
  const tunableBlock: BlockExpr = {
3455
3512
  kind: "block",
@@ -3475,6 +3532,7 @@ export function compile(
3475
3532
  parties,
3476
3533
  attachments,
3477
3534
  economics,
3535
+ template: targetTemplate,
3478
3536
  },
3479
3537
  templateFamily ? { ...templateFamily } : undefined,
3480
3538
  standardOrigins.get(template),
@@ -3689,6 +3747,7 @@ export function compile(
3689
3747
  if (bindingDiagnostics.length)
3690
3748
  return {
3691
3749
  verdict: "invalid",
3750
+ warnings: [],
3692
3751
  diagnostics: distinct(bindingDiagnostics)
3693
3752
  .sort((a, b) => a.span.start - b.span.start)
3694
3753
  .map((d) => diagnostic(d, "check")),
@@ -4500,6 +4559,7 @@ export function compile(
4500
4559
  if (collisions.length)
4501
4560
  return {
4502
4561
  verdict: "invalid",
4562
+ warnings: [],
4503
4563
  diagnostics: collisions
4504
4564
  .sort((a, b) => a.span.start - b.span.start)
4505
4565
  .map((d) => diagnostic(d, "check")),
@@ -4577,6 +4637,7 @@ export function compile(
4577
4637
  if (!validated.ok)
4578
4638
  return {
4579
4639
  verdict: "invalid",
4640
+ warnings: [],
4580
4641
  diagnostics: issues.map((i) => {
4581
4642
  const origin = [...origins]
4582
4643
  .reverse()
@@ -4652,6 +4713,9 @@ export function compile(
4652
4713
  return {
4653
4714
  verdict: "valid",
4654
4715
  diagnostics: [],
4716
+ warnings: purposeWarnings(document, attachedSelectors, attachEntries).map(
4717
+ (d) => ({ ...diagnostic(d, "check"), severity: "warning" }),
4718
+ ),
4655
4719
  artifacts: {
4656
4720
  document: validated.value,
4657
4721
  costManifest: buildUdlCostManifest(validated.value),
@@ -4662,6 +4726,7 @@ export function compile(
4662
4726
  if (error instanceof CompileFailure)
4663
4727
  return {
4664
4728
  verdict: "invalid",
4729
+ warnings: [],
4665
4730
  diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map(
4666
4731
  (d) => diagnostic(d, "check"),
4667
4732
  ),
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export {
3
3
  type CompileDiagnostic,
4
4
  type CompileOptions,
5
5
  type CompileResult,
6
+ type CompileWarning,
6
7
  type CompileOriginMapEntry,
7
8
  type AdapterBindingTarget,
8
9
  } from "./compile.ts";
package/src/parse.ts CHANGED
@@ -514,6 +514,21 @@ class Parser {
514
514
  };
515
515
  if (/[a-z]$/.test(token.text))
516
516
  return { kind: "duration", value: token.text, span: token.span };
517
+ // `1 month` reads as one value, so the checker names the tunable it
518
+ // was given to instead of failing on the next line. A unit followed by
519
+ // `:` is the next entry's key, as in `count: 2` then `months: 2`.
520
+ if (
521
+ /^(?:minute|hour|day|week|month|year)s?$/.test(this.peek().text) &&
522
+ this.peek().kind === "name" &&
523
+ this.tokens[this.index + 1]?.text !== ":"
524
+ ) {
525
+ const unit = this.take().text;
526
+ return {
527
+ kind: "duration",
528
+ value: `${token.text} ${unit}`,
529
+ span: this.span(start),
530
+ };
531
+ }
517
532
  if (/^[A-Z]{3}$/.test(this.peek().text)) {
518
533
  const currency = this.take().text;
519
534
  return {
@@ -49,6 +49,14 @@ export interface TemplateGuide {
49
49
  const RUN_BY_NOTE =
50
50
  "runBy names who runs each action: a party is whoever the attach binds to it (owner, actor, operator or a declared business), clock is the platform when the action falls due, parent runs only inside its parent record's action, and caller is anyone the exposed action admits.";
51
51
 
52
+ /**
53
+ * A stock money move declares no purpose, because the purpose follows how
54
+ * the attach binds its parties; the guide shows the line for this example.
55
+ */
56
+ function economicsNote(lines: string[]) {
57
+ return `economics declares what a payment is for the books. Add ${lines.map((line) => `\`${line}\``).join(" and ")} inside the attach. Use purpose earning when operator receives the money as revenue, participant_payout when operator pays a participant, and pass_through when money passes between participants. Without it hsx check warns and the books report leaves the payment out.`;
58
+ }
59
+
52
60
  const SUBJECT_NOTE =
53
61
  "subject names fields the attached object must carry, or rename to with rename { field: yourField }, before the actions that read them can run.";
54
62
 
@@ -110,8 +118,13 @@ export function templateGuide(
110
118
  )
111
119
  .map((action) => `expose ${action.name}`);
112
120
  const program = example.render(exposes);
113
- const create = compiledAttachment(program, example.attachment, options)
114
- .actions.create;
121
+ const compiled = compiledAttachment(program, example.attachment, options);
122
+ const create = compiled.actions.create;
123
+ const economics = compiled.warnings.flatMap((warning) =>
124
+ warning.message.startsWith(`${example.attachment}: `)
125
+ ? [/economics [^}]+\}/.exec(warning.fix)![0]]
126
+ : [],
127
+ );
115
128
  return {
116
129
  qualifiedName,
117
130
  summary: manifest.summary,
@@ -135,7 +148,11 @@ export function templateGuide(
135
148
  subject: manifest.subject,
136
149
  actions,
137
150
  example: program,
138
- notes: [RUN_BY_NOTE, ...(manifest.subject.length ? [SUBJECT_NOTE] : [])],
151
+ notes: [
152
+ RUN_BY_NOTE,
153
+ ...(manifest.subject.length ? [SUBJECT_NOTE] : []),
154
+ ...(economics.length ? [economicsNote(economics)] : []),
155
+ ],
139
156
  };
140
157
  }
141
158
 
@@ -553,7 +570,7 @@ function compiledAttachment(
553
570
  (candidate) => candidate.id === instrumentId,
554
571
  );
555
572
  if (!instrument) throw new Error(`${attachment} example has no instrument`);
556
- return instrument;
573
+ return Object.assign(instrument, { warnings: result.warnings });
557
574
  }
558
575
 
559
576
  function inputType(field: Record<string, unknown>) {