create-substrat 0.7.0 → 0.7.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/index.js +4 -4
- package/package.json +1 -1
- package/template/.substrat/playbook.md +8 -7
- package/template/AGENTS.md +23 -12
- package/template/src/module.ts +2 -2
package/index.js
CHANGED
|
@@ -35,10 +35,10 @@ const TEMPLATE = join(HERE, 'template');
|
|
|
35
35
|
// The runtime packages release together off one version line (the changesets `fixed`
|
|
36
36
|
// group), so one constant is right for all of them. Engines do NOT share a line —
|
|
37
37
|
// each versions on its own, so one pin per engine, deliberately.
|
|
38
|
-
const SUBSTRAT = '^0.
|
|
39
|
-
const ENGINE_WORKORDER = '^0.
|
|
40
|
-
const ENGINE_INVOICING = '^0.
|
|
41
|
-
const BOUNDARY_LINT = '^0.0
|
|
38
|
+
const SUBSTRAT = '^0.84.0';
|
|
39
|
+
const ENGINE_WORKORDER = '^0.8.0';
|
|
40
|
+
const ENGINE_INVOICING = '^0.9.0';
|
|
41
|
+
const BOUNDARY_LINT = '^0.1.0';
|
|
42
42
|
|
|
43
43
|
const DOCS = 'https://substrat.net';
|
|
44
44
|
|
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@ reshape the reference into it. Read the whole thing before starting — both the
|
|
|
7
7
|
(Step 4) and the checkpoints (Step 7) are hard stops.
|
|
8
8
|
|
|
9
9
|
**The target is a reviewed design, not running code.** Steps 1–2 learn the domain and map it
|
|
10
|
-
onto what already exists; Step 3 writes a checked-in `
|
|
11
|
-
Step 4 is a **hard stop** where the user reads and approves it. Only then does Step 5 reshape
|
|
10
|
+
onto what already exists; Step 3 writes a checked-in `spec/concept.md` in the user's own
|
|
11
|
+
vocabulary; Step 4 is a **hard stop** where the user reads and approves it. Only then does Step 5 reshape
|
|
12
12
|
the reference into their domain. The design gate (Step 4) is *upstream* of the two code
|
|
13
13
|
checkpoints (Step 7) — a user with zero Substrat knowledge gets to say "yes, that's the app I
|
|
14
14
|
want" before implementation, not after.
|
|
@@ -147,8 +147,9 @@ stop. Do not scaffold.
|
|
|
147
147
|
## Step 3 — Write the design document
|
|
148
148
|
|
|
149
149
|
**This is the deliverable.** Everything before now was learning; this is where it lands
|
|
150
|
-
somewhere the user can hold. Write a **checked-in `
|
|
151
|
-
|
|
150
|
+
somewhere the user can hold. Write a **checked-in `spec/concept.md`** — beside
|
|
151
|
+
`spec/model.ts`, which is this same design one rung more concrete — in the user's own
|
|
152
|
+
vocabulary — no Substrat internals, no decision refs, no cross-references to
|
|
152
153
|
platform docs. Someone who has never heard of Substrat must be able to read it and recognise
|
|
153
154
|
their own business.
|
|
154
155
|
|
|
@@ -291,8 +292,8 @@ and event subjects all need one id, so naming such an entity in `parents`,
|
|
|
291
292
|
compile error. It is still a full model member with migrations and a row type. A
|
|
292
293
|
single-column key that is not called `id` stays fully pointable.
|
|
293
294
|
|
|
294
|
-
Behaviour stays prose in `
|
|
295
|
-
the boundary slipped.
|
|
295
|
+
Behaviour stays prose in `spec/concept.md`. Inventing a way to declare a state *transition*
|
|
296
|
+
means the boundary slipped.
|
|
296
297
|
|
|
297
298
|
Full reference: https://substrat.net/concepts/model
|
|
298
299
|
|
|
@@ -307,7 +308,7 @@ to make the build pass.
|
|
|
307
308
|
The design is approved. The scaffold already contains a working vertical in `src/` + `test/` —
|
|
308
309
|
the bike-repair shop. **Read it first** (it's your Callout: the real, green implementation of
|
|
309
310
|
every pattern this step describes), then reshape it into the user's domain from the approved
|
|
310
|
-
`
|
|
311
|
+
`spec/concept.md`:
|
|
311
312
|
|
|
312
313
|
- **Rename the vocabulary** — `shop_customers`/`shop_bikes` → the user's nouns, the `shop/*`
|
|
313
314
|
operation names, the roles, the price-list shape. If the user's core noun maps onto a work
|
package/template/AGENTS.md
CHANGED
|
@@ -87,10 +87,15 @@ push` reads the JSON, not the TypeScript.
|
|
|
87
87
|
## The rules (non-negotiable)
|
|
88
88
|
|
|
89
89
|
**Module code** = everything reachable from a `ModuleRegistration` (operations,
|
|
90
|
-
consumers). Rules 1–
|
|
91
|
-
|
|
92
|
-
1. **Data access is `ctx.sql` only.** Never import `better-sqlite3`, an adapter,
|
|
93
|
-
`node
|
|
90
|
+
consumers). Rules 1–5 are enforced mechanically by `boundary-lint`.
|
|
91
|
+
|
|
92
|
+
1. **Data access is `ctx.sql` only.** Never import `better-sqlite3`, an adapter,
|
|
93
|
+
`node:*`, or `cloudflare:workers` in module code. That last one is not a style rule:
|
|
94
|
+
it exports an ambient `env`, so a single import hands module code every binding and
|
|
95
|
+
secret your worker declares — including its own `SCOPE` Durable Object namespace,
|
|
96
|
+
which reaches *another scope's* data. `ctx.sql` is closed over one scope and cannot.
|
|
97
|
+
Capabilities arrive on `ctx`; `DurableObject` is imported in harness code
|
|
98
|
+
(`worker.ts`, `*-do.ts`), never here.
|
|
94
99
|
2. **No `fetch` / network in module code.** It would hold the scope's transaction open on
|
|
95
100
|
a third party. The sanctioned path is a **connector**: emit a fat event, register a
|
|
96
101
|
handler that runs outside the transaction. An integration is never impossible because
|
|
@@ -102,16 +107,22 @@ consumers). Rules 1–4 are enforced mechanically by `boundary-lint`.
|
|
|
102
107
|
the shortcut *works* and silently welds you to an engine's private schema forever. Need
|
|
103
108
|
extra data on an engine entity? Add **your own side table keyed by the engine's id** —
|
|
104
109
|
never a column upstream.
|
|
105
|
-
5. **
|
|
110
|
+
5. **Time comes from `ctx.now()`.** Module code has no other clock — `new Date()` and
|
|
111
|
+
`Date.now()` are banned exactly like `node:*`. It is the same instant for the whole
|
|
112
|
+
operation, so your rows and the events announcing them agree about when. Store it as
|
|
113
|
+
ISO text, never an epoch integer. Because the host injects the clock, a scenario can
|
|
114
|
+
test elapsed time (`manualClock` from `@substrat-run/kernel`) instead of sleeping or
|
|
115
|
+
shrinking the window to zero — the workaround that proves nothing.
|
|
116
|
+
6. **Every operation checks a permission first.** `assertAllowed(await ctx.check(PERM))`
|
|
106
117
|
is the first line.
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
7. **Every mutation emits a fat event** — a consumer must never need a cross-module read.
|
|
119
|
+
8. **Never fork an engine.** Extend by composition. If you must fork, the engine drew its
|
|
109
120
|
line wrong — that's design feedback, not a coding problem.
|
|
110
|
-
|
|
121
|
+
9. **IDs are `ulid()`. Money is strings** via `@substrat-run/contracts` helpers
|
|
111
122
|
(`moneyOf`, `mulMoney`, `addDecimal`, `compareDecimal`) — never floats.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
10. **Web-standard APIs always** — `globalThis.crypto`, `TextEncoder`, `URL`. Never
|
|
124
|
+
hand-roll a hash to dodge an import ban.
|
|
125
|
+
11. **Parse, don't trust.** Zod at every boundary — but import `z` from
|
|
115
126
|
`@substrat-run/contracts`, **never from `zod`**. Zod schemas don't compose across
|
|
116
127
|
copies or majors; composing a contracts schema into one built from a separate `zod`
|
|
117
128
|
fails at *runtime* (`expected a Zod schema`) with an error pointing nowhere near the
|
|
@@ -128,7 +139,7 @@ This is also what lets a portal permission-walk reach the owner.
|
|
|
128
139
|
|
|
129
140
|
```sh
|
|
130
141
|
npm test # the scenario, including the denials
|
|
131
|
-
npx @substrat-run/boundary-lint # the layer rules (1–
|
|
142
|
+
npx @substrat-run/boundary-lint # the layer rules (1–5)
|
|
132
143
|
npm run typecheck
|
|
133
144
|
```
|
|
134
145
|
|
package/template/src/module.ts
CHANGED
|
@@ -71,7 +71,7 @@ const createCustomerOp: OperationHandler<
|
|
|
71
71
|
const id = ulid();
|
|
72
72
|
ctx.sql.exec(
|
|
73
73
|
`INSERT INTO shop_customers (id, number, name, phone, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
74
|
-
[id, input.number, input.name, input.phone ?? null,
|
|
74
|
+
[id, input.number, input.name, input.phone ?? null, ctx.now()],
|
|
75
75
|
);
|
|
76
76
|
return ctx.sql.query<CustomerRow>('SELECT * FROM shop_customers WHERE id = ?', [id])[0]!;
|
|
77
77
|
};
|
|
@@ -101,7 +101,7 @@ const registerBikeOp: OperationHandler<
|
|
|
101
101
|
const id = ulid();
|
|
102
102
|
ctx.sql.exec(
|
|
103
103
|
`INSERT INTO shop_bikes (id, customer_id, label, frame_no, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
104
|
-
[id, customer.id, input.label, input.frameNo ?? null,
|
|
104
|
+
[id, customer.id, input.label, input.frameNo ?? null, ctx.now()],
|
|
105
105
|
);
|
|
106
106
|
// Record the bike → customer edge the manifest declared, so the portal walk
|
|
107
107
|
// (workorder → bike → customer) can resolve an entity-narrowed grant.
|