create-substrat 0.8.3 → 0.8.5
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/index.js +5 -5
- package/package.json +1 -1
- package/template/.substrat/playbook.md +44 -2
- package/template/AGENTS.md +43 -2
package/index.js
CHANGED
|
@@ -36,11 +36,11 @@ const TEMPLATE = join(HERE, 'template');
|
|
|
36
36
|
// The runtime packages release together off one version line (the changesets `fixed`
|
|
37
37
|
// group), so one constant is right for all of them. Engines do NOT share a line —
|
|
38
38
|
// each versions on its own, so one pin per engine, deliberately.
|
|
39
|
-
const SUBSTRAT = '^0.
|
|
40
|
-
const ENGINE_WORKORDER = '^0.10.
|
|
41
|
-
const ENGINE_INVOICING = '^0.9.
|
|
42
|
-
const BOUNDARY_LINT = '^0.
|
|
43
|
-
const DEV_ISSUER = '^0.1.
|
|
39
|
+
const SUBSTRAT = '^0.97.0';
|
|
40
|
+
const ENGINE_WORKORDER = '^0.10.5';
|
|
41
|
+
const ENGINE_INVOICING = '^0.9.13';
|
|
42
|
+
const BOUNDARY_LINT = '^0.3.0';
|
|
43
|
+
const DEV_ISSUER = '^0.1.12';
|
|
44
44
|
|
|
45
45
|
const DOCS = 'https://substrat.net';
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -104,6 +104,15 @@ Imported directly; their in-scope functions run in **your** transaction. Read ea
|
|
|
104
104
|
- **`engine-invites`** — how a person joins an org they are not in. Identifiers stored
|
|
105
105
|
hashed and never returned; an invitation confers nothing until accepted. Reach for it
|
|
106
106
|
before hand-rolling any invite flow.
|
|
107
|
+
- **`engine-absence`** — leave and absence: leave types, requests that are decided rather
|
|
108
|
+
than simply written, and the balance they draw down (`requestAbsence`, `decideAbsence`,
|
|
109
|
+
`balanceAsOf`, `availability`). Which days count and which year a balance belongs to is
|
|
110
|
+
calendar policy, and calendar policy is yours.
|
|
111
|
+
- **`engine-metering`** — metered usage: meters, usage recorded against them, and periods
|
|
112
|
+
closed over them (`configureMeter`, `recordUsage`, `closePeriod`, `usageTotal`). By call
|
|
113
|
+
rather than by event on purpose — you record usage inside the same transaction as the
|
|
114
|
+
work that produced it, so the ledger row and the work commit or roll back together.
|
|
115
|
+
Reach for it before adding a `usage` table and a monthly `SUM` of your own.
|
|
107
116
|
|
|
108
117
|
### Tier 2 — engines you feed by event
|
|
109
118
|
|
|
@@ -363,11 +372,44 @@ operations + the `ModuleRegistration`. Keep the split — the linter and tests e
|
|
|
363
372
|
the customer.
|
|
364
373
|
- Migrations: `SqlMigration[]`, tables prefixed `<vertical>_`, TEXT ids, ISO-8601 TEXT
|
|
365
374
|
timestamps, money/decimals as TEXT. **Append-only forever after first ship.**
|
|
366
|
-
- Operations: first line is always `assertAllowed(await ctx.check(PERM))`.
|
|
367
|
-
|
|
375
|
+
- Operations: first line is always `assertAllowed(await ctx.check(PERM))`.
|
|
376
|
+
`ctx.link(child, parent)` when creating related entities.
|
|
377
|
+
- **Handlers do not hand-parse their input.** The module passes
|
|
378
|
+
`operationInputs: operationInputsOf(<vertical>Operations)` beside its `operations`, and
|
|
379
|
+
the host parses every invocation against the declared schema before the guards and the
|
|
380
|
+
handler run — on every path in (HTTP, test, seed, schedule). The reference module already
|
|
381
|
+
does this; keep it. An inline `z.object(…).parse(input)` at the top of a handler is a
|
|
382
|
+
second description of a schema the model already declares, and it only covers the paths
|
|
383
|
+
that happen to reach that handler.
|
|
384
|
+
- **Time comes from `ctx.now()`.** Module code has no other clock; `new Date()` and
|
|
385
|
+
`Date.now()` are banned exactly like `node:*`. It is the same instant for the whole
|
|
386
|
+
invocation, so your rows and the events announcing them agree about when.
|
|
368
387
|
- **The pricing moment is the pattern to copy**: read the engine's reported lines with
|
|
369
388
|
`getReportedLines(ctx, orderId)` → apply the vertical's price list → call the engine's
|
|
370
389
|
`completeWorkOrder`. One transaction, invariants intact.
|
|
390
|
+
- **Swallowing an engine error requires `ctx.atomic`.** An engine call composed inside your
|
|
391
|
+
transaction has no boundary of its own, so a `catch` that handles the failure and carries
|
|
392
|
+
on leaves you holding its partial writes — the rows its invariants were protecting — and
|
|
393
|
+
then commits them. Wrap it instead:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
try {
|
|
397
|
+
await ctx.atomic(() => completeWorkOrder(ctx, { orderId, billable }));
|
|
398
|
+
} catch {
|
|
399
|
+
// the engine's rows, events, links and grants are all gone; your own writes
|
|
400
|
+
// survive, and the operation still commits once
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
A succeeded `ctx.atomic` is still provisional: if the operation later throws, its writes
|
|
405
|
+
go too. Sub-transactions nest but must not interleave — starting two concurrently throws.
|
|
406
|
+
The line is whether the failure still reaches the caller. A catch that **swallows** an
|
|
407
|
+
engine error — one that does not rethrow — is what needs the boundary, and outside
|
|
408
|
+
`ctx.atomic` `boundary-lint` rejects it with no escape hatch. A catch that always
|
|
409
|
+
rethrows (`catch (e) { log(e); throw e }`) needs nothing, and neither does
|
|
410
|
+
`try`/`finally` with no `catch`: the operation still fails and the whole transaction
|
|
411
|
+
rolls back, which is already the outcome the rule protects. Reach for `ctx.atomic` when
|
|
412
|
+
you intend to *continue* past the failure.
|
|
371
413
|
- Portal listing: iterate and `ctx.check(perm, entityRef)` **per entity** — a proof walk,
|
|
372
414
|
not UI filtering.
|
|
373
415
|
- **An entity's history is `readTimeline(ctx, entity, input)` from `@substrat-run/kernel`** —
|
package/template/AGENTS.md
CHANGED
|
@@ -108,7 +108,14 @@ consumers). Rules 1–5 are enforced mechanically by `boundary-lint`.
|
|
|
108
108
|
handler that runs outside the transaction. An integration is never impossible because
|
|
109
109
|
of this rule — it has an answer.
|
|
110
110
|
3. **Never write `_substrat_*` tables.** Reads are fine (timelines are projections);
|
|
111
|
-
writes forge the audit spine.
|
|
111
|
+
writes forge the audit spine. Do the read with `readTimeline` / `readHistory` from
|
|
112
|
+
`@substrat-run/kernel` rather than a `SELECT` of your own: both take an `EntityRef`,
|
|
113
|
+
page like a list read, and decode the envelope for you. `readHistory` also returns the
|
|
114
|
+
payload, the authorization chain (which checks the operation passed, and under which
|
|
115
|
+
grant), the impersonation stamp and the PII class. On the first three, a `null` is a
|
|
116
|
+
*fact* rather than data you failed to fetch — the payload was erased, the row predates
|
|
117
|
+
authorization recording, nobody was impersonating — so render it as that. Neither
|
|
118
|
+
helper checks a permission; you do, before you call it.
|
|
112
119
|
4. **Another module's tables are private.** Never `SELECT` from `workorder_*` etc. — use
|
|
113
120
|
the engine's exported in-scope functions. This is the rule with no runtime equivalent:
|
|
114
121
|
the shortcut *works* and silently welds you to an engine's private schema forever. Need
|
|
@@ -129,12 +136,46 @@ consumers). Rules 1–5 are enforced mechanically by `boundary-lint`.
|
|
|
129
136
|
(`moneyOf`, `mulMoney`, `addDecimal`, `compareDecimal`) — never floats.
|
|
130
137
|
10. **Web-standard APIs always** — `globalThis.crypto`, `TextEncoder`, `URL`. Never
|
|
131
138
|
hand-roll a hash to dodge an import ban.
|
|
132
|
-
11. **Parse, don't trust
|
|
139
|
+
11. **Parse, don't trust** — and the **host** is what parses. A module passes
|
|
140
|
+
`operationInputs: operationInputsOf(ops)` beside its `operations`, and every invocation
|
|
141
|
+
is parsed against the declared schema before the guards and the handler, on every path
|
|
142
|
+
in (HTTP, test, seed, schedule). Handlers do not hand-parse; a declared input nobody
|
|
143
|
+
parses stops being possible rather than merely discouraged. Import `z` from
|
|
133
144
|
`@substrat-run/contracts`, **never from `zod`**. Zod schemas don't compose across
|
|
134
145
|
copies or majors; composing a contracts schema into one built from a separate `zod`
|
|
135
146
|
fails at *runtime* (`expected a Zod schema`) with an error pointing nowhere near the
|
|
136
147
|
cause.
|
|
137
148
|
|
|
149
|
+
## Swallowing an engine error requires `ctx.atomic`
|
|
150
|
+
|
|
151
|
+
An engine call composed inside your transaction has no boundary of its own. A `catch` that
|
|
152
|
+
handles the failure and carries on therefore leaves you holding the engine's partial
|
|
153
|
+
writes — the rows its invariants were protecting — and then commits them. Give the call a
|
|
154
|
+
boundary instead:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
try {
|
|
158
|
+
await ctx.atomic(() => completeWorkOrder(ctx, { orderId, billable }));
|
|
159
|
+
} catch {
|
|
160
|
+
// the engine's rows, events, links and grants are all gone; your own writes
|
|
161
|
+
// survive, and the operation still commits once
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A succeeded `ctx.atomic` is still provisional: if the operation later throws, its writes go
|
|
166
|
+
too. Sub-transactions nest but must not interleave — starting two concurrently throws.
|
|
167
|
+
|
|
168
|
+
The line is whether the failure still reaches the caller. A catch that **swallows** an
|
|
169
|
+
engine error — one that does not rethrow — is what needs the boundary, and outside
|
|
170
|
+
`ctx.atomic` `boundary-lint` rejects it with **no** escape hatch. A catch that always
|
|
171
|
+
rethrows (`catch (e) { log(e); throw e }`) needs nothing, and neither does `try`/`finally`
|
|
172
|
+
with no `catch`: the operation still fails and the whole transaction rolls back, which is
|
|
173
|
+
already the outcome the rule protects. `ctx.atomic` is what you reach for when you intend
|
|
174
|
+
to *continue* past the failure.
|
|
175
|
+
|
|
176
|
+
"Always rethrows" is read literally — the catch's last statement is the `throw`. A `throw`
|
|
177
|
+
buried in an `if` block is not that, because the catch runs on past it.
|
|
178
|
+
|
|
138
179
|
## Declare every link edge
|
|
139
180
|
|
|
140
181
|
`entityRelations` in the manifest must declare every edge you traverse — both your own
|