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.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bin/create-justscale.js +3 -0
- package/dist/detect.d.ts +11 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +68 -0
- package/dist/detect.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -0
- package/dist/scaffold.d.ts +19 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +324 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +35 -0
- package/templates/skills/audit-domain-purity/SKILL.md +124 -0
- package/templates/skills/justscale-concepts/SKILL.md +191 -0
- package/templates/skills/multi-instance-test/SKILL.md +185 -0
- package/templates/skills/new-process/SKILL.md +119 -0
package/dist/scaffold.js
ADDED
|
@@ -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.
|