@hyperscale0/hsx 1.0.128 → 1.0.132
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/dist/src/cli.js +1 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +173 -23
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.js +6 -6
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/parse.js +4 -1
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.js +10 -10
- package/dist/src/std-bundle.js.map +1 -1
- package/docs/README.md +78 -27
- package/examples/cards.hsx +7 -1
- package/examples/collections.hsx +4 -0
- package/examples/cost-table.json +80 -56
- package/examples/insurance.hsx +2 -1
- package/examples/marketplace.hsx +3 -1
- package/examples/rental-deposit.hsx +2 -0
- package/examples/wallet.hsx +1 -1
- package/package.json +4 -4
- package/src/cli.ts +1 -1
- package/src/compile.ts +246 -23
- package/src/examples-bundle.ts +6 -6
- package/src/parse.ts +4 -1
- package/src/std-bundle.ts +10 -10
- package/std/booking.hsx +1 -1
- package/std/cards.hsx +20 -11
- package/std/escrow.hsx +45 -10
- package/std/financing.hsx +214 -62
- package/std/insurance.hsx +3 -3
- package/std/lending.hsx +16 -8
- package/std/marketplace.hsx +4 -3
- package/std/money.hsx +28 -14
- package/std/savings.hsx +3 -2
- package/std/wallet.hsx +13 -8
package/src/compile.ts
CHANGED
|
@@ -457,6 +457,9 @@ export function compile(
|
|
|
457
457
|
UdlSubjectRequirement,
|
|
458
458
|
{ source: string; span: Span; message: string }
|
|
459
459
|
>();
|
|
460
|
+
// Instruments that lost a std action to an empty all() parent. Their
|
|
461
|
+
// lifecycles drop the states only that action reached.
|
|
462
|
+
const trimmed = new Set<string>();
|
|
460
463
|
const used = new Set<string>();
|
|
461
464
|
for (const use of program.decls.filter((d) => d.kind === "use")) {
|
|
462
465
|
if (used.has(use.name))
|
|
@@ -551,6 +554,15 @@ export function compile(
|
|
|
551
554
|
const assignments = new Map<string, AssignmentDecl>();
|
|
552
555
|
const attachmentSubjects = new Map<string, string>();
|
|
553
556
|
const names = new Set<string>();
|
|
557
|
+
// A record's create turns public only through a program-level expose,
|
|
558
|
+
// which applies after lowering, so creation input needs the list first.
|
|
559
|
+
const exposedCreates = new Set(
|
|
560
|
+
program.decls.flatMap((decl) =>
|
|
561
|
+
decl.kind === "expose" && decl.target.endsWith(".create")
|
|
562
|
+
? [decl.target.split(".").slice(0, -1).join("_")]
|
|
563
|
+
: [],
|
|
564
|
+
),
|
|
565
|
+
);
|
|
554
566
|
for (const decl of program.decls) {
|
|
555
567
|
if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
|
|
556
568
|
continue;
|
|
@@ -1039,6 +1051,29 @@ export function compile(
|
|
|
1039
1051
|
(!!attachmentInfo &&
|
|
1040
1052
|
subjectPartyRoles.includes(name as SubjectPartyRole));
|
|
1041
1053
|
const resolvedParties = new Set<string>();
|
|
1054
|
+
// A party of a sibling attachment this one references, such as the
|
|
1055
|
+
// borrower of `self.plan`, binds here the way it binds there.
|
|
1056
|
+
const reachedParty = (
|
|
1057
|
+
name: string,
|
|
1058
|
+
): AttachmentPartyBinding | undefined => {
|
|
1059
|
+
if (!attachmentInfo) return;
|
|
1060
|
+
const targets = fields.flatMap((field) =>
|
|
1061
|
+
field.type === "ref" && field.targetKind === "instrument"
|
|
1062
|
+
? [field.target].flat()
|
|
1063
|
+
: [],
|
|
1064
|
+
);
|
|
1065
|
+
const bindings = attachmentInfo.attachments.flatMap((attachment) =>
|
|
1066
|
+
targets.some(
|
|
1067
|
+
(target) =>
|
|
1068
|
+
target === attachment.instrument ||
|
|
1069
|
+
target.startsWith(`${attachment.instrument}_`),
|
|
1070
|
+
) && attachment.parties[name]
|
|
1071
|
+
? [attachment.parties[name]]
|
|
1072
|
+
: [],
|
|
1073
|
+
);
|
|
1074
|
+
const distinct = new Set(bindings.map((item) => JSON.stringify(item)));
|
|
1075
|
+
return distinct.size === 1 ? bindings[0] : undefined;
|
|
1076
|
+
};
|
|
1042
1077
|
const resolve = (
|
|
1043
1078
|
expr: Expr,
|
|
1044
1079
|
seen = new Set<string>(),
|
|
@@ -1098,6 +1133,18 @@ export function compile(
|
|
|
1098
1133
|
attachmentSubjects.has(name)
|
|
1099
1134
|
? name.slice(attachmentInfo.subjectKindId.length + 1)
|
|
1100
1135
|
: `${name} (program)`;
|
|
1136
|
+
const elsewhere = [...assignments.values()]
|
|
1137
|
+
.filter(
|
|
1138
|
+
(assignment) =>
|
|
1139
|
+
assignment.target === type &&
|
|
1140
|
+
attachmentSubjects.has(assignment.name) &&
|
|
1141
|
+
attachmentSubjects.get(assignment.name) !==
|
|
1142
|
+
attachmentInfo.subjectKindId,
|
|
1143
|
+
)
|
|
1144
|
+
.map((assignment) => {
|
|
1145
|
+
const subject = attachmentSubjects.get(assignment.name)!;
|
|
1146
|
+
return `\`${subject}.${assignment.name.slice(subject.length + 1)}\``;
|
|
1147
|
+
});
|
|
1101
1148
|
const target = templates.get(type);
|
|
1102
1149
|
const required =
|
|
1103
1150
|
target?.parameters
|
|
@@ -1116,7 +1163,7 @@ export function compile(
|
|
|
1116
1163
|
}`,
|
|
1117
1164
|
matches.length
|
|
1118
1165
|
? `Bind \`${parameter}\` explicitly to one of: ${matches.map(local).join(", ")}.`
|
|
1119
|
-
: `
|
|
1166
|
+
: `Attach \`${type}\` to \`${attachmentInfo.subjectKindId}\` and bind \`${parameter}\` to that attachment's name. Choose ${required.map((name) => (name.startsWith("per_") ? `its ${name.slice(4).replaceAll("_", " ")} limit` : `\`${name}\``)).join(", ") || "its required bindings"} explicitly.${elsewhere.length ? ` ${elsewhere.join(", ")} ${elsewhere.length === 1 ? "is" : "are"} on another object, which this binding cannot reach.` : ""}`,
|
|
1120
1167
|
);
|
|
1121
1168
|
}
|
|
1122
1169
|
if (expr.name !== "all" && matches.length !== 1)
|
|
@@ -1555,6 +1602,28 @@ export function compile(
|
|
|
1555
1602
|
value: `${id}_${key}`,
|
|
1556
1603
|
span: origin,
|
|
1557
1604
|
});
|
|
1605
|
+
// A subject role only has money here when this attachment binds it.
|
|
1606
|
+
const moneyPath = (expr: Expr): string => {
|
|
1607
|
+
const lowered = path(expr);
|
|
1608
|
+
const role = lowered.slice("party.".length) as SubjectPartyRole;
|
|
1609
|
+
if (
|
|
1610
|
+
attachmentInfo &&
|
|
1611
|
+
lowered.startsWith("party.") &&
|
|
1612
|
+
subjectPartyRoles.includes(role) &&
|
|
1613
|
+
!Object.values(attachmentInfo.parties).some(
|
|
1614
|
+
(binding) => "role" in binding && binding.role === role,
|
|
1615
|
+
)
|
|
1616
|
+
)
|
|
1617
|
+
failWithCode(
|
|
1618
|
+
expr,
|
|
1619
|
+
"subject_party_unbound",
|
|
1620
|
+
`\`${role}\` has no money account in \`${decl.name}\``,
|
|
1621
|
+
role === "operator"
|
|
1622
|
+
? "Use `programOperator` for the company's own account."
|
|
1623
|
+
: `Add a party parameter to \`${decl.name}\` and bind it to \`${role}\` where you attach it.`,
|
|
1624
|
+
);
|
|
1625
|
+
return lowered;
|
|
1626
|
+
};
|
|
1558
1627
|
const fields: UdlField[] = [];
|
|
1559
1628
|
const calculations: UdlCalculation[] = [];
|
|
1560
1629
|
let currentAction: UdlAction | undefined;
|
|
@@ -2472,9 +2541,18 @@ export function compile(
|
|
|
2472
2541
|
`unknown economic purpose ${purposeName}`,
|
|
2473
2542
|
"choose a declared economic purpose",
|
|
2474
2543
|
);
|
|
2475
|
-
|
|
2544
|
+
let sourcePartyName = sourceParty
|
|
2476
2545
|
? String(data(sourceParty))
|
|
2477
2546
|
: undefined;
|
|
2547
|
+
const reached =
|
|
2548
|
+
sourcePartyName && !isParty(sourcePartyName)
|
|
2549
|
+
? reachedParty(sourcePartyName)
|
|
2550
|
+
: undefined;
|
|
2551
|
+
if (reached) {
|
|
2552
|
+
attachmentInfo!.parties[sourcePartyName!] = reached;
|
|
2553
|
+
sourcePartyName =
|
|
2554
|
+
"role" in reached ? reached.role : reached.party;
|
|
2555
|
+
}
|
|
2478
2556
|
if (sourcePartyName && !isParty(sourcePartyName))
|
|
2479
2557
|
fail(
|
|
2480
2558
|
sourceParty!,
|
|
@@ -2608,7 +2686,7 @@ export function compile(
|
|
|
2608
2686
|
operation: "internal_transfer.create",
|
|
2609
2687
|
...(economics ? { economics } : {}),
|
|
2610
2688
|
amount: piece,
|
|
2611
|
-
from:
|
|
2689
|
+
from: moneyPath(from),
|
|
2612
2690
|
to: `party.${recipient.value}`,
|
|
2613
2691
|
});
|
|
2614
2692
|
}
|
|
@@ -2636,8 +2714,8 @@ export function compile(
|
|
|
2636
2714
|
? { capture: String(data(parts.get("capture")!)) }
|
|
2637
2715
|
: {}),
|
|
2638
2716
|
amount: val(amount!),
|
|
2639
|
-
from:
|
|
2640
|
-
to:
|
|
2717
|
+
from: moneyPath(from!),
|
|
2718
|
+
to: moneyPath(to!),
|
|
2641
2719
|
};
|
|
2642
2720
|
const fee = parts.get("fee");
|
|
2643
2721
|
if (!fee)
|
|
@@ -2766,6 +2844,15 @@ export function compile(
|
|
|
2766
2844
|
} as Omit<UdlCalculation, "target">)
|
|
2767
2845
|
: undefined;
|
|
2768
2846
|
const charges = vat ? [charge, vat] : [charge];
|
|
2847
|
+
// A payer's limit reads this: a buyer pays the charges on
|
|
2848
|
+
// top, a seller pays them out of the amount.
|
|
2849
|
+
derived("debit", "Payer debit", {
|
|
2850
|
+
op: "sum",
|
|
2851
|
+
values:
|
|
2852
|
+
paidBy === "buyer"
|
|
2853
|
+
? [transfer.amount, ...charges]
|
|
2854
|
+
: [transfer.amount],
|
|
2855
|
+
} as Omit<UdlCalculation, "target">);
|
|
2769
2856
|
const net =
|
|
2770
2857
|
paidBy === "seller"
|
|
2771
2858
|
? derived("net", "Amount after fee", {
|
|
@@ -2842,7 +2929,7 @@ export function compile(
|
|
|
2842
2929
|
}
|
|
2843
2930
|
if (actionFeeCalculations.length)
|
|
2844
2931
|
a.calculate = [...(a.calculate ?? []), ...actionFeeCalculations];
|
|
2845
|
-
if (attachmentInfo
|
|
2932
|
+
if (attachmentInfo) {
|
|
2846
2933
|
if (attachmentInfo.exposed.has(name)) {
|
|
2847
2934
|
if (automatic(a.actor))
|
|
2848
2935
|
fail(
|
|
@@ -2909,6 +2996,34 @@ export function compile(
|
|
|
2909
2996
|
message: `${id}.${name}.subject.${requirement.field.name}`,
|
|
2910
2997
|
});
|
|
2911
2998
|
}
|
|
2999
|
+
// A parent list may name a list parameter such as `[parent, on]`.
|
|
3000
|
+
if (
|
|
3001
|
+
typeof a.actor === "object" &&
|
|
3002
|
+
"parent" in a.actor &&
|
|
3003
|
+
Array.isArray(a.actor.parent)
|
|
3004
|
+
)
|
|
3005
|
+
a.actor.parent = a.actor.parent.flat();
|
|
3006
|
+
// A parent actor over an empty all() names no instrument that could
|
|
3007
|
+
// invoke the action. A std action then does not exist in this program;
|
|
3008
|
+
// in user code the all() is a mistake.
|
|
3009
|
+
if (
|
|
3010
|
+
typeof a.actor === "object" &&
|
|
3011
|
+
"parent" in a.actor &&
|
|
3012
|
+
Array.isArray(a.actor.parent) &&
|
|
3013
|
+
!a.actor.parent.length
|
|
3014
|
+
) {
|
|
3015
|
+
if ((declarationSources.get(decl) ?? "program") === "program") {
|
|
3016
|
+
const parent = entries(asBlock(slots.get("actor"))).get("parent");
|
|
3017
|
+
fail(
|
|
3018
|
+
parent ?? row,
|
|
3019
|
+
`${parent?.kind === "call" ? `${parent.name}(${text(parent.args[0]!)})` : `the parent actor of ${name}`} matches no instrument`,
|
|
3020
|
+
"name an instrument this program declares or attaches",
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
delete lifecycle.transitions[name];
|
|
3024
|
+
trimmed.add(id);
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
2912
3027
|
inst.actions[name] = a;
|
|
2913
3028
|
inst.actionOrder.push(name);
|
|
2914
3029
|
}
|
|
@@ -3196,8 +3311,7 @@ export function compile(
|
|
|
3196
3311
|
.slice(firstAttachedInstrument)
|
|
3197
3312
|
.filter(
|
|
3198
3313
|
(instrument) =>
|
|
3199
|
-
instrument.id === instId ||
|
|
3200
|
-
instrument.actions.create?.publicAction,
|
|
3314
|
+
instrument.id === instId || exposedCreates.has(instrument.id),
|
|
3201
3315
|
)) {
|
|
3202
3316
|
if (!createdInst.actions.create) continue;
|
|
3203
3317
|
const owned = new Set(
|
|
@@ -3210,9 +3324,15 @@ export function compile(
|
|
|
3210
3324
|
}
|
|
3211
3325
|
const create = createdInst.actions.create;
|
|
3212
3326
|
for (const field of createdInst.fields) {
|
|
3327
|
+
// The engine resolves a lone candidate itself; with several (a
|
|
3328
|
+
// plan's slices) the caller names one, as late_charge does.
|
|
3329
|
+
if (field.type === "ref" && field.targetKind === "instrument") {
|
|
3330
|
+
if (!create.input.some((input) => input.name === field.name))
|
|
3331
|
+
create.input.push({ ...field, optional: true });
|
|
3332
|
+
continue;
|
|
3333
|
+
}
|
|
3213
3334
|
if (
|
|
3214
3335
|
field.type === "account" ||
|
|
3215
|
-
(field.type === "ref" && field.targetKind === "instrument") ||
|
|
3216
3336
|
(field.type === "list" &&
|
|
3217
3337
|
field.item === "ref" &&
|
|
3218
3338
|
field.targetKind === "instrument") ||
|
|
@@ -3222,8 +3342,7 @@ export function compile(
|
|
|
3222
3342
|
create.input.some((input) => input.name === field.name)
|
|
3223
3343
|
)
|
|
3224
3344
|
continue;
|
|
3225
|
-
create.subject
|
|
3226
|
-
const existing = create.subject.requirements.find(
|
|
3345
|
+
const existing = create.subject?.requirements.find(
|
|
3227
3346
|
(item) => item.field.name === field.name,
|
|
3228
3347
|
);
|
|
3229
3348
|
if (existing) {
|
|
@@ -3236,11 +3355,16 @@ export function compile(
|
|
|
3236
3355
|
);
|
|
3237
3356
|
continue;
|
|
3238
3357
|
}
|
|
3358
|
+
// A creation value belongs to this agreement. Only a rename shares
|
|
3359
|
+
// it as object metadata; a matching authored field name does not,
|
|
3360
|
+
// so two agreements never read each other's amount.
|
|
3239
3361
|
const objectField = renames.get(field.name);
|
|
3240
|
-
|
|
3241
|
-
field
|
|
3242
|
-
|
|
3243
|
-
}
|
|
3362
|
+
if (!objectField) {
|
|
3363
|
+
create.input.push({ ...field });
|
|
3364
|
+
continue;
|
|
3365
|
+
}
|
|
3366
|
+
const requirement = { field, objectField };
|
|
3367
|
+
create.subject ??= { requirements: [], adapters: [] };
|
|
3244
3368
|
create.subject.requirements.push(requirement);
|
|
3245
3369
|
requirementOrigins.set(requirement, {
|
|
3246
3370
|
source: declarationSources.get(template) ?? "program",
|
|
@@ -3259,6 +3383,20 @@ export function compile(
|
|
|
3259
3383
|
);
|
|
3260
3384
|
if (!found) {
|
|
3261
3385
|
const renameEntry = renameEntries.get(oldName) ?? entry;
|
|
3386
|
+
const record = document.instruments
|
|
3387
|
+
.slice(firstAttachedInstrument)
|
|
3388
|
+
.find(
|
|
3389
|
+
(instrument) =>
|
|
3390
|
+
instrument.id.startsWith(`${instId}_`) &&
|
|
3391
|
+
instrument.fields.some((field) => field.name === oldName),
|
|
3392
|
+
);
|
|
3393
|
+
if (record)
|
|
3394
|
+
failWithCode(
|
|
3395
|
+
renameEntry,
|
|
3396
|
+
"subject_field_unknown",
|
|
3397
|
+
`rename source '${oldName}' belongs to the ${record.id.slice(instId.length + 1)} record of ${targetTemplate}; rename binds only fields of ${targetTemplate} itself`,
|
|
3398
|
+
`remove this rename; supply ${oldName} as input when the record is created, or set it as a tunable where the header offers one`,
|
|
3399
|
+
);
|
|
3262
3400
|
const declared = attachedInst
|
|
3263
3401
|
? Object.values(attachedInst.actions).flatMap(
|
|
3264
3402
|
(action) =>
|
|
@@ -3746,7 +3884,7 @@ export function compile(
|
|
|
3746
3884
|
// A family member drops an optional reference field its binding left
|
|
3747
3885
|
// unset (a borrower-funded plan has no `funds`), so it cannot match and
|
|
3748
3886
|
// leaves the selection. The header must still declare the field.
|
|
3749
|
-
const
|
|
3887
|
+
const declaredMembers = (selection: UdlSelection) => {
|
|
3750
3888
|
const members = [selection.instrument].flat();
|
|
3751
3889
|
const header = selection.family
|
|
3752
3890
|
? templates.get(
|
|
@@ -3764,13 +3902,64 @@ export function compile(
|
|
|
3764
3902
|
resolveField(document, byId.get(id)!, `self.${selection.reference}`),
|
|
3765
3903
|
);
|
|
3766
3904
|
};
|
|
3767
|
-
// A
|
|
3905
|
+
// A member whose reference points at another attachment of the anchor's
|
|
3906
|
+
// own template (the plan funding another object's escrow) cannot match
|
|
3907
|
+
// either, so each object only selects what can reference it.
|
|
3908
|
+
const kin = (a: string, b: string) => {
|
|
3909
|
+
const left = byId.get(a);
|
|
3910
|
+
const right = byId.get(b);
|
|
3911
|
+
return (
|
|
3912
|
+
!!left && !!right && declarations.get(left) === declarations.get(right)
|
|
3913
|
+
);
|
|
3914
|
+
};
|
|
3915
|
+
const matchable = (
|
|
3916
|
+
selection: UdlSelection,
|
|
3917
|
+
inst: UdlInstrument,
|
|
3918
|
+
action?: UdlAction,
|
|
3919
|
+
) =>
|
|
3920
|
+
declaredMembers(selection).filter((id) => {
|
|
3921
|
+
const member = byId.get(id);
|
|
3922
|
+
if (!member) return true;
|
|
3923
|
+
const anchor = resolveField(
|
|
3924
|
+
document,
|
|
3925
|
+
inst,
|
|
3926
|
+
selection.anchor,
|
|
3927
|
+
action?.input,
|
|
3928
|
+
action,
|
|
3929
|
+
);
|
|
3930
|
+
const reference = resolveField(
|
|
3931
|
+
document,
|
|
3932
|
+
member,
|
|
3933
|
+
`self.${selection.reference}`,
|
|
3934
|
+
);
|
|
3935
|
+
if (anchor?.type !== "ref" || reference?.type !== "ref") return true;
|
|
3936
|
+
const targets = [anchor.target].flat();
|
|
3937
|
+
const candidates = [reference.target].flat();
|
|
3938
|
+
return !candidates.every(
|
|
3939
|
+
(candidate) =>
|
|
3940
|
+
!targets.includes(candidate) &&
|
|
3941
|
+
targets.some((target) => kin(target, candidate)),
|
|
3942
|
+
);
|
|
3943
|
+
});
|
|
3944
|
+
// A count over no matchable member is zero, in a requirement or a calculation.
|
|
3768
3945
|
for (const inst of document.instruments)
|
|
3769
|
-
for (const action of Object.values(inst.actions))
|
|
3946
|
+
for (const action of Object.values(inst.actions)) {
|
|
3947
|
+
if (action.calculate)
|
|
3948
|
+
action.calculate = action.calculate.map((calculation) =>
|
|
3949
|
+
calculation.op === "aggregate" &&
|
|
3950
|
+
calculation.measure === "count" &&
|
|
3951
|
+
!matchable(calculation.selection, inst, action).length
|
|
3952
|
+
? {
|
|
3953
|
+
target: calculation.target,
|
|
3954
|
+
op: "sum",
|
|
3955
|
+
values: [{ literal: 0 }],
|
|
3956
|
+
}
|
|
3957
|
+
: calculation,
|
|
3958
|
+
);
|
|
3770
3959
|
action.requires = action.requires.map((requirement) =>
|
|
3771
3960
|
requirement.kind === "aggregate" &&
|
|
3772
3961
|
requirement.measure === "count" &&
|
|
3773
|
-
!matchable(requirement.selection).length
|
|
3962
|
+
!matchable(requirement.selection, inst, action).length
|
|
3774
3963
|
? {
|
|
3775
3964
|
kind: "compare",
|
|
3776
3965
|
left: { literal: 0 },
|
|
@@ -3779,14 +3968,21 @@ export function compile(
|
|
|
3779
3968
|
}
|
|
3780
3969
|
: requirement,
|
|
3781
3970
|
);
|
|
3971
|
+
}
|
|
3782
3972
|
const checkSelection = (
|
|
3783
3973
|
selection: UdlSelection,
|
|
3784
3974
|
at: Located,
|
|
3785
3975
|
scope: Scope,
|
|
3786
3976
|
) => {
|
|
3977
|
+
walk(
|
|
3978
|
+
selection.overlaps,
|
|
3979
|
+
"overlaps",
|
|
3980
|
+
site(selection, "overlaps", at),
|
|
3981
|
+
scope,
|
|
3982
|
+
);
|
|
3787
3983
|
walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
|
|
3788
3984
|
walk(selection.where, "where", site(selection, "where", at), scope);
|
|
3789
|
-
const members = matchable(selection);
|
|
3985
|
+
const members = matchable(selection, scope.inst, scope.action);
|
|
3790
3986
|
if (
|
|
3791
3987
|
members.length &&
|
|
3792
3988
|
members.length < [selection.instrument].flat().length
|
|
@@ -3807,6 +4003,9 @@ export function compile(
|
|
|
3807
4003
|
selection.order?.forEach((key, index) =>
|
|
3808
4004
|
member(key, site(selection.order!, index, at)),
|
|
3809
4005
|
);
|
|
4006
|
+
if (selection.overlaps)
|
|
4007
|
+
for (const key of ["start", "end"] as const)
|
|
4008
|
+
member(selection.overlaps[key], site(selection.overlaps, key, at));
|
|
3810
4009
|
if (selection.window)
|
|
3811
4010
|
member(selection.window.field, site(selection.window, "field", at));
|
|
3812
4011
|
}
|
|
@@ -3942,7 +4141,7 @@ export function compile(
|
|
|
3942
4141
|
)
|
|
3943
4142
|
)
|
|
3944
4143
|
continue;
|
|
3945
|
-
let specialized =
|
|
4144
|
+
let specialized = trimmed.has(inst.id);
|
|
3946
4145
|
for (const [key, action] of Object.entries(inst.actions)) {
|
|
3947
4146
|
const constant = (value: import("@hyperscale0/udl").UdlValue) => {
|
|
3948
4147
|
if ("literal" in value) return value.literal;
|
|
@@ -4060,6 +4259,28 @@ export function compile(
|
|
|
4060
4259
|
"give each exposed action a distinct public name",
|
|
4061
4260
|
);
|
|
4062
4261
|
}
|
|
4262
|
+
// A public action on a fresh attachment creates it first with no input
|
|
4263
|
+
// (in the engine executor), so creation that needs input must be public.
|
|
4264
|
+
for (const kind of document.objects)
|
|
4265
|
+
for (const attachment of kind.attachments) {
|
|
4266
|
+
if (attachment.parent) continue;
|
|
4267
|
+
const instrument = byId.get(attachment.instrument)!;
|
|
4268
|
+
const create = instrument.actions.create;
|
|
4269
|
+
const needed = create?.input.filter((field) => !field.optional) ?? [];
|
|
4270
|
+
const entry = Object.values(instrument.actions).find(
|
|
4271
|
+
(action) => action.publicAction,
|
|
4272
|
+
);
|
|
4273
|
+
if (!needed.length || create!.publicAction || !entry) continue;
|
|
4274
|
+
fail(
|
|
4275
|
+
objects
|
|
4276
|
+
.get(kind.id)!
|
|
4277
|
+
.body.entries.find((row) =>
|
|
4278
|
+
new RegExp(`^attach\\s+${attachment.name}\\s*=`).test(row.key),
|
|
4279
|
+
) ?? objects.get(kind.id)!,
|
|
4280
|
+
`${attachment.name} starts from ${entry.publicAction}, but its create needs ${needed.map((field) => field.name).join(", ")} and is not exposed`,
|
|
4281
|
+
`expose create on ${attachment.name} so the caller supplies ${needed.length === 1 ? "it" : "them"}, or rename { ${needed[0]!.name}: ${needed[0]!.name} } to read object metadata`,
|
|
4282
|
+
);
|
|
4283
|
+
}
|
|
4063
4284
|
for (const kind of document.objects) {
|
|
4064
4285
|
for (const name of kind.entryActions ?? []) {
|
|
4065
4286
|
const eligible = kind.attachments.some((attachment) => {
|
|
@@ -4112,13 +4333,15 @@ export function compile(
|
|
|
4112
4333
|
),
|
|
4113
4334
|
),
|
|
4114
4335
|
);
|
|
4336
|
+
const reaching = stranded.actions.at(-1);
|
|
4337
|
+
const drain = `move ${stranded.accounts.map((account) => `\`self.${account}.balance\``).join(" and ")} out in ${reaching ? `\`${reaching}\`` : "the action that reaches it"}, or add an action from \`${stranded.state}\` that does`;
|
|
4115
4338
|
return diagnostic(
|
|
4116
4339
|
{
|
|
4117
4340
|
code: i.code,
|
|
4118
4341
|
message: `\`${attachment?.name ?? instrument.id}\` can reach \`${stranded.state}\` with money in ${stranded.accounts.map((account) => `\`${account}\``).join(", ")}, but no action leaves that state and disposes of the balance.`,
|
|
4119
4342
|
fix: binding
|
|
4120
|
-
? `Restore \`${binding.parameter}: ${stranded.state}\`, or
|
|
4121
|
-
:
|
|
4343
|
+
? `Restore \`${binding.parameter}: ${stranded.state}\`, or ${drain}.`
|
|
4344
|
+
: `${drain[0]!.toUpperCase()}${drain.slice(1)}.`,
|
|
4122
4345
|
span: binding?.span ?? origin?.span ?? program.span,
|
|
4123
4346
|
related: stranded.accounts.map((account) => ({
|
|
4124
4347
|
source: "program",
|
package/src/examples-bundle.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// Generated by open/hsx/scripts/bundle-examples.ts. Do not edit by hand.
|
|
2
2
|
import type { HsxExample } from "./examples.ts";
|
|
3
3
|
export const examples: HsxExample[] = [
|
|
4
|
-
{"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
|
|
5
|
-
{"id":"collections","title":"Small business collections","summary":"A business lender refers overdue instalments to an agency with limits on collection contacts.","headers":["collections","financing"],"source":"// A business lender refers overdue instalments to an agency with limits on collection contacts.\nprogram business_collections \"Small business collections\"\nuse collections\nuse financing\nparty agency: business\n\nobject facility \"Business loan\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits { expose create as create_allowance, expose approve as approve_borrower, borrower: owner, per_borrower: 100000 SAR }\n attach budget = financing.portfolio_limit { expose create as create_budget, expose approve as approve_budget, limit: 2000000 SAR }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach recovery = collections.case {\n expose create as create_recovery\n on: loan, agency: agency\n expose assign as assign_case\n expose recover as recover_payment\n expose recall as recall_case\n }\n attach contact = collections.contact {\n expose create as create_contact\n case: recovery, agency: agency\n }\n}\n\nexpose facility.loan.payment.create as create_payment\nexpose facility.loan.payment.pay as pay_instalment\nexpose facility.loan.payment.refund as refund_payment\nexpose facility.loan.payoff_quote.create as request_payoff_quote\n"},
|
|
4
|
+
{"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\n// supplier is a business this program declares, so plan needs {\"partyBindings\":{\"supplier\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the payment\n// and receipt attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, person: owner, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n expose issuer_freeze as block_card\n expose issuer_unfreeze as unblock_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
|
|
5
|
+
{"id":"collections","title":"Small business collections","summary":"A business lender refers overdue instalments to an agency with limits on collection contacts.","headers":["collections","financing"],"source":"// A business lender refers overdue instalments to an agency with limits on collection contacts.\nprogram business_collections \"Small business collections\"\nuse collections\nuse financing\n// agency is a business this program declares, so plan needs {\"partyBindings\":{\"agency\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the recovery\n// and contact attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty agency: business\n\nobject facility \"Business loan\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits { expose create as create_allowance, expose approve as approve_borrower, borrower: owner, per_borrower: 100000 SAR }\n attach budget = financing.portfolio_limit { expose create as create_budget, expose approve as approve_budget, limit: 2000000 SAR }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach recovery = collections.case {\n expose create as create_recovery\n on: loan, agency: agency\n expose assign as assign_case\n expose recover as recover_payment\n expose recall as recall_case\n }\n attach contact = collections.contact {\n expose create as create_contact\n case: recovery, agency: agency\n }\n}\n\nexpose facility.loan.payment.create as create_payment\nexpose facility.loan.payment.pay as pay_instalment\nexpose facility.loan.payment.refund as refund_payment\nexpose facility.loan.payoff_quote.create as request_payoff_quote\n"},
|
|
6
6
|
{"id":"escrow","title":"Used device sales","summary":"Buyers fund a used device purchase, then accept delivery or raise a dispute before release.","headers":["escrow"],"source":"// Buyers fund a used device purchase, then accept delivery or raise a dispute before release.\nprogram used_device_sales \"Used device sales\"\nuse escrow\n\nobject device \"Device\" {\n fields {\n model: text\n condition: text\n serialNumber: text\n price: money\n }\n columns: [model, condition, price]\n attach sale = escrow.hold {\n expose create as create_sale\n payer: actor, payee: owner\n expose fund as pay_for_device\n expose deliver as confirm_delivery\n expose accept as accept_device\n expose dispute as report_problem\n expose verify_return as verify_return\n expose refund as refund_buyer\n expose cancel as cancel_sale\n }\n}\n"},
|
|
7
7
|
{"id":"financing","title":"Tuition instalments","summary":"A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.","headers":["financing"],"source":"// A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.\nprogram tuition_instalments \"Tuition instalments\"\nuse financing\n\nobject enrolment \"Enrolment\" {\n fields { student: text, course: text, price: money }\n columns: [student, course, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 12000 SAR\n expose approve as approve_student\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 500000 SAR\n expose approve as approve_budget\n }\n attach tuition = financing.installments {\n expose create as create_tuition\n borrower: owner, capital: operator\n months: 3, pricing: flat_total, profit_rate: 0%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_tuition_plan\n expose disburse as fund_tuition\n expose payoff as repay_early\n }\n}\n\nexpose enrolment.tuition.payment.create as create_payment\nexpose enrolment.tuition.payment.pay as pay_instalment\nexpose enrolment.tuition.payment.refund as refund_payment\nexpose enrolment.tuition.payoff_quote.create as request_payoff_quote\n"},
|
|
8
|
-
{"id":"insurance","title":"Device insurance","summary":"A device retailer collects the purchase price and attaches cover with a claim review process.","headers":["insurance","money"],"source":"// A device retailer collects the purchase price and attaches cover with a claim review process.\nprogram device_insurance \"Device insurance\"\nuse insurance\nuse money\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n expose create as create_purchase\n payer: owner, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose pay as pay_for_device\n }\n // conformance_boundary is the sandbox insurer adapter; swap in the insurer's registered adapter to go live.\n // Premiums net of commission go to its account. claim_limit is the sum insured, set per cover at creation.\n attach protection = insurance.cover {\n expose create as create_protection\n holder: owner, adapter: \"conformance_boundary\", commission: 10%, covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n expose create as create_claim\n cover: protection, inspector: operator\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n"},
|
|
8
|
+
{"id":"insurance","title":"Device insurance","summary":"A device retailer collects the purchase price and attaches cover with a claim review process.","headers":["insurance","money"],"source":"// A device retailer collects the purchase price and attaches cover with a claim review process.\nprogram device_insurance \"Device insurance\"\nuse insurance\nuse money\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n expose create as create_purchase\n payer: owner, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose pay as pay_for_device\n }\n // conformance_boundary is the sandbox insurer adapter; swap in the insurer's registered adapter to go live.\n // Premiums net of commission go to its account. The default broker is the retailer, which keeps the commission.\n // claim_limit is the sum insured, set per cover at creation.\n attach protection = insurance.cover {\n expose create as create_protection\n holder: owner, adapter: \"conformance_boundary\", commission: 10%, covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n expose create as create_claim\n cover: protection, inspector: operator\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n"},
|
|
9
9
|
{"id":"lending","title":"Community business lending","summary":"A lending composition with limit approvals, wallet commitments and cash distribution transitions.","headers":["lending","financing","wallet"],"source":"// A lending composition with limit approvals, wallet commitments and cash distribution transitions.\nprogram community_business_lending \"Community business lending\"\nuse lending\nuse financing\nuse wallet\n\nobject business \"Business funding\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 100000 SAR\n expose approve as approve_borrower\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 2000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach investor_balance = wallet.balance {\n expose create as create_investor_balance\n holder: actor\n expose activate as open_wallet\n expose topup as add_funds\n }\n attach funding = lending.round {\n borrower: owner, plan: loan\n expose create as open_funding\n expose close as close_funding\n }\n attach investment = lending.commitment {\n round: funding, wallet: investor_balance, investor: actor\n expose create as invest\n expose withdraw as withdraw_investment\n }\n attach returns = lending.distribution {\n expose create as create_returns\n round: funding, receipt: loan.settlement\n expose prepare_cash as prepare_returns\n expose distribute_cash as distribute_returns\n }\n}\n\nexpose business.loan.payment.create as create_payment\nexpose business.loan.payment.pay as pay_instalment\nexpose business.loan.payment.refund as refund_payment\nexpose business.loan.payoff_quote.create as request_payoff_quote\nexpose business.returns.share.create as create_return_share\n"},
|
|
10
10
|
{"id":"library","title":"Financed car sales","summary":"A minimal financed car sale connects escrow to a three-instalment plan and lending limits.","headers":["escrow","financing"],"source":"// A minimal financed car sale connects escrow to a three-instalment plan and lending limits.\nprogram cars \"Financed car sales\"\nuse escrow\nuse financing\nobject car \"Cars\" {\n fields { make: text, model: text, year: integer }\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: owner, expose fund as sell }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 60000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 1500000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator, share: 25%\n months: 3, pricing: flat_total, profit_rate: 2.5%, funds: sale, limits: limits, portfolio: ceiling\n expose create as finance\n }\n}\n"},
|
|
11
|
-
{"id":"marketplace","title":"Freelance marketplace","summary":"Freelancers publish offers and clients place orders with funds held until work is accepted.","headers":["marketplace","escrow"],"source":"// Freelancers publish offers and clients place orders with funds held until work is accepted.\nprogram freelance_marketplace \"Freelance marketplace\"\nuse marketplace\nuse escrow\n\nobject project \"Project\" {\n fields { brief: text, deliverable: text, price: money }\n columns: [brief, deliverable, price]\n attach offer = marketplace.listing {\n expose create as create_offer\n seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.order {\n expose create as create_order\n listing: offer, buyer: actor\n expose commit as place_order\n expose fulfill as finish_order\n }\n attach payment = escrow.hold {\n expose create as create_payment\n payer: actor, payee: owner\n expose fund as fund_project\n expose deliver as submit_work\n expose accept as accept_work\n expose dispute as dispute_work\n expose verify_return as verify_return\n expose refund as refund_client\n expose cancel as cancel_payment\n }\n}\n"},
|
|
11
|
+
{"id":"marketplace","title":"Freelance marketplace","summary":"Freelancers publish offers and clients place orders with funds held until work is accepted.","headers":["marketplace","escrow"],"source":"// Freelancers publish offers and clients place orders with funds held until work is accepted.\nprogram freelance_marketplace \"Freelance marketplace\"\nuse marketplace\nuse escrow\n\nobject project \"Project\" {\n fields { brief: text, deliverable: text, price: money }\n columns: [brief, deliverable, price]\n attach offer = marketplace.listing {\n rename { price: price }\n expose create as create_offer\n seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.order {\n rename { price: price }\n expose create as create_order\n listing: offer, buyer: actor, fulfiller: owner\n expose commit as place_order\n expose fulfill as finish_order\n }\n attach payment = escrow.hold {\n expose create as create_payment\n payer: actor, payee: owner\n expose fund as fund_project\n expose deliver as submit_work\n expose accept as accept_work\n expose dispute as dispute_work\n expose verify_return as verify_return\n expose refund as refund_client\n expose cancel as cancel_payment\n }\n}\n"},
|
|
12
12
|
{"id":"money","title":"Tutoring studio","summary":"A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.","headers":["money"],"source":"// A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.\nprogram tutoring_studio \"Tutoring studio\"\nuse money\n\nobject lesson \"Lesson\" {\n fields {\n student: text\n tutor: text\n startsAt: date\n }\n columns: [student, tutor, startsAt]\n attach payment = money.transfer {\n expose create as create_payment\n payer: actor, payee: operator, amount: 150 SAR\n economics pay { purpose: earning, sourceParty: actor }\n expose pay as pay_for_lesson\n expose cancel as cancel_payment\n }\n}\n"},
|
|
13
|
-
{"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// The renter is one declared business, so this instrument takes no parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
|
|
13
|
+
{"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// The renter is one declared business, so this instrument takes no parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n // dueAt is a column, so it stays shared object metadata.\n rename { dueAt: dueAt }\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
|
|
14
14
|
{"id":"repair-approval","title":"Repair approvals","summary":"A repair shop records inspection and approval evidence before it can complete a job.","headers":[],"source":"// A repair shop records inspection and approval evidence before it can complete a job.\nprogram repair_shop \"Repair approvals\"\n\ninstrument assessment {\n summary: \"Inspect the repair, collect approval, then record completion.\"\n fields { inspectedAt: date?, approvedAt: date? }\n lifecycle { states: [submitted, inspected, approved, completed, rejected], initial: submitted }\n action create {}\n action inspect {\n from: submitted, to: inspected, actor: { party: programOperator }\n subject { inspectionNotes: text }\n set: { inspectedAt: { field: self.now } }\n }\n action approve {\n from: inspected, to: approved, actor: { party: programOperator }\n subject { approvalReference: text }\n set: { approvedAt: { field: self.now } }\n }\n action reject {\n from: inspected, to: rejected, actor: { party: programOperator }\n }\n action complete {\n from: approved, to: completed, actor: { party: programOperator }\n subject { completionNotes: text }\n }\n}\n\nobject repair \"Repair\" {\n fields { item: text, problem: text }\n columns: [item, problem]\n attach review = assessment {\n expose create as create_review\n expose inspect as inspect_repair\n expose approve as approve_repair\n expose reject as reject_repair\n expose complete as complete_repair\n }\n}\n\nhide assessment.create\nhide assessment.inspect\nhide assessment.approve\nhide assessment.reject\nhide assessment.complete\n"},
|
|
15
15
|
{"id":"reporting","title":"Equipment loan reports","summary":"An equipment lender keeps an ageing report over its financed purchases and recorded repayments.","headers":["reporting","financing"],"source":"// An equipment lender keeps an ageing report over its financed purchases and recorded repayments.\nprogram equipment_loan_reports \"Equipment loan reports\"\nuse reporting\nuse financing\n\nobject purchase \"Equipment loan\" {\n fields { businessName: text, equipment: text, price: money }\n columns: [businessName, equipment, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 200000 SAR\n expose approve as approve_business\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 5000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_purchase_loan\n expose disburse as fund_purchase\n }\n attach reports = reporting.portfolio { expose create as create_reports, on: loan }\n}\n"},
|
|
16
16
|
{"id":"savings","title":"Savings circle","summary":"Six members contribute to a shared pot and receive it in their agreed calendar order.","headers":["savings"],"source":"// Six members contribute to a shared pot and receive it in their agreed calendar order.\nprogram neighbourhood_savings \"Savings circle\"\nuse savings\n\nobject group \"Savings group\" {\n fields { groupName: text, purpose: text }\n columns: [groupName, purpose]\n attach circle = savings.circle {\n expose create as create_circle\n contribution: 500 SAR, members: 6, starts: 2027-01-01\n expose disband as disband_circle\n expose close as close_circle\n }\n attach membership = savings.membership {\n expose create as create_membership\n circle: circle, member: actor\n expose activate as confirm_membership\n expose withdraw as leave_circle\n }\n // Contributions and each member's payout follow the declared calendar.\n}\n"},
|
|
17
17
|
{"id":"serviced","title":"Car financing","summary":"A car finance company services loans with late charges, payment reminders and early repayment.","headers":["escrow","financing","collections"],"source":"// A car finance company services loans with late charges, payment reminders and early repayment.\nprogram car_financing \"Car financing\"\nuse escrow\nuse financing\nuse collections\nobject car \"Car\" {\n fields { make: text, model: text, year: integer, vin: text, price: money }\n columns: [make, model, year, vin, price]\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: operator, expose fund as purchase }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 250000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 10000000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator\n months: 36, pricing: flat_total, profit_rate: 8.5%, down_payment: 10%\n apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d\n funds: sale, limits: limits, portfolio: ceiling\n expose create as finance, expose sign as sign_contract, expose payoff as payoff\n }\n attach late = financing.late_charge {\n expose create as create_late\n on: plan, grace: 3d, fine: 50 SAR, cap: 25 SAR\n fines_to: operator, costs_to: operator, borrower: actor\n }\n attach reminders = collections.reminder { expose create as create_reminders, on: plan, operator: operator, before_days: 3, overdue_days: 1 }\n}\n"},
|
|
18
|
-
{"id":"wallet","title":"Workshop credits","summary":"Members prepay for workshop visits and the studio reserves each spend before charging it.","headers":["wallet"],"source":"// Members prepay for workshop visits and the studio reserves each spend before charging it.\nprogram workshop_credits \"Workshop credits\"\nuse wallet\n\nobject membership \"Membership\" {\n fields { memberName: text, membershipNumber: text }\n columns: [memberName, membershipNumber]\n attach balance = wallet.balance {\n expose create as create_balance\n holder: owner\n expose activate as open_balance\n expose topup as add_credit\n expose withdraw as withdraw_credit\n expose close as close_balance\n }\n attach visit = wallet.spend {\n expose create as create_visit\n wallet: balance, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
|
|
18
|
+
{"id":"wallet","title":"Workshop credits","summary":"Members prepay for workshop visits and the studio reserves each spend before charging it.","headers":["wallet"],"source":"// Members prepay for workshop visits and the studio reserves each spend before charging it.\nprogram workshop_credits \"Workshop credits\"\nuse wallet\n\nobject membership \"Membership\" {\n fields { memberName: text, membershipNumber: text }\n columns: [memberName, membershipNumber]\n attach balance = wallet.balance {\n expose create as create_balance\n holder: owner\n expose activate as open_balance\n expose topup as add_credit\n expose withdraw as withdraw_credit\n expose close as close_balance\n }\n attach visit = wallet.spend {\n expose create as create_visit\n wallet: balance, payee: operator, holder: owner\n economics pay { purpose: earning, sourceParty: owner }\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
|
|
19
19
|
];
|
package/src/parse.ts
CHANGED
|
@@ -471,13 +471,16 @@ class Parser {
|
|
|
471
471
|
this.fail("expression nesting exceeds 32", "reduce nested values");
|
|
472
472
|
const start = this.peek().span.start;
|
|
473
473
|
let value = this.atom();
|
|
474
|
-
|
|
474
|
+
// `cap:` starts the next entry, so a field may still be named cap.
|
|
475
|
+
if (this.at("cap") && this.tokens[this.index + 1]?.text !== ":") {
|
|
476
|
+
this.take();
|
|
475
477
|
value = {
|
|
476
478
|
kind: "capped",
|
|
477
479
|
rate: value,
|
|
478
480
|
cap: this.atom(),
|
|
479
481
|
span: this.span(start),
|
|
480
482
|
};
|
|
483
|
+
}
|
|
481
484
|
if (this.eat("="))
|
|
482
485
|
value = {
|
|
483
486
|
kind: "default",
|