@ultime/codecraft 0.0.0-stage → 1.0.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/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +210 -2
- package/SECURITY.md +22 -0
- package/VERSION +1 -0
- package/adapters/README.md +10 -0
- package/adapters/claude/SKILL.md +40 -0
- package/adapters/generic/AGENTS.md +15 -0
- package/adapters/trae/SKILL.md +40 -0
- package/bin/codecraft.mjs +4 -0
- package/codecraft.config.example.json +15 -0
- package/docs/00-constitution.md +310 -0
- package/docs/01-family-cards.md +169 -0
- package/docs/02-ai-contract.md +68 -0
- package/docs/03-style-map.md +90 -0
- package/docs/04-gates.md +85 -0
- package/docs/05-patterns.md +243 -0
- package/docs/06-architecture.md +263 -0
- package/evals/golden-tasks.md +114 -0
- package/package.json +49 -4
- package/prompts/output.md +27 -0
- package/scenarios/audit-existing.md +41 -0
- package/scenarios/new-project.md +50 -0
- package/scenarios/refactor-review.md +31 -0
- package/scenarios/report-only.md +33 -0
- package/src/check.mjs +451 -0
- package/src/cli.mjs +384 -0
- package/src/gates.mjs +67 -0
- package/src/init.mjs +98 -0
- package/src/plan.mjs +35 -0
- package/templates/README.md +11 -0
- package/templates/app/README.md +28 -0
- package/templates/app/package.json +8 -0
- package/templates/library/CHANGELOG.md +3 -0
- package/templates/library/README.md +26 -0
- package/templates/library/compat.md +5 -0
- package/templates/library/package.json +14 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { check } from './check.mjs'
|
|
4
|
+
import { loadConfig, runGates } from './gates.mjs'
|
|
5
|
+
import { buildPlan, summarizePlan } from './plan.mjs'
|
|
6
|
+
import { applyInitPlan, buildInitPlan } from './init.mjs'
|
|
7
|
+
|
|
8
|
+
const PACKAGE = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
9
|
+
const VERSION = PACKAGE.version
|
|
10
|
+
|
|
11
|
+
const HELP = `codecraft <command> [options]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
check Scan for boundary violations (default)
|
|
15
|
+
audit check + configured gates; read-only report
|
|
16
|
+
plan Read-only upgrade plan from findings
|
|
17
|
+
init Scaffold an app or package (dry-run unless --apply)
|
|
18
|
+
version Print version
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--root <path> Project root (default: cwd)
|
|
22
|
+
--pack app|library|design-system|auto Rule pack (default: auto)
|
|
23
|
+
--config <path> Config file (default: codecraft.config.json)
|
|
24
|
+
--no-gates Skip gates in audit / plan
|
|
25
|
+
--with-gates Run configured gates in plan
|
|
26
|
+
--fail-fast Stop gates after the first failure
|
|
27
|
+
--name <name> Scaffold name (required with --apply)
|
|
28
|
+
--apply Write the scaffold; existing paths are skipped
|
|
29
|
+
--format md|json Output format (default: md)
|
|
30
|
+
--severity warn|error Reported severity (default: warn)
|
|
31
|
+
--ci Treat findings as errors
|
|
32
|
+
--help, -h
|
|
33
|
+
--version, -v
|
|
34
|
+
|
|
35
|
+
Packs:
|
|
36
|
+
app A17 / A21 — pages / components / behavior must not import external or generated;
|
|
37
|
+
high-confidence secrets are always scanned
|
|
38
|
+
library A2 / A11 / A21 — package boundaries, generated-file markers, secret scan
|
|
39
|
+
design-system A2 / A11 / A21 — library rules + public docs must not reference internal docs
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
export function main(argv) {
|
|
43
|
+
const args = parseArgs(argv)
|
|
44
|
+
if (args.help) {
|
|
45
|
+
console.log(HELP)
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
if (args.version || args.command === 'version') {
|
|
49
|
+
console.log(VERSION)
|
|
50
|
+
return 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const command = args.command || 'check'
|
|
54
|
+
if (!['check', 'audit', 'plan', 'init'].includes(command)) {
|
|
55
|
+
console.error(`codecraft: unknown command "${command}"`)
|
|
56
|
+
return 2
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const root = path.resolve(process.cwd(), args.root)
|
|
60
|
+
if (!fs.existsSync(root)) {
|
|
61
|
+
console.error(`codecraft: root not found: ${root}`)
|
|
62
|
+
return 2
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let config
|
|
66
|
+
try {
|
|
67
|
+
config = loadConfig(root, args.config)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`codecraft: ${error.message}`)
|
|
70
|
+
return 2
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pack = resolvePack(args.pack, config.config.pack, root)
|
|
74
|
+
if (!pack) {
|
|
75
|
+
console.error('codecraft: invalid config pack; expected app, library or auto')
|
|
76
|
+
return 2
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (command === 'init') {
|
|
80
|
+
if (args.apply && !args.name) {
|
|
81
|
+
console.error('codecraft: init --apply requires --name <name>')
|
|
82
|
+
return 2
|
|
83
|
+
}
|
|
84
|
+
const plan = buildInitPlan(root, pack === 'design-system' ? 'library' : pack, args.name)
|
|
85
|
+
const applied = args.apply ? applyInitPlan(root, plan) : null
|
|
86
|
+
const meta = {
|
|
87
|
+
version: VERSION,
|
|
88
|
+
mode: 'init',
|
|
89
|
+
pack,
|
|
90
|
+
root,
|
|
91
|
+
configFile: config.file,
|
|
92
|
+
name: args.name,
|
|
93
|
+
dryRun: !args.apply,
|
|
94
|
+
entries: plan.entries.length,
|
|
95
|
+
created: applied?.created.length ?? 0,
|
|
96
|
+
skipped: applied?.skipped.length ?? 0,
|
|
97
|
+
}
|
|
98
|
+
if (args.format === 'json') {
|
|
99
|
+
console.log(JSON.stringify({ ...meta, entries: plan.entries, config: plan.config, applied }, null, 2))
|
|
100
|
+
} else {
|
|
101
|
+
console.log(formatInit(meta, plan, applied))
|
|
102
|
+
}
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = check(root, { pack })
|
|
107
|
+
const severity = args.ci ? 'error' : args.severity
|
|
108
|
+
const findings = result.findings.map((finding) => ({ ...finding, severity }))
|
|
109
|
+
const gates = shouldRunGates(command, args)
|
|
110
|
+
? runGates(root, config.config.gates ?? [], { failFast: args.failFast })
|
|
111
|
+
: []
|
|
112
|
+
const plan = command === 'plan' ? buildPlan(findings) : []
|
|
113
|
+
const meta = {
|
|
114
|
+
version: VERSION,
|
|
115
|
+
mode: command,
|
|
116
|
+
pack,
|
|
117
|
+
root,
|
|
118
|
+
config: config.file,
|
|
119
|
+
severity,
|
|
120
|
+
rules: pack === 'app' ? ['A17', 'A21'] : ['A2', 'A11', 'A21'],
|
|
121
|
+
files: result.files,
|
|
122
|
+
findings: findings.length,
|
|
123
|
+
plan: plan.length,
|
|
124
|
+
gates: gates.length,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (args.format === 'json') {
|
|
128
|
+
console.log(
|
|
129
|
+
JSON.stringify(
|
|
130
|
+
{
|
|
131
|
+
...meta,
|
|
132
|
+
findings,
|
|
133
|
+
plan,
|
|
134
|
+
gates,
|
|
135
|
+
summary: {
|
|
136
|
+
files: result.files,
|
|
137
|
+
findings: findings.length,
|
|
138
|
+
plan: plan.length,
|
|
139
|
+
gates: gates.length,
|
|
140
|
+
gateFailures: gates.filter((gate) => gate.exitCode !== 0).length,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
null,
|
|
144
|
+
2,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
} else if (command === 'plan') {
|
|
148
|
+
console.log(formatPlan(meta, findings, plan, gates))
|
|
149
|
+
} else {
|
|
150
|
+
console.log(formatReport(meta, findings, gates))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const failed = findings.some((finding) => finding.severity === 'error') || gates.some((gate) => gate.exitCode !== 0)
|
|
154
|
+
return failed ? 1 : 0
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function shouldRunGates(command, args) {
|
|
158
|
+
if (args.noGates) return false
|
|
159
|
+
if (command === 'audit') return true
|
|
160
|
+
if (command === 'plan') return args.withGates
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolvePack(cliPack, configPack, root) {
|
|
165
|
+
if (cliPack !== 'auto') return cliPack
|
|
166
|
+
if (configPack && configPack !== 'auto') {
|
|
167
|
+
return ['app', 'library', 'design-system'].includes(configPack) ? configPack : null
|
|
168
|
+
}
|
|
169
|
+
return detectPack(root)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseArgs(argv) {
|
|
173
|
+
const args = {
|
|
174
|
+
command: '',
|
|
175
|
+
root: '.',
|
|
176
|
+
pack: 'auto',
|
|
177
|
+
config: '',
|
|
178
|
+
noGates: false,
|
|
179
|
+
withGates: false,
|
|
180
|
+
failFast: false,
|
|
181
|
+
name: '',
|
|
182
|
+
apply: false,
|
|
183
|
+
format: 'md',
|
|
184
|
+
severity: 'warn',
|
|
185
|
+
ci: false,
|
|
186
|
+
help: false,
|
|
187
|
+
version: false,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
191
|
+
const arg = argv[i]
|
|
192
|
+
if (arg === '--help' || arg === '-h') args.help = true
|
|
193
|
+
else if (arg === '--version' || arg === '-v') args.version = true
|
|
194
|
+
else if (arg === '--ci') args.ci = true
|
|
195
|
+
else if (arg === '--no-gates') args.noGates = true
|
|
196
|
+
else if (arg === '--with-gates') args.withGates = true
|
|
197
|
+
else if (arg === '--fail-fast') args.failFast = true
|
|
198
|
+
else if (arg === '--apply') args.apply = true
|
|
199
|
+
else if (arg === '--name') args.name = argv[++i] ?? ''
|
|
200
|
+
else if (arg === '--root') args.root = argv[++i] ?? '.'
|
|
201
|
+
else if (arg === '--pack') args.pack = argv[++i] ?? 'auto'
|
|
202
|
+
else if (arg === '--config') args.config = argv[++i] ?? ''
|
|
203
|
+
else if (arg === '--format') args.format = argv[++i] ?? 'md'
|
|
204
|
+
else if (arg === '--severity') args.severity = argv[++i] ?? 'warn'
|
|
205
|
+
else if (!arg.startsWith('-') && !args.command) args.command = arg
|
|
206
|
+
else fail(`unknown option: ${arg}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!['app', 'library', 'design-system', 'auto'].includes(args.pack)) fail(`invalid --pack: ${args.pack}`)
|
|
210
|
+
if (args.format !== 'md' && args.format !== 'json') fail(`invalid --format: ${args.format}`)
|
|
211
|
+
if (args.severity !== 'warn' && args.severity !== 'error') {
|
|
212
|
+
fail(`invalid --severity: ${args.severity}`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return args
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function detectPack(root) {
|
|
219
|
+
if (isDir(path.join(root, 'apps')) || isDir(path.join(root, 'src', 'pages'))) return 'app'
|
|
220
|
+
if (isDir(path.join(root, 'packages'))) return 'library'
|
|
221
|
+
if (fs.existsSync(path.join(root, 'package.json'))) return 'library'
|
|
222
|
+
return 'app'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function fail(message) {
|
|
226
|
+
console.error(`codecraft: ${message}\n\n${HELP}`)
|
|
227
|
+
process.exit(2)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatReport(meta, findings, gates) {
|
|
231
|
+
const lines = header(meta)
|
|
232
|
+
|
|
233
|
+
if (!findings.length) {
|
|
234
|
+
lines.push('✓ 未发现边界问题。')
|
|
235
|
+
} else {
|
|
236
|
+
lines.push('## Findings', '', '| Rule | Severity | File | Line | Message |', '|---|---|---|---|---|')
|
|
237
|
+
for (const finding of findings) {
|
|
238
|
+
lines.push(row(finding))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
lines.push(...gateLines(gates))
|
|
243
|
+
return lines.join('\n')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatPlan(meta, findings, plan, gates) {
|
|
247
|
+
const lines = header(meta)
|
|
248
|
+
lines.pop()
|
|
249
|
+
lines.push(`- Plan Items: ${plan.length}`, '')
|
|
250
|
+
|
|
251
|
+
if (!plan.length) {
|
|
252
|
+
lines.push('✓ 没有需要处理的问题;无需计划。')
|
|
253
|
+
} else {
|
|
254
|
+
lines.push('## Summary', '', '| Rule | Count |', '|---|---:|')
|
|
255
|
+
for (const item of summarizePlan(findings)) lines.push(`| ${item.rule} | ${item.count} |`)
|
|
256
|
+
|
|
257
|
+
lines.push(
|
|
258
|
+
'',
|
|
259
|
+
'## Plan',
|
|
260
|
+
'',
|
|
261
|
+
'| # | Rule | Severity | File | Line | Action | Verification |',
|
|
262
|
+
'|---|---|---|---|---|---|---|',
|
|
263
|
+
)
|
|
264
|
+
for (const item of plan) {
|
|
265
|
+
lines.push(
|
|
266
|
+
`| ${item.id} | ${item.rule} | ${item.severity} | ${item.file} | ${item.line} | ${escapeCell(item.action)} | \`${item.verification}\` |`,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
lines.push('', '## Next', '', '- Apply in small batches; run `codecraft check` after each.')
|
|
270
|
+
lines.push('- No files were changed by this command.')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
lines.push(...gateLines(gates))
|
|
274
|
+
return lines.join('\n')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatInit(meta, plan, applied) {
|
|
278
|
+
const lines = [
|
|
279
|
+
'# Codecraft Init',
|
|
280
|
+
'',
|
|
281
|
+
`- Version: ${meta.version}`,
|
|
282
|
+
`- Mode: init`,
|
|
283
|
+
`- Pack: ${meta.pack}`,
|
|
284
|
+
`- Root: ${meta.root}`,
|
|
285
|
+
`- Name: ${meta.name || '(placeholder)'}`,
|
|
286
|
+
`- Dry run: ${meta.dryRun}`,
|
|
287
|
+
`- Entries: ${meta.entries}`,
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
if (applied) {
|
|
291
|
+
lines.push(`- Created: ${meta.created}`, `- Skipped: ${meta.skipped}`)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
lines.push('', '## Directories', '', '| Path | Action |', '|---|---|')
|
|
295
|
+
|
|
296
|
+
for (const entry of plan.entries.filter((item) => item.type === 'dir')) {
|
|
297
|
+
lines.push(`| ${entry.path} | ${entry.action} |`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
lines.push('', '## Files', '', '| Path | Action |', '|---|---|')
|
|
301
|
+
for (const entry of plan.entries.filter((item) => item.type === 'file')) {
|
|
302
|
+
lines.push(`| ${entry.path} | ${entry.action} |`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
lines.push('', '## Config', '', '```json', JSON.stringify(plan.config, null, 2), '```')
|
|
306
|
+
|
|
307
|
+
if (applied) {
|
|
308
|
+
lines.push('', '## Appended', '', '- Existing paths are skipped; nothing was overwritten.')
|
|
309
|
+
if (applied.created.length) {
|
|
310
|
+
lines.push('', 'Created:', '')
|
|
311
|
+
for (const item of applied.created) lines.push(`- ${item}`)
|
|
312
|
+
}
|
|
313
|
+
if (applied.skipped.length) {
|
|
314
|
+
lines.push('', 'Skipped:', '')
|
|
315
|
+
for (const item of applied.skipped) lines.push(`- ${item}`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
lines.push(
|
|
320
|
+
'',
|
|
321
|
+
'## Next',
|
|
322
|
+
'',
|
|
323
|
+
meta.dryRun
|
|
324
|
+
? '- Review the plan; nothing was written.'
|
|
325
|
+
: '- Run `codecraft check --pack <pack>` to confirm the boundary rules.',
|
|
326
|
+
meta.dryRun ? '- Re-run with `--apply --name <name>` to write the scaffold.' : '',
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return lines.filter((line, index) => !(line === '' && lines[index - 1] === '')).join('\n')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function header(meta) {
|
|
333
|
+
return [
|
|
334
|
+
`# Codecraft ${title(meta.mode)}`,
|
|
335
|
+
'',
|
|
336
|
+
`- Version: ${meta.version}`,
|
|
337
|
+
`- Mode: ${meta.mode}`,
|
|
338
|
+
`- Pack: ${meta.pack}`,
|
|
339
|
+
`- Root: ${meta.root}`,
|
|
340
|
+
`- Config: ${meta.config ?? '(none)'}`,
|
|
341
|
+
`- Rules: ${meta.rules.join(' / ')}`,
|
|
342
|
+
`- Severity: ${meta.severity}`,
|
|
343
|
+
`- Files: ${meta.files}`,
|
|
344
|
+
`- Findings: ${meta.findings}`,
|
|
345
|
+
`- Gates: ${meta.gates}`,
|
|
346
|
+
'',
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function title(mode) {
|
|
351
|
+
if (mode === 'audit') return 'Audit'
|
|
352
|
+
if (mode === 'plan') return 'Plan'
|
|
353
|
+
return 'Check'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function row(finding) {
|
|
357
|
+
return `| ${finding.rule} | ${finding.severity} | ${finding.file} | ${finding.line} | ${escapeCell(finding.message)} |`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function gateLines(gates) {
|
|
361
|
+
if (!gates.length) return []
|
|
362
|
+
const lines = ['', '## Gates', '', '| Gate | Exit | Duration | Command |', '|---|---:|---:|---|']
|
|
363
|
+
for (const gate of gates) {
|
|
364
|
+
lines.push(`| ${gate.name} | ${gate.exitCode} | ${gate.durationMs}ms | \`${gate.command}\` |`)
|
|
365
|
+
}
|
|
366
|
+
for (const gate of gates.filter((item) => item.exitCode !== 0)) {
|
|
367
|
+
lines.push('', `### Gate: ${gate.name}(exit ${gate.exitCode})`, '', '~~~text')
|
|
368
|
+
lines.push(gate.output || '(no output)')
|
|
369
|
+
lines.push('~~~')
|
|
370
|
+
}
|
|
371
|
+
return lines
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function escapeCell(value) {
|
|
375
|
+
return value.replace(/\|/g, '\\|')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isDir(target) {
|
|
379
|
+
try {
|
|
380
|
+
return fs.statSync(target).isDirectory()
|
|
381
|
+
} catch {
|
|
382
|
+
return false
|
|
383
|
+
}
|
|
384
|
+
}
|
package/src/gates.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
export function loadConfig(root, configPath) {
|
|
6
|
+
const file = configPath ? path.resolve(root, configPath) : path.join(root, 'codecraft.config.json')
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(file)) return { file: null, config: {} }
|
|
9
|
+
|
|
10
|
+
let config
|
|
11
|
+
try {
|
|
12
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw new Error(`invalid config ${file}: ${error.message}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (config.gates != null && !Array.isArray(config.gates)) {
|
|
18
|
+
throw new Error(`invalid config ${file}: "gates" must be an array`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const gate of config.gates ?? []) {
|
|
22
|
+
if (!gate || typeof gate.run !== 'string' || !gate.run.trim()) {
|
|
23
|
+
throw new Error(`invalid config ${file}: each gate needs a "run" string`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { file, config }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function runGates(root, gates, options = {}) {
|
|
31
|
+
if (!Array.isArray(gates)) throw new Error('gates must be an array')
|
|
32
|
+
const results = []
|
|
33
|
+
for (const gate of gates) {
|
|
34
|
+
const result = runGate(root, gate)
|
|
35
|
+
results.push(result)
|
|
36
|
+
if (options.failFast && result.exitCode !== 0) break
|
|
37
|
+
}
|
|
38
|
+
return results
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runGate(root, gate) {
|
|
42
|
+
const name = typeof gate.name === 'string' && gate.name.trim() ? gate.name.trim() : gate.run
|
|
43
|
+
const startedAt = Date.now()
|
|
44
|
+
const result = spawnSync(gate.run, {
|
|
45
|
+
cwd: root,
|
|
46
|
+
shell: true,
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
timeout: Number.isFinite(gate.timeoutMs) ? gate.timeoutMs : 120000,
|
|
49
|
+
env: { ...process.env, CI: '1' },
|
|
50
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
51
|
+
})
|
|
52
|
+
const durationMs = Date.now() - startedAt
|
|
53
|
+
const output = tail([result.error?.message, result.stdout, result.stderr].filter(Boolean).join('\n').trim(), 40, 4000)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
command: gate.run,
|
|
58
|
+
exitCode: result.status ?? 1,
|
|
59
|
+
durationMs,
|
|
60
|
+
output,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function tail(text, maxLines, maxChars) {
|
|
65
|
+
const sliced = text.split('\n').slice(-maxLines).join('\n')
|
|
66
|
+
return sliced.length > maxChars ? sliced.slice(-maxChars) : sliced
|
|
67
|
+
}
|
package/src/init.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const TEMPLATES = new URL('../templates/', import.meta.url)
|
|
5
|
+
|
|
6
|
+
export function buildInitPlan(root, pack, name = '') {
|
|
7
|
+
const appName = name || '<app>'
|
|
8
|
+
const packageName = name || '<package>'
|
|
9
|
+
const id = toPackageId(name)
|
|
10
|
+
const entries = []
|
|
11
|
+
|
|
12
|
+
const add = (rel, type, content) => {
|
|
13
|
+
const abs = path.join(root, rel)
|
|
14
|
+
entries.push({
|
|
15
|
+
path: rel,
|
|
16
|
+
type,
|
|
17
|
+
action: fs.existsSync(abs) ? 'exists' : 'create',
|
|
18
|
+
...(content == null ? {} : { content }),
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fill = (target, vars) => render(template(pack, target), vars)
|
|
23
|
+
|
|
24
|
+
if (pack === 'library') {
|
|
25
|
+
add('packages', 'dir')
|
|
26
|
+
add(`packages/${packageName}`, 'dir')
|
|
27
|
+
add(`packages/${packageName}/src`, 'dir')
|
|
28
|
+
add(`packages/${packageName}/tests`, 'dir')
|
|
29
|
+
const vars = { name: packageName, id }
|
|
30
|
+
add(`packages/${packageName}/README.md`, 'file', fill('README.md', vars))
|
|
31
|
+
add(`packages/${packageName}/CHANGELOG.md`, 'file', fill('CHANGELOG.md', vars))
|
|
32
|
+
add(`packages/${packageName}/compat.md`, 'file', fill('compat.md', vars))
|
|
33
|
+
add(`packages/${packageName}/package.json`, 'file', fill('package.json', vars))
|
|
34
|
+
} else {
|
|
35
|
+
add('apps', 'dir')
|
|
36
|
+
add(`apps/${appName}`, 'dir')
|
|
37
|
+
add(`apps/${appName}/src`, 'dir')
|
|
38
|
+
add(`apps/${appName}/src/pages`, 'dir')
|
|
39
|
+
add(`apps/${appName}/src/components`, 'dir')
|
|
40
|
+
add(`apps/${appName}/src/behavior`, 'dir')
|
|
41
|
+
add(`apps/${appName}/src/api`, 'dir')
|
|
42
|
+
add(`apps/${appName}/src/adapters`, 'dir')
|
|
43
|
+
add(`apps/${appName}/src/types`, 'dir')
|
|
44
|
+
add(`apps/${appName}/src/external`, 'dir')
|
|
45
|
+
const vars = { name: appName, id }
|
|
46
|
+
add(`apps/${appName}/README.md`, 'file', fill('README.md', vars))
|
|
47
|
+
add(`apps/${appName}/package.json`, 'file', fill('package.json', vars))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
add('docs', 'dir')
|
|
51
|
+
const config = { pack, gates: [] }
|
|
52
|
+
add('codecraft.config.json', 'file', `${JSON.stringify(config, null, 2)}\n`)
|
|
53
|
+
|
|
54
|
+
return { pack, name, entries, config }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function applyInitPlan(root, plan) {
|
|
58
|
+
const created = []
|
|
59
|
+
const skipped = []
|
|
60
|
+
|
|
61
|
+
for (const entry of plan.entries) {
|
|
62
|
+
const abs = path.join(root, entry.path)
|
|
63
|
+
if (entry.action === 'exists') {
|
|
64
|
+
skipped.push(entry.path)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (entry.type === 'dir') {
|
|
68
|
+
fs.mkdirSync(abs, { recursive: true })
|
|
69
|
+
} else {
|
|
70
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true })
|
|
71
|
+
fs.writeFileSync(abs, entry.content ?? '')
|
|
72
|
+
}
|
|
73
|
+
created.push(entry.path)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { created, skipped }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function template(pack, rel) {
|
|
80
|
+
try {
|
|
81
|
+
return fs.readFileSync(new URL(`${pack}/${rel}`, TEMPLATES), 'utf8')
|
|
82
|
+
} catch {
|
|
83
|
+
return ''
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function render(text, vars) {
|
|
88
|
+
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toPackageId(name) {
|
|
92
|
+
if (!name) return '<package>'
|
|
93
|
+
return name
|
|
94
|
+
.trim()
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
97
|
+
.replace(/^-+|-+$/g, '')
|
|
98
|
+
}
|
package/src/plan.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const ACTIONS = {
|
|
2
|
+
A2: 'Declare the workspace dependency in package.json, or remove the import; if shared, move it to a lower-layer package.',
|
|
3
|
+
A4: 'Move the external import to a Wrapper/API module; expose an internal type to the caller.',
|
|
4
|
+
A11: "Regenerate the file from source and add a 'do not edit' marker; if it is not generated, move it out of generated paths.",
|
|
5
|
+
A17: 'Move the external import to a Wrapper/API module; expose an internal type to the caller.',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const VERIFY = {
|
|
9
|
+
A2: 'codecraft check --pack library',
|
|
10
|
+
A4: 'codecraft check --pack app',
|
|
11
|
+
A11: 'codecraft check --pack library',
|
|
12
|
+
A17: 'codecraft check --pack app',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildPlan(findings) {
|
|
16
|
+
return findings.map((finding, index) => ({
|
|
17
|
+
id: `P${index + 1}`,
|
|
18
|
+
rule: finding.rule,
|
|
19
|
+
severity: finding.severity,
|
|
20
|
+
file: finding.file,
|
|
21
|
+
line: finding.line,
|
|
22
|
+
action: ACTIONS[finding.rule] ?? 'Review and fix according to the code constitution.',
|
|
23
|
+
verification: VERIFY[finding.rule] ?? 'codecraft check',
|
|
24
|
+
}))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function summarizePlan(findings) {
|
|
28
|
+
const counts = new Map()
|
|
29
|
+
for (const finding of findings) {
|
|
30
|
+
counts.set(finding.rule, (counts.get(finding.rule) ?? 0) + 1)
|
|
31
|
+
}
|
|
32
|
+
return [...counts.entries()]
|
|
33
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
34
|
+
.map(([rule, count]) => ({ rule, count }))
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Templates
|
|
2
|
+
|
|
3
|
+
`init --apply` 使用的项目骨架。占位符:`{{name}}`(展示名)/ `{{id}}`(package.json 名)。
|
|
4
|
+
|
|
5
|
+
| 目录 | 内容 |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `app/` | 应用骨架:`README.md` + `package.json` |
|
|
8
|
+
| `library/` | 包骨架:`README.md` + `CHANGELOG.md` + `compat.md` + `package.json` |
|
|
9
|
+
|
|
10
|
+
- 目录结构由 `src/init.mjs` 定义,模板只提供文件内容。
|
|
11
|
+
- 页面 / 组件 / Wrapper 的单文件形态见 `docs/01-family-cards.md`,不单独出模板:不同框架的写法差异大于共性。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
> 应用骨架。边界约定见 codecraft 宪法 A4(单一收口)/ A17(内外分离)。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
| 目录 | 职责 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `src/pages` | 页面:一个页面一个编排入口,只编排 |
|
|
10
|
+
| `src/components` | 展示与交互组件;原语薄、业务组件按域 |
|
|
11
|
+
| `src/behavior` | 页面无关行为(hooks / composables) |
|
|
12
|
+
| `src/api` | 请求收口;唯一可接触 `external/` 的层 |
|
|
13
|
+
| `src/adapters` | 平台适配(请求 / 存储 / 路由 / 导出) |
|
|
14
|
+
| `src/types` | 内部类型:通用层与业务层 |
|
|
15
|
+
| `src/external` | 外部契约(后端 / 第三方)原文或生成物 |
|
|
16
|
+
|
|
17
|
+
## 规则
|
|
18
|
+
|
|
19
|
+
- `pages / components / behavior` 不 import `external/` 或 `generated/`。
|
|
20
|
+
- 外部类型只在 `api` / `adapters` 内转成内部类型。
|
|
21
|
+
- 鉴权失效、错误提示、分页各一处收口。
|
|
22
|
+
- 状态用联合类型;非法状态不可表示。
|
|
23
|
+
|
|
24
|
+
## 验证
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
codecraft check --pack app
|
|
28
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
> 代码包骨架。边界约定见 codecraft 宪法 A2(依赖向内)/ A11(生成物只读)。
|
|
4
|
+
|
|
5
|
+
## 结构
|
|
6
|
+
|
|
7
|
+
| 路径 | 职责 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `src/` | 实现;对外只暴露 public contract |
|
|
10
|
+
| `tests/` | 测试:契约 / 单元 |
|
|
11
|
+
| `compat.md` | 兼容矩阵:支持范围与不可用能力 |
|
|
12
|
+
| `CHANGELOG.md` | 版本变更 |
|
|
13
|
+
|
|
14
|
+
## 规则
|
|
15
|
+
|
|
16
|
+
- 不 import 上层应用(`apps/`)。
|
|
17
|
+
- 不通过相对路径逃出自身目录。
|
|
18
|
+
- 依赖必须写进 `package.json`。
|
|
19
|
+
- 生成物必须有「勿手改 / 派生」标记。
|
|
20
|
+
- 破坏性变更升 major;发布不覆盖。
|
|
21
|
+
|
|
22
|
+
## 验证
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
codecraft check --pack library
|
|
26
|
+
```
|