@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/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,8 @@
1
+ {
2
+ "name": "{{id}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "check": "codecraft check --pack app"
7
+ }
8
+ }
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
@@ -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
+ ```
@@ -0,0 +1,5 @@
1
+ # 兼容矩阵
2
+
3
+ | 依赖 / 平台 | 支持范围 | 不可用能力 | 说明 |
4
+ |---|---|---|---|
5
+ | — | — | — | 待填写 |
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "{{id}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "files": [
6
+ "src"
7
+ ],
8
+ "type": "module",
9
+ "main": "./src/index.ts",
10
+ "types": "./src/index.ts",
11
+ "scripts": {
12
+ "check": "codecraft check --pack library"
13
+ }
14
+ }