scaffold-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +117 -0
  2. package/engine.project.example.json +23 -0
  3. package/package.json +49 -0
  4. package/scripts/postinstall.cjs +42 -0
  5. package/specs/catalogs/action-templates.yaml +189 -0
  6. package/specs/catalogs/child-templates.yaml +54 -0
  7. package/specs/catalogs/field-fragments.yaml +203 -0
  8. package/specs/catalogs/object-catalog.yaml +35 -0
  9. package/specs/catalogs/object-name-suggestions.yaml +30 -0
  10. package/specs/catalogs/object-templates.yaml +45 -0
  11. package/specs/catalogs/pattern-catalog.yaml +48 -0
  12. package/specs/catalogs/status-templates.yaml +16 -0
  13. package/specs/projects/crm-pilot/customer.yaml +122 -0
  14. package/specs/projects/crm-pilot/lead.from-nl.yaml +76 -0
  15. package/specs/projects/crm-pilot/lead.yaml +82 -0
  16. package/specs/projects/generated-from-nl/crm-customer.yaml +158 -0
  17. package/specs/projects/generated-from-nl/crm-lead.yaml +76 -0
  18. package/specs/projects/generated-from-nl/crm-opportunity.yaml +78 -0
  19. package/specs/projects/generated-from-nl/crm-quote.yaml +78 -0
  20. package/specs/projects/generated-from-nl/custom-documentLines.yaml +125 -0
  21. package/specs/projects/generated-from-nl/custom-treeEntity.yaml +78 -0
  22. package/specs/projects/generated-from-nl/erp-material-pattern-test.yaml +79 -0
  23. package/specs/projects/generated-from-nl/erp-material.yaml +78 -0
  24. package/specs/projects/generated-from-nl/hr-orgUnit.yaml +100 -0
  25. package/specs/projects/pattern-examples/document-lines-demo.yaml +125 -0
  26. package/specs/projects/pattern-examples/tree-entity-demo.yaml +79 -0
  27. package/specs/rules/business-model.schema.json +262 -0
  28. package/specs/rules/extension-boundaries.json +26 -0
  29. package/specs/rules/requirement-draft.schema.json +75 -0
  30. package/specs/rules/spec-governance.json +29 -0
  31. package/specs/templates/crm/customer.template.yaml +121 -0
  32. package/specs/templates/crm/lead.template.yaml +82 -0
  33. package/tools/analyze-requirement.cjs +950 -0
  34. package/tools/cli.cjs +59 -0
  35. package/tools/create-draft.cjs +18 -0
  36. package/tools/engine.cjs +47 -0
  37. package/tools/generate-draft.cjs +33 -0
  38. package/tools/generate-module.cjs +1218 -0
  39. package/tools/init-project.cjs +194 -0
  40. package/tools/lib/draft-toolkit.cjs +357 -0
  41. package/tools/lib/model-toolkit.cjs +482 -0
  42. package/tools/lib/pattern-renderers.cjs +166 -0
  43. package/tools/lib/renderers/detail-page-renderer.cjs +327 -0
  44. package/tools/lib/renderers/form-page-renderer.cjs +553 -0
  45. package/tools/lib/renderers/list-page-renderer.cjs +371 -0
  46. package/tools/lib/runtime-config.cjs +154 -0
  47. package/tools/patch-draft.cjs +57 -0
  48. package/tools/prompts/business-model-prompt.md +58 -0
  49. package/tools/run-requirement.cjs +672 -0
  50. package/tools/validate-draft.cjs +32 -0
  51. package/tools/validate-model.cjs +140 -0
  52. package/tools/verify-patterns.cjs +67 -0
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { loadRuntimeConfig } = require('./lib/model-toolkit.cjs');
5
+
6
+ function initProject(options = {}) {
7
+ const projectRoot = path.resolve(options.projectRoot || process.cwd());
8
+ const force = Boolean(options.force);
9
+ const withCursorRule = options.withCursorRule !== false;
10
+ const runtimeConfig = loadRuntimeConfig();
11
+
12
+ const configPath = path.join(projectRoot, 'scaffold.engine.json');
13
+ const bridgeDir = path.join(projectRoot, '.scaffold', 'bin');
14
+ const bridgePath = path.join(bridgeDir, 'scaffold-engine.cjs');
15
+ const directories = [
16
+ path.join(projectRoot, '.codex', 'scaffold-drafts'),
17
+ path.join(projectRoot, '.codex', 'generated-specs'),
18
+ bridgeDir,
19
+ path.join(projectRoot, 'src', 'generated', 'api'),
20
+ path.join(projectRoot, 'src', 'generated', 'admin'),
21
+ ];
22
+
23
+ for (const dirPath of directories) {
24
+ fs.mkdirSync(dirPath, { recursive: true });
25
+ }
26
+
27
+ const config = {
28
+ projectRoot,
29
+ catalogs: {
30
+ rootDir: runtimeConfig.catalogs.rootDir,
31
+ },
32
+ rules: {
33
+ rootDir: runtimeConfig.rules.rootDir,
34
+ },
35
+ drafts: {
36
+ rootDir: path.join(projectRoot, '.codex', 'scaffold-drafts'),
37
+ },
38
+ generatedSpecs: {
39
+ rootDir: path.join(projectRoot, '.codex', 'generated-specs'),
40
+ },
41
+ targets: {
42
+ adapter: 'external-project',
43
+ apiSrcDir: path.join(projectRoot, 'src', 'generated', 'api'),
44
+ adminSrcDir: path.join(projectRoot, 'src', 'generated', 'admin'),
45
+ toolCwd: runtimeConfig.scaffoldRoot,
46
+ pnpmCwd: path.resolve(runtimeConfig.scaffoldRoot, '..'),
47
+ apiPackageDir: runtimeConfig.targets.apiPackageDir,
48
+ },
49
+ };
50
+
51
+ writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, force);
52
+ writeFile(bridgePath, buildBridgeScript(runtimeConfig.scaffoldRoot), force);
53
+
54
+ let cursorRulePath = '';
55
+ if (withCursorRule) {
56
+ cursorRulePath = path.join(projectRoot, '.cursor', 'rules', 'scaffold-engine.mdc');
57
+ fs.mkdirSync(path.dirname(cursorRulePath), { recursive: true });
58
+ writeFile(cursorRulePath, buildCursorRule(), force);
59
+ }
60
+
61
+ return {
62
+ projectRoot,
63
+ configPath,
64
+ bridgePath,
65
+ directories,
66
+ cursorRulePath: cursorRulePath || null,
67
+ };
68
+ }
69
+
70
+ function writeFile(filePath, content, force) {
71
+ if (fs.existsSync(filePath) && !force) {
72
+ return;
73
+ }
74
+ fs.writeFileSync(filePath, content, 'utf8');
75
+ }
76
+
77
+ function buildCursorRule() {
78
+ return `---
79
+ description: Automatically use scaffold-engine for business module requirements in this project.
80
+ globs:
81
+ alwaysApply: true
82
+ ---
83
+
84
+ When the user asks for a new business object/module/system capability in this project, do not ask them to spell out tool steps.
85
+
86
+ Use the local bridge command:
87
+
88
+ - \`node ./.scaffold/bin/scaffold-engine.cjs ...\`
89
+
90
+ Use the project config:
91
+
92
+ - \`./scaffold.engine.json\`
93
+
94
+ Default behavior:
95
+
96
+ 1. Treat a plain natural-language requirement as an engine request.
97
+ 2. First run analyze.
98
+ 3. Then create a draft automatically.
99
+ 4. If the requirement is incomplete, summarize the missing information and show the draft id.
100
+ 5. If the draft is valid enough, run:
101
+ - \`draft:validate\`
102
+ - \`draft:generate --dry-run\`
103
+ 6. Do not do real generation until the user explicitly confirms.
104
+
105
+ The user should only need to say things like:
106
+
107
+ - “做一个员工管理,支持新建、编辑、列表、详情”
108
+ - “做一个组织树”
109
+ - “做一个候选人管理,包含跟进记录”
110
+
111
+ Always explain:
112
+
113
+ - recognized pattern
114
+ - candidate object
115
+ - draft id
116
+ - validation result
117
+ - dry-run target paths
118
+ `;
119
+ }
120
+
121
+ function buildBridgeScript(scaffoldRoot) {
122
+ const cliPath = path.join(scaffoldRoot, 'tools', 'cli.cjs');
123
+ const escapedCliPath = JSON.stringify(cliPath);
124
+ return `#!/usr/bin/env node
125
+ const path = require('path');
126
+ const { createRequire } = require('module');
127
+ const { spawnSync } = require('child_process');
128
+
129
+ const projectRoot = path.resolve(__dirname, '..', '..');
130
+ const configPath = path.join(projectRoot, 'scaffold.engine.json');
131
+ const fallbackCliPath = ${escapedCliPath};
132
+ const projectRequire = createRequire(path.join(projectRoot, 'package.json'));
133
+
134
+ let cliPath = fallbackCliPath;
135
+ try {
136
+ cliPath = projectRequire.resolve('scaffold-engine/cli');
137
+ } catch (error) {
138
+ cliPath = fallbackCliPath;
139
+ }
140
+
141
+ const args = process.argv.slice(2);
142
+ const hasConfig = args.includes('--config');
143
+ const nextArgs = hasConfig ? args : [...args, '--config', configPath];
144
+
145
+ const result = spawnSync(process.execPath, [cliPath, ...nextArgs], {
146
+ cwd: projectRoot,
147
+ stdio: 'inherit',
148
+ });
149
+
150
+ process.exit(result.status ?? 1);
151
+ `;
152
+ }
153
+
154
+ function parseArgs(argv) {
155
+ const options = {
156
+ projectRoot: '',
157
+ force: false,
158
+ withCursorRule: true,
159
+ };
160
+
161
+ for (let index = 0; index < argv.length; index += 1) {
162
+ const item = argv[index];
163
+ if (item === '--project-root') {
164
+ options.projectRoot = argv[index + 1] || '';
165
+ index += 1;
166
+ continue;
167
+ }
168
+ if (item === '--force') {
169
+ options.force = true;
170
+ continue;
171
+ }
172
+ if (item === '--no-cursor-rule') {
173
+ options.withCursorRule = false;
174
+ continue;
175
+ }
176
+ }
177
+
178
+ return options;
179
+ }
180
+
181
+ if (require.main === module) {
182
+ try {
183
+ const result = initProject(parseArgs(process.argv.slice(2)));
184
+ console.log(JSON.stringify(result, null, 2));
185
+ } catch (error) {
186
+ console.error(`\n❌ 项目初始化失败: ${error.message}`);
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ module.exports = {
192
+ initProject,
193
+ parseArgs,
194
+ };
@@ -0,0 +1,357 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const YAML = require('yaml');
7
+ const { loadRuntimeConfig, readJson, resolveScaffoldPath, validateModelFile } = require('./model-toolkit.cjs');
8
+ const { resolveDraftRoot, resolveRulePath } = require('./runtime-config.cjs');
9
+
10
+ function createDraft(requirement, analysis) {
11
+ const now = new Date().toISOString();
12
+ const decision = analysis?.decision || 'need_more_info';
13
+ return {
14
+ contractVersion: '1.0.0',
15
+ kind: 'requirementDraft',
16
+ id: buildDraftId(),
17
+ status: mapDraftStatus(decision),
18
+ rawRequirement: requirement,
19
+ analysis,
20
+ draftModel: analysis?.suggestedModel || null,
21
+ validation: null,
22
+ generation: null,
23
+ storage: null,
24
+ history: [
25
+ {
26
+ event: 'draft.created',
27
+ timestamp: now,
28
+ summary: `由 analyze 结果创建草稿,decision=${decision}`,
29
+ },
30
+ ],
31
+ };
32
+ }
33
+
34
+ function patchDraft(draft, patch) {
35
+ const now = new Date().toISOString();
36
+ const nextDraftModel = deepMerge(draft.draftModel || {}, patch || {});
37
+ return {
38
+ ...draft,
39
+ status: 'clarifying',
40
+ draftModel: nextDraftModel,
41
+ validation: null,
42
+ generation: null,
43
+ storage: draft.storage || null,
44
+ history: [
45
+ ...(draft.history || []),
46
+ {
47
+ event: 'draft.patched',
48
+ timestamp: now,
49
+ summary: buildPatchSummary(patch),
50
+ },
51
+ ],
52
+ };
53
+ }
54
+
55
+ function validateDraft(draft, options = {}) {
56
+ if (!draft?.draftModel) {
57
+ throw new Error('草稿中不存在 draftModel,无法执行校验');
58
+ }
59
+
60
+ const runtimeConfig = loadRuntimeConfig(options);
61
+ const result = withTempSpec(draft.draftModel, (specPath) => validateModelFile(specPath, runtimeConfig));
62
+ const report = buildDraftValidationReport(result);
63
+ return {
64
+ ...draft,
65
+ status: report.status === 'passed' ? 'validated' : 'clarifying',
66
+ validation: report,
67
+ storage: draft.storage || null,
68
+ history: [
69
+ ...(draft.history || []),
70
+ {
71
+ event: 'draft.validated',
72
+ timestamp: new Date().toISOString(),
73
+ summary: `执行草稿校验,status=${report.status}`,
74
+ },
75
+ ],
76
+ };
77
+ }
78
+
79
+ function generateDraft(draft, options = {}) {
80
+ if (!draft?.draftModel) {
81
+ throw new Error('草稿中不存在 draftModel,无法执行生成');
82
+ }
83
+
84
+ const runtimeConfig = loadRuntimeConfig(options);
85
+ const validatedDraft = validateDraft(draft, runtimeConfig);
86
+ if (validatedDraft.validation?.status !== 'passed') {
87
+ throw new Error('草稿校验未通过,无法执行生成');
88
+ }
89
+
90
+ const result = withTempSpec(draft.draftModel, (specPath) => {
91
+ const args = [path.join(resolveScaffoldPath('tools'), 'generate-module.cjs'), specPath];
92
+ if (options.force !== false) {
93
+ args.push('--force');
94
+ }
95
+ if (options.dryRun) {
96
+ args.push('--dry-run');
97
+ }
98
+ if (options.configPath) {
99
+ args.push('--config', path.resolve(options.configPath));
100
+ }
101
+
102
+ return spawnSync(process.execPath, args, {
103
+ cwd: runtimeConfig.targets.toolCwd,
104
+ encoding: 'utf8',
105
+ });
106
+ });
107
+
108
+ if (result.status !== 0) {
109
+ throw new Error(result.stderr || result.stdout || '草稿生成失败');
110
+ }
111
+
112
+ const generatedFiles = String(result.stdout || '')
113
+ .split('\n')
114
+ .map((line) => line.trim())
115
+ .filter((line) => line.startsWith('- ['))
116
+ .map((line) => line.replace(/^- \[[^\]]+\]\s+/, ''));
117
+
118
+ return {
119
+ ...validatedDraft,
120
+ status: 'generated',
121
+ generation: {
122
+ contractVersion: '1.0.0',
123
+ stage: 'generateDraft',
124
+ status: 'passed',
125
+ generatedFiles,
126
+ dryRun: Boolean(options.dryRun),
127
+ },
128
+ storage: validatedDraft.storage || null,
129
+ history: [
130
+ ...(validatedDraft.history || []),
131
+ {
132
+ event: 'draft.generated',
133
+ timestamp: new Date().toISOString(),
134
+ summary: `执行草稿生成,files=${generatedFiles.length}`,
135
+ },
136
+ ],
137
+ };
138
+ }
139
+
140
+ function buildDraftId() {
141
+ return `draft_${crypto.randomBytes(6).toString('hex')}`;
142
+ }
143
+
144
+ function getDefaultDraftRoot() {
145
+ return resolveDraftRoot(loadRuntimeConfig());
146
+ }
147
+
148
+ function saveDraft(draft, options = {}) {
149
+ if (!draft || !draft.id) {
150
+ throw new Error('保存 draft 失败:缺少有效 draft.id');
151
+ }
152
+
153
+ const draftRoot = path.resolve(options.draftRoot || getDefaultDraftRoot());
154
+ fs.mkdirSync(draftRoot, { recursive: true });
155
+
156
+ const fileName = options.fileName || buildDraftFileName(draft);
157
+ const absolutePath = path.join(draftRoot, fileName);
158
+ const nextDraft = {
159
+ ...draft,
160
+ storage: {
161
+ draftRoot,
162
+ fileName,
163
+ absolutePath,
164
+ savedAt: new Date().toISOString(),
165
+ },
166
+ };
167
+
168
+ fs.writeFileSync(absolutePath, `${JSON.stringify(nextDraft, null, 2)}\n`, 'utf8');
169
+ return nextDraft;
170
+ }
171
+
172
+ function loadDraft(reference, options = {}) {
173
+ const absolutePath = resolveDraftReference(reference, options);
174
+ return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
175
+ }
176
+
177
+ function resolveDraftReference(reference, options = {}) {
178
+ if (!reference || !String(reference).trim()) {
179
+ throw new Error('请提供 draft 引用,可以是 draft 路径或 draft id');
180
+ }
181
+
182
+ const value = String(reference).trim();
183
+ if (fs.existsSync(value) && fs.statSync(value).isFile()) {
184
+ return path.resolve(value);
185
+ }
186
+
187
+ if (value.includes(path.sep) || value.endsWith('.json')) {
188
+ const absolutePath = path.resolve(value);
189
+ if (!fs.existsSync(absolutePath)) {
190
+ throw new Error(`未找到 draft 文件: ${absolutePath}`);
191
+ }
192
+ return absolutePath;
193
+ }
194
+
195
+ const draftRoot = path.resolve(options.draftRoot || getDefaultDraftRoot());
196
+ if (!fs.existsSync(draftRoot)) {
197
+ throw new Error(`未找到 draft 目录: ${draftRoot}`);
198
+ }
199
+
200
+ const directPath = path.join(draftRoot, value);
201
+ if (fs.existsSync(directPath) && fs.statSync(directPath).isFile()) {
202
+ return directPath;
203
+ }
204
+
205
+ const directJsonPath = path.join(draftRoot, `${value}.json`);
206
+ if (fs.existsSync(directJsonPath) && fs.statSync(directJsonPath).isFile()) {
207
+ return directJsonPath;
208
+ }
209
+
210
+ const matches = fs
211
+ .readdirSync(draftRoot)
212
+ .filter((fileName) => fileName.startsWith(value) && fileName.endsWith('.json'))
213
+ .sort();
214
+
215
+ if (matches.length === 1) {
216
+ return path.join(draftRoot, matches[0]);
217
+ }
218
+ if (matches.length > 1) {
219
+ throw new Error(`找到多个匹配的 draft,请改用完整路径或文件名: ${matches.join(', ')}`);
220
+ }
221
+
222
+ throw new Error(`未找到 draft 引用: ${value}`);
223
+ }
224
+
225
+ function mapDraftStatus(decision) {
226
+ switch (decision) {
227
+ case 'extendable':
228
+ return 'analyzed';
229
+ case 'need_more_info':
230
+ return 'clarifying';
231
+ default:
232
+ return 'failed';
233
+ }
234
+ }
235
+
236
+ function loadDraftSchema() {
237
+ const runtimeConfig = loadRuntimeConfig();
238
+ return readJson(resolveRulePath(runtimeConfig, 'requirementDraftSchema'));
239
+ }
240
+
241
+ function buildDraftFileName(draft) {
242
+ const slug = slugifyDraft(draft);
243
+ return `${draft.id}--${slug}.json`;
244
+ }
245
+
246
+ function withTempSpec(model, runner) {
247
+ const tempPath = path.join(
248
+ os.tmpdir(),
249
+ `scaffold-draft-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.yaml`
250
+ );
251
+ fs.writeFileSync(tempPath, YAML.stringify(model), 'utf8');
252
+ try {
253
+ return runner(tempPath);
254
+ } finally {
255
+ if (fs.existsSync(tempPath)) {
256
+ fs.rmSync(tempPath, { force: true });
257
+ }
258
+ }
259
+ }
260
+
261
+ function buildDraftValidationReport(result) {
262
+ const schemaErrors = result.schemaErrors || [];
263
+ const boundaryErrors = result.boundaryErrors || [];
264
+ const governanceErrors = result.governanceErrors || [];
265
+ const warnings = result.warnings || [];
266
+ return {
267
+ contractVersion: '1.0.0',
268
+ stage: 'validateDraft',
269
+ status: result.valid ? 'passed' : 'failed',
270
+ nextAction: result.valid ? 'generate' : 'clarify',
271
+ scope: result.location?.scope || 'temporaryDraft',
272
+ object: {
273
+ domain: result.model?.meta?.domain || '',
274
+ object: result.model?.meta?.object || '',
275
+ label: result.model?.meta?.label || '',
276
+ pattern: result.model?.meta?.pattern || '',
277
+ },
278
+ stages: [
279
+ { stage: 'schema', status: schemaErrors.length === 0 ? 'passed' : 'failed', errors: schemaErrors },
280
+ { stage: 'boundary', status: boundaryErrors.length === 0 ? 'passed' : 'failed', errors: boundaryErrors },
281
+ { stage: 'governance', status: governanceErrors.length === 0 ? 'passed' : 'failed', errors: governanceErrors },
282
+ ],
283
+ errors: [
284
+ ...schemaErrors.map((message) => ({ stage: 'schema', message })),
285
+ ...boundaryErrors.map((message) => ({ stage: 'boundary', message })),
286
+ ...governanceErrors.map((message) => ({ stage: 'governance', message })),
287
+ ],
288
+ warnings,
289
+ };
290
+ }
291
+
292
+ function deepMerge(base, patch) {
293
+ if (Array.isArray(patch)) {
294
+ return patch.map((item) => cloneValue(item));
295
+ }
296
+ if (!isObject(base) || !isObject(patch)) {
297
+ return cloneValue(patch);
298
+ }
299
+
300
+ const next = { ...base };
301
+ for (const [key, value] of Object.entries(patch)) {
302
+ if (Array.isArray(value)) {
303
+ next[key] = value.map((item) => cloneValue(item));
304
+ continue;
305
+ }
306
+ if (isObject(value) && isObject(base[key])) {
307
+ next[key] = deepMerge(base[key], value);
308
+ continue;
309
+ }
310
+ next[key] = cloneValue(value);
311
+ }
312
+ return next;
313
+ }
314
+
315
+ function cloneValue(value) {
316
+ if (value === undefined) {
317
+ return undefined;
318
+ }
319
+ return JSON.parse(JSON.stringify(value));
320
+ }
321
+
322
+ function isObject(value) {
323
+ return value && typeof value === 'object' && !Array.isArray(value);
324
+ }
325
+
326
+ function buildPatchSummary(patch) {
327
+ if (!patch || !isObject(patch)) {
328
+ return '未提供有效 patch,草稿未变更';
329
+ }
330
+ return `更新草稿片段: ${Object.keys(patch).join(', ')}`;
331
+ }
332
+
333
+ function slugifyDraft(draft) {
334
+ const rawValue =
335
+ draft?.draftModel?.meta?.object ||
336
+ draft?.analysis?.candidate?.object ||
337
+ draft?.analysis?.inferred?.object ||
338
+ draft?.analysis?.candidate?.label ||
339
+ 'draft';
340
+ const normalized = String(rawValue)
341
+ .replace(/[^a-zA-Z0-9]+/g, '-')
342
+ .replace(/^-+|-+$/g, '')
343
+ .toLowerCase();
344
+ return normalized || 'draft';
345
+ }
346
+
347
+ module.exports = {
348
+ createDraft,
349
+ generateDraft,
350
+ getDefaultDraftRoot,
351
+ loadDraft,
352
+ loadDraftSchema,
353
+ patchDraft,
354
+ resolveDraftReference,
355
+ saveDraft,
356
+ validateDraft,
357
+ };