@ultime/codecraft 1.3.0 → 1.5.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +51 -81
  4. package/SKILL.md +2 -2
  5. package/VERSION +1 -1
  6. package/adapters/README.md +3 -4
  7. package/codecraft.config.example.json +5 -1
  8. package/docs/00-constitution.md +2 -2
  9. package/docs/02-ai-contract.md +12 -1
  10. package/docs/04-gates.md +21 -6
  11. package/docs/07-ai-prompts.md +9 -8
  12. package/docs/08-audit-contract.md +141 -0
  13. package/evals/cases.json +110 -0
  14. package/evals/fixtures/a11-marker/packages/core/dist/tokens.css +4 -0
  15. package/evals/fixtures/a11-marker/packages/core/package.json +1 -0
  16. package/evals/fixtures/a11-marker/packages/core/src/index.ts +1 -0
  17. package/evals/fixtures/a17-boundaries/codecraft.config.json +5 -0
  18. package/evals/fixtures/a17-boundaries/src/pages/home.ts +1 -0
  19. package/evals/fixtures/a17-default/src/pages/home.ts +1 -0
  20. package/evals/fixtures/a17-generated-data/src/icons/icons.ts +2 -0
  21. package/evals/fixtures/a17-generated-data/src/pages/home.ts +3 -0
  22. package/evals/fixtures/a17-generated-header/src/pages/home.ts +5 -0
  23. package/evals/fixtures/a17-generated-header/src/types/project.ts +4 -0
  24. package/evals/fixtures/a17-local/src/pages/home.ts +4 -0
  25. package/evals/fixtures/a17-local/src/types/local.ts +3 -0
  26. package/evals/fixtures/a17-local/src/utils/format.ts +3 -0
  27. package/evals/fixtures/a2-declared/packages/core/package.json +6 -0
  28. package/evals/fixtures/a2-declared/packages/core/src/index.ts +3 -0
  29. package/evals/fixtures/a2-declared/packages/kit/package.json +1 -0
  30. package/evals/fixtures/a2-workspace/packages/core/package.json +1 -0
  31. package/evals/fixtures/a2-workspace/packages/core/src/index.ts +3 -0
  32. package/evals/fixtures/a2-workspace/packages/kit/package.json +1 -0
  33. package/evals/fixtures/a21-doc-paths/packages/ui/README.md +5 -0
  34. package/evals/fixtures/a21-doc-paths/packages/ui/package.json +1 -0
  35. package/evals/fixtures/a21-doc-paths/packages/ui/src/index.ts +1 -0
  36. package/evals/fixtures/a21-placeholders/.env +2 -0
  37. package/evals/fixtures/a21-placeholders/server/application.yml +9 -0
  38. package/evals/fixtures/a21-placeholders/src/pages/home.ts +1 -0
  39. package/evals/fixtures/a21-plaintext/application.yml +5 -0
  40. package/evals/fixtures/a21-plaintext/src/main/java/Ali.java +3 -0
  41. package/evals/fixtures/a21-plaintext/src/pages/home.ts +1 -0
  42. package/evals/fixtures/a6-clean/src/pages/handled.ts +8 -0
  43. package/evals/fixtures/a6-empty-catch/src/pages/empty.ts +3 -0
  44. package/evals/fixtures/ignore-active/codecraft.config.json +5 -0
  45. package/evals/fixtures/ignore-active/src/pages/home.ts +1 -0
  46. package/evals/fixtures/ignore-expired/codecraft.config.json +5 -0
  47. package/evals/fixtures/ignore-expired/src/pages/home.ts +1 -0
  48. package/evals/fixtures/library-a11/dist/tokens.css +3 -0
  49. package/evals/fixtures/library-a11/package.json +4 -0
  50. package/evals/fixtures/library-a11/src/index.ts +1 -0
  51. package/evals/fixtures/zero-files/package.json +4 -0
  52. package/evals/golden-tasks.md +9 -5
  53. package/evals/run.mjs +75 -0
  54. package/package.json +6 -4
  55. package/prompts/output.md +15 -4
  56. package/scenarios/audit-existing.md +15 -8
  57. package/src/check.mjs +290 -40
  58. package/src/cli.mjs +159 -143
  59. package/src/gates.mjs +28 -1
  60. package/src/plan.mjs +2 -0
  61. package/adapters/claude/SKILL.md +0 -42
  62. package/adapters/trae/SKILL.md +0 -42
  63. package/src/install.mjs +0 -172
package/src/cli.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { spawnSync } from 'node:child_process'
3
4
  import { check } from './check.mjs'
4
5
  import { loadConfig, runGates } from './gates.mjs'
5
6
  import { buildPlan, summarizePlan } from './plan.mjs'
6
7
  import { applyInitPlan, buildInitPlan } from './init.mjs'
7
8
  import { applyIgnores } from './ignore.mjs'
8
- import { applyInstallPlan, buildInstallPlan, listAgents, loadAgents, parseAgents } from './install.mjs'
9
9
 
10
10
  const PACKAGE = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
11
11
  const VERSION = PACKAGE.version
@@ -17,25 +17,21 @@ Commands:
17
17
  audit check + configured gates; read-only report
18
18
  plan Read-only upgrade plan from findings
19
19
  init Scaffold an app or package (dry-run unless --apply)
20
- install Install the skill into agent skills directories
21
20
  version Print version
22
21
 
23
22
  Options:
24
23
  --root <path> Project root (default: cwd)
25
24
  --pack app|library|design-system|auto Rule pack (default: auto)
26
25
  --config <path> Config file (default: codecraft.config.json)
27
- --agent <name>[,<name>] Install target (default: auto; see --list-agents)
28
- --list-agents List registered agents, paths and detection
29
- --global Install into the home directory
30
- --dest <path> Explicit destination (relative to root / home), e.g. .trae-cn/skills
31
- --force Replace an existing codecraft skill directory
32
- --dry-run Show the install plan without writing
26
+ --changed Only report findings in changed / untracked files
27
+ --since <ref> Only report findings changed since a git ref (e.g. origin/main)
28
+ --exclude <glob> Skip files when scanning (repeatable; e.g. "evals/fixtures/**")
33
29
  --no-gates Skip gates in audit / plan
34
30
  --with-gates Run configured gates in plan
35
31
  --fail-fast Stop gates after the first failure
36
32
  --name <name> Scaffold name (required with --apply)
37
33
  --apply Write the scaffold; existing paths are skipped
38
- --format md|json Output format (default: md)
34
+ --format md|json|sarif Output format (default: md)
39
35
  --severity warn|error Reported severity (default: warn)
40
36
  --ci Treat findings as errors
41
37
  --help, -h
@@ -60,7 +56,7 @@ export function main(argv) {
60
56
  }
61
57
 
62
58
  const command = args.command || 'check'
63
- if (!['check', 'audit', 'plan', 'init', 'install'].includes(command)) {
59
+ if (!['check', 'audit', 'plan', 'init'].includes(command)) {
64
60
  console.error(`codecraft: unknown command "${command}"\n next: run codecraft --help`)
65
61
  return 2
66
62
  }
@@ -71,59 +67,6 @@ export function main(argv) {
71
67
  return 2
72
68
  }
73
69
 
74
- if (args.listAgents) {
75
- try {
76
- const agents = listAgents(root, { global: args.global })
77
- if (args.format === 'json') {
78
- console.log(JSON.stringify({ version: VERSION, agents }, null, 2))
79
- } else {
80
- console.log(formatAgents(agents))
81
- }
82
- return 0
83
- } catch (error) {
84
- console.error(`codecraft: ${error.message}`)
85
- return 2
86
- }
87
- }
88
-
89
- if (command === 'install') {
90
- let agents
91
- let registry
92
- try {
93
- registry = loadAgents(root, { global: args.global })
94
- agents = parseAgents(args.agent, [...registry.keys()])
95
- } catch (error) {
96
- console.error(`codecraft: ${error.message}`)
97
- return 2
98
- }
99
- try {
100
- const plan = buildInstallPlan(root, { agents, global: args.global, dest: args.dest, registry })
101
- const applied = args.dryRun ? null : applyInstallPlan(plan, { force: args.force })
102
- const meta = {
103
- version: VERSION,
104
- mode: 'install',
105
- root,
106
- scope: args.global ? 'global' : 'project',
107
- agent: args.agent,
108
- dest: args.dest || null,
109
- dryRun: args.dryRun,
110
- targets: plan.length,
111
- created: applied?.created.length ?? 0,
112
- replaced: applied?.replaced.length ?? 0,
113
- skipped: applied?.skipped.length ?? 0,
114
- }
115
- if (args.format === 'json') {
116
- console.log(JSON.stringify({ ...meta, plan, applied }, null, 2))
117
- } else {
118
- console.log(formatInstall(meta, plan, applied))
119
- }
120
- return plan.length ? 0 : 1
121
- } catch (error) {
122
- console.error(`codecraft: ${error.message}`)
123
- return 1
124
- }
125
- }
126
-
127
70
  let config
128
71
  try {
129
72
  config = loadConfig(root, args.config)
@@ -167,9 +110,28 @@ export function main(argv) {
167
110
  return 0
168
111
  }
169
112
 
170
- const result = check(root, { pack })
113
+ const result = check(root, {
114
+ pack,
115
+ boundaries: config.config.boundaries,
116
+ exclude: args.exclude,
117
+ })
118
+ let changed = null
119
+ let scoped = result.findings
120
+ if (args.changed || args.since) {
121
+ try {
122
+ changed = changedFiles(root, args.since)
123
+ } catch (error) {
124
+ console.error(`codecraft: ${error.message}`)
125
+ return 2
126
+ }
127
+ const files = new Set(changed)
128
+ scoped = scoped.filter((finding) => {
129
+ const file = finding.file.replace(/\\/g, '/')
130
+ return files.has(file) || [...files].some((candidate) => candidate.endsWith(`/${file}`))
131
+ })
132
+ }
171
133
  const severity = args.ci ? 'error' : args.severity
172
- const findings = result.findings.map((finding) => ({ ...finding, severity }))
134
+ const findings = scoped.map((finding) => ({ ...finding, severity }))
173
135
  const filtered = applyIgnores(findings, config.config.ignore ?? [])
174
136
  const activeFindings = filtered.findings
175
137
  const warnings = []
@@ -188,8 +150,11 @@ export function main(argv) {
188
150
  root,
189
151
  config: config.file,
190
152
  severity,
191
- rules: pack === 'app' ? ['A17', 'A21'] : ['A2', 'A11', 'A21'],
153
+ rules: pack === 'app' ? ['A6', 'A17', 'A21'] : ['A2', 'A6', 'A11', 'A21'],
192
154
  files: result.files,
155
+ scope: args.changed || args.since ? 'changed' : 'full',
156
+ changed: changed?.length ?? null,
157
+ exclude: args.exclude,
193
158
  findings: activeFindings.length,
194
159
  ignored: filtered.ignored.length,
195
160
  expired: filtered.expired.length,
@@ -222,6 +187,8 @@ export function main(argv) {
222
187
  2,
223
188
  ),
224
189
  )
190
+ } else if (args.format === 'sarif') {
191
+ console.log(formatSarif(meta, activeFindings))
225
192
  } else if (command === 'plan') {
226
193
  console.log(formatPlan(meta, activeFindings, plan, gates))
227
194
  } else {
@@ -261,12 +228,9 @@ function parseArgs(argv) {
261
228
  failFast: false,
262
229
  name: '',
263
230
  apply: false,
264
- agent: 'auto',
265
- global: false,
266
- dest: '',
267
- force: false,
268
- dryRun: false,
269
- listAgents: false,
231
+ changed: false,
232
+ since: '',
233
+ exclude: [],
270
234
  format: 'md',
271
235
  severity: 'warn',
272
236
  ci: false,
@@ -283,13 +247,14 @@ function parseArgs(argv) {
283
247
  else if (arg === '--with-gates') args.withGates = true
284
248
  else if (arg === '--fail-fast') args.failFast = true
285
249
  else if (arg === '--apply') args.apply = true
286
- else if (arg === '--global') args.global = true
287
- else if (arg === '--force') args.force = true
288
- else if (arg === '--dry-run') args.dryRun = true
289
- else if (arg === '--list-agents') args.listAgents = true
250
+ else if (arg === '--changed') args.changed = true
251
+ else if (arg === '--since') args.since = argv[++i] ?? ''
252
+ else if (arg === '--exclude') {
253
+ const pattern = argv[++i] ?? ''
254
+ if (!pattern) fail('--exclude needs a path or glob (e.g. "evals/fixtures/**")')
255
+ args.exclude.push(pattern)
256
+ }
290
257
  else if (arg === '--name') args.name = argv[++i] ?? ''
291
- else if (arg === '--agent') args.agent = argv[++i] ?? 'auto'
292
- else if (arg === '--dest') args.dest = argv[++i] ?? ''
293
258
  else if (arg === '--root') args.root = argv[++i] ?? '.'
294
259
  else if (arg === '--pack') args.pack = argv[++i] ?? 'auto'
295
260
  else if (arg === '--config') args.config = argv[++i] ?? ''
@@ -300,7 +265,9 @@ function parseArgs(argv) {
300
265
  }
301
266
 
302
267
  if (!['app', 'library', 'design-system', 'auto'].includes(args.pack)) fail(`invalid --pack: ${args.pack}`)
303
- if (args.format !== 'md' && args.format !== 'json') fail(`invalid --format: ${args.format}`)
268
+ if (args.format !== 'md' && args.format !== 'json' && args.format !== 'sarif') {
269
+ fail(`invalid --format: ${args.format}`)
270
+ }
304
271
  if (args.severity !== 'warn' && args.severity !== 'error') {
305
272
  fail(`invalid --severity: ${args.severity}`)
306
273
  }
@@ -308,6 +275,39 @@ function parseArgs(argv) {
308
275
  return args
309
276
  }
310
277
 
278
+ function changedFiles(root, since) {
279
+ const top = git(root, ['rev-parse', '--show-toplevel'])
280
+ if (top == null) {
281
+ throw new Error('not a git repository; --changed / --since need git (run inside a git repo or drop the flag)')
282
+ }
283
+
284
+ const lists = [git(root, ['diff', '--name-only', '--diff-filter=ACMRT', 'HEAD'])]
285
+ if (since) lists.push(git(root, ['diff', '--name-only', '--diff-filter=ACMRT', `${since}...HEAD`]))
286
+ lists.push(git(root, ['ls-files', '--others', '--exclude-standard']))
287
+
288
+ const realRoot = fs.realpathSync(root)
289
+ const files = new Set()
290
+ for (const list of lists) {
291
+ if (!list) continue
292
+ for (const line of list.split('\n')) {
293
+ const file = line.trim()
294
+ if (!file) continue
295
+ const relative = path.relative(realRoot, path.resolve(top, file)).split(path.sep).join('/')
296
+ if (relative && !relative.startsWith('..')) files.add(relative)
297
+ }
298
+ }
299
+ return [...files]
300
+ }
301
+
302
+ function git(root, args) {
303
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
304
+ if (result.status !== 0) {
305
+ const message = (result.stderr || '').trim()
306
+ throw new Error(`git ${args.join(' ')} failed${message ? `: ${message}` : ''}`)
307
+ }
308
+ return result.stdout.trim()
309
+ }
310
+
311
311
  export function detectPack(root) {
312
312
  if (isDir(path.join(root, 'apps'))) return 'app'
313
313
  if (hasAppMarkers(root)) return 'app'
@@ -329,13 +329,28 @@ const APP_FILE_MARKERS = [
329
329
  'src/app.config.ts',
330
330
  'taro.config.js',
331
331
  'taro.config.ts',
332
+ 'vue.config.js',
333
+ 'vue.config.ts',
332
334
  ]
333
335
  const APP_DEPS = ['nuxt', '@tarojs/taro', '@dcloudio/uni-app', 'uni-app', 'next']
336
+ /** 单页 Web 应用入口;库的 demo 页通常不具备「框架依赖 + 入口 + html」三件套。 */
337
+ const WEB_ENTRIES = [
338
+ 'src/main.ts',
339
+ 'src/main.js',
340
+ 'src/main.tsx',
341
+ 'src/main.jsx',
342
+ 'src/index.ts',
343
+ 'src/index.js',
344
+ 'src/index.tsx',
345
+ 'src/index.jsx',
346
+ ]
347
+ const WEB_DEPS = ['vue', 'react', 'react-dom', 'react-scripts', 'svelte', 'solid-js', 'preact', '@angular/core']
334
348
 
335
349
  function hasAppMarkers(root) {
336
350
  if (APP_DIR_MARKERS.some((dir) => isDir(path.join(root, dir)))) return true
337
351
  if (APP_FILE_MARKERS.some((file) => isFile(path.join(root, file)))) return true
338
352
  if (hasUniAppFiles(root)) return true
353
+ if (hasWebEntry(root)) return true
339
354
  return hasAppDependency(root)
340
355
  }
341
356
 
@@ -355,6 +370,7 @@ function hasNestedApp(root) {
355
370
  if (APP_DIR_MARKERS.some((dir) => isDir(path.join(child, dir)))) return true
356
371
  if (APP_FILE_MARKERS.some((file) => isFile(path.join(child, file)))) return true
357
372
  if (hasUniAppFiles(child)) return true
373
+ if (hasWebEntry(child)) return true
358
374
  if (hasAppDependency(child)) return true
359
375
  }
360
376
  return false
@@ -367,14 +383,25 @@ function hasUniAppFiles(root) {
367
383
  }
368
384
 
369
385
  function hasAppDependency(root) {
386
+ return dependencyNames(root).some((dep) => APP_DEPS.includes(dep))
387
+ }
388
+
389
+ /** 框架依赖 + html + 入口;三者齐全才按单页应用处理,避免把带 demo 的库判成 app。 */
390
+ function hasWebEntry(root) {
391
+ const html = isFile(path.join(root, 'index.html')) || isFile(path.join(root, 'public', 'index.html'))
392
+ if (!html) return false
393
+ if (!dependencyNames(root).some((dep) => WEB_DEPS.includes(dep))) return false
394
+ return WEB_ENTRIES.some((file) => isFile(path.join(root, file)))
395
+ }
396
+
397
+ function dependencyNames(root) {
370
398
  let manifest
371
399
  try {
372
400
  manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
373
401
  } catch {
374
- return false
402
+ return []
375
403
  }
376
- const deps = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) }
377
- return APP_DEPS.some((dep) => dep in deps)
404
+ return Object.keys({ ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) })
378
405
  }
379
406
 
380
407
  function isFile(target) {
@@ -390,6 +417,55 @@ function fail(message) {
390
417
  process.exit(2)
391
418
  }
392
419
 
420
+ function formatSarif(meta, findings) {
421
+ const rules = meta.rules.map((id) => ({
422
+ id,
423
+ name: SARIF_RULES[id]?.name ?? id,
424
+ shortDescription: { text: SARIF_RULES[id]?.short ?? id },
425
+ }))
426
+ return JSON.stringify(
427
+ {
428
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
429
+ version: '2.1.0',
430
+ runs: [
431
+ {
432
+ tool: {
433
+ driver: {
434
+ name: 'codecraft',
435
+ informationUri: 'https://github.com/ultime-space/skills',
436
+ version: meta.version,
437
+ rules,
438
+ },
439
+ },
440
+ results: findings.map((finding) => ({
441
+ ruleId: finding.rule,
442
+ level: finding.severity === 'error' ? 'error' : 'warning',
443
+ message: { text: finding.message },
444
+ locations: [
445
+ {
446
+ physicalLocation: {
447
+ artifactLocation: { uri: finding.file.replace(/\\/g, '/') },
448
+ region: { startLine: finding.line },
449
+ },
450
+ },
451
+ ],
452
+ })),
453
+ },
454
+ ],
455
+ },
456
+ null,
457
+ 2,
458
+ )
459
+ }
460
+
461
+ const SARIF_RULES = {
462
+ A2: { name: 'DependencyDirection', short: 'Dependencies must point inward' },
463
+ A6: { name: 'EmptyCatch', short: 'Empty catch swallows failures' },
464
+ A11: { name: 'GeneratedReadOnly', short: 'Generated files must be read-only and marked' },
465
+ A17: { name: 'ExternalBoundary', short: 'Internal layers must not import external or generated types' },
466
+ A21: { name: 'SecretScan', short: 'No secrets or internal paths in repository' },
467
+ }
468
+
393
469
  function formatReport(meta, findings, ignored, expired, gates) {
394
470
  const lines = header(meta)
395
471
 
@@ -521,67 +597,6 @@ function formatInit(meta, plan, applied) {
521
597
  return lines.filter((line, index) => !(line === '' && lines[index - 1] === '')).join('\n')
522
598
  }
523
599
 
524
- function formatAgents(agents) {
525
- const lines = [
526
- '# Codecraft Agents',
527
- '',
528
- `- Version: ${VERSION}`,
529
- '',
530
- '| Name | Source | Project | Global | Detected |',
531
- '|---|---|---|---|---|',
532
- ]
533
- for (const agent of agents) {
534
- const detected =
535
- [agent.detected.project ? 'project' : '', agent.detected.global ? 'global' : ''].filter(Boolean).join(', ') ||
536
- '—'
537
- lines.push(`| ${agent.name} | ${agent.source} | ${agent.project} | ${agent.global} | ${detected} |`)
538
- }
539
- return lines.join('\n')
540
- }
541
-
542
- function formatInstall(meta, plan, applied) {
543
- const lines = [
544
- '# Codecraft Install',
545
- '',
546
- `- Version: ${meta.version}`,
547
- `- Mode: install`,
548
- `- Scope: ${meta.scope}`,
549
- `- Agent: ${meta.agent}`,
550
- `- Root: ${meta.root}`,
551
- `- Dest: ${meta.dest ?? '(auto)'}`,
552
- `- Dry run: ${meta.dryRun}`,
553
- `- Targets: ${meta.targets}`,
554
- ]
555
-
556
- if (applied) {
557
- lines.push(
558
- `- Created: ${meta.created}`,
559
- `- Replaced: ${meta.replaced}`,
560
- `- Skipped: ${meta.skipped}`,
561
- )
562
- }
563
-
564
- if (!plan.length) {
565
- lines.push('', '未找到可安装的目标;请用 `--agent` 指定,或先创建 agent 配置目录。')
566
- } else {
567
- lines.push('', '## Plan', '', '| Agent | Target | Action |', '|---|---|---|')
568
- for (const entry of plan) {
569
- lines.push(`| ${entry.agent} | ${entry.target} | ${entry.action} |`)
570
- }
571
- }
572
-
573
- lines.push(
574
- '',
575
- '## Next',
576
- '',
577
- meta.dryRun
578
- ? '- Re-run without `--dry-run` to write the skill.'
579
- : '- Restart the agent or reload skills;然后说:`用 Codecraft 审计本项目,只读,不改文件。`',
580
- )
581
-
582
- return lines.join('\n')
583
- }
584
-
585
600
  function header(meta) {
586
601
  return [
587
602
  `# Codecraft ${title(meta.mode)}`,
@@ -591,6 +606,7 @@ function header(meta) {
591
606
  `- Pack: ${meta.pack}`,
592
607
  `- Root: ${meta.root}`,
593
608
  `- Config: ${meta.config ?? '(none)'}`,
609
+ `- Exclude: ${meta.exclude?.length ? meta.exclude.join(', ') : '(none)'}`,
594
610
  `- Rules: ${meta.rules.join(' / ')}`,
595
611
  `- Severity: ${meta.severity}`,
596
612
  `- Files: ${meta.files}`,
package/src/gates.mjs CHANGED
@@ -27,7 +27,34 @@ export function loadConfig(root, configPath) {
27
27
  }
28
28
  }
29
29
 
30
- return { file, config: { ...config, ignore: validateIgnore(config.ignore, file) } }
30
+ return {
31
+ file,
32
+ config: {
33
+ ...config,
34
+ ignore: validateIgnore(config.ignore, file),
35
+ ...(config.boundaries ? { boundaries: validateBoundaries(config.boundaries, file) } : {}),
36
+ },
37
+ }
38
+ }
39
+
40
+ function validateBoundaries(value, file) {
41
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
42
+ throw new Error(`invalid config ${file}: "boundaries" must be an object with external / generated arrays`)
43
+ }
44
+ const boundaries = {}
45
+ for (const key of ['external', 'generated']) {
46
+ if (value[key] == null) continue
47
+ if (!Array.isArray(value[key])) {
48
+ throw new Error(`invalid config ${file}: boundaries.${key} must be an array of import prefixes or globs`)
49
+ }
50
+ boundaries[key] = value[key].map((item) => {
51
+ if (typeof item !== 'string' || !item.trim()) {
52
+ throw new Error(`invalid config ${file}: boundaries.${key} entries must be non-empty strings`)
53
+ }
54
+ return item.trim()
55
+ })
56
+ }
57
+ return boundaries
31
58
  }
32
59
 
33
60
  export function runGates(root, gates, options = {}) {
package/src/plan.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  const ACTIONS = {
2
2
  A2: 'Declare the workspace dependency in package.json, or remove the import; if shared, move it to a lower-layer package.',
3
3
  A4: 'Move the external import to a Wrapper/API module; expose an internal type to the caller.',
4
+ A6: 'Return a Result or handle the error; never swallow it in an empty catch.',
4
5
  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
6
  A17: 'Move the external import to a Wrapper/API module; expose an internal type to the caller.',
6
7
  }
@@ -8,6 +9,7 @@ const ACTIONS = {
8
9
  const VERIFY = {
9
10
  A2: 'codecraft check --pack library',
10
11
  A4: 'codecraft check --pack app',
12
+ A6: 'codecraft check',
11
13
  A11: 'codecraft check --pack library',
12
14
  A17: 'codecraft check --pack app',
13
15
  }
@@ -1,42 +0,0 @@
1
- ---
2
- name: codecraft
3
- description: 面向 JS/TS 的代码规范与架构设计(Codecraft)。默认 Web 前端,Node 后端可复用结构规则;用于审计、架构设计、重构评审、规范检查与脚手架。
4
- ---
5
-
6
- # Codecraft(代码手艺)
7
-
8
- > 方向:JS/TS(默认前端)。后端只复用结构规则;Page / Component 等前端族不套用到后端。
9
-
10
- ## 先读
11
-
12
- 1. 项目 `AGENTS.md` / `rules/`
13
- 2. `../../docs/00-constitution.md`
14
- 3. 只读相关族卡:`../../docs/01-family-cards.md`
15
- 4. 设计架构时:`../../docs/06-architecture.md`
16
- 5. 需要选型时:`../../docs/05-patterns.md`
17
- 6. 选场景:`../../scenarios/`
18
-
19
- ## 模式
20
-
21
- - **Audit(审计)**:只读,出报告。
22
- - **Plan(计划)**:只读,出架构 / 升级计划。
23
- - **Apply(执行)**:改代码 + 跑门禁。
24
-
25
- ## 工作流
26
-
27
- 1. 判定任务属于哪个场景。
28
- 2. 只读相关族卡,不要一次加载全部。
29
- 3. 先出计划;Apply 前确认范围。
30
- 4. 跑项目验证命令;不能跑要说明原因。
31
- 5. 按 `../../prompts/output.md` 输出。
32
-
33
- ## 禁止
34
-
35
- - Audit / Plan 模式改文件。
36
- - 把业务逻辑放进 Transport 或 Page。
37
- - 内部代码 import `external/` 或生成类型。
38
- - 手改生成物。
39
- - 写敏感信息;发现即停止、脱敏、提示轮换。
40
- - 安全红线登记例外。
41
-
42
- 规则正文只在 `docs/`;本文件只做入口。
@@ -1,42 +0,0 @@
1
- ---
2
- name: codecraft
3
- description: 面向 JS/TS 的代码规范与架构设计(Codecraft)。默认 Web 前端,Node 后端可复用结构规则;用于审计、架构设计、重构评审、规范检查与脚手架。
4
- ---
5
-
6
- # Codecraft(代码手艺)
7
-
8
- > 方向:JS/TS(默认前端)。后端只复用结构规则;Page / Component 等前端族不套用到后端。
9
-
10
- ## 先读
11
-
12
- 1. 项目 `AGENTS.md` / `rules/`
13
- 2. `../../docs/00-constitution.md`
14
- 3. 只读相关族卡:`../../docs/01-family-cards.md`
15
- 4. 设计架构时:`../../docs/06-architecture.md`
16
- 5. 需要选型时:`../../docs/05-patterns.md`
17
- 6. 选场景:`../../scenarios/`
18
-
19
- ## 模式
20
-
21
- - **Audit(审计)**:只读,出报告。
22
- - **Plan(计划)**:只读,出架构 / 升级计划。
23
- - **Apply(执行)**:改代码 + 跑门禁。
24
-
25
- ## 工作流
26
-
27
- 1. 判定任务属于哪个场景。
28
- 2. 只读相关族卡,不要一次加载全部。
29
- 3. 先出计划;Apply 前确认范围。
30
- 4. 跑项目验证命令;不能跑要说明原因。
31
- 5. 按 `../../prompts/output.md` 输出。
32
-
33
- ## 禁止
34
-
35
- - Audit / Plan 模式改文件。
36
- - 把业务逻辑放进 Transport 或 Page。
37
- - 内部代码 import `external/` 或生成类型。
38
- - 手改生成物。
39
- - 写敏感信息;发现即停止、脱敏、提示轮换。
40
- - 安全红线登记例外。
41
-
42
- 规则正文只在 `docs/`;本文件只做入口。