@hyperscale0/udl 4.9.0 → 5.1.0
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 +15 -0
- package/README.md +4 -13
- package/dist/diagnostics.d.ts +1 -15
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +3 -12
- package/dist/diagnostics.js.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/object-contract.d.ts +0 -52
- package/dist/object-contract.d.ts.map +1 -1
- package/dist/object-contract.js +0 -19
- package/dist/object-contract.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +29 -0
- package/dist/validation.js.map +1 -1
- package/package.json +4 -8
- package/spec/README.md +6 -0
- package/src/diagnostics.ts +10 -18
- package/src/index.ts +1 -13
- package/src/object-contract.ts +0 -47
- package/src/validation.ts +57 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -184
- package/dist/cli.js.map +0 -1
- package/dist/evolution.d.ts +0 -5
- package/dist/evolution.d.ts.map +0 -1
- package/dist/evolution.js +0 -27
- package/dist/evolution.js.map +0 -1
- package/dist/instrument-references.d.ts +0 -5
- package/dist/instrument-references.d.ts.map +0 -1
- package/dist/instrument-references.js +0 -90
- package/dist/instrument-references.js.map +0 -1
- package/dist/parser.d.ts +0 -4
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -67
- package/dist/parser.js.map +0 -1
- package/docs/README.md +0 -294
- package/src/cli.ts +0 -196
- package/src/evolution.ts +0 -46
- package/src/instrument-references.ts +0 -123
- package/src/parser.ts +0 -80
package/docs/README.md
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
# UDL 4
|
|
2
|
-
|
|
3
|
-
UDL is the Universal Domain Language, the typed contract between an HSX program
|
|
4
|
-
and its executor. The grammar
|
|
5
|
-
lives in `src/schema.ts`. Generate `udl.schema.json` with
|
|
6
|
-
`bun scripts/emit-spec.ts --write`. There is no migration reader for earlier UDL.
|
|
7
|
-
|
|
8
|
-
## Reading a compiled program
|
|
9
|
-
|
|
10
|
-
Start with `product`, `title` and `currency`. `objects` describes business records
|
|
11
|
-
and their attachments. Follow an attachment's instrument identity into
|
|
12
|
-
`instruments` for its retained fields, lifecycle and actions. `actionOrder`
|
|
13
|
-
preserves authored action order even though canonical JSON sorts object keys.
|
|
14
|
-
An action's `publicAction` supplies its exposed name; its actor and requirements
|
|
15
|
-
still control who may execute it and when.
|
|
16
|
-
|
|
17
|
-
Object metadata describes the subject. Instrument fields retain the agreement's
|
|
18
|
-
terms, account bindings and receipts. Party names identify authenticated or
|
|
19
|
-
configured participants. None of these makes a caller-supplied account ID valid.
|
|
20
|
-
A Build freezes this contract and its bindings for the executor.
|
|
21
|
-
|
|
22
|
-
Business instructions are actions composed from typed requirements and effects.
|
|
23
|
-
The four `internal_transfer` instructions are their money effects. The contract
|
|
24
|
-
expresses valid conditions; execution can still refuse for the current state,
|
|
25
|
-
authority, evidence or balance. Parsing and validation execute nothing.
|
|
26
|
-
|
|
27
|
-
## JSON admission and canonical bytes
|
|
28
|
-
|
|
29
|
-
The parser refuses duplicate JSON member names, including escaped spellings of
|
|
30
|
-
one name. Decoded documents must contain finite JSON data. Accessors, cycles,
|
|
31
|
-
sparse arrays, symbol keys, hidden properties and `__proto__` members refuse
|
|
32
|
-
before schema parsing. Shared values count once per occurrence toward the budget.
|
|
33
|
-
Admission permits at most 100,000 visited values, 32 nesting levels, 240 code units
|
|
34
|
-
per key and 1 MiB of UTF-8 key and string data. Source text has a separate 1 MiB
|
|
35
|
-
byte limit. These bounds live in `src/limits.ts`.
|
|
36
|
-
|
|
37
|
-
Canonical JSON sorts object keys recursively, preserves array order and ends
|
|
38
|
-
with a newline. Schema defaults are part of the admitted document. Equivalent
|
|
39
|
-
key insertion orders and explicit defaults have the same digest. Object field
|
|
40
|
-
compatibility and evolution comparisons use the same key ordering.
|
|
41
|
-
|
|
42
|
-
## Objects and subjects
|
|
43
|
-
|
|
44
|
-
An object kind declares identity, title, authored field names, normalized fields
|
|
45
|
-
and display columns. Each attachment freezes its instrument identity and party
|
|
46
|
-
parameter bindings to `owner`, `actor`, `operator`, or a declared Product party. Core resolves role bindings from the object row, session and Product, and
|
|
47
|
-
declared parties from the Product party binding. `authoredFields` records only names declared in the HSX
|
|
48
|
-
object block. `fields` is their union with attached action requirements. Matching
|
|
49
|
-
names must have matching types and constraints. Account fields belong to
|
|
50
|
-
instruments, never objects.
|
|
51
|
-
|
|
52
|
-
An instrument may name its object kind with `subject`. An action's `subject`
|
|
53
|
-
contains its requirements and any frozen adapter binding. Each requirement keeps
|
|
54
|
-
its source field and optional `objectField` rename. Adapter snapshots contain
|
|
55
|
-
provider, capability, operation, declaration digest and requirements. A null snapshot
|
|
56
|
-
marks an unbound adapter. It permits discovery but blocks that action. No provider
|
|
57
|
-
requirement is inferred from an operation name.
|
|
58
|
-
|
|
59
|
-
Object creation accepts `{}`. Its optional `fields` property accepts every
|
|
60
|
-
normalized field as optional. Requirements become mandatory only when the
|
|
61
|
-
attached action runs. Object creation performs no financial action. Instrument
|
|
62
|
-
creation remains internal; an attached create action may expose a public name.
|
|
63
|
-
Attachment parties resolve from authenticated authority, never from party account
|
|
64
|
-
IDs supplied in a caller body.
|
|
65
|
-
|
|
66
|
-
`object-contract.ts` projects `ObjectDiscovery` for
|
|
67
|
-
`GET /v1/products/{productId}/objects`. It exports the discovery, instance, list
|
|
68
|
-
and action response types. Discovery preserves Build identity, authored field
|
|
69
|
-
names, the full optional create schema and each exposed action's requirements.
|
|
70
|
-
Object instances carry `objectId`, `kind`, `revision`, optional `externalId`,
|
|
71
|
-
`fields` and `productBuildId`. Action responses carry the object and either a
|
|
72
|
-
financial instrument outcome or null.
|
|
73
|
-
|
|
74
|
-
An object may declare `entryActions`, a list of exposed attachment creation
|
|
75
|
-
names. The host admits an active enrolled customer only to an entry action whose
|
|
76
|
-
actor is the session's `actor` role. Other records remain closed. Retained
|
|
77
|
-
participants can read their record and use the actions granted to their roles;
|
|
78
|
-
participation never changes the owner.
|
|
79
|
-
|
|
80
|
-
An instrument may declare `scope: "product"`. Object creation can resolve a
|
|
81
|
-
reference to that instrument on another record in the same Product when every
|
|
82
|
-
retained party account matches the current bindings. Otherwise references stay
|
|
83
|
-
on the current record. Create and approve shared limits once per Product or
|
|
84
|
-
borrower; referencing them does not create a new limit.
|
|
85
|
-
|
|
86
|
-
## Object operations
|
|
87
|
-
|
|
88
|
-
The host owns transport routes for object discovery, creation, listing,
|
|
89
|
-
retrieval, action availability, execution and agreement evidence.
|
|
90
|
-
`src/object-contract.ts` defines the language package's object projections.
|
|
91
|
-
|
|
92
|
-
Execution binds `productBuildId`, `digest`, `target` and `expectedRevision`.
|
|
93
|
-
The target identifies an attachment or an existing instance on that attachment.
|
|
94
|
-
The optional `fields` collect subject metadata; optional `input` carries the action's typed input.
|
|
95
|
-
Missing requirements refuse execution. Callers reuse an idempotency key only for
|
|
96
|
-
retries of the same request. Staff Create for may name `ownerPrincipalId` through
|
|
97
|
-
its authorized path; ordinary ownership comes from the session.
|
|
98
|
-
|
|
99
|
-
## Accounts and money
|
|
100
|
-
|
|
101
|
-
A document declares SAR once. Money is a nonnegative minor-unit decimal string
|
|
102
|
-
of at most 18 digits. Percentages are basis points, durations are positive integer
|
|
103
|
-
milliseconds, and dates are timestamps with explicit offsets.
|
|
104
|
-
|
|
105
|
-
An account field binds a party, the instrument itself, or an adapter. Party owners
|
|
106
|
-
are names; `owner: "self"` provisions an account per instance. An adapter owner
|
|
107
|
-
is `{ "adapter": "insurer" }`, where `insurer` names an adapter declared in one
|
|
108
|
-
of the instrument's action subjects. The agreement retains that declaration's
|
|
109
|
-
provider identity. Missing or conflicting bindings refuse with
|
|
110
|
-
`subject_adapter_unbound`; changing an existing account binding refuses with
|
|
111
|
-
`account_binding_changed`. A null declaration can describe a draft but cannot
|
|
112
|
-
create an adapter-owned account.
|
|
113
|
-
|
|
114
|
-
Account creation and binding are executor work. Callers never supply account IDs.
|
|
115
|
-
Accounts declare `book: "cash" | "claim"`, defaulting to cash. Moves never cross books.
|
|
116
|
-
Only claim accounts may declare `contra: true` and permit a negative balance.
|
|
117
|
-
Party and adapter accounts share owner, book and key within a Product; the key
|
|
118
|
-
defaults to `balance`. Adapter aliases for the same provider share that account. Transfers between
|
|
119
|
-
known aliases of one account refuse, including self fields with the same key.
|
|
120
|
-
The executor scopes provider accounts to the tenant and Product, with the Product
|
|
121
|
-
participant as ledger custodian. Providers are not parties. An account owned by
|
|
122
|
-
self defaults its key to the field name. Named capital, premium, income, debt and
|
|
123
|
-
loss balances use explicit keys. `party.buyer` is the buyer's default cash account.
|
|
124
|
-
|
|
125
|
-
A plain move to an adapter account credits its ledger balance. It does not prove
|
|
126
|
-
an external payout. Provider confirmation still uses a boundary reservation and
|
|
127
|
-
instruction-bound evidence. Account ownership adds no settlement operation.
|
|
128
|
-
|
|
129
|
-
Disbursement moves cash from lender to borrower and claims from borrower debt to
|
|
130
|
-
principal and profit receivables. Repayment moves borrower cash to lender cash and
|
|
131
|
-
the same claim amounts from receivables back to borrower debt. Unearned profit is
|
|
132
|
-
cancelled by the claim move alone. Write-off moves the principal claim to the
|
|
133
|
-
lender's loss account, with no cash movement. The library declares when profit
|
|
134
|
-
is earned: on payment, by schedule or at disbursement.
|
|
135
|
-
These are ordinary paired moves in HSX, not executor loan rules.
|
|
136
|
-
|
|
137
|
-
A value is `{literal: value}` or `{field: path}`. Paths start with `self`, `input`, `subject`
|
|
138
|
-
or `party`. Reference fields allow typed traversal. Account paths expose locked,
|
|
139
|
-
read-only `.balance` and `.reserved` money values. `self.id`, `self.status`,
|
|
140
|
-
`self.createdAt` and `self.now` are sealed executor values. Callers cannot set
|
|
141
|
-
constants, calculated fields, account bindings or capture fields. Create supplies
|
|
142
|
-
declared typed references; later actions cannot replace them. A ref declares `targetKind: "object" | "instrument"` and one target id or
|
|
143
|
-
a list of 1 to 16 distinct target ids in that namespace. Its stored value is an
|
|
144
|
-
identity from one listed target. Path traversal exposes only fields
|
|
145
|
-
with compatible types on every target. Nested refs combine their target sets;
|
|
146
|
-
accounts must agree on owner, key, book and contra flags, and enums must
|
|
147
|
-
have the same values. A comparison or selection anchor must share a possible
|
|
148
|
-
reference target. An invoked input must accept every possible supplied target.
|
|
149
|
-
The executor checks the actual instance type at admission.
|
|
150
|
-
|
|
151
|
-
## Calculation and movement
|
|
152
|
-
|
|
153
|
-
Calculations form a finite dependency graph. `sum`, `subtract` and `minimum`
|
|
154
|
-
operate on money or integer fields, with operands of the same type as the target.
|
|
155
|
-
`rate`, `multiply`, `divide` and `shift` retain their typed operands. A shift's
|
|
156
|
-
`milliseconds` operand is a duration field or a non-negative safe integer literal.
|
|
157
|
-
Zero leaves the date unchanged; duration fields still require positive values. Rate and
|
|
158
|
-
division round down. Weighted shares also round down; residual minor units go
|
|
159
|
-
to the declared residual account. Cash and loss use this one rounding rule.
|
|
160
|
-
There is no largest-remainder allocation. Subtraction refuses a negative result. Integer results must
|
|
161
|
-
be safe integers. No calculation evaluates source text.
|
|
162
|
-
|
|
163
|
-
The only move instructions are `internal_transfer.create`,
|
|
164
|
-
`internal_transfer.reserve`, `internal_transfer.post` and `internal_transfer.void`.
|
|
165
|
-
Create and reserve declare an amount, from account and to account. Reserve captures
|
|
166
|
-
its executor-produced transfer identity into a declared self text field. Post and
|
|
167
|
-
void consume that identity. Callers cannot create or replace captured identities.
|
|
168
|
-
Adapter-owned endpoints use the same move vocabulary.
|
|
169
|
-
|
|
170
|
-
Each move may declare `economics: { purpose, sourceParty, reversalOf? }`.
|
|
171
|
-
Purpose is `earning`, `principal`, `participant_payout`, `internal`,
|
|
172
|
-
`prepaid_credit`, or `pass_through`. Pass-through money is excluded from earnings
|
|
173
|
-
and costs. `sourceParty` names an authored party whose retained
|
|
174
|
-
participant must own the debit account. For an earning realized from company-held
|
|
175
|
-
customer money, it identifies the retained payer instead. The debit must be an
|
|
176
|
-
instrument account bound to the same agreement or a retained referenced agreement
|
|
177
|
-
in the same Build with the same payer; the company credit must be outside the
|
|
178
|
-
agreement. Evidence also retains the actual debit account participant.
|
|
179
|
-
`reversalOf` is a transfer reference path. A reversal preserves purpose, Product, Build and agreement, debits the
|
|
180
|
-
original recipient account and credits the original source participant. The
|
|
181
|
-
combined refunds cannot exceed the original posted amount. A post inherits its
|
|
182
|
-
reservation's economics; it cannot change them.
|
|
183
|
-
The terminal ledger evidence keeps these fields with the agreement, Build,
|
|
184
|
-
account participants, and move key. A move without economics has no inferred
|
|
185
|
-
classification.
|
|
186
|
-
|
|
187
|
-
A loan, refund, payoff, write-off or distribution is library behavior built from
|
|
188
|
-
accounts, calculations and ordered moves. None has a privileged executor clause.
|
|
189
|
-
Outstanding principal is an account balance. A schedule consists of explicit
|
|
190
|
-
dated child records, with positions 1 through n in declaration order. The library
|
|
191
|
-
uses ordinary comparisons and aggregates to constrain those records. There is no
|
|
192
|
-
recurrence process, allocation bucket, partition expander or schedule requirement
|
|
193
|
-
in the UDL kernel.
|
|
194
|
-
|
|
195
|
-
## Admission and lifecycle
|
|
196
|
-
|
|
197
|
-
Actions declare typed input lists, requirements, an actor and an event. Lifecycle
|
|
198
|
-
edges name their source and destination states. Requirements and effects execute
|
|
199
|
-
atomically under the same account and reference locks. `set` copies typed values
|
|
200
|
-
to mutable fields. Action calculations read the locked snapshot and populate the
|
|
201
|
-
action draft before requirements. Ordered moves and invocations follow admission;
|
|
202
|
-
a zero create move refuses unless the action declares `allowZero: true`.
|
|
203
|
-
With that permission it records no transfer or capture. A reserve amount must
|
|
204
|
-
always be positive.
|
|
205
|
-
Invariants check the completed transaction. A due instant
|
|
206
|
-
is inclusive; a deadline is exclusive. Clock delays never extend deadlines.
|
|
207
|
-
|
|
208
|
-
Requirements are compare, state, unique, aggregate, evidence and hours.
|
|
209
|
-
A typed selection names one instrument or a bounded union, a reference field,
|
|
210
|
-
anchor, accepted states and row limit. Exceeding the limit refuses rather than
|
|
211
|
-
truncates. Optional equality filters apply to every selected type. An optional
|
|
212
|
-
window selects date values between `self.now - milliseconds` and `self.now`.
|
|
213
|
-
Aggregate sums use typed paths on the selected records, including account
|
|
214
|
-
balances. An invariant holds before and after every affected transaction.
|
|
215
|
-
|
|
216
|
-
`hours` converts a date path to its literal IANA timezone and accepts `[start,end)`.
|
|
217
|
-
A start greater than end wraps midnight; equal endpoints admit no time.
|
|
218
|
-
`evidence` declares subject, family, check, result and maxAge. The subject is an
|
|
219
|
-
account or text id. The executor selects the newest matching subject binding,
|
|
220
|
-
checks its retained Build, protected input digest and adapter declaration, then
|
|
221
|
-
requires the declared result and age. A newer matching refusal or pending result
|
|
222
|
-
prevents reuse of an older success.
|
|
223
|
-
|
|
224
|
-
`invoke` supplies typed inputs to a linked action or bounded selection. Its graph
|
|
225
|
-
is acyclic and bounded. Public names grant no authority; clock and parent actors
|
|
226
|
-
remain executor-owned. A public action on a subject instrument requires an
|
|
227
|
-
attachment on that object kind. State requirements refer to instrument instances;
|
|
228
|
-
objects have no instrument lifecycle. List extraction preserves the reference
|
|
229
|
-
namespace and target. Selection anchors and filters participate in calculation
|
|
230
|
-
cycle detection. Literal assignments must satisfy the destination field's constraints.
|
|
231
|
-
|
|
232
|
-
A captured transfer exposes its executor-owned `.status`. A boundary payout
|
|
233
|
-
reserves before dispatch. Only matching terminal confirmation can post or void
|
|
234
|
-
the reservation. An acknowledgement, transport failure or missing response
|
|
235
|
-
keeps the reservation. A refund is a separately funded move. Account balances
|
|
236
|
-
alone do not prove an external effect. The language adds no automatic
|
|
237
|
-
provider-failure reversal or deadline settlement policy.
|
|
238
|
-
|
|
239
|
-
## Ten laws
|
|
240
|
-
|
|
241
|
-
1. Each concept has one concrete meaning.
|
|
242
|
-
2. Provider transport and credentials stay outside the contract.
|
|
243
|
-
3. Every state change emits its declared event.
|
|
244
|
-
4. Four transfer instructions are the only money movement vocabulary.
|
|
245
|
-
5. Instances have an opaque identity and money uses integer minor units.
|
|
246
|
-
6. Typed requirements and effects execute atomically.
|
|
247
|
-
7. Live additions preserve existing meaning; development estates may recreate.
|
|
248
|
-
8. Business names identify object kinds, attached instruments and public actions.
|
|
249
|
-
9. Waiting uses lifecycle states and clocks. Before admission, the executor
|
|
250
|
-
catches up actor: clock actions with due instants on the locked instance,
|
|
251
|
-
references and selected rows, in chronological order. Catch-up commits its
|
|
252
|
-
own transaction and events before the caller request is evaluated. A refused
|
|
253
|
-
caller request rolls back only its own drafts. Caller deadlines are refusal
|
|
254
|
-
boundaries; they never execute the caller action. Separate clock actions
|
|
255
|
-
carry expiry consequences. Requirements and selections observe the resulting
|
|
256
|
-
clock state.
|
|
257
|
-
10. References close, states are reachable, captures are linear, and terminal
|
|
258
|
-
instances have no remaining owned-account balances or reservations.
|
|
259
|
-
|
|
260
|
-
## Clause inventory
|
|
261
|
-
|
|
262
|
-
The generated [JSON schema](../spec/udl.schema.json) owns the complete field
|
|
263
|
-
inventory, defaults and constraints. `src/schema.ts` is its source.
|
|
264
|
-
|
|
265
|
-
`calculate.aggregate` reads a typed selection and yields its count or a money sum. `calculate.ratio` computes floor(amount * numerator / denominator) with arbitrary-precision intermediates and refuses a zero denominator. Numerator and denominator share a numeric type. Selection order is a list of typed ascending paths, followed by identity as the final tie-break. `invoke {instrument, action: "create", input}` creates a child record in the same transaction; its inputs resolve in the caller, and the ordinary create actor and requirements still apply.
|
|
266
|
-
|
|
267
|
-
`calculate.at` reads a typed list at a one-based position and refuses an out-of-range index. Its result has the list item type. Integer divide accepts integer operands and rounds down. Every move may capture its transfer identity into a declared self text field; reserve requires a capture. Captures and their status paths are executor-owned.
|
|
268
|
-
|
|
269
|
-
## Reporting definitions
|
|
270
|
-
|
|
271
|
-
An instrument may declare `reports`, a bounded array of strict ReportDefinition
|
|
272
|
-
values. Each definition contains identity/version, scope, datasets, time,
|
|
273
|
-
selection, calculation, validation, output and authority. The JSON schema defines
|
|
274
|
-
all fields. Expressions form an ordered graph with backward references, typed
|
|
275
|
-
money in integer minor units, explicit ratios and date operations. Source fields
|
|
276
|
-
must exist in every bound instrument. A scalar field cannot acquire a suffix;
|
|
277
|
-
account balances use the declared `<account>.balance` path. Company instrument reports require
|
|
278
|
-
cross-Build bindings and are not admitted in this version.
|
|
279
|
-
|
|
280
|
-
Joins declare one-or-many cardinality, missing-record policy and aggregates;
|
|
281
|
-
implicit row multiplication is forbidden. Compilation checks currencies, column
|
|
282
|
-
and expression types, output columns and sort keys. Execution must check complete
|
|
283
|
-
source populations, required facts, unique row and sort identities, reconciliation
|
|
284
|
-
and declared limits before publishing any result. Empty-population behavior and
|
|
285
|
-
unavailable facts are definition data. Request, read and release roles are
|
|
286
|
-
separate. Independent activation and retained deterministic replay belong to the
|
|
287
|
-
host; language acceptance alone does not authorize an artifact or external release.
|
|
288
|
-
|
|
289
|
-
Reporting source selections and foreign join keys must be computable from
|
|
290
|
-
PostgreSQL facts; ledger balances and ratios belong after capture. Aggregate
|
|
291
|
-
names are unique across stages. `average` requires an explicit `rounding` policy
|
|
292
|
-
(`floor` or `halfUp`) to retain integer and money minor-unit types. Numeric and
|
|
293
|
-
min/max aggregates skip nulls, return null for all-null inputs, and emit per-row
|
|
294
|
-
`_skippedNulls_<name>` counters in JSON and CSV. Required facts still refuse nulls.
|
package/src/cli.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* The `udl` command. Actions over the same library the compiler uses:
|
|
4
|
-
* `validate` answers whether a document is admissible, `fmt` writes the
|
|
5
|
-
* canonical bytes, and `diff` answers whether a change is legal against a
|
|
6
|
-
* document that already has live instances.
|
|
7
|
-
*/
|
|
8
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
canonicalizeUdl,
|
|
12
|
-
canonicalDigest,
|
|
13
|
-
diffValidatedUdlEvolution,
|
|
14
|
-
parseUdl,
|
|
15
|
-
UdlError,
|
|
16
|
-
type UdlDocument,
|
|
17
|
-
udlDiagnostic,
|
|
18
|
-
} from "./index.js";
|
|
19
|
-
|
|
20
|
-
const USAGE = `udl - the Universal Domain Language toolchain
|
|
21
|
-
|
|
22
|
-
usage:
|
|
23
|
-
udl validate <file> parse <file> and report every issue found
|
|
24
|
-
udl fmt <file> [--write] print the canonical form, or rewrite the file
|
|
25
|
-
udl canon <file> [--digest] print canonical bytes, or their SHA-256 digest
|
|
26
|
-
udl diff <live> <next> check <next> against the append-only law
|
|
27
|
-
udl explain UDL#### explain one stable diagnostic code
|
|
28
|
-
udl help print this
|
|
29
|
-
|
|
30
|
-
exit codes:
|
|
31
|
-
0 the document is admissible, or the change is additive
|
|
32
|
-
1 the document was refused, or the change breaks the append-only law
|
|
33
|
-
2 the command line was wrong, or a file could not be read`;
|
|
34
|
-
|
|
35
|
-
async function main(argv: readonly string[]): Promise<number> {
|
|
36
|
-
const [command, ...rest] = argv;
|
|
37
|
-
switch (command) {
|
|
38
|
-
case "help":
|
|
39
|
-
case "-h":
|
|
40
|
-
case "--help":
|
|
41
|
-
console.log(USAGE);
|
|
42
|
-
return 0;
|
|
43
|
-
case "validate":
|
|
44
|
-
return validateCommand(rest);
|
|
45
|
-
case "fmt":
|
|
46
|
-
return formatCommand(rest);
|
|
47
|
-
case "canon":
|
|
48
|
-
return canonCommand(rest);
|
|
49
|
-
case "diff":
|
|
50
|
-
return diffCommand(rest);
|
|
51
|
-
case "explain":
|
|
52
|
-
return explainCommand(rest);
|
|
53
|
-
case undefined:
|
|
54
|
-
return usageError("udl: no command given");
|
|
55
|
-
default:
|
|
56
|
-
return usageError(`udl: unknown command ${command}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function canonCommand(args: readonly string[]): Promise<number> {
|
|
61
|
-
const digest = args.includes("--digest");
|
|
62
|
-
const [file, ...extra] = args.filter((argument) => argument !== "--digest");
|
|
63
|
-
if (file === undefined || extra.length > 0) {
|
|
64
|
-
return usageError("usage: udl canon <file> [--digest]");
|
|
65
|
-
}
|
|
66
|
-
const bytes = await readDocument(file);
|
|
67
|
-
if (bytes === null) return 2;
|
|
68
|
-
let document: UdlDocument;
|
|
69
|
-
try {
|
|
70
|
-
document = parseUdl(bytes);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
return reportIssues(file, error);
|
|
73
|
-
}
|
|
74
|
-
if (digest) console.log(await canonicalDigest(document));
|
|
75
|
-
else process.stdout.write(canonicalizeUdl(bytes));
|
|
76
|
-
return 0;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function explainCommand(args: readonly string[]): number {
|
|
80
|
-
const [code, ...extra] = args;
|
|
81
|
-
if (code === undefined || extra.length > 0) {
|
|
82
|
-
return usageError("usage: udl explain UDL####");
|
|
83
|
-
}
|
|
84
|
-
const diagnostic = udlDiagnostic(code);
|
|
85
|
-
if (!diagnostic) {
|
|
86
|
-
return usageError(`udl: unknown diagnostic code ${code}`);
|
|
87
|
-
}
|
|
88
|
-
console.log(`${code} ${diagnostic.title}`);
|
|
89
|
-
console.log(`category: ${diagnostic.category}`);
|
|
90
|
-
console.log(`family: ${diagnostic.family}`);
|
|
91
|
-
console.log(`fix: ${diagnostic.fix}`);
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function validateCommand(args: readonly string[]): Promise<number> {
|
|
96
|
-
const [file, ...extra] = args;
|
|
97
|
-
if (file === undefined || extra.length > 0) {
|
|
98
|
-
return usageError("usage: udl validate <file>");
|
|
99
|
-
}
|
|
100
|
-
const bytes = await readDocument(file);
|
|
101
|
-
if (bytes === null) return 2;
|
|
102
|
-
if (parseDocument(file, bytes) === null) return 1;
|
|
103
|
-
console.log(`${file}: ok`);
|
|
104
|
-
return 0;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function formatCommand(args: readonly string[]): Promise<number> {
|
|
108
|
-
const write = args.includes("--write");
|
|
109
|
-
const [file, ...extra] = args.filter((argument) => argument !== "--write");
|
|
110
|
-
if (file === undefined || extra.length > 0) {
|
|
111
|
-
return usageError("usage: udl fmt <file> [--write]");
|
|
112
|
-
}
|
|
113
|
-
const bytes = await readDocument(file);
|
|
114
|
-
if (bytes === null) return 2;
|
|
115
|
-
|
|
116
|
-
let canonical: string;
|
|
117
|
-
try {
|
|
118
|
-
canonical = canonicalizeUdl(bytes);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
return reportIssues(file, error);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!write) {
|
|
124
|
-
process.stdout.write(canonical);
|
|
125
|
-
return 0;
|
|
126
|
-
}
|
|
127
|
-
// The bytes decoded cleanly already: canonicalizeUdl refuses anything else.
|
|
128
|
-
if (new TextDecoder().decode(bytes) === canonical) return 0;
|
|
129
|
-
await writeFile(file, canonical);
|
|
130
|
-
console.log(`${file}: formatted`);
|
|
131
|
-
return 0;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function diffCommand(args: readonly string[]): Promise<number> {
|
|
135
|
-
const [livePath, nextPath, ...extra] = args;
|
|
136
|
-
if (livePath === undefined || nextPath === undefined || extra.length > 0) {
|
|
137
|
-
return usageError("usage: udl diff <live> <next>");
|
|
138
|
-
}
|
|
139
|
-
const liveBytes = await readDocument(livePath);
|
|
140
|
-
const nextBytes = await readDocument(nextPath);
|
|
141
|
-
if (liveBytes === null || nextBytes === null) return 2;
|
|
142
|
-
|
|
143
|
-
const live = parseDocument(livePath, liveBytes);
|
|
144
|
-
const next = parseDocument(nextPath, nextBytes);
|
|
145
|
-
if (live === null || next === null) return 1;
|
|
146
|
-
|
|
147
|
-
// `parseDocument` already validated both, so take the door that does not
|
|
148
|
-
// validate a second time.
|
|
149
|
-
const violations = diffValidatedUdlEvolution(live, next);
|
|
150
|
-
if (violations.length === 0) {
|
|
151
|
-
console.log(`${nextPath}: additive`);
|
|
152
|
-
return 0;
|
|
153
|
-
}
|
|
154
|
-
for (const violation of violations) {
|
|
155
|
-
console.error(
|
|
156
|
-
`${nextPath}: ${violation.code} ${violation.path}: ${violation.message}`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
return 1;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function readDocument(file: string): Promise<Uint8Array | null> {
|
|
163
|
-
try {
|
|
164
|
-
return await readFile(file);
|
|
165
|
-
} catch (error) {
|
|
166
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
167
|
-
console.error(`udl: cannot read ${file}: ${reason}`);
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Parses one document, printing its issues. Null means the document was refused. */
|
|
173
|
-
function parseDocument(file: string, bytes: Uint8Array): UdlDocument | null {
|
|
174
|
-
try {
|
|
175
|
-
return parseUdl(bytes);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
reportIssues(file, error);
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function reportIssues(file: string, error: unknown): number {
|
|
183
|
-
if (!(error instanceof UdlError)) throw error;
|
|
184
|
-
for (const issue of error.issues) {
|
|
185
|
-
console.error(`${file}: ${issue.code} ${issue.path}: ${issue.message}`);
|
|
186
|
-
}
|
|
187
|
-
return 1;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function usageError(message: string): number {
|
|
191
|
-
console.error(message);
|
|
192
|
-
console.error(USAGE);
|
|
193
|
-
return 2;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
process.exitCode = await main(process.argv.slice(2));
|
package/src/evolution.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { UdlDocument } from "./schema.js";
|
|
2
|
-
import { issue, type UdlIssue } from "./diagnostics.js";
|
|
3
|
-
import { writeJson } from "./json.js";
|
|
4
|
-
|
|
5
|
-
/** Existing instruments are frozen; a live document may add independent instruments. */
|
|
6
|
-
export function diffValidatedUdlEvolution(
|
|
7
|
-
live: UdlDocument,
|
|
8
|
-
next: UdlDocument,
|
|
9
|
-
): UdlIssue[] {
|
|
10
|
-
const issues: UdlIssue[] = [];
|
|
11
|
-
const refuse = (path: string, message: string) =>
|
|
12
|
-
issues.push(issue("UDL7001", path, message));
|
|
13
|
-
if (live.product !== next.product || live.currency !== next.currency)
|
|
14
|
-
refuse("$", "product identity and currency are immutable");
|
|
15
|
-
for (const [name, party] of Object.entries(live.parties))
|
|
16
|
-
if (
|
|
17
|
-
!Object.hasOwn(next.parties, name) ||
|
|
18
|
-
writeJson(party) !== writeJson(next.parties[name])
|
|
19
|
-
)
|
|
20
|
-
refuse(`$.parties.${name}`, "a live party cannot change or disappear");
|
|
21
|
-
for (const object of live.objects) {
|
|
22
|
-
const updated = next.objects.find((o) => o.id === object.id);
|
|
23
|
-
if (!updated || writeJson(object) !== writeJson(updated))
|
|
24
|
-
refuse(
|
|
25
|
-
`$.objects.${object.id}`,
|
|
26
|
-
"a live object kind cannot change or disappear; recreate development estates",
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
for (const instrument of live.instruments) {
|
|
30
|
-
const updated = next.instruments.find((i) => i.id === instrument.id);
|
|
31
|
-
if (!updated || writeJson(instrument) !== writeJson(updated))
|
|
32
|
-
refuse(
|
|
33
|
-
`$.instruments.${instrument.id}`,
|
|
34
|
-
"a live instrument cannot change or disappear; recreate development estates",
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
if (writeJson(live) !== writeJson(next) && next.version <= live.version)
|
|
38
|
-
issues.push(
|
|
39
|
-
issue(
|
|
40
|
-
"UDL7002",
|
|
41
|
-
"$.version",
|
|
42
|
-
"a changed document needs a higher version",
|
|
43
|
-
),
|
|
44
|
-
);
|
|
45
|
-
return issues;
|
|
46
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
import {
|
|
3
|
-
udlDocumentSchema,
|
|
4
|
-
type UdlDocument,
|
|
5
|
-
type UdlInstrument,
|
|
6
|
-
} from "./schema.js";
|
|
7
|
-
|
|
8
|
-
type Schema = Record<string, unknown>;
|
|
9
|
-
const object = (value: unknown): value is Schema =>
|
|
10
|
-
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11
|
-
let documentSchema: Schema | undefined;
|
|
12
|
-
|
|
13
|
-
/** Visit identities through schema annotations, never through user JSON keys. */
|
|
14
|
-
export function mapUdlInstrumentReferences(
|
|
15
|
-
document: UdlDocument,
|
|
16
|
-
map: (id: string) => string,
|
|
17
|
-
): UdlDocument {
|
|
18
|
-
const root = (documentSchema ??= z.toJSONSchema(udlDocumentSchema));
|
|
19
|
-
const result = structuredClone(document);
|
|
20
|
-
const locations = new Map<
|
|
21
|
-
string,
|
|
22
|
-
{ parent: Schema | unknown[]; key: string | number; kind: string }
|
|
23
|
-
>();
|
|
24
|
-
function visit(
|
|
25
|
-
schema: unknown,
|
|
26
|
-
value: unknown,
|
|
27
|
-
parent: Schema | unknown[],
|
|
28
|
-
key: string | number,
|
|
29
|
-
path: readonly (string | number)[],
|
|
30
|
-
): void {
|
|
31
|
-
if (!object(schema)) return;
|
|
32
|
-
if (typeof schema.$ref === "string") {
|
|
33
|
-
if (!schema.$ref.startsWith("#/"))
|
|
34
|
-
throw new Error("UDL reference schema must be local");
|
|
35
|
-
let target: unknown = root;
|
|
36
|
-
for (const part of schema.$ref.slice(2).split("/")) {
|
|
37
|
-
target = object(target)
|
|
38
|
-
? target[part.replaceAll("~1", "/").replaceAll("~0", "~")]
|
|
39
|
-
: undefined;
|
|
40
|
-
}
|
|
41
|
-
visit(target, value, parent, key, path);
|
|
42
|
-
}
|
|
43
|
-
const kind = schema["x-udl-reference"];
|
|
44
|
-
if (typeof kind === "string" && typeof value === "string")
|
|
45
|
-
locations.set(JSON.stringify(path), { parent, key, kind });
|
|
46
|
-
for (const union of [schema.anyOf, schema.oneOf, schema.allOf])
|
|
47
|
-
if (Array.isArray(union))
|
|
48
|
-
for (const branch of union) visit(branch, value, parent, key, path);
|
|
49
|
-
if (Array.isArray(value)) {
|
|
50
|
-
value.forEach((entry, index) =>
|
|
51
|
-
visit(schema.items, entry, value, index, [...path, index]),
|
|
52
|
-
);
|
|
53
|
-
} else if (object(value)) {
|
|
54
|
-
const properties = object(schema.properties) ? schema.properties : {};
|
|
55
|
-
const discriminator = object(properties.type)
|
|
56
|
-
? properties.type.const
|
|
57
|
-
: undefined;
|
|
58
|
-
if (
|
|
59
|
-
(discriminator === "ref" || discriminator === "list") &&
|
|
60
|
-
value.type === discriminator &&
|
|
61
|
-
value.targetKind === "instrument"
|
|
62
|
-
) {
|
|
63
|
-
if (typeof value.target === "string")
|
|
64
|
-
locations.set(JSON.stringify([...path, "target"]), {
|
|
65
|
-
parent: value,
|
|
66
|
-
key: "target",
|
|
67
|
-
kind: "instrument",
|
|
68
|
-
});
|
|
69
|
-
else if (Array.isArray(value.target))
|
|
70
|
-
value.target.forEach((_, index) =>
|
|
71
|
-
locations.set(JSON.stringify([...path, "target", index]), {
|
|
72
|
-
parent: value.target as unknown[],
|
|
73
|
-
key: index,
|
|
74
|
-
kind: "instrument",
|
|
75
|
-
}),
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
for (const [name, entry] of Object.entries(value))
|
|
79
|
-
visit(
|
|
80
|
-
properties[name] ?? schema.additionalProperties,
|
|
81
|
-
entry,
|
|
82
|
-
value,
|
|
83
|
-
name,
|
|
84
|
-
[...path, name],
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
visit(root, result, [result], 0, []);
|
|
89
|
-
for (const { parent, key, kind } of locations.values()) {
|
|
90
|
-
const holder = parent as Record<string | number, unknown>;
|
|
91
|
-
const value = holder[key] as string;
|
|
92
|
-
if (kind === "instrument") holder[key] = map(value);
|
|
93
|
-
else if (kind === "instrument_event") {
|
|
94
|
-
const separator = value.indexOf(".");
|
|
95
|
-
holder[key] = map(value.slice(0, separator)) + value.slice(separator);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function referencedUdlInstrumentIds(
|
|
102
|
-
instrument: UdlInstrument,
|
|
103
|
-
): readonly string[] {
|
|
104
|
-
const found = new Set<string>();
|
|
105
|
-
mapUdlInstrumentReferences(
|
|
106
|
-
{
|
|
107
|
-
udl: 4,
|
|
108
|
-
version: 1,
|
|
109
|
-
product: "references",
|
|
110
|
-
title: "References",
|
|
111
|
-
currency: "SAR",
|
|
112
|
-
parties: {},
|
|
113
|
-
objects: [],
|
|
114
|
-
instruments: [instrument],
|
|
115
|
-
},
|
|
116
|
-
(id) => {
|
|
117
|
-
found.add(id);
|
|
118
|
-
return id;
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
found.delete(instrument.id);
|
|
122
|
-
return [...found];
|
|
123
|
-
}
|