@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.
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +3 -0
- package/README.md +51 -81
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/adapters/README.md +3 -4
- package/codecraft.config.example.json +5 -1
- package/docs/00-constitution.md +2 -2
- package/docs/02-ai-contract.md +12 -1
- package/docs/04-gates.md +21 -6
- package/docs/07-ai-prompts.md +9 -8
- package/docs/08-audit-contract.md +141 -0
- package/evals/cases.json +110 -0
- package/evals/fixtures/a11-marker/packages/core/dist/tokens.css +4 -0
- package/evals/fixtures/a11-marker/packages/core/package.json +1 -0
- package/evals/fixtures/a11-marker/packages/core/src/index.ts +1 -0
- package/evals/fixtures/a17-boundaries/codecraft.config.json +5 -0
- package/evals/fixtures/a17-boundaries/src/pages/home.ts +1 -0
- package/evals/fixtures/a17-default/src/pages/home.ts +1 -0
- package/evals/fixtures/a17-generated-data/src/icons/icons.ts +2 -0
- package/evals/fixtures/a17-generated-data/src/pages/home.ts +3 -0
- package/evals/fixtures/a17-generated-header/src/pages/home.ts +5 -0
- package/evals/fixtures/a17-generated-header/src/types/project.ts +4 -0
- package/evals/fixtures/a17-local/src/pages/home.ts +4 -0
- package/evals/fixtures/a17-local/src/types/local.ts +3 -0
- package/evals/fixtures/a17-local/src/utils/format.ts +3 -0
- package/evals/fixtures/a2-declared/packages/core/package.json +6 -0
- package/evals/fixtures/a2-declared/packages/core/src/index.ts +3 -0
- package/evals/fixtures/a2-declared/packages/kit/package.json +1 -0
- package/evals/fixtures/a2-workspace/packages/core/package.json +1 -0
- package/evals/fixtures/a2-workspace/packages/core/src/index.ts +3 -0
- package/evals/fixtures/a2-workspace/packages/kit/package.json +1 -0
- package/evals/fixtures/a21-doc-paths/packages/ui/README.md +5 -0
- package/evals/fixtures/a21-doc-paths/packages/ui/package.json +1 -0
- package/evals/fixtures/a21-doc-paths/packages/ui/src/index.ts +1 -0
- package/evals/fixtures/a21-placeholders/.env +2 -0
- package/evals/fixtures/a21-placeholders/server/application.yml +9 -0
- package/evals/fixtures/a21-placeholders/src/pages/home.ts +1 -0
- package/evals/fixtures/a21-plaintext/application.yml +5 -0
- package/evals/fixtures/a21-plaintext/src/main/java/Ali.java +3 -0
- package/evals/fixtures/a21-plaintext/src/pages/home.ts +1 -0
- package/evals/fixtures/a6-clean/src/pages/handled.ts +8 -0
- package/evals/fixtures/a6-empty-catch/src/pages/empty.ts +3 -0
- package/evals/fixtures/ignore-active/codecraft.config.json +5 -0
- package/evals/fixtures/ignore-active/src/pages/home.ts +1 -0
- package/evals/fixtures/ignore-expired/codecraft.config.json +5 -0
- package/evals/fixtures/ignore-expired/src/pages/home.ts +1 -0
- package/evals/fixtures/library-a11/dist/tokens.css +3 -0
- package/evals/fixtures/library-a11/package.json +4 -0
- package/evals/fixtures/library-a11/src/index.ts +1 -0
- package/evals/fixtures/zero-files/package.json +4 -0
- package/evals/golden-tasks.md +9 -5
- package/evals/run.mjs +75 -0
- package/package.json +6 -4
- package/prompts/output.md +15 -4
- package/scenarios/audit-existing.md +15 -8
- package/src/check.mjs +290 -40
- package/src/cli.mjs +159 -143
- package/src/gates.mjs +28 -1
- package/src/plan.mjs +2 -0
- package/adapters/claude/SKILL.md +0 -42
- package/adapters/trae/SKILL.md +0 -42
- 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
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
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
|
|
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'
|
|
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, {
|
|
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 =
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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 === '--
|
|
287
|
-
else if (arg === '--
|
|
288
|
-
else if (arg === '--
|
|
289
|
-
|
|
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'
|
|
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
|
|
402
|
+
return []
|
|
375
403
|
}
|
|
376
|
-
|
|
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 {
|
|
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
|
}
|
package/adapters/claude/SKILL.md
DELETED
|
@@ -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/`;本文件只做入口。
|
package/adapters/trae/SKILL.md
DELETED
|
@@ -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/`;本文件只做入口。
|