@hyperscale0/hsx 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -4
- package/README.md +58 -38
- package/dist/src/ast.d.ts +4 -2
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts +8 -2
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +8 -1
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +3 -1
- package/dist/src/cost.d.ts.map +1 -1
- package/dist/src/cost.js +124 -94
- package/dist/src/cost.js.map +1 -1
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +96 -5
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/emit.d.ts +3 -1
- package/dist/src/emit.d.ts.map +1 -1
- package/dist/src/emit.js +27 -10
- package/dist/src/emit.js.map +1 -1
- package/dist/src/format.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/ir.d.ts.map +1 -1
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lsp/server.js.map +1 -1
- package/dist/src/modules.js +7 -3
- package/dist/src/modules.js.map +1 -1
- package/dist/src/parse.js +3 -3
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.js +41 -41
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/std-library.js +2 -2
- package/dist/src/typecheck.d.ts.map +1 -1
- package/dist/src/typecheck.js +948 -157
- package/dist/src/typecheck.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.md +10 -1
- package/docs/assets/hsx.svg +18 -0
- package/docs/guide/01-first-program.md +1 -1
- package/docs/guide/02-money.md +1 -1
- package/docs/guide/03-instruments.md +44 -3
- package/docs/guide/04-lifecycles.md +18 -3
- package/docs/guide/05-fees-and-splits.md +2 -2
- package/docs/guide/06-schedules.md +1 -1
- package/docs/guide/07-composition.md +8 -2
- package/docs/guide/08-writing-a-module.md +13 -2
- package/docs/guide/09-cost.md +3 -3
- package/docs/guide/10-diagnostics.md +1 -1
- package/docs/llms-full.txt +473 -190
- package/docs/llms.txt +2 -2
- package/docs/playground.md +4 -3
- package/docs/reference/cli.md +2 -2
- package/docs/reference/diagnostics.md +243 -37
- package/docs/reference/grammar.md +2 -2
- package/docs/reference/std/advance.md +2 -2
- package/docs/reference/std/cancellable_booking.md +10 -10
- package/docs/reference/std/captured_payment.md +14 -14
- package/docs/reference/std/conditional_disbursement.md +3 -3
- package/docs/reference/std/credit_facility.md +2 -2
- package/docs/reference/std/held_payment.md +37 -33
- package/docs/reference/std/instant_transfer.md +8 -8
- package/docs/reference/std/metered.md +2 -2
- package/docs/reference/std/pooled_split.md +2 -2
- package/docs/reference/std/premium_forward.md +13 -13
- package/docs/reference/std/reconciled_payout.md +2 -2
- package/docs/reference/std/recurring_collection.md +2 -2
- package/docs/reference/std/rotating_pool.md +2 -2
- package/docs/reference/std/scheduled.md +18 -18
- package/docs/reference/std/security_deposit.md +14 -14
- package/docs/reference/std/settlement_batch.md +2 -2
- package/docs/reference/std/swap.md +18 -18
- package/docs/reference/std/threshold_pool.md +2 -2
- package/docs/reference/std/weighted_distribution.md +2 -2
- package/docs/reference/types.md +1 -1
- package/docs/reference/udl-output.md +1 -1
- package/examples/01-first-program/README.md +1 -1
- package/examples/01-first-program/tip-jar.hsx +1 -1
- package/examples/02-imports-and-archetypes/photo-booth.hsx +1 -1
- package/examples/03-diagnostics/corner-shop-fixed.hsx +1 -1
- package/examples/03-diagnostics/corner-shop.hsx +1 -1
- package/examples/04-complete-product/study-hall.hsx +1 -1
- package/examples/05-watch-club/README.md +0 -3
- package/examples/05-watch-club/watch-club.hsx +1 -1
- package/examples/README.md +35 -4
- package/examples/advance/advance.hsx +1 -1
- package/examples/advance/advance.udl +16 -11
- package/examples/cancellable_booking/cancellable_booking.hsx +1 -1
- package/examples/cancellable_booking/cancellable_booking.udl +13 -11
- package/examples/captured_payment/captured_payment.hsx +1 -1
- package/examples/captured_payment/captured_payment.udl +10 -7
- package/examples/conditional_disbursement/conditional_disbursement.hsx +1 -1
- package/examples/conditional_disbursement/conditional_disbursement.udl +7 -5
- package/examples/cost-table.json +1542 -382
- package/examples/credit_facility/credit_facility.hsx +1 -1
- package/examples/credit_facility/credit_facility.udl +26 -19
- package/examples/held_payment/held_payment.hsx +1 -1
- package/examples/held_payment/held_payment.udl +61 -15
- package/examples/instant_transfer/instant_transfer.hsx +1 -1
- package/examples/instant_transfer/instant_transfer.udl +17 -11
- package/examples/metered/metered.hsx +1 -1
- package/examples/metered/metered.udl +8 -6
- package/examples/pooled_split/pooled_split.hsx +1 -1
- package/examples/pooled_split/pooled_split.udl +16 -13
- package/examples/premium_forward/premium_forward.hsx +1 -1
- package/examples/premium_forward/premium_forward.udl +22 -16
- package/examples/reconciled_payout/reconciled_payout.hsx +1 -1
- package/examples/reconciled_payout/reconciled_payout.udl +4 -2
- package/examples/recurring_collection/recurring_collection.hsx +1 -1
- package/examples/recurring_collection/recurring_collection.udl +20 -15
- package/examples/rotating_pool/rotating_pool.hsx +1 -1
- package/examples/rotating_pool/rotating_pool.udl +92 -82
- package/examples/scheduled/scheduled.hsx +1 -1
- package/examples/scheduled/scheduled.udl +15 -10
- package/examples/security_deposit/security_deposit.hsx +1 -1
- package/examples/security_deposit/security_deposit.udl +19 -5
- package/examples/settlement_batch/settlement_batch.hsx +1 -1
- package/examples/swap/swap.hsx +1 -1
- package/examples/swap/swap.udl +34 -28
- package/examples/threshold_pool/threshold_pool.hsx +1 -1
- package/examples/threshold_pool/threshold_pool.udl +14 -12
- package/examples/weighted_distribution/weighted_distribution.hsx +1 -1
- package/examples/weighted_distribution/weighted_distribution.udl +6 -4
- package/package.json +6 -5
- package/skills/hsx/SKILL.md +26 -26
- package/src/ast.ts +4 -2
- package/src/cli.ts +4 -4
- package/src/compile.ts +14 -3
- package/src/cost.ts +176 -104
- package/src/diagnostics.ts +99 -4
- package/src/emit.ts +32 -18
- package/src/index.ts +2 -1
- package/src/modules.ts +9 -3
- package/src/parse.ts +3 -3
- package/src/std-bundle.ts +41 -41
- package/src/std-library.ts +2 -2
- package/src/typecheck.ts +1251 -235
- package/src/version.ts +1 -1
- package/std/SEMANTICS.md +12 -3
- package/std/{settlements → money_flows}/advance.hsx +1 -1
- package/std/{settlements → money_flows}/cancellable_booking.hsx +4 -3
- package/std/{settlements → money_flows}/captured_payment.hsx +35 -6
- package/std/{settlements → money_flows}/conditional_disbursement.hsx +17 -16
- package/std/{settlements → money_flows}/credit_facility.hsx +1 -2
- package/std/{settlements → money_flows}/held_payment.hsx +61 -25
- package/std/money_flows/index.hsx +3 -0
- package/std/{settlements → money_flows}/instant_transfer.hsx +2 -3
- package/std/{settlements → money_flows}/metered.hsx +1 -1
- package/std/{settlements → money_flows}/pooled_split.hsx +3 -3
- package/std/{settlements → money_flows}/premium_forward.hsx +22 -23
- package/std/{settlements → money_flows}/reconciled_payout.hsx +3 -3
- package/std/{settlements → money_flows}/recurring_collection.hsx +1 -1
- package/std/{settlements → money_flows}/rotating_pool.hsx +8 -6
- package/std/{settlements → money_flows}/scheduled.hsx +22 -27
- package/std/{settlements → money_flows}/security_deposit.hsx +38 -14
- package/std/{settlements → money_flows}/settlement_batch.hsx +3 -3
- package/std/{settlements → money_flows}/swap.hsx +41 -16
- package/std/{settlements → money_flows}/threshold_pool.hsx +5 -5
- package/std/{settlements → money_flows}/weighted_distribution.hsx +6 -6
- package/docs/sessions/2026-09-02-two-instruments.hsx +0 -21
- package/docs/sessions/2026-09-02-two-instruments.md +0 -41
- package/std/settlements/index.hsx +0 -3
package/dist/src/typecheck.js
CHANGED
|
@@ -5,6 +5,7 @@ const CAMEL_CASE = /^[a-z][A-Za-z0-9]*$/;
|
|
|
5
5
|
const CURRENCY = /^[A-Z]{3}$/;
|
|
6
6
|
const MONEY_PATTERN = "^[1-9][0-9]{0,17}$";
|
|
7
7
|
const OPTIONAL_MONEY_PATTERN = "^(0|[1-9][0-9]{0,17})$";
|
|
8
|
+
const ACCOUNT_PATTERN = "^acct_(sandbox|live)_[a-z0-9]{8,64}$";
|
|
8
9
|
const MAX_COMPREHENSION_EXPANSIONS = 256;
|
|
9
10
|
const NONE_SENTINEL = "__hsx_none__";
|
|
10
11
|
const actionClauseBySpelling = new Map(udlClauseVocabulary
|
|
@@ -43,6 +44,26 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
43
44
|
const ports = new Map(program.decls
|
|
44
45
|
.filter((decl) => decl.kind === "port")
|
|
45
46
|
.map((decl) => [decl.name.name, decl]));
|
|
47
|
+
const checkedPorts = new Set();
|
|
48
|
+
const checkPortDeclarations = (declared, knownParties) => {
|
|
49
|
+
for (const port of declared.values()) {
|
|
50
|
+
if (checkedPorts.has(port))
|
|
51
|
+
continue;
|
|
52
|
+
checkedPorts.add(port);
|
|
53
|
+
const allowed = entry(port.body, "allowed")?.value;
|
|
54
|
+
for (const item of allowed?.kind === "list"
|
|
55
|
+
? allowed.items
|
|
56
|
+
: allowed
|
|
57
|
+
? [allowed]
|
|
58
|
+
: []) {
|
|
59
|
+
const party = nameText(item);
|
|
60
|
+
if (knownParties.has(party))
|
|
61
|
+
continue;
|
|
62
|
+
report("HSX1024", item.span, `decision port ${port.name.name} allows undeclared party ${party}; declare the party before using it`, `declare party ${party} with its party kind before using it in this port`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
checkPortDeclarations(ports, parties);
|
|
46
67
|
const assetSubjects = program.decls
|
|
47
68
|
.filter((decl) => decl.kind === "asset")
|
|
48
69
|
.map((decl) => ({
|
|
@@ -89,16 +110,17 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
89
110
|
});
|
|
90
111
|
}
|
|
91
112
|
const compositionDeclarations = program.decls.filter((declaration) => declaration.kind === "use" || declaration.kind === "expose");
|
|
92
|
-
|
|
93
|
-
declaration
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
authoredInstrumentDeclarations.length === 0)) {
|
|
97
|
-
return checkPublishedProgram(program, options.publishedCatalog, header, subjects, diagnostics, report);
|
|
98
|
-
}
|
|
99
|
-
for (const declaration of compositionDeclarations) {
|
|
100
|
-
report("HSX1018", declaration.span, "this compiler host did not supply a published instrument catalog", "pass the canonical UDL catalog as the publishedCatalog compile option");
|
|
113
|
+
if (!options.publishedCatalog) {
|
|
114
|
+
for (const declaration of compositionDeclarations) {
|
|
115
|
+
report("HSX1018", declaration.span, "this compiler host did not supply a published instrument catalog", "pass the canonical UDL catalog as the publishedCatalog compile option");
|
|
116
|
+
}
|
|
101
117
|
}
|
|
118
|
+
const catalogById = options.publishedCatalog
|
|
119
|
+
? new Map(options.publishedCatalog.instruments.map((instrument) => [
|
|
120
|
+
instrument.id,
|
|
121
|
+
instrument,
|
|
122
|
+
]))
|
|
123
|
+
: undefined;
|
|
102
124
|
const aliases = new Map(program.decls
|
|
103
125
|
.filter((decl) => decl.kind === "type")
|
|
104
126
|
.map((decl) => [decl.name.name, decl.value]));
|
|
@@ -115,6 +137,8 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
115
137
|
templates.set(decl.name.name, decl);
|
|
116
138
|
}
|
|
117
139
|
const concrete = [];
|
|
140
|
+
const reachedPorts = new Set();
|
|
141
|
+
const droppedPorts = new Set();
|
|
118
142
|
const scopedDeclarations = (declarationScope) => {
|
|
119
143
|
const scopedAliases = new Map(aliases);
|
|
120
144
|
const scopedConstants = resolveConstants([
|
|
@@ -135,6 +159,7 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
135
159
|
scopedTemplates.set(local.name.name, local);
|
|
136
160
|
}
|
|
137
161
|
}
|
|
162
|
+
checkPortDeclarations(scopedPorts, scopedParties);
|
|
138
163
|
checkConstants(declarationScope, scopedAliases, diagnostics);
|
|
139
164
|
return {
|
|
140
165
|
aliases: scopedAliases,
|
|
@@ -149,13 +174,25 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
149
174
|
!decl.hasParameterList &&
|
|
150
175
|
decl.typeParameters.length === 0) {
|
|
151
176
|
const scoped = scopedDeclarations(decl.declarationScope ?? []);
|
|
152
|
-
|
|
177
|
+
const values = new Map(scoped.constants);
|
|
178
|
+
const dependencies = new Map();
|
|
179
|
+
const shapes = new Map();
|
|
180
|
+
for (const [name, port] of scoped.ports) {
|
|
181
|
+
const reference = {
|
|
182
|
+
kind: "port_ref",
|
|
183
|
+
name: port.name,
|
|
184
|
+
span: port.span,
|
|
185
|
+
};
|
|
186
|
+
values.set(name, reference);
|
|
187
|
+
bindPortCompileValues(values, name, reference, scoped.ports, diagnostics, scoped.aliases, shapes, dependencies);
|
|
188
|
+
}
|
|
189
|
+
const selected = selectCompileTimeRows(decl.body, values, diagnostics, dependencies);
|
|
190
|
+
const body = expandComprehensions(selected, diagnostics, values, dependencies);
|
|
191
|
+
concrete.push(...bindActionPorts(constructedInstruments(decl.name, body, diagnostics), scoped.ports, reachedPorts).map((candidate) => ({
|
|
192
|
+
...candidate,
|
|
153
193
|
aliases: scoped.aliases,
|
|
154
|
-
body: substituteExpr(decl.body, scoped.constants),
|
|
155
|
-
generatedPrefix: false,
|
|
156
|
-
name: decl.name,
|
|
157
194
|
parties: scoped.parties,
|
|
158
|
-
});
|
|
195
|
+
})));
|
|
159
196
|
}
|
|
160
197
|
if (decl.kind !== "instrument_apply")
|
|
161
198
|
continue;
|
|
@@ -174,18 +211,31 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
174
211
|
report("HSX1001", application.callee.span, `instrument function ${callee} is not bound by this module graph`, `import ${callee} from its module or declare it before instantiation`);
|
|
175
212
|
continue;
|
|
176
213
|
}
|
|
177
|
-
const
|
|
214
|
+
const boundPorts = new Map();
|
|
215
|
+
const body = instantiate(template, application, diagnostics, decl.name, scoped.ports, scoped.parties, scoped.aliases, scoped.constants, boundPorts);
|
|
178
216
|
if (body) {
|
|
217
|
+
const applicationPorts = new Set();
|
|
179
218
|
const merged = decl.metadata
|
|
180
219
|
? mergeApplicationMetadata(body, decl.metadata, diagnostics)
|
|
181
220
|
: body;
|
|
182
|
-
concrete.push(...constructedInstruments(decl.name, merged, diagnostics).map((candidate) => ({
|
|
221
|
+
concrete.push(...bindActionPorts(constructedInstruments(decl.name, merged, diagnostics), boundPorts, applicationPorts).map((candidate) => ({
|
|
183
222
|
...candidate,
|
|
184
223
|
aliases: scoped.aliases,
|
|
185
224
|
parties: scoped.parties,
|
|
186
225
|
})));
|
|
226
|
+
for (const port of boundPorts.values()) {
|
|
227
|
+
if (applicationPorts.has(port))
|
|
228
|
+
reachedPorts.add(port);
|
|
229
|
+
else
|
|
230
|
+
droppedPorts.add(port);
|
|
231
|
+
}
|
|
187
232
|
}
|
|
188
233
|
}
|
|
234
|
+
for (const port of checkedPorts) {
|
|
235
|
+
if (reachedPorts.has(port) && !droppedPorts.has(port))
|
|
236
|
+
continue;
|
|
237
|
+
report("HSX1024", port.name.span, `wired decision port ${port.name.name} reaches no action`, `carry ${port.name.name}'s condition into the action it authorizes`);
|
|
238
|
+
}
|
|
189
239
|
const allocated = allocateGeneratedPrefixes(concrete);
|
|
190
240
|
const instruments = [];
|
|
191
241
|
const ids = new Set();
|
|
@@ -195,11 +245,18 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
195
245
|
continue;
|
|
196
246
|
}
|
|
197
247
|
ids.add(candidate.name.name);
|
|
248
|
+
if (catalogById?.has(candidate.name.name)) {
|
|
249
|
+
report("HSX1022", candidate.name.span, `authored instrument ${candidate.name.name} collides with published catalog instrument`, "rename the authored instrument to avoid colliding with the published catalog");
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
198
252
|
const expandedBody = expandComprehensions(candidate.body, diagnostics);
|
|
199
|
-
const checked = checkInstrument(candidate.name, expandedBody, candidate.parties, candidate.aliases, diagnostics);
|
|
253
|
+
const checked = checkInstrument(candidate.name, expandedBody, candidate.parties, candidate.ports, candidate.aliases, diagnostics);
|
|
200
254
|
if (checked)
|
|
201
255
|
instruments.push(checked);
|
|
202
256
|
}
|
|
257
|
+
if (options.publishedCatalog) {
|
|
258
|
+
return checkPublishedProgram(program, options.publishedCatalog, header, subjects, instruments, diagnostics, report);
|
|
259
|
+
}
|
|
203
260
|
crossInstrumentReferenceDiagnostics(instruments, diagnostics);
|
|
204
261
|
if (header &&
|
|
205
262
|
instruments.length === 0 &&
|
|
@@ -220,25 +277,22 @@ export function checkGeneralProgram(program, options = {}) {
|
|
|
220
277
|
};
|
|
221
278
|
return { diagnostics, program: typed };
|
|
222
279
|
}
|
|
223
|
-
function checkPublishedProgram(program, catalog, header, authoredSubjects, diagnostics, report) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const authoredInstruments = program.decls.filter((decl) => decl.kind === "instrument" || decl.kind === "instrument_apply");
|
|
237
|
-
for (const declaration of authoredInstruments) {
|
|
238
|
-
report("HSX1022", declaration.span, "a composed program cannot author an instrument", "publish the instrument in the catalog, then select it with use");
|
|
280
|
+
function checkPublishedProgram(program, catalog, header, authoredSubjects, authoredInstruments, diagnostics, report) {
|
|
281
|
+
const compositionDeclarations = program.decls.filter((decl) => decl.kind === "use" || decl.kind === "expose");
|
|
282
|
+
if (header &&
|
|
283
|
+
(compositionDeclarations.length > 0 || authoredInstruments.length === 0)) {
|
|
284
|
+
if (!header.title || header.title.value.length === 0) {
|
|
285
|
+
report("HSX1015", header.span, `composed program ${header.name.name} needs a product title`, 'write the title after the product id, like `program product_name "Product title"`');
|
|
286
|
+
}
|
|
287
|
+
if (header.name.name.length > 60) {
|
|
288
|
+
report("HSX1015", header.name.span, "a composed program product id is at most 60 characters", "shorten the product id without changing its snake_case spelling");
|
|
289
|
+
}
|
|
290
|
+
if ((header.title?.value.length ?? 0) > 80) {
|
|
291
|
+
report("HSX1015", header.title?.span ?? header.span, "a composed program title is at most 80 characters", "shorten the product title to 80 characters or fewer");
|
|
292
|
+
}
|
|
239
293
|
}
|
|
240
294
|
const uses = program.decls.filter((decl) => decl.kind === "use");
|
|
241
|
-
if (uses.length === 0) {
|
|
295
|
+
if (header && uses.length === 0 && authoredInstruments.length === 0) {
|
|
242
296
|
report("HSX1016", header.span, `composed program ${header.name.name} uses no published instruments`, "add at least one `use published_instrument` declaration");
|
|
243
297
|
}
|
|
244
298
|
const catalogById = new Map(catalog.instruments.map((instrument) => [instrument.id, instrument]));
|
|
@@ -261,6 +315,7 @@ function checkPublishedProgram(program, catalog, header, authoredSubjects, diagn
|
|
|
261
315
|
const selected = publishedInstrumentClosure(uses, catalogById);
|
|
262
316
|
const selectedById = new Map(selected.map((entry) => [entry.instrument.id, entry]));
|
|
263
317
|
const exposures = program.decls.filter((decl) => decl.kind === "expose");
|
|
318
|
+
const authoredById = new Map(authoredInstruments.map((instrument) => [instrument.id, instrument]));
|
|
264
319
|
const publicNames = new Map();
|
|
265
320
|
const exposedTargets = new Map();
|
|
266
321
|
for (const exposure of exposures) {
|
|
@@ -268,8 +323,11 @@ function checkPublishedProgram(program, catalog, header, authoredSubjects, diagn
|
|
|
268
323
|
const explicitlyUsed = useById.has(exposure.instrument.name);
|
|
269
324
|
const instrument = selectedById.get(exposure.instrument.name)?.instrument;
|
|
270
325
|
const action = instrument?.actions[exposure.action.name];
|
|
271
|
-
|
|
272
|
-
|
|
326
|
+
const authoredInstrument = authoredById.get(exposure.instrument.name);
|
|
327
|
+
const authoredAction = authoredInstrument?.actions.find((candidate) => candidate.name === exposure.action.name);
|
|
328
|
+
if ((!explicitlyUsed || !instrument || !action) &&
|
|
329
|
+
(!authoredInstrument || !authoredAction)) {
|
|
330
|
+
report("HSX1021", exposure.span, `${target} is not an action on an explicitly used published instrument or authored instrument`, "add the matching use declaration and choose one of that instrument's actions");
|
|
273
331
|
continue;
|
|
274
332
|
}
|
|
275
333
|
if (!CAMEL_CASE.test(exposure.publicName.name)) {
|
|
@@ -287,10 +345,44 @@ function checkPublishedProgram(program, catalog, header, authoredSubjects, diagn
|
|
|
287
345
|
publicNames.set(exposure.publicName.name, exposure);
|
|
288
346
|
exposedTargets.set(target, exposure);
|
|
289
347
|
}
|
|
348
|
+
const updatedAuthoredInstruments = authoredInstruments.map((authoredInstrument) => {
|
|
349
|
+
const instrumentExposures = exposures.filter((exposure) => exposure.instrument.name === authoredInstrument.id);
|
|
350
|
+
if (instrumentExposures.length === 0)
|
|
351
|
+
return authoredInstrument;
|
|
352
|
+
const exposureByAction = new Map(instrumentExposures.map((exposure) => [exposure.action.name, exposure]));
|
|
353
|
+
return {
|
|
354
|
+
...authoredInstrument,
|
|
355
|
+
actions: authoredInstrument.actions.map((action) => {
|
|
356
|
+
const exposure = exposureByAction.get(action.name);
|
|
357
|
+
if (!exposure)
|
|
358
|
+
return action;
|
|
359
|
+
return {
|
|
360
|
+
...action,
|
|
361
|
+
origin: exposure.span,
|
|
362
|
+
slots: {
|
|
363
|
+
...action.slots,
|
|
364
|
+
publicAction: exposure.publicName.name,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}),
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
const publishedInstruments = selected.map(({ instrument, origin }) => {
|
|
371
|
+
const projected = projectedPublishedInstrument(instrument, exposures);
|
|
372
|
+
return typedPublishedInstrument(projected, origin, exposures.filter((exposure) => exposure.instrument.name === instrument.id), catalog.instruments);
|
|
373
|
+
});
|
|
374
|
+
const instruments = [...publishedInstruments, ...updatedAuthoredInstruments];
|
|
375
|
+
crossInstrumentReferenceDiagnostics(instruments, diagnostics);
|
|
376
|
+
if (header &&
|
|
377
|
+
instruments.length === 0 &&
|
|
378
|
+
!diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
379
|
+
report("HSX1502", header.span, `program ${header.name.name} emits no instruments`, "declare or instantiate at least one instrument");
|
|
380
|
+
}
|
|
290
381
|
const referencedSubjectKinds = new Set(selected.flatMap(({ instrument }) => instrument.subject?.kinds ?? []));
|
|
382
|
+
const defaultSpan = uses[0]?.span ?? header?.span ?? program.span;
|
|
291
383
|
const catalogSubjects = catalog.subjects
|
|
292
384
|
.filter((subject) => referencedSubjectKinds.has(subject.kind))
|
|
293
|
-
.map((subject) => typedPublishedSubject(subject,
|
|
385
|
+
.map((subject) => typedPublishedSubject(subject, defaultSpan));
|
|
294
386
|
const catalogSubjectKinds = new Set(catalogSubjects.map((subject) => subject.kind));
|
|
295
387
|
for (const subject of authoredSubjects) {
|
|
296
388
|
if (!catalogSubjectKinds.has(subject.kind))
|
|
@@ -300,19 +392,18 @@ function checkPublishedProgram(program, catalog, header, authoredSubjects, diagn
|
|
|
300
392
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
301
393
|
return { diagnostics };
|
|
302
394
|
}
|
|
395
|
+
const module = program.decls.find((decl) => decl.kind === "module");
|
|
396
|
+
const name = header?.name.name ?? module?.name.parts.at(-1)?.name ?? "module";
|
|
303
397
|
const typed = {
|
|
304
|
-
instruments
|
|
305
|
-
const projected = projectedPublishedInstrument(instrument, exposures);
|
|
306
|
-
return typedPublishedInstrument(projected, origin, exposures.filter((exposure) => exposure.instrument.name === instrument.id));
|
|
307
|
-
}),
|
|
398
|
+
instruments,
|
|
308
399
|
kind: "typed_program",
|
|
309
|
-
name
|
|
400
|
+
name,
|
|
310
401
|
origin: program.span,
|
|
311
402
|
subjects: [
|
|
312
403
|
...catalogSubjects,
|
|
313
404
|
...authoredSubjects.filter((subject) => !catalogSubjectKinds.has(subject.kind)),
|
|
314
405
|
],
|
|
315
|
-
title: header
|
|
406
|
+
title: header?.title?.value ?? sentenceCase(name),
|
|
316
407
|
};
|
|
317
408
|
return { diagnostics, program: typed };
|
|
318
409
|
}
|
|
@@ -390,7 +481,7 @@ function publishedInstrumentDependencies(instrument, idByPrefix) {
|
|
|
390
481
|
found.delete(instrument.id);
|
|
391
482
|
return [...found];
|
|
392
483
|
}
|
|
393
|
-
function typedPublishedInstrument(source, origin, exposures) {
|
|
484
|
+
function typedPublishedInstrument(source, origin, exposures, catalog) {
|
|
394
485
|
const exposureByAction = new Map(exposures.map((exposure) => [exposure.action.name, exposure]));
|
|
395
486
|
const { actionOrder, actions, fields, id: _id, ...slots } = structuredClone(source);
|
|
396
487
|
const requiredFields = new Set(source.required);
|
|
@@ -412,7 +503,7 @@ function typedPublishedInstrument(source, origin, exposures) {
|
|
|
412
503
|
origin,
|
|
413
504
|
required: requiredFields.has(name),
|
|
414
505
|
schema,
|
|
415
|
-
type:
|
|
506
|
+
type: publishedFieldType(schema, catalog),
|
|
416
507
|
})),
|
|
417
508
|
id: source.id,
|
|
418
509
|
origin,
|
|
@@ -422,6 +513,25 @@ function typedPublishedInstrument(source, origin, exposures) {
|
|
|
422
513
|
},
|
|
423
514
|
};
|
|
424
515
|
}
|
|
516
|
+
/** Recover the semantic types retained by UDL before checking mixed relations. */
|
|
517
|
+
function publishedFieldType(schema, catalog) {
|
|
518
|
+
if (schema.type === "integer")
|
|
519
|
+
return { kind: "integer" };
|
|
520
|
+
if (schema.type === "boolean")
|
|
521
|
+
return { kind: "boolean" };
|
|
522
|
+
if (schema.type !== "string")
|
|
523
|
+
return { kind: "unknown" };
|
|
524
|
+
if (schema.pattern === MONEY_PATTERN ||
|
|
525
|
+
schema.pattern === OPTIONAL_MONEY_PATTERN) {
|
|
526
|
+
const currency = schema["x-hyperscale-currency"];
|
|
527
|
+
return typeof currency === "string"
|
|
528
|
+
? { currency, kind: "money" }
|
|
529
|
+
: { kind: "money" };
|
|
530
|
+
}
|
|
531
|
+
const target = catalog.find((instrument) => schema.pattern ===
|
|
532
|
+
`^${instrument.idPrefix}_(sandbox|live)_[a-z0-9]{8,64}$`);
|
|
533
|
+
return target ? { kind: "ref", target: target.id } : { kind: "text" };
|
|
534
|
+
}
|
|
425
535
|
function typedPublishedSubject(subject, origin) {
|
|
426
536
|
return {
|
|
427
537
|
declaredValue: subject.declaredValue,
|
|
@@ -441,6 +551,7 @@ const applicationMetadataKeys = new Set([
|
|
|
441
551
|
"action",
|
|
442
552
|
"agent_description",
|
|
443
553
|
"description",
|
|
554
|
+
"journeys",
|
|
444
555
|
"nav",
|
|
445
556
|
"navigation",
|
|
446
557
|
"summary",
|
|
@@ -452,6 +563,7 @@ const applicationMetadataKeys = new Set([
|
|
|
452
563
|
const applicationActionMetadataKeys = new Set([
|
|
453
564
|
"agent_description",
|
|
454
565
|
"description",
|
|
566
|
+
"examples",
|
|
455
567
|
"public",
|
|
456
568
|
"public_action",
|
|
457
569
|
"summary",
|
|
@@ -532,6 +644,71 @@ function mergeApplicationMetadata(body, metadata, diagnostics) {
|
|
|
532
644
|
}
|
|
533
645
|
return { ...body, entries };
|
|
534
646
|
}
|
|
647
|
+
function bindActionPorts(candidates, bindings, reached) {
|
|
648
|
+
const declarations = new Map([...bindings.values()].map((port) => [port.name.name, port]));
|
|
649
|
+
return candidates.map((candidate) => {
|
|
650
|
+
const ports = new Map();
|
|
651
|
+
const entries = candidate.body.entries.map((row) => {
|
|
652
|
+
if (row.key.name !== "action" || row.value.kind !== "block")
|
|
653
|
+
return row;
|
|
654
|
+
const action = row.qualifiers[0]?.name;
|
|
655
|
+
if (!action)
|
|
656
|
+
return row;
|
|
657
|
+
const declared = (row.conditionPorts ?? []).flatMap((name) => {
|
|
658
|
+
const port = declarations.get(name);
|
|
659
|
+
if (!port)
|
|
660
|
+
return [];
|
|
661
|
+
reached.add(port);
|
|
662
|
+
return [port];
|
|
663
|
+
});
|
|
664
|
+
if (!declared.length)
|
|
665
|
+
return row;
|
|
666
|
+
ports.set(action, declared);
|
|
667
|
+
if (entry(row.value, "port"))
|
|
668
|
+
return row;
|
|
669
|
+
const source = declared[0];
|
|
670
|
+
const allowed = entry(source.body, "allowed")?.value;
|
|
671
|
+
const span = source.span;
|
|
672
|
+
return {
|
|
673
|
+
...row,
|
|
674
|
+
value: {
|
|
675
|
+
...row.value,
|
|
676
|
+
entries: [
|
|
677
|
+
...row.value.entries,
|
|
678
|
+
{
|
|
679
|
+
key: { kind: "ident", name: "port", span },
|
|
680
|
+
qualifiers: [],
|
|
681
|
+
span,
|
|
682
|
+
value: {
|
|
683
|
+
kind: "block",
|
|
684
|
+
span,
|
|
685
|
+
entries: [
|
|
686
|
+
{
|
|
687
|
+
key: {
|
|
688
|
+
kind: "ident",
|
|
689
|
+
name: "allowed_parties",
|
|
690
|
+
span,
|
|
691
|
+
},
|
|
692
|
+
qualifiers: [],
|
|
693
|
+
span,
|
|
694
|
+
value: allowed?.kind === "list"
|
|
695
|
+
? allowed
|
|
696
|
+
: {
|
|
697
|
+
kind: "list",
|
|
698
|
+
span,
|
|
699
|
+
items: allowed ? [allowed] : [],
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
return { ...candidate, body: { ...candidate.body, entries }, ports };
|
|
710
|
+
});
|
|
711
|
+
}
|
|
535
712
|
function constructedInstruments(rootName, body, diagnostics) {
|
|
536
713
|
const expanded = expandComprehensions(body, diagnostics);
|
|
537
714
|
const construction = entry(expanded, "instruments");
|
|
@@ -700,7 +877,9 @@ function validateMoneyLiteral(decl, diagnostics) {
|
|
|
700
877
|
span: decl.value.span,
|
|
701
878
|
});
|
|
702
879
|
}
|
|
703
|
-
function instantiate(template, application, diagnostics, instrumentName, ports, parties, aliases, constants) {
|
|
880
|
+
function instantiate(template, application, diagnostics, instrumentName, ports, parties, aliases, constants, boundPorts) {
|
|
881
|
+
const loweredPortShapes = new Map();
|
|
882
|
+
const portDependencies = new Map();
|
|
704
883
|
const partyKeyedParameters = partyKeyedBlockParameters(template, aliases);
|
|
705
884
|
const named = new Map();
|
|
706
885
|
const positional = [];
|
|
@@ -768,9 +947,11 @@ function instantiate(template, application, diagnostics, instrumentName, ports,
|
|
|
768
947
|
});
|
|
769
948
|
for (const [name, value] of named)
|
|
770
949
|
values.set(name, value);
|
|
950
|
+
const resolvedArgs = [];
|
|
771
951
|
let position = 0;
|
|
772
952
|
for (const parameter of template.parameters) {
|
|
773
953
|
const value = named.get(parameter.name.name) ?? positional[position++];
|
|
954
|
+
resolvedArgs.push({ parameter, value });
|
|
774
955
|
if (!value) {
|
|
775
956
|
if (optionalInner(parameter.type)) {
|
|
776
957
|
values.set(parameter.name.name, booleanExpr(false, parameter.span));
|
|
@@ -845,11 +1026,17 @@ function instantiate(template, application, diagnostics, instrumentName, ports,
|
|
|
845
1026
|
else {
|
|
846
1027
|
values.set(parameter.name.name, value);
|
|
847
1028
|
}
|
|
848
|
-
|
|
849
|
-
|
|
1029
|
+
}
|
|
1030
|
+
for (const { parameter, value } of resolvedArgs) {
|
|
1031
|
+
if (value?.kind === "port_ref") {
|
|
1032
|
+
const port = ports.get(value.name.name);
|
|
1033
|
+
if (port)
|
|
1034
|
+
boundPorts.set(parameter.name.name, port);
|
|
1035
|
+
bindPortCompileValues(values, parameter.name.name, value, ports, diagnostics, aliases, loweredPortShapes, portDependencies);
|
|
850
1036
|
}
|
|
851
1037
|
}
|
|
852
|
-
|
|
1038
|
+
checkClaimAmountPortField(resolvedArgs, ports, aliases, diagnostics);
|
|
1039
|
+
const selected = selectCompileTimeRows(template.body, values, diagnostics, portDependencies);
|
|
853
1040
|
const dependencyDiagnosticStart = diagnostics.length;
|
|
854
1041
|
for (const [name, parameter] of missingOptional) {
|
|
855
1042
|
if (!referencesIdentifier(selected, name, values))
|
|
@@ -867,7 +1054,7 @@ function instantiate(template, application, diagnostics, instrumentName, ports,
|
|
|
867
1054
|
}
|
|
868
1055
|
if (diagnostics.length > dependencyDiagnosticStart)
|
|
869
1056
|
return undefined;
|
|
870
|
-
const body =
|
|
1057
|
+
const body = expandComprehensions(selected, diagnostics, values, portDependencies);
|
|
871
1058
|
return {
|
|
872
1059
|
...body,
|
|
873
1060
|
entries: body.entries.map((row) => {
|
|
@@ -1039,7 +1226,231 @@ function partyKeyedBlockParameters(template, aliases) {
|
|
|
1039
1226
|
nonPartyKeys.get(parameter) ?? new Set(),
|
|
1040
1227
|
]));
|
|
1041
1228
|
}
|
|
1042
|
-
function
|
|
1229
|
+
function shapeTypeText(expr) {
|
|
1230
|
+
if (expr.kind === "ident")
|
|
1231
|
+
return expr.name;
|
|
1232
|
+
if (expr.kind === "block")
|
|
1233
|
+
return "block";
|
|
1234
|
+
if (expr.kind === "list")
|
|
1235
|
+
return "list";
|
|
1236
|
+
if (expr.kind === "number")
|
|
1237
|
+
return expr.raw;
|
|
1238
|
+
if (expr.kind === "string")
|
|
1239
|
+
return `"${expr.value}"`;
|
|
1240
|
+
if (expr.kind === "boolean")
|
|
1241
|
+
return String(expr.value);
|
|
1242
|
+
if (expr.kind === "call") {
|
|
1243
|
+
const args = expr.args.map(shapeTypeText).join(", ");
|
|
1244
|
+
return `${expr.callee.name}(${args})`;
|
|
1245
|
+
}
|
|
1246
|
+
if (expr.kind === "type_apply") {
|
|
1247
|
+
const args = expr.args.map(shapeTypeText).join(", ");
|
|
1248
|
+
return `${expr.callee.name}<${args}>`;
|
|
1249
|
+
}
|
|
1250
|
+
return nameText(expr);
|
|
1251
|
+
}
|
|
1252
|
+
function lowerPortShapeField(port, row, aliases, diagnostics) {
|
|
1253
|
+
const type = typeOf(row.value, aliases);
|
|
1254
|
+
if (![
|
|
1255
|
+
"text",
|
|
1256
|
+
"money",
|
|
1257
|
+
"date",
|
|
1258
|
+
"integer",
|
|
1259
|
+
"boolean",
|
|
1260
|
+
"account",
|
|
1261
|
+
"ref",
|
|
1262
|
+
"bps",
|
|
1263
|
+
"percent",
|
|
1264
|
+
].includes(type.kind) ||
|
|
1265
|
+
((type.kind === "money" || type.kind === "account") &&
|
|
1266
|
+
(!type.currency || !CURRENCY.test(type.currency)))) {
|
|
1267
|
+
diagnostics.push({
|
|
1268
|
+
code: "HSX1025",
|
|
1269
|
+
fix: `declare ${row.key.name} as text, money(CUR), date, integer, boolean, account(CUR), ref<instrument_id>, id(instrument_id), bps, or percent`,
|
|
1270
|
+
message: `decision port ${port.name.name} shape field ${row.key.name} has unknown type ${shapeTypeText(row.value)}`,
|
|
1271
|
+
severity: "error",
|
|
1272
|
+
span: row.value.span,
|
|
1273
|
+
});
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
const extras = {};
|
|
1277
|
+
if (type.kind === "text") {
|
|
1278
|
+
extras.description =
|
|
1279
|
+
"Required decision reference retained in the operation receipt";
|
|
1280
|
+
extras.max_length = 180;
|
|
1281
|
+
extras.min_length = 1;
|
|
1282
|
+
}
|
|
1283
|
+
else if (type.kind === "money") {
|
|
1284
|
+
extras.description = `Decided amount in ${type.currency} minor units`;
|
|
1285
|
+
}
|
|
1286
|
+
else if (type.kind === "ref") {
|
|
1287
|
+
extras.max_length = 180;
|
|
1288
|
+
extras.min_length = 1;
|
|
1289
|
+
if (type.target)
|
|
1290
|
+
extras.pattern = `^${prefixFor(type.target)}_(sandbox|live)_[a-z0-9]{8,64}$`;
|
|
1291
|
+
}
|
|
1292
|
+
else if (type.kind === "account") {
|
|
1293
|
+
extras.pattern = ACCOUNT_PATTERN;
|
|
1294
|
+
}
|
|
1295
|
+
const scalarEntries = (values) => Object.entries(values).map(([name, value]) => ({
|
|
1296
|
+
key: { kind: "ident", name, span: row.span },
|
|
1297
|
+
qualifiers: [],
|
|
1298
|
+
span: row.span,
|
|
1299
|
+
value: typeof value === "number"
|
|
1300
|
+
? { kind: "number", raw: String(value), span: row.span }
|
|
1301
|
+
: { kind: "string", value: String(value), span: row.span },
|
|
1302
|
+
}));
|
|
1303
|
+
// Port inputs require every key, but a caller-decided amount may be zero.
|
|
1304
|
+
const field = lowerField({
|
|
1305
|
+
...row,
|
|
1306
|
+
value: {
|
|
1307
|
+
kind: "block",
|
|
1308
|
+
span: row.span,
|
|
1309
|
+
entries: [
|
|
1310
|
+
{
|
|
1311
|
+
key: { kind: "ident", name: "type", span: row.span },
|
|
1312
|
+
qualifiers: [],
|
|
1313
|
+
span: row.span,
|
|
1314
|
+
value: row.value,
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
key: { kind: "ident", name: "optional", span: row.span },
|
|
1318
|
+
qualifiers: [],
|
|
1319
|
+
span: row.span,
|
|
1320
|
+
value: { kind: "boolean", value: true, span: row.span },
|
|
1321
|
+
},
|
|
1322
|
+
...scalarEntries(extras),
|
|
1323
|
+
],
|
|
1324
|
+
},
|
|
1325
|
+
}, aliases, diagnostics);
|
|
1326
|
+
const schema = type.kind === "text"
|
|
1327
|
+
? {
|
|
1328
|
+
type: field.schema.type,
|
|
1329
|
+
description: field.schema.description,
|
|
1330
|
+
...field.schema,
|
|
1331
|
+
}
|
|
1332
|
+
: field.schema;
|
|
1333
|
+
return {
|
|
1334
|
+
key: row.key,
|
|
1335
|
+
qualifiers: [],
|
|
1336
|
+
span: row.span,
|
|
1337
|
+
value: {
|
|
1338
|
+
kind: "block",
|
|
1339
|
+
span: row.span,
|
|
1340
|
+
entries: scalarEntries(schema),
|
|
1341
|
+
},
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function lowerPortShape(port, rawShape, aliases, diagnostics) {
|
|
1345
|
+
if (rawShape.kind !== "block") {
|
|
1346
|
+
diagnostics.push({
|
|
1347
|
+
code: "HSX1025",
|
|
1348
|
+
fix: "declare shape as a block of fields, like shape { note: text; }",
|
|
1349
|
+
message: `decision port ${port.name.name} shape must be a block`,
|
|
1350
|
+
severity: "error",
|
|
1351
|
+
span: rawShape.span,
|
|
1352
|
+
});
|
|
1353
|
+
return { entries: [], kind: "block", span: rawShape.span };
|
|
1354
|
+
}
|
|
1355
|
+
const entries = [];
|
|
1356
|
+
const names = new Set();
|
|
1357
|
+
for (const row of rawShape.entries) {
|
|
1358
|
+
if (names.has(row.key.name)) {
|
|
1359
|
+
diagnostics.push({
|
|
1360
|
+
code: "HSX1004",
|
|
1361
|
+
fix: `keep one ${row.key.name} field in decision port ${port.name.name}`,
|
|
1362
|
+
message: `decision port ${port.name.name} shape field ${row.key.name} is declared twice`,
|
|
1363
|
+
severity: "error",
|
|
1364
|
+
span: row.key.span,
|
|
1365
|
+
});
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
names.add(row.key.name);
|
|
1369
|
+
const lowered = lowerPortShapeField(port, row, aliases, diagnostics);
|
|
1370
|
+
if (lowered)
|
|
1371
|
+
entries.push(lowered);
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
entries,
|
|
1375
|
+
kind: "block",
|
|
1376
|
+
span: rawShape.span,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function checkClaimAmountPortField(resolvedArgs, ports, aliases, diagnostics) {
|
|
1380
|
+
for (const { value } of resolvedArgs) {
|
|
1381
|
+
if (value?.kind !== "block")
|
|
1382
|
+
continue;
|
|
1383
|
+
const fieldEntry = entry(value, "field");
|
|
1384
|
+
const boundEntry = entry(value, "bound");
|
|
1385
|
+
if (!fieldEntry || !boundEntry)
|
|
1386
|
+
continue;
|
|
1387
|
+
const claimFieldName = fieldEntry.value.kind === "ident"
|
|
1388
|
+
? fieldEntry.value.name
|
|
1389
|
+
: fieldEntry.value.kind === "string"
|
|
1390
|
+
? fieldEntry.value.value
|
|
1391
|
+
: undefined;
|
|
1392
|
+
const boundFieldName = boundEntry.value.kind === "ident"
|
|
1393
|
+
? boundEntry.value.name
|
|
1394
|
+
: boundEntry.value.kind === "string"
|
|
1395
|
+
? boundEntry.value.value
|
|
1396
|
+
: undefined;
|
|
1397
|
+
if (!claimFieldName || !boundFieldName)
|
|
1398
|
+
continue;
|
|
1399
|
+
const boundArg = resolvedArgs.find((arg) => (arg.value?.kind === "binding" &&
|
|
1400
|
+
arg.value.name.name === boundFieldName) ||
|
|
1401
|
+
arg.parameter.name.name === boundFieldName);
|
|
1402
|
+
const boundTypeExpr = boundArg?.value?.kind === "binding"
|
|
1403
|
+
? boundArg.value.type
|
|
1404
|
+
: boundArg?.value;
|
|
1405
|
+
if (!boundTypeExpr)
|
|
1406
|
+
continue;
|
|
1407
|
+
const boundType = typeOf(boundTypeExpr, aliases);
|
|
1408
|
+
if (boundType.kind !== "money")
|
|
1409
|
+
continue;
|
|
1410
|
+
const portArg = resolvedArgs.find((arg) => arg.parameter.name.name === "claim");
|
|
1411
|
+
if (portArg?.value?.kind !== "port_ref")
|
|
1412
|
+
continue;
|
|
1413
|
+
const portDecl = ports.get(portArg.value.name.name);
|
|
1414
|
+
if (!portDecl)
|
|
1415
|
+
continue;
|
|
1416
|
+
const shapeExpr = entry(portDecl.body, "shape")?.value;
|
|
1417
|
+
if (shapeExpr?.kind !== "block")
|
|
1418
|
+
continue;
|
|
1419
|
+
const shapeField = entry(shapeExpr, claimFieldName);
|
|
1420
|
+
if (!shapeField) {
|
|
1421
|
+
diagnostics.push({
|
|
1422
|
+
code: "HSX1011",
|
|
1423
|
+
fix: `add ${claimFieldName}: money(${boundType.currency ?? "CUR"}) to the shape of decision port ${portDecl.name.name}`,
|
|
1424
|
+
message: `decision port ${portDecl.name.name} shape lacks claim field ${claimFieldName}`,
|
|
1425
|
+
severity: "error",
|
|
1426
|
+
span: portArg.value.span,
|
|
1427
|
+
});
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const actualType = typeOf(shapeField.value, aliases);
|
|
1431
|
+
if (actualType.kind !== "money") {
|
|
1432
|
+
diagnostics.push({
|
|
1433
|
+
code: "HSX1104",
|
|
1434
|
+
fix: `pass money<${boundType.currency ?? "CUR"}> to ${shapeField.key.name}`,
|
|
1435
|
+
message: `${shapeField.key.name} needs money<${boundType.currency ?? "CUR"}> but receives ${argumentWords(shapeField.value, aliases)}`,
|
|
1436
|
+
severity: "error",
|
|
1437
|
+
span: shapeField.value.span,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
else if (actualType.currency &&
|
|
1441
|
+
boundType.currency &&
|
|
1442
|
+
actualType.currency !== boundType.currency) {
|
|
1443
|
+
diagnostics.push({
|
|
1444
|
+
code: "HSX1101",
|
|
1445
|
+
fix: `pass money<${boundType.currency}> to ${shapeField.key.name}`,
|
|
1446
|
+
message: `${shapeField.key.name} needs money<${boundType.currency}> but receives money<${actualType.currency}>`,
|
|
1447
|
+
severity: "error",
|
|
1448
|
+
span: shapeField.value.span,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
function bindPortCompileValues(values, binding, value, ports, diagnostics, aliases = new Map(), loweredPortShapes = new Map(), dependencies = new Map()) {
|
|
1043
1454
|
const port = ports.get(value.name.name);
|
|
1044
1455
|
if (!port) {
|
|
1045
1456
|
diagnostics.push({
|
|
@@ -1051,14 +1462,36 @@ function bindPortCompileValues(values, binding, value, ports, diagnostics) {
|
|
|
1051
1462
|
});
|
|
1052
1463
|
return;
|
|
1053
1464
|
}
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1465
|
+
for (const key of [binding, `${binding}_allowed`, `${binding}_fields`]) {
|
|
1466
|
+
dependencies.set(key, [port.name.name]);
|
|
1467
|
+
}
|
|
1468
|
+
const allowed = entry(port.body, "allowed")?.value;
|
|
1469
|
+
let shape = loweredPortShapes.get(value.name.name);
|
|
1470
|
+
if (!shape) {
|
|
1471
|
+
const rawShape = entry(port.body, "shape")?.value ?? {
|
|
1472
|
+
entries: [],
|
|
1473
|
+
kind: "block",
|
|
1474
|
+
span: port.body.span,
|
|
1475
|
+
};
|
|
1476
|
+
const shapeDiagnostics = [];
|
|
1477
|
+
shape = lowerPortShape(port, rawShape, aliases, shapeDiagnostics);
|
|
1478
|
+
for (const diagnostic of shapeDiagnostics) {
|
|
1479
|
+
if (!diagnostics.some((prior) => prior.code === diagnostic.code &&
|
|
1480
|
+
prior.span === diagnostic.span &&
|
|
1481
|
+
prior.message === diagnostic.message))
|
|
1482
|
+
diagnostics.push(diagnostic);
|
|
1483
|
+
}
|
|
1484
|
+
loweredPortShapes.set(value.name.name, shape);
|
|
1485
|
+
}
|
|
1486
|
+
const normalizedAllowed = allowed?.kind === "ident"
|
|
1487
|
+
? {
|
|
1488
|
+
items: [allowed],
|
|
1489
|
+
kind: "list",
|
|
1490
|
+
span: allowed.span,
|
|
1491
|
+
}
|
|
1492
|
+
: allowed;
|
|
1493
|
+
if (normalizedAllowed)
|
|
1494
|
+
values.set(`${binding}_allowed`, normalizedAllowed);
|
|
1062
1495
|
values.set(`${binding}_fields`, shape);
|
|
1063
1496
|
if (value.within)
|
|
1064
1497
|
values.set(`${binding}_within`, value.within);
|
|
@@ -1264,14 +1697,54 @@ function argumentWords(expression, aliases) {
|
|
|
1264
1697
|
function booleanExpr(value, span) {
|
|
1265
1698
|
return { kind: "boolean", span, value };
|
|
1266
1699
|
}
|
|
1267
|
-
function
|
|
1700
|
+
function selectedControls(value) {
|
|
1701
|
+
if (value.kind !== "block")
|
|
1702
|
+
return [];
|
|
1703
|
+
return value.entries.flatMap((row) => [
|
|
1704
|
+
...(row.conditionPorts ?? []),
|
|
1705
|
+
...selectedControls(row.value),
|
|
1706
|
+
]);
|
|
1707
|
+
}
|
|
1708
|
+
// Retain dependencies before compile-time evaluation erases identifiers.
|
|
1709
|
+
function expressionPorts(value, dependencies) {
|
|
1710
|
+
if (!value || typeof value !== "object")
|
|
1711
|
+
return [];
|
|
1712
|
+
if (Array.isArray(value))
|
|
1713
|
+
return [
|
|
1714
|
+
...new Set(value.flatMap((item) => expressionPorts(item, dependencies))),
|
|
1715
|
+
];
|
|
1716
|
+
const node = value;
|
|
1717
|
+
const found = new Set(node.conditionPorts ?? []);
|
|
1718
|
+
if (node.kind === "ident" && typeof node.name === "string") {
|
|
1719
|
+
for (const port of dependencies.get(node.name.replace(/^\[|\]$/g, "")) ??
|
|
1720
|
+
[])
|
|
1721
|
+
found.add(port);
|
|
1722
|
+
}
|
|
1723
|
+
if (node.kind === "string" && typeof node.value === "string") {
|
|
1724
|
+
for (const match of node.value.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)) {
|
|
1725
|
+
for (const port of dependencies.get(match[1]) ?? [])
|
|
1726
|
+
found.add(port);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
for (const [key, child] of Object.entries(node)) {
|
|
1730
|
+
if (key === "span" || key === "conditionPorts")
|
|
1731
|
+
continue;
|
|
1732
|
+
for (const port of expressionPorts(child, dependencies))
|
|
1733
|
+
found.add(port);
|
|
1734
|
+
}
|
|
1735
|
+
return [...found];
|
|
1736
|
+
}
|
|
1737
|
+
function selectCompileTimeRows(block, values, diagnostics, dependencies = new Map(), controls = []) {
|
|
1268
1738
|
const entries = [];
|
|
1269
1739
|
const locals = new Map(values);
|
|
1740
|
+
const localDependencies = new Map(dependencies);
|
|
1270
1741
|
for (const row of block.entries) {
|
|
1271
1742
|
if (row.key.name === "let") {
|
|
1272
1743
|
const binding = row.qualifiers[0];
|
|
1273
|
-
if (binding)
|
|
1744
|
+
if (binding) {
|
|
1745
|
+
localDependencies.set(binding.name, expressionPorts(row.value, localDependencies));
|
|
1274
1746
|
locals.set(binding.name, substituteExpr(row.value, locals));
|
|
1747
|
+
}
|
|
1275
1748
|
entries.push(row);
|
|
1276
1749
|
continue;
|
|
1277
1750
|
}
|
|
@@ -1305,12 +1778,34 @@ function selectCompileTimeRows(block, values, diagnostics) {
|
|
|
1305
1778
|
if (row.key.name !== "when" &&
|
|
1306
1779
|
row.key.name !== "when_not" &&
|
|
1307
1780
|
row.key.name !== "when_eq") {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1781
|
+
const value = row.value.kind === "block" && !row.iteration
|
|
1782
|
+
? selectCompileTimeRows(row.value, locals, diagnostics, localDependencies, controls)
|
|
1783
|
+
: row.value;
|
|
1784
|
+
const conditionPorts = row.key.name === "action"
|
|
1785
|
+
? [
|
|
1786
|
+
...new Set([
|
|
1787
|
+
...(value.kind === "block" && entry(value, "due")
|
|
1788
|
+
? []
|
|
1789
|
+
: controls),
|
|
1790
|
+
...row.qualifiers
|
|
1791
|
+
.filter((qualifier) => qualifier.name.startsWith("["))
|
|
1792
|
+
.flatMap((qualifier) => expressionPorts(qualifier, localDependencies)),
|
|
1793
|
+
...expressionPorts(value.kind === "block"
|
|
1794
|
+
? entry(value, "port")?.value
|
|
1795
|
+
: undefined, localDependencies),
|
|
1796
|
+
...expressionPorts(value.kind === "block"
|
|
1797
|
+
? entry(value, "input")?.value
|
|
1798
|
+
: undefined, localDependencies),
|
|
1799
|
+
...expressionPorts(value.kind === "block"
|
|
1800
|
+
? entry(value, "capture_input", "captureInput")?.value
|
|
1801
|
+
: undefined, localDependencies),
|
|
1802
|
+
...(value.kind === "block" && entry(value, "due")
|
|
1803
|
+
? []
|
|
1804
|
+
: selectedControls(value)),
|
|
1805
|
+
]),
|
|
1806
|
+
]
|
|
1807
|
+
: controls;
|
|
1808
|
+
entries.push({ ...row, conditionPorts, value });
|
|
1314
1809
|
continue;
|
|
1315
1810
|
}
|
|
1316
1811
|
const subject = row.qualifiers[0];
|
|
@@ -1336,7 +1831,12 @@ function selectCompileTimeRows(block, values, diagnostics) {
|
|
|
1336
1831
|
continue;
|
|
1337
1832
|
}
|
|
1338
1833
|
if (include) {
|
|
1339
|
-
entries.push(...selectCompileTimeRows(row.value, locals, diagnostics
|
|
1834
|
+
entries.push(...selectCompileTimeRows(row.value, locals, diagnostics, localDependencies, [
|
|
1835
|
+
...new Set([
|
|
1836
|
+
...controls,
|
|
1837
|
+
...(subject ? expressionPorts(subject, localDependencies) : []),
|
|
1838
|
+
]),
|
|
1839
|
+
]).entries);
|
|
1340
1840
|
}
|
|
1341
1841
|
}
|
|
1342
1842
|
return { ...block, entries };
|
|
@@ -1589,11 +2089,12 @@ function nameText(value) {
|
|
|
1589
2089
|
return `${value.owner.name}.${value.member.name}`;
|
|
1590
2090
|
return "invalid_compile_time_name";
|
|
1591
2091
|
}
|
|
1592
|
-
function expandComprehensions(block, diagnostics) {
|
|
2092
|
+
function expandComprehensions(block, diagnostics, values = new Map(), dependencies = new Map()) {
|
|
1593
2093
|
const budget = { used: 0 };
|
|
1594
|
-
const expand = (current, inherited = new Map()) => {
|
|
2094
|
+
const expand = (current, inherited = new Map(), inheritedDependencies = dependencies) => {
|
|
1595
2095
|
const entries = [];
|
|
1596
2096
|
const locals = new Map(inherited);
|
|
2097
|
+
const localDependencies = new Map(inheritedDependencies);
|
|
1597
2098
|
for (const row of current.entries) {
|
|
1598
2099
|
if (row.key.name === "let") {
|
|
1599
2100
|
const binding = row.qualifiers[0];
|
|
@@ -1607,6 +2108,7 @@ function expandComprehensions(block, diagnostics) {
|
|
|
1607
2108
|
});
|
|
1608
2109
|
continue;
|
|
1609
2110
|
}
|
|
2111
|
+
localDependencies.set(binding.name, expressionPorts(row.value, localDependencies));
|
|
1610
2112
|
locals.set(binding.name, substituteExpr(row.value, locals));
|
|
1611
2113
|
continue;
|
|
1612
2114
|
}
|
|
@@ -1622,7 +2124,7 @@ function expandComprehensions(block, diagnostics) {
|
|
|
1622
2124
|
if (row.key.name === "when_not")
|
|
1623
2125
|
include = !include;
|
|
1624
2126
|
if (include && row.value.kind === "block") {
|
|
1625
|
-
entries.push(...expand(row.value, locals).entries);
|
|
2127
|
+
entries.push(...expand(row.value, locals, localDependencies).entries);
|
|
1626
2128
|
}
|
|
1627
2129
|
continue;
|
|
1628
2130
|
}
|
|
@@ -1641,8 +2143,8 @@ function expandComprehensions(block, diagnostics) {
|
|
|
1641
2143
|
};
|
|
1642
2144
|
entries.push({
|
|
1643
2145
|
...substituted,
|
|
1644
|
-
value:
|
|
1645
|
-
? expand(
|
|
2146
|
+
value: row.value.kind === "block"
|
|
2147
|
+
? expand(row.value, locals, localDependencies)
|
|
1646
2148
|
: substituted.value,
|
|
1647
2149
|
});
|
|
1648
2150
|
continue;
|
|
@@ -1677,14 +2179,13 @@ function expandComprehensions(block, diagnostics) {
|
|
|
1677
2179
|
...locals,
|
|
1678
2180
|
[row.iteration.binding.name, value],
|
|
1679
2181
|
]);
|
|
1680
|
-
const body =
|
|
1681
|
-
|
|
1682
|
-
entries.push(...expand(body, substitutions).entries);
|
|
2182
|
+
const body = selectCompileTimeRows(row.value, substitutions, diagnostics, localDependencies, row.conditionPorts);
|
|
2183
|
+
entries.push(...expand(body, substitutions, localDependencies).entries);
|
|
1683
2184
|
}
|
|
1684
2185
|
}
|
|
1685
2186
|
return { ...current, entries };
|
|
1686
2187
|
};
|
|
1687
|
-
return expand(block);
|
|
2188
|
+
return expand(block, values);
|
|
1688
2189
|
}
|
|
1689
2190
|
function finiteIterationValues(bound) {
|
|
1690
2191
|
if (bound.kind === "call") {
|
|
@@ -1704,7 +2205,7 @@ function finiteIterationValues(bound) {
|
|
|
1704
2205
|
span: bound.span,
|
|
1705
2206
|
}));
|
|
1706
2207
|
}
|
|
1707
|
-
function checkInstrument(name, body, parties, aliases, diagnostics) {
|
|
2208
|
+
function checkInstrument(name, body, parties, ports, aliases, diagnostics) {
|
|
1708
2209
|
if (!SNAKE_CASE.test(name.name)) {
|
|
1709
2210
|
diagnostics.push({
|
|
1710
2211
|
code: "HSX1003",
|
|
@@ -1752,8 +2253,57 @@ function checkInstrument(name, body, parties, aliases, diagnostics) {
|
|
|
1752
2253
|
}
|
|
1753
2254
|
const slots = {};
|
|
1754
2255
|
const partiesEntry = entry(body, "parties");
|
|
1755
|
-
|
|
1756
|
-
|
|
2256
|
+
const roleObject = blockToObject(partiesEntry?.value.kind === "block" ? partiesEntry.value : undefined, new Map(), diagnostics);
|
|
2257
|
+
const partyAccounts = new Map([...parties].map((party) => [party, `${camel(party)}AccountId`]));
|
|
2258
|
+
for (const [role, binding] of Object.entries(roleObject)) {
|
|
2259
|
+
if (!isJsonObject(binding))
|
|
2260
|
+
continue;
|
|
2261
|
+
if (typeof binding.party !== "string" ||
|
|
2262
|
+
!parties.has(binding.party) ||
|
|
2263
|
+
typeof binding.account !== "string" ||
|
|
2264
|
+
!endpointFields.has(binding.account)) {
|
|
2265
|
+
diagnostics.push({
|
|
2266
|
+
code: "HSX1024",
|
|
2267
|
+
fix: "bind a declared party to a declared account field",
|
|
2268
|
+
message: `party role ${role} has an invalid party/account binding`,
|
|
2269
|
+
severity: "error",
|
|
2270
|
+
span: partiesEntry?.span ?? name.span,
|
|
2271
|
+
});
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
partyAccounts.set(binding.party, binding.account);
|
|
2275
|
+
roleObject[role] = binding.account;
|
|
2276
|
+
}
|
|
2277
|
+
const decisionRoles = new Map();
|
|
2278
|
+
const inferredDecisionFields = new Set();
|
|
2279
|
+
// Decision authority needs an identity binding, even when no money moves
|
|
2280
|
+
// through that party. Reuse the same account binding as economic roles.
|
|
2281
|
+
for (const action of body.entries) {
|
|
2282
|
+
if (action.key.name !== "action" || action.value.kind !== "block")
|
|
2283
|
+
continue;
|
|
2284
|
+
const port = entry(action.value, "port")?.value;
|
|
2285
|
+
if (port?.kind !== "block")
|
|
2286
|
+
continue;
|
|
2287
|
+
const allowed = entry(port, "allowed_parties", "allowedParties")?.value;
|
|
2288
|
+
if (allowed?.kind !== "list")
|
|
2289
|
+
continue;
|
|
2290
|
+
for (const item of allowed.items) {
|
|
2291
|
+
const party = nameText(item);
|
|
2292
|
+
if (!parties.has(party))
|
|
2293
|
+
continue;
|
|
2294
|
+
const accountField = partyAccounts.get(party);
|
|
2295
|
+
const existing = Object.entries(roleObject).find(([, value]) => value === party || value === accountField);
|
|
2296
|
+
let role = existing?.[0] ?? party;
|
|
2297
|
+
for (let suffix = 2; !existing && Object.hasOwn(roleObject, role); suffix++)
|
|
2298
|
+
role = `${party}_${suffix}`;
|
|
2299
|
+
if (!existing) {
|
|
2300
|
+
roleObject[role] = party;
|
|
2301
|
+
inferredDecisionFields.add(accountField);
|
|
2302
|
+
}
|
|
2303
|
+
decisionRoles.set(party, role);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (Object.keys(roleObject).length > 0) {
|
|
1757
2307
|
for (const [role, value] of Object.entries(roleObject)) {
|
|
1758
2308
|
if (typeof value !== "string")
|
|
1759
2309
|
continue;
|
|
@@ -1770,11 +2320,11 @@ function checkInstrument(name, body, parties, aliases, diagnostics) {
|
|
|
1770
2320
|
if (!fieldNames.has(field)) {
|
|
1771
2321
|
fields.push({
|
|
1772
2322
|
name: field,
|
|
1773
|
-
origin: partiesEntry.span,
|
|
2323
|
+
origin: partiesEntry?.span ?? name.span,
|
|
1774
2324
|
required: true,
|
|
1775
2325
|
schema: {
|
|
1776
2326
|
description: `The ${value.replaceAll("_", " ")} account`,
|
|
1777
|
-
pattern:
|
|
2327
|
+
pattern: ACCOUNT_PATTERN,
|
|
1778
2328
|
type: "string",
|
|
1779
2329
|
"x-hyperscale-reference-filter": {
|
|
1780
2330
|
column: "role",
|
|
@@ -1792,8 +2342,11 @@ function checkInstrument(name, body, parties, aliases, diagnostics) {
|
|
|
1792
2342
|
fix: "bind the role to a declared party or field",
|
|
1793
2343
|
message: `party role refers to ${String(value)}, which is not declared`,
|
|
1794
2344
|
severity: "error",
|
|
1795
|
-
span: partiesEntry
|
|
1796
|
-
|
|
2345
|
+
span: (partiesEntry?.value.kind === "block"
|
|
2346
|
+
? partiesEntry.value.entries
|
|
2347
|
+
: []).find((entry) => entry.key.name === role)?.value.span ??
|
|
2348
|
+
partiesEntry?.span ??
|
|
2349
|
+
name.span,
|
|
1797
2350
|
});
|
|
1798
2351
|
}
|
|
1799
2352
|
slots.parties = roleObject;
|
|
@@ -1831,7 +2384,7 @@ function checkInstrument(name, body, parties, aliases, diagnostics) {
|
|
|
1831
2384
|
origin: actionEntry.span,
|
|
1832
2385
|
required: true,
|
|
1833
2386
|
schema: {
|
|
1834
|
-
pattern:
|
|
2387
|
+
pattern: ACCOUNT_PATTERN,
|
|
1835
2388
|
type: "string",
|
|
1836
2389
|
},
|
|
1837
2390
|
type: { kind: "account" },
|
|
@@ -1841,14 +2394,98 @@ function checkInstrument(name, body, parties, aliases, diagnostics) {
|
|
|
1841
2394
|
}
|
|
1842
2395
|
}
|
|
1843
2396
|
}
|
|
1844
|
-
const actions = actionEntries
|
|
2397
|
+
const actions = actionEntries
|
|
2398
|
+
.flatMap((candidate) => {
|
|
1845
2399
|
const actionName = candidate.qualifiers[0];
|
|
1846
2400
|
if (!actionName || candidate.value.kind !== "block")
|
|
1847
2401
|
return [];
|
|
1848
2402
|
return [
|
|
1849
2403
|
checkAction(actionName, candidate.value, fields, name.name, endpointFields, holdDestinations, diagnostics),
|
|
1850
2404
|
];
|
|
1851
|
-
})
|
|
2405
|
+
})
|
|
2406
|
+
.map((action) => {
|
|
2407
|
+
const port = action.slots.port;
|
|
2408
|
+
if (port === undefined ||
|
|
2409
|
+
!isJsonObject(port) ||
|
|
2410
|
+
!Array.isArray(port.allowedParties))
|
|
2411
|
+
return action;
|
|
2412
|
+
return {
|
|
2413
|
+
...action,
|
|
2414
|
+
slots: {
|
|
2415
|
+
...action.slots,
|
|
2416
|
+
port: {
|
|
2417
|
+
...port,
|
|
2418
|
+
allowedParties: port.allowedParties.map((party) => typeof party === "string"
|
|
2419
|
+
? (decisionRoles.get(party) ?? party)
|
|
2420
|
+
: party),
|
|
2421
|
+
},
|
|
2422
|
+
},
|
|
2423
|
+
};
|
|
2424
|
+
})
|
|
2425
|
+
.map((action) => pinFixedCurrencyBindings(action, fields, diagnostics));
|
|
2426
|
+
for (const action of actions) {
|
|
2427
|
+
for (const declared of ports.get(action.name) ?? []) {
|
|
2428
|
+
const allowed = entry(declared.body, "allowed")?.value;
|
|
2429
|
+
const allowedItems = allowed?.kind === "list" ? allowed.items : allowed ? [allowed] : [];
|
|
2430
|
+
if (allowedItems.length === 0) {
|
|
2431
|
+
diagnostics.push({
|
|
2432
|
+
code: "HSX1024",
|
|
2433
|
+
fix: `declare a nonempty allowed list of declared parties on port ${declared.name.name}`,
|
|
2434
|
+
message: `wired decision port ${declared.name.name} reaches action ${action.name} without declared allowed parties`,
|
|
2435
|
+
severity: "error",
|
|
2436
|
+
span: declared.name.span,
|
|
2437
|
+
});
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
const port = action.slots.port;
|
|
2441
|
+
if (port === undefined ||
|
|
2442
|
+
!isJsonObject(port) ||
|
|
2443
|
+
!Array.isArray(port.allowedParties)) {
|
|
2444
|
+
diagnostics.push({
|
|
2445
|
+
code: "HSX1024",
|
|
2446
|
+
fix: `emit the allowed parties of ${declared.name.name} in action ${action.name}'s port`,
|
|
2447
|
+
message: `wired decision port ${declared.name.name} reaches action ${action.name} without a decision port`,
|
|
2448
|
+
severity: "error",
|
|
2449
|
+
span: action.origin,
|
|
2450
|
+
});
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
const allowedFields = new Set();
|
|
2454
|
+
for (const item of allowedItems) {
|
|
2455
|
+
const party = nameText(item);
|
|
2456
|
+
const role = decisionRoles.get(party) ??
|
|
2457
|
+
Object.keys(roleObject).find((role) => roleObject[role] ===
|
|
2458
|
+
(partyAccounts.get(party) ?? `${camel(party)}AccountId`));
|
|
2459
|
+
const field = role === undefined ? undefined : roleObject[role];
|
|
2460
|
+
if (typeof field === "string") {
|
|
2461
|
+
allowedFields.add(field);
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
if (!parties.has(party))
|
|
2465
|
+
continue;
|
|
2466
|
+
diagnostics.push({
|
|
2467
|
+
code: "HSX1024",
|
|
2468
|
+
fix: `bind ${party} to an account field and use that binding in action ${action.name}'s port`,
|
|
2469
|
+
message: `decision port ${action.name} allows party ${party}, but settlement ${name.name} does not bind that party to the decision action`,
|
|
2470
|
+
severity: "error",
|
|
2471
|
+
span: item.span,
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
if (port.allowedParties.length === 0 ||
|
|
2475
|
+
allowedFields.size !==
|
|
2476
|
+
new Set(port.allowedParties.map((role) => String(roleObject[String(role)]))).size ||
|
|
2477
|
+
port.allowedParties.some((role) => typeof role !== "string" ||
|
|
2478
|
+
!allowedFields.has(String(roleObject[role])))) {
|
|
2479
|
+
diagnostics.push({
|
|
2480
|
+
code: "HSX1024",
|
|
2481
|
+
fix: `use only the account bindings allowed by ${declared.name.name}`,
|
|
2482
|
+
message: `action ${action.name} does not preserve the allowed parties of decision port ${declared.name.name}`,
|
|
2483
|
+
severity: "error",
|
|
2484
|
+
span: action.origin,
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
1852
2489
|
if (!actionNames.has("create")) {
|
|
1853
2490
|
diagnostics.push({
|
|
1854
2491
|
code: "HSX1504",
|
|
@@ -1911,8 +2548,43 @@ function checkInstrument(name, body, parties, aliases, diagnostics) {
|
|
|
1911
2548
|
slots.summary ??= `${titleize(name.name)} instrument`;
|
|
1912
2549
|
slots.title ??= titleize(name.name);
|
|
1913
2550
|
linearityDiagnostics(actionEntries, diagnostics);
|
|
2551
|
+
agentDescriptionDiagnostics(name, slots, actions, diagnostics);
|
|
2552
|
+
if (Array.isArray(slots.required)) {
|
|
2553
|
+
slots.required = [
|
|
2554
|
+
...new Set([...slots.required, ...inferredDecisionFields]),
|
|
2555
|
+
];
|
|
2556
|
+
}
|
|
1914
2557
|
return { actions, fields, id: name.name, origin: name.span, slots };
|
|
1915
2558
|
}
|
|
2559
|
+
// Mirrors the catalog law agent_description_required: every action a caller
|
|
2560
|
+
// can reach becomes an agent tool whose description is authored, never
|
|
2561
|
+
// synthesized from the human-facing summary. System actions with a due clause
|
|
2562
|
+
// are exempt because no caller can reach them.
|
|
2563
|
+
function agentDescriptionDiagnostics(name, slots, actions, diagnostics) {
|
|
2564
|
+
const callable = actions.filter((action) => action.slots.due === undefined);
|
|
2565
|
+
if (callable.length === 0)
|
|
2566
|
+
return;
|
|
2567
|
+
if (slots.agentDescription === undefined) {
|
|
2568
|
+
diagnostics.push({
|
|
2569
|
+
code: "HSX1509",
|
|
2570
|
+
fix: 'add agent_description: "..." telling an agent when to reach for this instrument',
|
|
2571
|
+
message: `instrument ${name.name} exposes callable actions without an agent_description`,
|
|
2572
|
+
severity: "error",
|
|
2573
|
+
span: name.span,
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
for (const action of callable) {
|
|
2577
|
+
if (action.slots.agentDescription !== undefined)
|
|
2578
|
+
continue;
|
|
2579
|
+
diagnostics.push({
|
|
2580
|
+
code: "HSX1509",
|
|
2581
|
+
fix: 'add agent_description: "..." telling an agent when to call this action',
|
|
2582
|
+
message: `action ${action.name} of ${name.name} is callable without an agent_description`,
|
|
2583
|
+
severity: "error",
|
|
2584
|
+
span: action.origin,
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
1916
2588
|
function checkRequiredFields(value, fields, span, diagnostics) {
|
|
1917
2589
|
const report = (message, fix) => {
|
|
1918
2590
|
diagnostics.push({
|
|
@@ -1980,62 +2652,76 @@ function checkFields(block, aliases, diagnostics) {
|
|
|
1980
2652
|
continue;
|
|
1981
2653
|
}
|
|
1982
2654
|
names.add(name);
|
|
1983
|
-
|
|
1984
|
-
let description;
|
|
1985
|
-
let required = true;
|
|
1986
|
-
let extra = {};
|
|
1987
|
-
if (row.value.kind === "block") {
|
|
1988
|
-
const typeRow = entry(row.value, "type");
|
|
1989
|
-
if (typeRow)
|
|
1990
|
-
typeExpr = typeRow.value;
|
|
1991
|
-
const desc = entry(row.value, "description") ?? entry(row.value, "desc");
|
|
1992
|
-
if (desc?.value.kind === "string")
|
|
1993
|
-
description = desc.value.value;
|
|
1994
|
-
const optional = entry(row.value, "optional");
|
|
1995
|
-
if (optional?.value.kind === "boolean")
|
|
1996
|
-
required = !optional.value.value;
|
|
1997
|
-
extra = blockToObject({
|
|
1998
|
-
...row.value,
|
|
1999
|
-
entries: row.value.entries.filter((item) => !["type", "description", "desc", "optional"].includes(item.key.name)),
|
|
2000
|
-
}, new Map(), diagnostics);
|
|
2001
|
-
}
|
|
2002
|
-
const type = typeOf(typeExpr, aliases);
|
|
2003
|
-
const fixedMoneyIssue = invalidFixedMoneyBinding(typeExpr);
|
|
2004
|
-
if (fixedMoneyIssue) {
|
|
2005
|
-
diagnostics.push({
|
|
2006
|
-
code: "HSX1103",
|
|
2007
|
-
fix: `write ${name} as money(SAR, 2500) with a positive safe integer in minor units`,
|
|
2008
|
-
message: `field ${name} ${fixedMoneyIssue}`,
|
|
2009
|
-
severity: "error",
|
|
2010
|
-
span: typeExpr.span,
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
if ((typeExpr.kind === "ident" && typeExpr.name === "ref") ||
|
|
2014
|
-
(type.kind === "ref" && !type.target)) {
|
|
2015
|
-
diagnostics.push({
|
|
2016
|
-
code: "HSX1007",
|
|
2017
|
-
fix: "declare the target instrument as ref<instrument_id>",
|
|
2018
|
-
message: `field ${name} declares ref without an instrument target`,
|
|
2019
|
-
severity: "error",
|
|
2020
|
-
span: typeExpr.span,
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2023
|
-
const structuralType = Object.hasOwn(extra, "items")
|
|
2024
|
-
? "array"
|
|
2025
|
-
: Object.hasOwn(extra, "properties") ||
|
|
2026
|
-
Object.hasOwn(extra, "additionalProperties")
|
|
2027
|
-
? "object"
|
|
2028
|
-
: undefined;
|
|
2029
|
-
const schema = {
|
|
2030
|
-
...schemaFor(type, required),
|
|
2031
|
-
...extra,
|
|
2032
|
-
...(structuralType ? { type: structuralType } : {}),
|
|
2033
|
-
...(description ? { description } : {}),
|
|
2034
|
-
};
|
|
2035
|
-
result.push({ name, origin: row.span, required, schema, type });
|
|
2655
|
+
result.push(lowerField(row, aliases, diagnostics));
|
|
2036
2656
|
}
|
|
2037
2657
|
return result;
|
|
2038
2658
|
}
|
|
2659
|
+
function lowerField(row, aliases, diagnostics) {
|
|
2660
|
+
const name = camel(row.key.name);
|
|
2661
|
+
let typeExpr = row.value;
|
|
2662
|
+
let description;
|
|
2663
|
+
let required = true;
|
|
2664
|
+
let extra = {};
|
|
2665
|
+
if (row.value.kind === "block") {
|
|
2666
|
+
const typeRow = entry(row.value, "type");
|
|
2667
|
+
if (typeRow)
|
|
2668
|
+
typeExpr = typeRow.value;
|
|
2669
|
+
const desc = entry(row.value, "description") ?? entry(row.value, "desc");
|
|
2670
|
+
if (desc?.value.kind === "string")
|
|
2671
|
+
description = desc.value.value;
|
|
2672
|
+
const optional = entry(row.value, "optional");
|
|
2673
|
+
if (optional?.value.kind === "boolean")
|
|
2674
|
+
required = !optional.value.value;
|
|
2675
|
+
extra = blockToObject({
|
|
2676
|
+
...row.value,
|
|
2677
|
+
entries: row.value.entries.filter((item) => !["type", "description", "desc", "optional"].includes(item.key.name)),
|
|
2678
|
+
}, new Map(), diagnostics);
|
|
2679
|
+
}
|
|
2680
|
+
const type = typeOf(typeExpr, aliases);
|
|
2681
|
+
// Diagnostics point at the field's own type expression, not at an alias
|
|
2682
|
+
// declaration that may live in an imported module.
|
|
2683
|
+
const declaredSpan = typeExpr.span;
|
|
2684
|
+
const seen = new Set();
|
|
2685
|
+
while (typeExpr.kind === "ident" &&
|
|
2686
|
+
aliases.has(typeExpr.name) &&
|
|
2687
|
+
!seen.has(typeExpr.name)) {
|
|
2688
|
+
seen.add(typeExpr.name);
|
|
2689
|
+
typeExpr = aliases.get(typeExpr.name);
|
|
2690
|
+
}
|
|
2691
|
+
const fixedMoneyIssue = invalidFixedMoneyBinding(typeExpr);
|
|
2692
|
+
if (fixedMoneyIssue) {
|
|
2693
|
+
diagnostics.push({
|
|
2694
|
+
code: "HSX1103",
|
|
2695
|
+
fix: `write ${name} as money(SAR, 2500) with a positive safe integer in minor units`,
|
|
2696
|
+
message: `field ${name} ${fixedMoneyIssue}`,
|
|
2697
|
+
severity: "error",
|
|
2698
|
+
span: declaredSpan,
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
if ((typeExpr.kind === "ident" && typeExpr.name === "ref") ||
|
|
2702
|
+
(type.kind === "ref" && !type.target)) {
|
|
2703
|
+
diagnostics.push({
|
|
2704
|
+
code: "HSX1007",
|
|
2705
|
+
fix: "declare the target instrument as ref<instrument_id>",
|
|
2706
|
+
message: `field ${name} declares ref without an instrument target`,
|
|
2707
|
+
severity: "error",
|
|
2708
|
+
span: declaredSpan,
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
const structuralType = Object.hasOwn(extra, "items")
|
|
2712
|
+
? "array"
|
|
2713
|
+
: Object.hasOwn(extra, "properties") ||
|
|
2714
|
+
Object.hasOwn(extra, "additionalProperties")
|
|
2715
|
+
? "object"
|
|
2716
|
+
: undefined;
|
|
2717
|
+
const schema = {
|
|
2718
|
+
...schemaFor(type, required),
|
|
2719
|
+
...extra,
|
|
2720
|
+
...(structuralType ? { type: structuralType } : {}),
|
|
2721
|
+
...(description ? { description } : {}),
|
|
2722
|
+
};
|
|
2723
|
+
return { name, origin: row.span, required, schema, type };
|
|
2724
|
+
}
|
|
2039
2725
|
function checkLifecycle(instrument, block, actions, diagnostics) {
|
|
2040
2726
|
const statesRows = block.entries.filter((row) => row.key.name === "states");
|
|
2041
2727
|
const initialRow = entry(block, "initial");
|
|
@@ -2139,6 +2825,18 @@ function checkAction(name, body, fields, instrumentId, endpointFields, holdDesti
|
|
|
2139
2825
|
continue;
|
|
2140
2826
|
}
|
|
2141
2827
|
if (definition?.scope === "action") {
|
|
2828
|
+
if (definition.target === "port" && row.value.kind === "block") {
|
|
2829
|
+
const allowedEntry = entry(row.value, "allowed");
|
|
2830
|
+
if (allowedEntry) {
|
|
2831
|
+
diagnostics.push({
|
|
2832
|
+
code: "HSX1508",
|
|
2833
|
+
fix: "Use allowed_parties: [...] inside an action-level port clause; allowed: is for top-level port declarations.",
|
|
2834
|
+
message: "action-level port clause uses allowed: instead of allowed_parties:",
|
|
2835
|
+
severity: "error",
|
|
2836
|
+
span: allowedEntry.key.span,
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2142
2840
|
const rawValue = exprToJsonForUdlSlot(row.value, definition.target, diagnostics);
|
|
2143
2841
|
const moveOffset = Array.isArray(slots.moves) ? slots.moves.length : 0;
|
|
2144
2842
|
const value = definition.target === "moves"
|
|
@@ -2302,6 +3000,93 @@ function friendlyMoveRows(body) {
|
|
|
2302
3000
|
function isJsonObject(value) {
|
|
2303
3001
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2304
3002
|
}
|
|
3003
|
+
/**
|
|
3004
|
+
* A money<SAR> field moves SAR, whatever the caller wrote in the instance
|
|
3005
|
+
* currency field. Every move of a fixed-currency amount, and every account
|
|
3006
|
+
* step of an instrument whose money is fixed, binds that currency as a
|
|
3007
|
+
* constant: a binding that reads one of the instrument's own text fields is
|
|
3008
|
+
* pinned here, and one the compiler cannot pin (an input, a ref, or a
|
|
3009
|
+
* constant naming another currency) is refused as HSX1306.
|
|
3010
|
+
*/
|
|
3011
|
+
function pinFixedCurrencyBindings(action, fields, diagnostics) {
|
|
3012
|
+
const fieldByName = new Map(fields.map((field) => [field.name, field]));
|
|
3013
|
+
const fixedCurrencyOf = (field) => field?.type.kind === "money" &&
|
|
3014
|
+
field.type.currency &&
|
|
3015
|
+
CURRENCY.test(field.type.currency)
|
|
3016
|
+
? field.type.currency
|
|
3017
|
+
: undefined;
|
|
3018
|
+
const fixed = new Set(fields.flatMap((field) => fixedCurrencyOf(field) ?? []));
|
|
3019
|
+
// Two fixed currencies is HSX1305's refusal; one decides here, none skips.
|
|
3020
|
+
const ledger = fixed.size === 1 ? [...fixed][0] : undefined;
|
|
3021
|
+
if (!ledger)
|
|
3022
|
+
return action;
|
|
3023
|
+
const instanceField = (binding) => binding !== undefined &&
|
|
3024
|
+
isJsonObject(binding) &&
|
|
3025
|
+
binding.from === "instance" &&
|
|
3026
|
+
typeof binding.path === "string" &&
|
|
3027
|
+
binding.path.startsWith("fields.")
|
|
3028
|
+
? fieldByName.get(binding.path.slice("fields.".length))
|
|
3029
|
+
: undefined;
|
|
3030
|
+
const pin = (entry, currency, subject) => {
|
|
3031
|
+
if (!isJsonObject(entry))
|
|
3032
|
+
return entry;
|
|
3033
|
+
const bind = entry.bind;
|
|
3034
|
+
if (bind === undefined || !isJsonObject(bind))
|
|
3035
|
+
return entry;
|
|
3036
|
+
const binding = bind.currency;
|
|
3037
|
+
if (binding === undefined)
|
|
3038
|
+
return entry;
|
|
3039
|
+
const bound = isJsonObject(binding) && binding.from === "const"
|
|
3040
|
+
? String(binding.value)
|
|
3041
|
+
: undefined;
|
|
3042
|
+
if (bound === currency)
|
|
3043
|
+
return entry;
|
|
3044
|
+
if (bound === undefined && instanceField(binding)?.type.kind === "text") {
|
|
3045
|
+
return {
|
|
3046
|
+
...entry,
|
|
3047
|
+
bind: { ...bind, currency: { from: "const", value: currency } },
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
diagnostics.push({
|
|
3051
|
+
code: "HSX1306",
|
|
3052
|
+
fix: `bind currency as { from: const; value: ${currency}; }`,
|
|
3053
|
+
message: `action ${action.name} binds the currency of ${subject} to ${bound ?? "a caller-supplied value"}; a money<${currency}> amount moves in ${currency}`,
|
|
3054
|
+
severity: "error",
|
|
3055
|
+
span: action.origin,
|
|
3056
|
+
});
|
|
3057
|
+
return entry;
|
|
3058
|
+
};
|
|
3059
|
+
const moves = action.slots.moves ?? [];
|
|
3060
|
+
const steps = action.slots.steps ?? [];
|
|
3061
|
+
return {
|
|
3062
|
+
...action,
|
|
3063
|
+
slots: {
|
|
3064
|
+
...action.slots,
|
|
3065
|
+
moves: Array.isArray(moves)
|
|
3066
|
+
? moves.map((move) => {
|
|
3067
|
+
if (!isJsonObject(move))
|
|
3068
|
+
return move;
|
|
3069
|
+
// A post settles a reservation; the ledger refuses a post whose
|
|
3070
|
+
// currency differs from the reserve, which is pinned here.
|
|
3071
|
+
if (move.operation === "internal_transfer.post")
|
|
3072
|
+
return move;
|
|
3073
|
+
const bind = move.bind;
|
|
3074
|
+
const amount = bind !== undefined && isJsonObject(bind)
|
|
3075
|
+
? instanceField(bind.amount)
|
|
3076
|
+
: undefined;
|
|
3077
|
+
return pin(move, fixedCurrencyOf(amount) ?? ledger, amount ? `fields.${amount.name}` : "the moved amount");
|
|
3078
|
+
})
|
|
3079
|
+
: moves,
|
|
3080
|
+
steps: Array.isArray(steps)
|
|
3081
|
+
? steps.map((step) => isJsonObject(step) &&
|
|
3082
|
+
typeof step.operation === "string" &&
|
|
3083
|
+
step.operation.startsWith("account.")
|
|
3084
|
+
? pin(step, ledger, `the ${step.operation} account`)
|
|
3085
|
+
: step)
|
|
3086
|
+
: steps,
|
|
3087
|
+
},
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
2305
3090
|
function bindFieldReferences(slots, fields, action, diagnostics) {
|
|
2306
3091
|
const externalFieldSlot = (path) => {
|
|
2307
3092
|
const key = path.at(-1);
|
|
@@ -2385,9 +3170,18 @@ function crossInstrumentReferenceDiagnostics(instruments, diagnostics) {
|
|
|
2385
3170
|
report(owner, owner.origin, `${childId}.${childRefField} is not ref<${owner.id}>`, `declare ${childId}.${childRefField} as ref<${owner.id}> or correct childRefField`);
|
|
2386
3171
|
}
|
|
2387
3172
|
}
|
|
3173
|
+
const parentField = invariant.parentField;
|
|
3174
|
+
const parent = typeof parentField === "string"
|
|
3175
|
+
? fieldIn(owner, parentField)
|
|
3176
|
+
: undefined;
|
|
2388
3177
|
const childField = invariant.childField;
|
|
3178
|
+
const childFieldDef = typeof childField === "string" ? fieldIn(child, childField) : undefined;
|
|
3179
|
+
const isIntegerSum = invariant.count !== true &&
|
|
3180
|
+
parent?.type.kind === "integer" &&
|
|
3181
|
+
childFieldDef?.type.kind === "integer";
|
|
2389
3182
|
if (typeof childField === "string" &&
|
|
2390
|
-
|
|
3183
|
+
!isIntegerSum &&
|
|
3184
|
+
childFieldDef?.type.kind !== "money") {
|
|
2391
3185
|
report(owner, owner.origin, `${childId}.${childField} is not declared money`, `declare ${childId}.${childField} as money<C> or correct childField`);
|
|
2392
3186
|
}
|
|
2393
3187
|
const childStates = statesOf(child);
|
|
@@ -2398,16 +3192,12 @@ function crossInstrumentReferenceDiagnostics(instruments, diagnostics) {
|
|
|
2398
3192
|
report(owner, owner.origin, `${childId} has no lifecycle state ${status}`, `add ${status} to ${childId} or correct childStatuses`);
|
|
2399
3193
|
}
|
|
2400
3194
|
}
|
|
2401
|
-
const parentField = invariant.parentField;
|
|
2402
|
-
const parent = typeof parentField === "string"
|
|
2403
|
-
? fieldIn(owner, parentField)
|
|
2404
|
-
: undefined;
|
|
2405
3195
|
if (typeof parentField === "string" &&
|
|
2406
3196
|
(!parent ||
|
|
2407
|
-
(invariant.count === true
|
|
3197
|
+
(invariant.count === true || isIntegerSum
|
|
2408
3198
|
? parent.type.kind !== "integer"
|
|
2409
3199
|
: parent.type.kind !== "money"))) {
|
|
2410
|
-
const expected = invariant.count === true ? "integer" : "money<C>";
|
|
3200
|
+
const expected = invariant.count === true || isIntegerSum ? "integer" : "money<C>";
|
|
2411
3201
|
report(owner, owner.origin, `${owner.id}.${parentField} is not declared ${expected}`, `declare ${owner.id}.${parentField} as ${expected} or correct parentField`);
|
|
2412
3202
|
}
|
|
2413
3203
|
}
|
|
@@ -2885,16 +3675,15 @@ function typeOf(expr, aliases = new Map(), seen = new Set()) {
|
|
|
2885
3675
|
function schemaFor(type, required) {
|
|
2886
3676
|
switch (type.kind) {
|
|
2887
3677
|
case "money":
|
|
2888
|
-
return
|
|
2889
|
-
|
|
2890
|
-
const: type.fixedAmount,
|
|
2891
|
-
pattern: MONEY_PATTERN,
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
};
|
|
3678
|
+
return {
|
|
3679
|
+
...(type.fixedAmount
|
|
3680
|
+
? { const: type.fixedAmount, pattern: MONEY_PATTERN }
|
|
3681
|
+
: { pattern: required ? MONEY_PATTERN : OPTIONAL_MONEY_PATTERN }),
|
|
3682
|
+
type: "string",
|
|
3683
|
+
...(type.currency && CURRENCY.test(type.currency)
|
|
3684
|
+
? { "x-hyperscale-currency": type.currency }
|
|
3685
|
+
: {}),
|
|
3686
|
+
};
|
|
2898
3687
|
case "bps":
|
|
2899
3688
|
case "percent":
|
|
2900
3689
|
return { maximum: 10_000, minimum: 0, type: "integer" };
|
|
@@ -2906,6 +3695,8 @@ function schemaFor(type, required) {
|
|
|
2906
3695
|
return { format: "hyperscale-date-time", type: "string" };
|
|
2907
3696
|
case "ref":
|
|
2908
3697
|
return { type: "string" };
|
|
3698
|
+
case "account":
|
|
3699
|
+
return { pattern: ACCOUNT_PATTERN, type: "string" };
|
|
2909
3700
|
default:
|
|
2910
3701
|
return { type: "string" };
|
|
2911
3702
|
}
|