@hyperscale0/hsx 1.0.337 → 1.0.339

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 (51) hide show
  1. package/dist/src/ast.d.ts +1 -0
  2. package/dist/src/ast.d.ts.map +1 -1
  3. package/dist/src/ast.js.map +1 -1
  4. package/dist/src/attachment-economics.d.ts +37 -0
  5. package/dist/src/attachment-economics.d.ts.map +1 -1
  6. package/dist/src/attachment-economics.js +183 -3
  7. package/dist/src/attachment-economics.js.map +1 -1
  8. package/dist/src/binding-contract.d.ts.map +1 -1
  9. package/dist/src/binding-contract.js +10 -4
  10. package/dist/src/binding-contract.js.map +1 -1
  11. package/dist/src/catalog-program.d.ts +1 -1
  12. package/dist/src/catalog-program.d.ts.map +1 -1
  13. package/dist/src/catalog-program.js +1 -1
  14. package/dist/src/compile.d.ts.map +1 -1
  15. package/dist/src/compile.js +133 -41
  16. package/dist/src/compile.js.map +1 -1
  17. package/dist/src/examples-bundle.js +1 -1
  18. package/dist/src/examples-bundle.js.map +1 -1
  19. package/dist/src/headers.d.ts.map +1 -1
  20. package/dist/src/headers.js +3 -1
  21. package/dist/src/headers.js.map +1 -1
  22. package/dist/src/language.d.ts.map +1 -1
  23. package/dist/src/language.js +1 -0
  24. package/dist/src/language.js.map +1 -1
  25. package/dist/src/parse.js +13 -1
  26. package/dist/src/parse.js.map +1 -1
  27. package/dist/src/std-bundle.js +6 -6
  28. package/dist/src/std-bundle.js.map +1 -1
  29. package/dist/src/template-guide.d.ts.map +1 -1
  30. package/dist/src/template-guide.js +49 -18
  31. package/dist/src/template-guide.js.map +1 -1
  32. package/docs/README.md +5 -2
  33. package/examples/camera-rental.hsx +6 -1
  34. package/package.json +4 -4
  35. package/src/ast.ts +7 -1
  36. package/src/attachment-economics.ts +230 -2
  37. package/src/binding-contract.ts +13 -6
  38. package/src/catalog-program.ts +1 -1
  39. package/src/compile.ts +185 -58
  40. package/src/examples-bundle.ts +1 -1
  41. package/src/headers.ts +6 -1
  42. package/src/language.ts +1 -0
  43. package/src/parse.ts +16 -1
  44. package/src/std-bundle.ts +6 -6
  45. package/src/template-guide.ts +74 -22
  46. package/std/collections.hsx +4 -4
  47. package/std/financing.hsx +461 -266
  48. package/std/insurance.hsx +21 -5
  49. package/std/lending.hsx +13 -9
  50. package/std/money.hsx +148 -38
  51. package/std/reporting.hsx +6 -6
@@ -5,7 +5,7 @@ import { validateUdl, resolveField, sameAccount, sameObjectField, subjectPartyRo
5
5
  import { bindingDependencies, parameterDiagnostics, BindingContractError, } from "./binding-contract.js";
6
6
  import { tunableBounds } from "./tunables.js";
7
7
  import { parseProgram } from "./parse.js";
8
- import { applyAttachmentEconomics, purposeWarnings, } from "./attachment-economics.js";
8
+ import { applyAttachmentEconomics, purposeWarnings, samePartyMoves, unboundEconomicSources, } from "./attachment-economics.js";
9
9
  import { parseHeader } from "./header-source.js";
10
10
  import { lineColAt, } from "./ast.js";
11
11
  import { bundledStandardLibrary } from "./std-library.js";
@@ -200,6 +200,24 @@ function decimal(raw, scale, expr) {
200
200
  return (BigInt(whole) * 10n ** BigInt(scale) +
201
201
  BigInt(fraction.padEnd(scale, "0") || "0")).toString();
202
202
  }
203
+ /**
204
+ * A calendar period as ISO-8601 text: `1 month`, `2 weeks`, `7d` or `P1M`.
205
+ * Days, weeks, months and years only, the units Stripe and Shopify bill in.
206
+ */
207
+ function periodText(expr) {
208
+ if (!["duration", "text", "name"].includes(expr.kind))
209
+ return undefined;
210
+ const value = expr.value.trim();
211
+ const units = { d: "D", w: "W", m: "M", y: "Y" };
212
+ const words = /^([1-9]\d{0,3}) ?(day|week|month|year)s?$/.exec(value);
213
+ const short = /^([1-9]\d{0,3})(d|w)$/.exec(value);
214
+ const iso = /^P(?!$)(?:[1-9]\d{0,3}Y)?(?:[1-9]\d{0,3}M)?(?:[1-9]\d{0,3}W)?(?:[1-9]\d{0,3}D)?$/.exec(value);
215
+ if (words)
216
+ return `P${words[1]}${units[words[2][0]]}`;
217
+ if (short)
218
+ return `P${short[1]}${units[short[2]]}`;
219
+ return iso ? value : undefined;
220
+ }
203
221
  function literal(expr, expectedType) {
204
222
  if (expr.kind === "text" && expectedType === "duration")
205
223
  return literal({ ...expr, kind: "duration" });
@@ -501,7 +519,9 @@ export function compile(source, options = {}) {
501
519
  }
502
520
  return { module: moduleName, exportPath, revision };
503
521
  };
504
- const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration, standardOrigin) => {
522
+ const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration, standardOrigin,
523
+ /** Declared tunables, true where the author bound them. */
524
+ inheritedTunables = new Map()) => {
505
525
  const resolveFamilyInstruments = (family, currentInstId, expr) => {
506
526
  const found = new Set();
507
527
  if (currentInstId && familyDeclaration) {
@@ -635,13 +655,16 @@ export function compile(source, options = {}) {
635
655
  suppliedEntries.set(entry.key, entry);
636
656
  }
637
657
  const environment = new Map(inherited);
658
+ // `when every is set` asks whether the author bound a tunable; a
659
+ // default leaves it unset.
660
+ const tunables = new Map(inheritedTunables);
638
661
  const parameterErrors = [];
639
662
  for (const param of decl.parameters) {
640
663
  const type = param.value.kind === "default" ? param.value.type : param.value;
641
664
  const fallback = param.value.kind === "default" ? param.value.value : undefined;
642
665
  const typeName = type.kind === "type" || type.kind === "call" ? type.name : text(type);
643
666
  const partyParameter = attachmentInfo && typeName === "party";
644
- const optional = type.kind === "type" && type.optional;
667
+ const optional = (type.kind === "type" || type.kind === "call") && !!type.optional;
645
668
  // An optional party stays unset unless the attachment binds it, so a
646
669
  // program party that shares its name does not switch it on.
647
670
  const byName = partyParameter &&
@@ -650,6 +673,7 @@ export function compile(source, options = {}) {
650
673
  document.parties[param.key])
651
674
  ? { kind: "name", value: param.key, span: origin }
652
675
  : undefined;
676
+ tunables.set(param.key, supplied.has(param.key) || !!byName);
653
677
  const actual = supplied.get(param.key) ??
654
678
  byName ??
655
679
  (fallback && {
@@ -699,9 +723,10 @@ export function compile(source, options = {}) {
699
723
  }
700
724
  for (const dependency of dependencies) {
701
725
  const selected = environment.get(dependency.when);
702
- if (selected?.kind === "name" &&
703
- selected.value === dependency.is &&
704
- !environment.has(dependency.binding)) {
726
+ const holds = enums.has(dependency.when)
727
+ ? selected?.kind === "name" && selected.value === dependency.is
728
+ : tunables.get(dependency.when) === (dependency.is === "set");
729
+ if (holds && !environment.has(dependency.binding)) {
705
730
  throw new CompileFailure({
706
731
  code: "HSX1001",
707
732
  message: dependency.message.replaceAll("{attachment}", attachmentInfo?.attachmentName ?? id),
@@ -812,7 +837,8 @@ export function compile(source, options = {}) {
812
837
  const target = templates.get(type);
813
838
  const required = target?.parameters
814
839
  .filter((p) => p.value.kind !== "default" &&
815
- !(p.value.kind === "type" && p.value.optional))
840
+ !((p.value.kind === "type" || p.value.kind === "call") &&
841
+ p.value.optional))
816
842
  .map((p) => p.key) ?? [];
817
843
  fail({ span: origin }, `Attachment \`${attachmentInfo.attachmentName}\` needs a \`${parameter}\` binding. ${matches.length === 0
818
844
  ? `No \`${type}\` attachment exists on \`${attachmentInfo.subjectKindId}\`.`
@@ -895,6 +921,10 @@ export function compile(source, options = {}) {
895
921
  (v.kind === "text" || v.kind === "name") &&
896
922
  /^P/.test(v.value))
897
923
  v = { ...v, kind: "duration" };
924
+ // A period lowers as ISO text, so `after` steps calendar months.
925
+ const period = type === "period" ? periodText(v) : undefined;
926
+ if (period)
927
+ v = { kind: "text", value: period, span: v.span };
898
928
  environment.set(param.key, v);
899
929
  if (type === "enum" && t.kind === "call") {
900
930
  if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
@@ -982,6 +1012,10 @@ export function compile(source, options = {}) {
982
1012
  span: v.span,
983
1013
  });
984
1014
  }
1015
+ else if (type === "period") {
1016
+ if (!period)
1017
+ fail(v, `${param.key} needs a calendar period`, `write ${param.key}: 1 month, 2 weeks, 1 year or 7 days`);
1018
+ }
985
1019
  else if (type === "fee" || type === "split" || type === "policy") {
986
1020
  if (v.kind !== "block")
987
1021
  fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
@@ -1067,6 +1101,27 @@ export function compile(source, options = {}) {
1067
1101
  throw new CompileFailure(parameterErrors[0]);
1068
1102
  }
1069
1103
  const body = entries(decl.body);
1104
+ // A ceiling caps a tunable by the value of an enum tunable, such as the
1105
+ // longest term per loan kind. An unset tunable takes the ceiling; a
1106
+ // supplied one may lower it, never raise it.
1107
+ for (const ceiling of asBlock(body.get("ceilings")).entries) {
1108
+ const table = entries(asBlock(ceiling.value));
1109
+ const by = table.get("by");
1110
+ const selected = by && environment.get(text(by));
1111
+ if (!by || selected?.kind !== "name")
1112
+ fail(ceiling, `ceiling ${ceiling.key} needs by: an enum tunable`, "write by: kind and one maximum per enum value");
1113
+ const maximum = table.get(selected.value);
1114
+ if (maximum?.kind !== "number")
1115
+ fail(ceiling, `ceiling ${ceiling.key} has no maximum for ${selected.value}`, `add ${selected.value}: number to the ceiling`);
1116
+ if (!tunables.get(ceiling.key)) {
1117
+ environment.set(ceiling.key, { ...maximum, span: origin });
1118
+ continue;
1119
+ }
1120
+ const supplied = environment.get(ceiling.key);
1121
+ if (supplied.kind !== "number" ||
1122
+ BigInt(supplied.value) > BigInt(maximum.value))
1123
+ fail(supplied, `${ceiling.key} for ${selected.value} is at most ${maximum.value}`, `choose ${ceiling.key} of ${maximum.value} or less, or omit it`);
1124
+ }
1070
1125
  for (const constraint of asBlock(body.get("constraints")).entries) {
1071
1126
  const rule = constraint.value;
1072
1127
  if (rule.kind !== "call" ||
@@ -1076,6 +1131,9 @@ export function compile(source, options = {}) {
1076
1131
  const left = environment.get(constraint.key), right = environment.get(text(rule.args[0]));
1077
1132
  if (!left || !right)
1078
1133
  fail(rule, "constraint needs two declared tunables", "name the compared tunables");
1134
+ // A runtime value is checked when the agreement is created.
1135
+ if ([left, right].some((expr) => expr.kind === "name" && expr.value === "runtime"))
1136
+ continue;
1079
1137
  const numericKind = (expr) => expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
1080
1138
  ? "duration"
1081
1139
  : expr.kind;
@@ -1099,6 +1157,7 @@ export function compile(source, options = {}) {
1099
1157
  "summary",
1100
1158
  "invariants",
1101
1159
  "constraints",
1160
+ "ceilings",
1102
1161
  "dependencies",
1103
1162
  "parameterDiagnostics",
1104
1163
  "reports",
@@ -1289,9 +1348,52 @@ export function compile(source, options = {}) {
1289
1348
  : value.value;
1290
1349
  return literal(value);
1291
1350
  };
1351
+ const branchMatches = (entry) => {
1352
+ const [, tunable, relation, choice] = entry.key.split(" ");
1353
+ // `when every is set` holds when the author bound the tunable, so an
1354
+ // optional tunable and a defaulted one both read as unset.
1355
+ if (relation === "is" &&
1356
+ (choice === "set" || choice === "unset") &&
1357
+ tunables.has(tunable) &&
1358
+ !enums.has(tunable))
1359
+ return tunables.get(tunable) === (choice === "set");
1360
+ const binding = environment.get(tunable);
1361
+ if (!binding)
1362
+ fail(entry, `unknown branch tunable ${tunable}`, "name an enum, reference or optional tunable declared by this header");
1363
+ if (relation === "has") {
1364
+ // Inspect declarations, not lowering order: a bound object may
1365
+ // appear after the instrument that asks about its fields.
1366
+ const bound = resolve(binding);
1367
+ const [root, ...children] = text(bound).split(".");
1368
+ const assignment = assignments.get(root);
1369
+ let target = assignment
1370
+ ? templates.get(assignment.target)?.body
1371
+ : program.decls
1372
+ .filter((decl) => decl.kind === "instrument")
1373
+ .find((decl) => decl.name === root)?.body;
1374
+ for (const child of children) {
1375
+ const record = target
1376
+ ? entries(asBlock(entries(target).get("records"))).get(child)
1377
+ : undefined;
1378
+ target = record ? asBlock(record) : undefined;
1379
+ }
1380
+ if (!target)
1381
+ fail(entry, "field branch needs a declared object", "bind a reference to a declared object");
1382
+ return entries(asBlock(entries(target).get("fields"))).has(choice);
1383
+ }
1384
+ if (!enums.get(tunable)?.includes(choice))
1385
+ fail(entry, `unknown enum branch ${choice}`, "use a declared enum value, or set or unset");
1386
+ return text(binding) === choice;
1387
+ };
1388
+ // Fields and inputs take `when` branches too; a branch holds field rows.
1389
+ const branchRows = (block) => block.entries.flatMap((row) => row.key.startsWith("when ")
1390
+ ? branchMatches(row)
1391
+ ? branchRows(asBlock(row.value))
1392
+ : []
1393
+ : [row]);
1292
1394
  const lowerFields = (block) => {
1293
1395
  const result = [];
1294
- for (const row of block.entries) {
1396
+ for (const row of branchRows(block)) {
1295
1397
  const { type: t } = fieldType(row);
1296
1398
  const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
1297
1399
  const f = lowerFieldShape(row, resolve);
@@ -1476,6 +1578,15 @@ export function compile(source, options = {}) {
1476
1578
  milliseconds: args[1],
1477
1579
  direction: constant.name,
1478
1580
  });
1581
+ // after(start, 1 month, 3) steps calendar periods from start.
1582
+ else if (constant.name === "after" && args.length === 3)
1583
+ calculations.push({
1584
+ target,
1585
+ op: "step",
1586
+ date: args[0],
1587
+ period: args[1],
1588
+ times: args[2],
1589
+ });
1479
1590
  else
1480
1591
  fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
1481
1592
  authored.set(calculations.at(-1), constant);
@@ -1563,37 +1674,7 @@ export function compile(source, options = {}) {
1563
1674
  entries: block.entries.flatMap((entry) => {
1564
1675
  if (!entry.key.startsWith("when "))
1565
1676
  return [entry];
1566
- const [, tunable, relation, choice] = entry.key.split(" ");
1567
- const binding = environment.get(tunable);
1568
- if (!binding)
1569
- fail(entry, `unknown branch tunable ${tunable}`, "name an enum or reference tunable declared by this header");
1570
- let matches;
1571
- if (relation === "has") {
1572
- // Inspect declarations, not lowering order: a bound object may
1573
- // appear after the instrument that asks about its fields.
1574
- const bound = resolve(binding);
1575
- const [root, ...children] = text(bound).split(".");
1576
- const assignment = assignments.get(root);
1577
- let target = assignment
1578
- ? templates.get(assignment.target)?.body
1579
- : program.decls
1580
- .filter((decl) => decl.kind === "instrument")
1581
- .find((decl) => decl.name === root)?.body;
1582
- for (const child of children) {
1583
- const record = target
1584
- ? entries(asBlock(entries(target).get("records"))).get(child)
1585
- : undefined;
1586
- target = record ? asBlock(record) : undefined;
1587
- }
1588
- if (!target)
1589
- fail(entry, "field branch needs a declared object", "bind a reference to a declared object");
1590
- matches = entries(asBlock(entries(target).get("fields"))).has(choice);
1591
- }
1592
- else {
1593
- if (!enums.get(tunable)?.includes(choice))
1594
- fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
1595
- matches = text(binding) === choice;
1596
- }
1677
+ const matches = branchMatches(entry);
1597
1678
  const body = asBlock(entry.value);
1598
1679
  for (const clause of body.entries)
1599
1680
  if (!["requires", "moves", "invoke", "calculate"].includes(clause.key) &&
@@ -2446,7 +2527,7 @@ export function compile(source, options = {}) {
2446
2527
  .filter(Boolean)
2447
2528
  .join("."),
2448
2529
  }
2449
- : undefined);
2530
+ : undefined, tunables);
2450
2531
  }
2451
2532
  };
2452
2533
  const compileObject = (decl) => {
@@ -3388,10 +3469,21 @@ export function compile(source, options = {}) {
3388
3469
  }, "lower");
3389
3470
  }),
3390
3471
  };
3472
+ const sameParty = samePartyMoves(document, attachedSelectors, attachEntries);
3473
+ const unbound = unboundEconomicSources(document, attachedSelectors, attachEntries);
3474
+ if (sameParty.errors.length || unbound.length)
3475
+ return {
3476
+ verdict: "invalid",
3477
+ warnings: [],
3478
+ diagnostics: [...sameParty.errors, ...unbound].map((d) => diagnostic(d, "check")),
3479
+ };
3391
3480
  return {
3392
3481
  verdict: "valid",
3393
3482
  diagnostics: [],
3394
- warnings: purposeWarnings(document, attachedSelectors, attachEntries).map((d) => ({ ...diagnostic(d, "check"), severity: "warning" })),
3483
+ warnings: [
3484
+ ...sameParty.warnings,
3485
+ ...purposeWarnings(document, attachedSelectors, attachEntries),
3486
+ ].map((d) => ({ ...diagnostic(d, "check"), severity: "warning" })),
3395
3487
  artifacts: {
3396
3488
  document: validated.value,
3397
3489
  costManifest: buildUdlCostManifest(validated.value),