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 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.83.0';
39
- const ENGINE_WORKORDER = '^0.7.3';
40
- const ENGINE_INVOICING = '^0.8.3';
41
- const BOUNDARY_LINT = '^0.0.8';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-substrat",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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 `DESIGN.md` in the user's own vocabulary;
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 `DESIGN.md`** in the project root, in the
151
- user's own vocabulary — no Substrat internals, no decision refs, no cross-references to
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 `DESIGN.md`. Inventing a way to declare a state *transition* means
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
- `DESIGN.md`:
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
@@ -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–4 are enforced mechanically by `boundary-lint`.
91
-
92
- 1. **Data access is `ctx.sql` only.** Never import `better-sqlite3`, an adapter, or
93
- `node:*` in module code.
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. **Every operation checks a permission first.** `assertAllowed(await ctx.check(PERM))`
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
- 6. **Every mutation emits a fat event** — a consumer must never need a cross-module read.
108
- 7. **Never fork an engine.** Extend by composition. If you must fork, the engine drew its
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
- 8. **IDs are `ulid()`. Money is strings** via `@substrat-run/contracts` helpers
121
+ 9. **IDs are `ulid()`. Money is strings** via `@substrat-run/contracts` helpers
111
122
  (`moneyOf`, `mulMoney`, `addDecimal`, `compareDecimal`) — never floats.
112
- 9. **Web-standard APIs always** — `globalThis.crypto`, `TextEncoder`, `URL`. Never
113
- hand-roll a hash to dodge an import ban.
114
- 10. **Parse, don't trust.** Zod at every boundary — but import `z` from
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–4)
142
+ npx @substrat-run/boundary-lint # the layer rules (1–5)
132
143
  npm run typecheck
133
144
  ```
134
145
 
@@ -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, new Date().toISOString()],
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, new Date().toISOString()],
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.