create-justscale 0.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.
@@ -0,0 +1,324 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ // Skills shipped with the installer live under packages/misc/install/templates/skills/
6
+ // (single source of truth - repo root .claude/skills/ symlinks here). At runtime
7
+ // this resolves to <package>/templates/skills/ in the published install.
8
+ const SKILLS_TEMPLATE_DIR = join(__dirname, '..', 'templates', 'skills');
9
+ export function scaffoldProject(options) {
10
+ const { projectDir, projectName, system, coreVersion = '^0.1.0' } = options;
11
+ const generated = [];
12
+ mkdirSync(projectDir, { recursive: true });
13
+ mkdirSync(join(projectDir, 'src'), { recursive: true });
14
+ // package.json
15
+ writeFile(projectDir, 'package.json', JSON.stringify({
16
+ name: projectName,
17
+ version: '0.1.0',
18
+ type: 'module',
19
+ scripts: {
20
+ build: 'just build',
21
+ test: 'just test',
22
+ dev: 'just dev',
23
+ },
24
+ dependencies: {
25
+ '@justscale/core': coreVersion,
26
+ },
27
+ devDependencies: {
28
+ '@justscale/typescript': coreVersion,
29
+ 'tsx': '^4.0.0',
30
+ },
31
+ }, null, 2) + '\n', generated);
32
+ // tsconfig.json
33
+ writeFile(projectDir, 'tsconfig.json', JSON.stringify({
34
+ compilerOptions: {
35
+ target: 'ES2022',
36
+ module: 'NodeNext',
37
+ moduleResolution: 'NodeNext',
38
+ outDir: 'dist',
39
+ rootDir: 'src',
40
+ declaration: true,
41
+ strict: true,
42
+ esModuleInterop: true,
43
+ skipLibCheck: true,
44
+ },
45
+ include: ['src'],
46
+ }, null, 2) + '\n', generated);
47
+ // justscale.config.ts
48
+ writeFile(projectDir, 'justscale.config.ts', `import { defineProject } from '@justscale/core'
49
+
50
+ export default defineProject({
51
+ modes: {
52
+ serve: () => import('./src/serve.js'),
53
+ cli: () => import('./src/cli.js'),
54
+ },
55
+ build: {
56
+ outDir: './dist',
57
+ },
58
+ })
59
+ `, generated);
60
+ // src/app.ts
61
+ writeFile(projectDir, 'src/app.ts', `import JustScale from '@justscale/core'
62
+
63
+ export const app = JustScale()
64
+ // Add services, features, and adapters here
65
+ // .add(PostgresClient)
66
+ // .add(AuthFeature)
67
+ `, generated);
68
+ // src/serve.ts
69
+ writeFile(projectDir, 'src/serve.ts', `import { app } from './app.js'
70
+
71
+ // HTTP mode - add controllers and start listening
72
+ export default app
73
+ // .add(AuthEndpointsFeature)
74
+ // .add(ApiController)
75
+ .build()
76
+ `, generated);
77
+ // src/cli.ts
78
+ writeFile(projectDir, 'src/cli.ts', `import { app } from './app.js'
79
+
80
+ // CLI mode - add custom CLI controllers
81
+ // Package CLI commands (user add, pg migrate, etc.) are auto-discovered
82
+ export default app
83
+ .build()
84
+ `, generated);
85
+ // .gitignore
86
+ writeFile(projectDir, '.gitignore', `node_modules/
87
+ dist/
88
+ .justscale/
89
+ *.tsbuildinfo
90
+ `, generated);
91
+ // IDE config
92
+ if (system.ides.includes('jetbrains')) {
93
+ generateJetBrainsConfig(projectDir, generated);
94
+ }
95
+ if (system.ides.includes('vscode') || system.ides.includes('cursor')) {
96
+ generateVSCodeConfig(projectDir, generated);
97
+ }
98
+ // AI config
99
+ if (system.aiTools.includes('claude')) {
100
+ generateClaudeConfig(projectDir, projectName, generated);
101
+ }
102
+ // CI/CD
103
+ if (system.gitHosting === 'github') {
104
+ generateGitHubActions(projectDir, system.packageManager, generated);
105
+ }
106
+ else if (system.gitHosting === 'gitlab') {
107
+ generateGitLabCI(projectDir, system.packageManager, generated);
108
+ }
109
+ return generated;
110
+ }
111
+ function writeFile(dir, relativePath, content, generated) {
112
+ const fullPath = join(dir, relativePath);
113
+ const parentDir = join(fullPath, '..');
114
+ if (parentDir !== dir)
115
+ mkdirSync(parentDir, { recursive: true });
116
+ // Refuse to follow symlinks. Without this check, an attacker (or a
117
+ // hostile pre-existing target dir) could plant a symlink at e.g.
118
+ // `package.json -> ~/.ssh/authorized_keys`, and the scaffold would
119
+ // happily clobber the link target with our generated content.
120
+ // lstatSync resolves the link itself (not the target).
121
+ if (existsSync(fullPath)) {
122
+ const st = lstatSync(fullPath);
123
+ if (st.isSymbolicLink()) {
124
+ throw new Error(`Refusing to overwrite symlink at ${relativePath} (would write to the link target outside the project directory).`);
125
+ }
126
+ }
127
+ writeFileSync(fullPath, content);
128
+ generated.push(relativePath);
129
+ }
130
+ function generateJetBrainsConfig(projectDir, generated) {
131
+ const ideaDir = join(projectDir, '.idea');
132
+ mkdirSync(ideaDir, { recursive: true });
133
+ writeFile(projectDir, '.idea/typescript.xml', `<?xml version="1.0" encoding="UTF-8"?>
134
+ <project version="4">
135
+ <component name="TypeScriptCompilerConfiguration">
136
+ <option name="useService" value="true" />
137
+ <option name="typeScriptServiceDirectory" value="$PROJECT_DIR$/node_modules/@justscale/typescript" />
138
+ <option name="versionType" value="SERVICE_DIRECTORY" />
139
+ </component>
140
+ </project>
141
+ `, generated);
142
+ const runConfigDir = join(ideaDir, 'runConfigurations');
143
+ mkdirSync(runConfigDir, { recursive: true });
144
+ for (const [name, cmd] of [['just_dev', 'dev'], ['just_build', 'build'], ['just_test', 'test']]) {
145
+ writeFile(projectDir, `.idea/runConfigurations/${name}.xml`, `<component name="ProjectRunConfigurationManager">
146
+ <configuration default="false" name="just ${cmd}" type="js.build_tools.npm">
147
+ <package-json value="$PROJECT_DIR$/package.json" />
148
+ <command value="run" />
149
+ <scripts>
150
+ <script value="${cmd}" />
151
+ </scripts>
152
+ <node-interpreter value="project" />
153
+ <envs />
154
+ <method v="2" />
155
+ </configuration>
156
+ </component>
157
+ `, generated);
158
+ }
159
+ }
160
+ function generateVSCodeConfig(projectDir, generated) {
161
+ writeFile(projectDir, '.vscode/settings.json', JSON.stringify({
162
+ 'typescript.tsdk': './node_modules/@justscale/typescript/lib',
163
+ 'typescript.enablePromptUseWorkspaceTsdk': true,
164
+ }, null, 2) + '\n', generated);
165
+ writeFile(projectDir, '.vscode/launch.json', JSON.stringify({
166
+ version: '0.2.0',
167
+ configurations: [{
168
+ type: 'node',
169
+ request: 'launch',
170
+ name: 'just dev',
171
+ runtimeExecutable: 'npx',
172
+ runtimeArgs: ['just', 'dev'],
173
+ cwd: '${workspaceFolder}',
174
+ console: 'integratedTerminal',
175
+ }],
176
+ }, null, 2) + '\n', generated);
177
+ }
178
+ function generateClaudeConfig(projectDir, projectName, generated) {
179
+ writeFile(projectDir, '.claude/settings.json', JSON.stringify({
180
+ mcpServers: {
181
+ justscale: {
182
+ command: './node_modules/.bin/just',
183
+ args: ['mcp', 'serve'],
184
+ },
185
+ },
186
+ }, null, 2) + '\n', generated);
187
+ writeFile(projectDir, 'CLAUDE.md', `# ${projectName}
188
+
189
+ ## Commands
190
+
191
+ \`\`\`bash
192
+ just build # Build the project
193
+ just test # Run tests
194
+ just dev # Development mode with hot reload
195
+ just init # Re-run project setup
196
+ just install <package> # Install a JustScale plugin
197
+ \`\`\`
198
+
199
+ ## Architecture
200
+
201
+ This project uses JustScale — a TypeScript framework with:
202
+ - Custom TypeScript compiler (\`ptsc\`) for durable process compilation
203
+ - Dependency injection with compile-time validation
204
+ - CLI commands discoverable from installed packages
205
+ - Mode-based entry points defined in \`justscale.config.ts\`
206
+
207
+ ## Conventions
208
+
209
+ - ESM everywhere (\`"type": "module"\`)
210
+ - 2-space indent, single quotes, semicolons
211
+ - Tests: \`node:test\` runner via \`tsx --test\`
212
+
213
+ ## Claude Code skills
214
+
215
+ The installer scaffolded a set of JustScale-aware Claude Code skills under
216
+ \`.claude/skills/\`. Each one encodes a recurring framework rule as an
217
+ executable workflow or a load-on-demand reference. Invoke them from inside
218
+ Claude Code:
219
+
220
+ - \`/justscale-concepts\` — orientation: the four core principles
221
+ (durable processes, ID-free domain, type-states, distributed-first)
222
+ and the canonical project layout. Auto-loads when starting work on
223
+ this codebase.
224
+ - \`/new-process\` — scaffold a durable process (signals + handler) with
225
+ the framework's distributed-safety rules baked in.
226
+ - \`/audit-domain-purity\` — static check for ID leaks, infra imports
227
+ from domain, hand-edited migrations, missing \`Locked<T>\`.
228
+ - \`/multi-instance-test\` — scaffold a two-instance e2e test that
229
+ proves a behavior holds across nodes (the canonical JustScale test
230
+ shape for distributed primitives).
231
+
232
+ Edit them in place — they're yours now.
233
+ `, generated);
234
+ // JustScale-aware Claude Code skills, scaffolded into the user's
235
+ // project so every dev gets the framework's rules as one-shot tools.
236
+ // Templates live alongside the install package; symlinked from the repo
237
+ // root .claude/skills/ for our own dev workflow.
238
+ copyJustScaleSkills(projectDir, generated);
239
+ }
240
+ /**
241
+ * Defense-in-depth against a compromised template package: returns false
242
+ * for any entry whose name contains path separators, parent traversal, or
243
+ * is a hidden-dot reference. readdirSync should never return such names on
244
+ * a real filesystem, but if a malicious npm package or symlink injects one,
245
+ * a naive `join(dir, name, ...)` would escape the intended destination.
246
+ *
247
+ * Exported for unit-testing.
248
+ */
249
+ export function isSafePathSegment(name) {
250
+ if (name === '' || name === '.' || name === '..')
251
+ return false;
252
+ if (name.includes('/') || name.includes('\\'))
253
+ return false;
254
+ // Reject NUL just to be paranoid — fs APIs would throw anyway, but better
255
+ // explicit refusal than implementation-dependent behavior.
256
+ if (name.includes('\0'))
257
+ return false;
258
+ return true;
259
+ }
260
+ function copyJustScaleSkills(projectDir, generated) {
261
+ if (!existsSync(SKILLS_TEMPLATE_DIR)) {
262
+ // Skills directory is optional - older installs ship without it.
263
+ return;
264
+ }
265
+ for (const name of readdirSync(SKILLS_TEMPLATE_DIR)) {
266
+ if (!isSafePathSegment(name))
267
+ continue;
268
+ const src = join(SKILLS_TEMPLATE_DIR, name, 'SKILL.md');
269
+ if (!existsSync(src))
270
+ continue;
271
+ const body = readFileSync(src, 'utf8');
272
+ writeFile(projectDir, `.claude/skills/${name}/SKILL.md`, body, generated);
273
+ }
274
+ }
275
+ function generateGitHubActions(projectDir, pm, generated) {
276
+ const setupStep = pm === 'pnpm'
277
+ ? ' - uses: pnpm/action-setup@v4\n'
278
+ : '';
279
+ const cache = pm === 'pnpm' ? 'pnpm' : pm === 'yarn' ? 'yarn' : 'npm';
280
+ writeFile(projectDir, '.github/workflows/ci.yml', `name: CI
281
+
282
+ on:
283
+ push:
284
+ branches: [main]
285
+ pull_request:
286
+ branches: [main]
287
+
288
+ jobs:
289
+ build:
290
+ runs-on: ubuntu-latest
291
+ steps:
292
+ - uses: actions/checkout@v4
293
+ ${setupStep} - uses: actions/setup-node@v4
294
+ with:
295
+ node-version: 22
296
+ cache: ${cache}
297
+ - run: ${pm} install
298
+ - run: ${pm} run build
299
+ - run: ${pm} run test
300
+ `, generated);
301
+ }
302
+ function generateGitLabCI(projectDir, pm, generated) {
303
+ const enablePm = pm === 'pnpm' ? ' - corepack enable\n' : '';
304
+ writeFile(projectDir, '.gitlab-ci.yml', `image: node:22
305
+
306
+ stages:
307
+ - build
308
+ - test
309
+
310
+ build:
311
+ stage: build
312
+ script:
313
+ ${enablePm} - ${pm} install
314
+ - ${pm} run build
315
+
316
+ test:
317
+ stage: test
318
+ script:
319
+ ${enablePm} - ${pm} install
320
+ - ${pm} run build
321
+ - ${pm} run test
322
+ `, generated);
323
+ }
324
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "create-justscale",
3
+ "version": "0.1.0",
4
+ "description": "Create a new JustScale project",
5
+ "author": "JustScale",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/justscale/justscale.git",
10
+ "directory": "packages/misc/install"
11
+ },
12
+ "homepage": "https://justscale.sh",
13
+ "bugs": {
14
+ "url": "https://github.com/justscale/justscale/issues"
15
+ },
16
+ "type": "module",
17
+ "bin": {
18
+ "create-justscale": "./bin/create-justscale.js"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "dist",
23
+ "templates"
24
+ ],
25
+ "devDependencies": {
26
+ "@types/node": "^25.6.0",
27
+ "tsx": "^4.20.6",
28
+ "@justscale/typescript": "6.0.0"
29
+ },
30
+ "scripts": {
31
+ "build": "ptsc -b tsconfig.build.json",
32
+ "test": "node --import @justscale/typescript/register --import tsx --test --test-timeout=10000 --test-force-exit 'test/**/*.test.ts'",
33
+ "typecheck": "ptsc --noEmit"
34
+ }
35
+ }
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: audit-domain-purity
3
+ description: Static audit for JustScale domain-purity violations — `id`/`createdAt`/`updatedAt` fields in `defineModel`, infra package imports from `domain/`, `findById('...')` patterns, repository mutators called without `Locked<T>`, and hand-painted span-coloured code in docs. Run before commits or during code review. Reports violations with file:line; does NOT auto-fix.
4
+ allowed-tools: Bash, Read, Grep
5
+ ---
6
+
7
+ # Skill: audit-domain-purity
8
+
9
+ Run a static audit against `$ARGUMENTS` (default: `.`) for JustScale's
10
+ domain-purity rules. This is the rule book made executable.
11
+
12
+ The script-style checks below are intentionally crude — `grep` and `git
13
+ log` over a real AST. Crude is fine: this is a triage tool. False
14
+ positives are easier than missed violations. Run before a commit, in CI,
15
+ or during review.
16
+
17
+ ## What to check
18
+
19
+ ### 1. Infrastructure fields in domain models
20
+
21
+ Domain `defineModel` blocks must NOT define `id`, `createdAt`, or
22
+ `updatedAt`. The adapter owns those concerns and stores them via
23
+ non-enumerable symbols. Putting them in domain fields breaks the
24
+ ID-free principle and leaks adapter details upward.
25
+
26
+ ```bash
27
+ !grep -rEn "(^|[[:space:]])(id|createdAt|updatedAt):[[:space:]]*field\." "$ARGUMENTS" --include="*.ts" 2>/dev/null \
28
+ | grep -v "/dist/" \
29
+ | grep -v "/migration"
30
+ ```
31
+
32
+ ### 2. Infra imports from domain
33
+
34
+ Files under any `domain/` folder must NOT import from
35
+ `@justscale/postgres`, `@justscale/redis`, or sibling `infra/` paths.
36
+ Domain is storage-agnostic; if it knows about Postgres, the abstraction
37
+ is broken.
38
+
39
+ ```bash
40
+ !for d in $(find "$ARGUMENTS" -type d -name "domain*" 2>/dev/null); do
41
+ grep -rEn "from ['\"]@justscale/(postgres|redis)['\"]|from ['\"](\.\./)*infra/" "$d" --include="*.ts" 2>/dev/null
42
+ done
43
+ ```
44
+
45
+ ### 3. String IDs leaking into domain code
46
+
47
+ `findById('...')` reveals that callers are passing loose strings instead
48
+ of typed refs. Domain methods take `Ref<T>` / `Persistent<T>` /
49
+ `Locked<T>`. If a service does `findById`, the boundary above it should
50
+ have already converted the string via `Model.ref\`${id}\``.
51
+
52
+ ```bash
53
+ !grep -rEn "\.findById\(['\"]" "$ARGUMENTS" --include="*.ts" 2>/dev/null \
54
+ | grep -v "/test/" \
55
+ | grep -v "/dist/"
56
+ ```
57
+
58
+ ### 4. Repository mutators without `Locked<T>`
59
+
60
+ `repo.update` / `repo.save` / `repo.delete` requires `Locked<T>` at
61
+ compile time, but it's easy to miss in review. Best-effort grep —
62
+ inspect each match by hand:
63
+
64
+ ```bash
65
+ !grep -rEn "\.(update|save|delete)\([a-zA-Z_][a-zA-Z0-9_]*," "$ARGUMENTS" --include="*.ts" 2>/dev/null \
66
+ | grep -v "/test/" \
67
+ | grep -v "/dist/"
68
+ ```
69
+
70
+ For each match: was the variable declared with `using x = await
71
+ repo.lock(...)`? If not, flag it. The compiler catches this, but a grep
72
+ sweep before commit catches drift faster than a build.
73
+
74
+ ### 5. Hand-painted span-coloured code in docs
75
+
76
+ Doc-page code samples should use `FileTreeServer` / `FileTreeClient` /
77
+ `Code`. Hand-rolled `<span className="text-purple-400">` colouring drifts
78
+ from real syntax over time — and it doesn't get type-hinting. If you
79
+ find any, replace with a real Monaco panel.
80
+
81
+ ```bash
82
+ !grep -rEn '<span className="text-(purple|blue|green|orange|zinc)-[0-9]+">' "$ARGUMENTS" --include="*.tsx" 2>/dev/null \
83
+ | grep -v "/dist/" \
84
+ | head -30
85
+ ```
86
+
87
+ ### 6. (Optional) Hand-edited migrations
88
+
89
+ Migrations are generated artifacts. A migration file with multiple
90
+ commits in its history was probably edited by hand — fix the generator
91
+ instead. This check has false positives (legitimate fixes also produce
92
+ multiple commits), so present it as a flag, not a violation.
93
+
94
+ ```bash
95
+ !for f in $(find "$ARGUMENTS" -path "*/migrations/*.ts" -type f 2>/dev/null); do
96
+ count=$(git log --oneline -- "$f" 2>/dev/null | wc -l | tr -d ' ')
97
+ [ "$count" -gt "1" ] && echo "$f: $count commits (hand-edit suspected)"
98
+ done
99
+ ```
100
+
101
+ ## Report format
102
+
103
+ Group findings by check number. For each finding:
104
+
105
+ ```
106
+ [1 infra-fields] file:line — id/createdAt/updatedAt in defineModel
107
+ > offending excerpt
108
+ ```
109
+
110
+ End with a count summary:
111
+
112
+ ```
113
+ N findings: A infra-fields, B infra-imports, C string-ids, D missing-Locked, ...
114
+ ```
115
+
116
+ If a check has zero hits, omit it entirely from the report — silence is
117
+ the success state.
118
+
119
+ ## Don't auto-fix
120
+
121
+ This is a triage tool. Print findings; let the user decide what to fix
122
+ and how. Auto-fixing rule violations across a codebase causes more churn
123
+ than it saves and obscures the real problem (often a misplaced
124
+ abstraction, not the surface symptom).
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: justscale-concepts
3
+ description: JustScale orientation skill — what the framework is for, the four core principles (durable processes, ID-free domain, type-states, distributed-first), the canonical project layout (domain/infra/app), and where things go. Load when starting work on a JustScale codebase, when the user asks "what is JustScale", "where does X go", "how is this structured", or before generating non-trivial framework code.
4
+ ---
5
+
6
+ # Skill: justscale-concepts
7
+
8
+ Orientation. Loads what the framework is for, the principles that
9
+ constrain its API, and the canonical project layout — so subsequent code
10
+ generation matches the framework's grain instead of fighting it.
11
+
12
+ The full philosophy lives at `CORE_PHILOSOPHY.md` in the repo root. The
13
+ canonical example project is `examples/simple-app/`. When in doubt, read
14
+ those — this skill is a fast load for what's already there.
15
+
16
+ ## What JustScale is
17
+
18
+ A TypeScript backend framework where **domain code describes what
19
+ happens, not how**. Infrastructure (databases, persistence across
20
+ restarts, coordination across nodes) is removed from the surface area
21
+ you write. The code reads like a description of the workflow.
22
+
23
+ ## The four principles
24
+
25
+ ### 1. Domain code describes WHAT, never HOW
26
+
27
+ A subscription that charges monthly until cancelled looks like a `while`
28
+ loop with a monthly timer:
29
+
30
+ ```typescript
31
+ const subscription = createProcess({
32
+ path: '/subscription/:user',
33
+ types: { user: User },
34
+ inject: { billing: BillingService, notifications: NotificationService },
35
+
36
+ async handler({ billing, notifications }, { user }) {
37
+ while (true) {
38
+ const r = race();
39
+ switch (true) {
40
+ case delay.days(r, 30):
41
+ await billing.charge(user);
42
+ await notifications.send(user, 'Payment processed');
43
+ continue;
44
+ case signal(r, billing.cancellation):
45
+ return { status: 'cancelled' as const };
46
+ }
47
+ }
48
+ },
49
+ });
50
+ ```
51
+
52
+ The compiler turns this into an opcode-based state machine that survives
53
+ restarts and routes signals across nodes. You don't see that.
54
+
55
+ ### 2. IDs do not exist in domain code
56
+
57
+ Domain methods take `Ref<T>`, `Persistent<T>`, or `Locked<T>`. A
58
+ persistent entity IS its own reference.
59
+
60
+ ```typescript
61
+ // Domain: pass entities directly
62
+ await transfer(fromAccount, toAccount, amount);
63
+
64
+ // Boundary: strings become typed refs
65
+ const user = User.ref(userId);
66
+ ```
67
+
68
+ `defineModel` blocks NEVER include `id`, `createdAt`, or `updatedAt` —
69
+ those are adapter concerns, stored via non-enumerable symbols.
70
+
71
+ Inter-entity links are typed refs, not foreign keys:
72
+
73
+ ```typescript
74
+ export class Order extends defineModel({
75
+ fields: {
76
+ customer: field.ref(User),
77
+ total: field.decimal(10, 2),
78
+ },
79
+ permissions: ({ customer }) => ({
80
+ view: permit(User).when(customer),
81
+ requestReturn: permit(User).when(customer),
82
+ }),
83
+ }) {}
84
+ ```
85
+
86
+ Permissions live with the model. `permit(User).when(customer)` reads as
87
+ "a User can view this Order when they are its `customer`".
88
+
89
+ ### 3. Type-states as compile-time contracts
90
+
91
+ A method that mutates says so in its signature:
92
+
93
+ ```typescript
94
+ async addLine(
95
+ cart: Locked<Cart>,
96
+ product: Ref<Product>,
97
+ quantity: number,
98
+ ): Promise<Persistent<CartLine>> { /* ... */ }
99
+
100
+ async removeLine(cart: Locked<Cart>, line: Locked<CartLine>): Promise<void> {
101
+ using stock = await inventory.lockFor(line.product);
102
+ await inventory.release(stock, line.quantity);
103
+ }
104
+ ```
105
+
106
+ `Locked<T>` is the only thing `repo.update`/`save`/`delete` accepts. The
107
+ only way to obtain one is `using x = await repo.lock(ref)`, which is
108
+ atomic with the read under `SELECT ... FOR UPDATE`. Stale-write bugs
109
+ are structurally impossible.
110
+
111
+ ### 4. Distributed-first by default
112
+
113
+ Channels, locks, signals, and durable processes are framework primitives —
114
+ not transport helpers. The same domain code that runs against an
115
+ in-memory lock locally runs correctly against Postgres advisory locks
116
+ across 20 nodes, unchanged. Four mechanical rules close the loop:
117
+
118
+ - Every mutating repository method requires `Locked<T>`.
119
+ - `repo.lock()` is atomic with the read.
120
+ - `Locked<T>` cannot cross process boundaries (the serializer refuses).
121
+ - Signals carry routable identity. Every signal path param goes through
122
+ `.types({...})`.
123
+
124
+ ## The canonical project layout
125
+
126
+ Look at `examples/simple-app/`. Each domain owns its own folder; infra
127
+ is separate; app composes them.
128
+
129
+ ```
130
+ src/
131
+ ├── app.ts ← composes everything via JustScale().add(...)
132
+ ├── controllers/ ← HTTP/protocol surface (status codes, auth, serialization)
133
+ ├── domains/
134
+ │ ├── order/
135
+ │ │ ├── order.model.ts ← defineModel + permissions
136
+ │ │ ├── order.service.ts ← defineService — mutators take Locked<T>
137
+ │ │ ├── order.feature.ts ← createFeature — bundles the domain
138
+ │ │ └── order.cli.ts ← Cli('order ...') routes
139
+ │ └── cart/
140
+ │ ├── cart.model.ts
141
+ │ ├── cart.service.ts
142
+ │ ├── cart.signals.ts ← defineSignals
143
+ │ ├── cart-lifecycle.process.ts ← createProcess
144
+ │ ├── cart.feature.ts
145
+ │ └── cart.cli.ts
146
+ └── infra/
147
+ └── pg/ ← createPgModel + createPgRepository per model
148
+ ```
149
+
150
+ Conventions:
151
+
152
+ - **`<domain>.model.ts`** — `defineModel` with fields, refs, and a
153
+ `permissions` block.
154
+ - **`<domain>.service.ts`** — `defineService`. Mutators take `Locked<T>`.
155
+ - **`<domain>.signals.ts`** — `defineSignals`. Path params get `.types`.
156
+ - **`<domain>-<verb>.process.ts`** — `createProcess`.
157
+ - **`<domain>.feature.ts`** — `createFeature` bundling model, service,
158
+ signals, processes, CLI. `app.ts` imports the feature.
159
+ - **`<domain>.cli.ts`** — `Cli('<verb>')` routes live in the domain
160
+ folder. CLI is domain logic; HTTP is presentation.
161
+ - **`infra/pg/<model>.pg.ts`** — adapter bindings. Domain code does NOT
162
+ import from `infra/`. Only `app.ts` does.
163
+
164
+ ## Where things go
165
+
166
+ | Question | Answer |
167
+ |-|-|
168
+ | New domain entity? | `src/domains/<domain>/<entity>.model.ts` |
169
+ | Domain logic? | `src/domains/<domain>/<domain>.service.ts` |
170
+ | CLI command? | `src/domains/<domain>/<domain>.cli.ts` (NOT `controllers/`) |
171
+ | HTTP controller? | `src/controllers/<group>/...` |
172
+ | Durable workflow? | `src/domains/<domain>/<verb>.process.ts` |
173
+ | Storage details? | `src/infra/pg/...` |
174
+ | Migration? | Generated by `just migrate make` — never hand-author |
175
+
176
+ ## Companion skills
177
+
178
+ When generating framework code, prefer the dedicated skills:
179
+
180
+ - `/new-process` — durable process with the rules baked in.
181
+ - `/audit-domain-purity` — static check before commit.
182
+ - `/multi-instance-test` — distributed e2e test scaffold (real
183
+ `child_process.spawn` workers, not two builders in one process).
184
+
185
+ ## When to load
186
+
187
+ - Starting a session on a JustScale codebase.
188
+ - The user asks "what is JustScale", "where does X go", "how should I
189
+ structure this", or "why does the framework do Y".
190
+ - Before generating any non-trivial framework code (model, service,
191
+ process, controller). The principles prevent drift.