@xenonbyte/da-vinci-workflow 0.2.4 → 0.2.5
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 +19 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/SKILL.md +45 -704
- package/docs/dv-command-reference.md +15 -3
- package/docs/prompt-entrypoints.md +1 -0
- package/docs/skill-contract-maintenance.md +14 -0
- package/docs/zh-CN/dv-command-reference.md +15 -3
- package/docs/zh-CN/prompt-entrypoints.md +1 -0
- package/lib/cli/helpers.js +43 -0
- package/lib/cli/lint-family.js +56 -0
- package/lib/cli/verify-family.js +79 -0
- package/lib/cli.js +45 -172
- package/lib/planning-parsers.js +8 -1
- package/lib/scaffold.js +454 -23
- package/lib/utils.js +19 -0
- package/lib/verify.js +1160 -88
- package/package.json +1 -1
- package/references/skill-workflow-detail.md +66 -0
package/lib/scaffold.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { STATUS } = require("./workflow-contract");
|
|
4
|
-
const { writeFileAtomic } = require("./utils");
|
|
4
|
+
const { writeFileAtomic, normalizeRelativePath, pathWithinRoot } = require("./utils");
|
|
5
5
|
const {
|
|
6
6
|
unique,
|
|
7
7
|
resolveChangeDir,
|
|
8
8
|
parseBindingsArtifact,
|
|
9
9
|
readChangeArtifacts,
|
|
10
|
-
readArtifactTexts
|
|
10
|
+
readArtifactTexts,
|
|
11
|
+
resolveImplementationLanding
|
|
11
12
|
} = require("./planning-parsers");
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
const FRAMEWORKS = ["next", "react", "vue", "svelte"];
|
|
15
|
+
const FRAMEWORK_EXTENSIONS = {
|
|
16
|
+
next: ".tsx",
|
|
17
|
+
react: ".tsx",
|
|
18
|
+
vue: ".vue",
|
|
19
|
+
svelte: ".svelte",
|
|
20
|
+
html: ".html"
|
|
21
|
+
};
|
|
22
|
+
const TARGET_SCAN_MAX_DEPTH = 6;
|
|
23
|
+
const TARGET_SCAN_MAX_FILES = 3000;
|
|
24
|
+
|
|
25
|
+
function sanitizeRoute(route) {
|
|
14
26
|
const normalized = String(route || "")
|
|
15
27
|
.trim()
|
|
16
28
|
.split(/[?#]/, 1)[0]
|
|
17
29
|
.replace(/\\/g, "/");
|
|
18
30
|
if (!normalized || normalized === "/") {
|
|
19
|
-
return
|
|
31
|
+
return {
|
|
32
|
+
safeSegments: [],
|
|
33
|
+
hasTraversal: false
|
|
34
|
+
};
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
const rawSegments = normalized.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
@@ -34,34 +49,327 @@ function sanitizeRouteToFileName(route) {
|
|
|
34
49
|
safeSegments.push(safeSegment || "index");
|
|
35
50
|
}
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
return {
|
|
53
|
+
safeSegments,
|
|
54
|
+
hasTraversal
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stripKnownFileExtension(segment) {
|
|
59
|
+
return String(segment || "").replace(/\.(html?|jsx?|tsx?|vue|svelte)$/i, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sanitizeRouteToHtmlFile(route) {
|
|
63
|
+
const parsed = sanitizeRoute(route);
|
|
64
|
+
if (parsed.hasTraversal) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (parsed.safeSegments.length === 0) {
|
|
41
68
|
return "index.html";
|
|
42
69
|
}
|
|
43
70
|
|
|
44
|
-
let relativePath = safeSegments.join("/");
|
|
71
|
+
let relativePath = parsed.safeSegments.join("/");
|
|
45
72
|
if (!path.extname(relativePath)) {
|
|
46
73
|
relativePath = `${relativePath}.html`;
|
|
47
74
|
}
|
|
48
|
-
|
|
75
|
+
return relativePath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function deriveFrameworkTargetPath(route, templateMode) {
|
|
79
|
+
if (!FRAMEWORKS.includes(templateMode)) {
|
|
80
|
+
return sanitizeRouteToHtmlFile(route);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const parsed = sanitizeRoute(route);
|
|
84
|
+
if (parsed.hasTraversal) {
|
|
49
85
|
return null;
|
|
50
86
|
}
|
|
51
|
-
|
|
87
|
+
|
|
88
|
+
const cleanedSegments = parsed.safeSegments
|
|
89
|
+
.map((segment) => stripKnownFileExtension(segment))
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
const extension = FRAMEWORK_EXTENSIONS[templateMode];
|
|
92
|
+
if (templateMode === "next") {
|
|
93
|
+
if (cleanedSegments.length === 0) {
|
|
94
|
+
return `app/page${extension}`;
|
|
95
|
+
}
|
|
96
|
+
return path.join("app", ...cleanedSegments, `page${extension}`);
|
|
97
|
+
}
|
|
98
|
+
if (templateMode === "react") {
|
|
99
|
+
if (cleanedSegments.length === 0) {
|
|
100
|
+
return `src/pages/index${extension}`;
|
|
101
|
+
}
|
|
102
|
+
return path.join("src", "pages", ...cleanedSegments, `index${extension}`);
|
|
103
|
+
}
|
|
104
|
+
if (templateMode === "vue") {
|
|
105
|
+
if (cleanedSegments.length === 0) {
|
|
106
|
+
return `src/pages/index${extension}`;
|
|
107
|
+
}
|
|
108
|
+
return path.join("src", "pages", `${cleanedSegments.join("/")}${extension}`);
|
|
109
|
+
}
|
|
110
|
+
if (templateMode === "svelte") {
|
|
111
|
+
if (cleanedSegments.length === 0) {
|
|
112
|
+
return "src/routes/+page.svelte";
|
|
113
|
+
}
|
|
114
|
+
return path.join("src", "routes", ...cleanedSegments, "+page.svelte");
|
|
115
|
+
}
|
|
116
|
+
return sanitizeRouteToHtmlFile(route);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collectPackageFrameworkSignals(projectRoot) {
|
|
120
|
+
const reasons = {
|
|
121
|
+
next: [],
|
|
122
|
+
react: [],
|
|
123
|
+
vue: [],
|
|
124
|
+
svelte: []
|
|
125
|
+
};
|
|
126
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
127
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
128
|
+
return reasons;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
134
|
+
} catch (_error) {
|
|
135
|
+
return reasons;
|
|
136
|
+
}
|
|
137
|
+
const allDeps = {
|
|
138
|
+
...(parsed.dependencies || {}),
|
|
139
|
+
...(parsed.devDependencies || {}),
|
|
140
|
+
...(parsed.peerDependencies || {})
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (allDeps.next) {
|
|
144
|
+
reasons.next.push("package:next");
|
|
145
|
+
}
|
|
146
|
+
if (allDeps.react || allDeps["react-dom"]) {
|
|
147
|
+
reasons.react.push("package:react");
|
|
148
|
+
}
|
|
149
|
+
if (allDeps.vue || allDeps.nuxt || allDeps["@vue/runtime-dom"]) {
|
|
150
|
+
reasons.vue.push("package:vue");
|
|
151
|
+
}
|
|
152
|
+
if (allDeps.svelte || allDeps["@sveltejs/kit"]) {
|
|
153
|
+
reasons.svelte.push("package:svelte");
|
|
154
|
+
}
|
|
155
|
+
return reasons;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function projectHasAnyFiles(projectRoot, options = {}) {
|
|
159
|
+
const {
|
|
160
|
+
roots = ["."],
|
|
161
|
+
extensions = [],
|
|
162
|
+
names = [],
|
|
163
|
+
maxDepth = TARGET_SCAN_MAX_DEPTH
|
|
164
|
+
} = options;
|
|
165
|
+
const extensionSet = new Set(extensions.map((value) => String(value || "").toLowerCase()));
|
|
166
|
+
const nameSet = new Set(names.map((value) => String(value || "").toLowerCase()));
|
|
167
|
+
const queue = [];
|
|
168
|
+
for (const root of roots) {
|
|
169
|
+
const absoluteRoot = path.resolve(projectRoot, root);
|
|
170
|
+
if (!pathWithinRoot(projectRoot, absoluteRoot) || !fs.existsSync(absoluteRoot)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
queue.push({
|
|
174
|
+
dir: absoluteRoot,
|
|
175
|
+
depth: 0
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const visited = new Set();
|
|
179
|
+
let scannedFiles = 0;
|
|
180
|
+
let scanLimitHit = false;
|
|
181
|
+
while (queue.length > 0) {
|
|
182
|
+
const current = queue.pop();
|
|
183
|
+
if (current.depth > maxDepth) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let resolvedCurrent;
|
|
188
|
+
try {
|
|
189
|
+
resolvedCurrent = fs.realpathSync(current.dir);
|
|
190
|
+
} catch (_error) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (visited.has(resolvedCurrent)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
visited.add(resolvedCurrent);
|
|
197
|
+
|
|
198
|
+
let entries = [];
|
|
199
|
+
try {
|
|
200
|
+
entries = fs.readdirSync(current.dir, { withFileTypes: true });
|
|
201
|
+
} catch (_error) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const absolutePath = path.join(current.dir, entry.name);
|
|
206
|
+
if (entry.isSymbolicLink()) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
queue.push({
|
|
211
|
+
dir: absolutePath,
|
|
212
|
+
depth: current.depth + 1
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!entry.isFile()) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
scannedFiles += 1;
|
|
220
|
+
if (scannedFiles > TARGET_SCAN_MAX_FILES) {
|
|
221
|
+
scanLimitHit = true;
|
|
222
|
+
return {
|
|
223
|
+
found: false,
|
|
224
|
+
scanLimitHit
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const normalizedName = entry.name.toLowerCase();
|
|
228
|
+
if (nameSet.has(normalizedName)) {
|
|
229
|
+
return {
|
|
230
|
+
found: true,
|
|
231
|
+
scanLimitHit
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const ext = path.extname(normalizedName);
|
|
235
|
+
if (extensionSet.has(ext)) {
|
|
236
|
+
return {
|
|
237
|
+
found: true,
|
|
238
|
+
scanLimitHit
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
found: false,
|
|
245
|
+
scanLimitHit
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function detectFramework(projectRoot) {
|
|
250
|
+
const reasons = collectPackageFrameworkSignals(projectRoot);
|
|
251
|
+
|
|
252
|
+
const nextConfigFiles = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
|
|
253
|
+
if (nextConfigFiles.some((file) => fs.existsSync(path.join(projectRoot, file)))) {
|
|
254
|
+
reasons.next.push("file:next.config");
|
|
255
|
+
}
|
|
256
|
+
const scanLimitSignals = [];
|
|
257
|
+
const nextAppSignal = projectHasAnyFiles(projectRoot, {
|
|
258
|
+
roots: ["app", "src/app"],
|
|
259
|
+
names: ["page.tsx", "page.jsx", "page.js", "page.ts"]
|
|
260
|
+
});
|
|
261
|
+
if (nextAppSignal.found) {
|
|
262
|
+
reasons.next.push("convention:app/page");
|
|
263
|
+
} else if (nextAppSignal.scanLimitHit) {
|
|
264
|
+
scanLimitSignals.push("next:convention:app/page");
|
|
265
|
+
}
|
|
266
|
+
const reactPagesSignal = projectHasAnyFiles(projectRoot, {
|
|
267
|
+
roots: ["src", "pages", "src/pages"],
|
|
268
|
+
extensions: [".tsx", ".jsx"]
|
|
269
|
+
});
|
|
270
|
+
if (reactPagesSignal.found) {
|
|
271
|
+
reasons.react.push("convention:tsx-jsx-pages");
|
|
272
|
+
} else if (reactPagesSignal.scanLimitHit) {
|
|
273
|
+
scanLimitSignals.push("react:convention:tsx-jsx-pages");
|
|
274
|
+
}
|
|
275
|
+
const vueSignal = projectHasAnyFiles(projectRoot, {
|
|
276
|
+
roots: ["src", "."],
|
|
277
|
+
extensions: [".vue"]
|
|
278
|
+
});
|
|
279
|
+
if (vueSignal.found) {
|
|
280
|
+
reasons.vue.push("convention:vue-files");
|
|
281
|
+
} else if (vueSignal.scanLimitHit) {
|
|
282
|
+
scanLimitSignals.push("vue:convention:vue-files");
|
|
283
|
+
}
|
|
284
|
+
const svelteSignal = projectHasAnyFiles(projectRoot, {
|
|
285
|
+
roots: ["src", "."],
|
|
286
|
+
extensions: [".svelte"]
|
|
287
|
+
});
|
|
288
|
+
if (svelteSignal.found) {
|
|
289
|
+
reasons.svelte.push("convention:svelte-files");
|
|
290
|
+
} else if (svelteSignal.scanLimitHit) {
|
|
291
|
+
scanLimitSignals.push("svelte:convention:svelte-files");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const candidates = FRAMEWORKS.filter((framework) => reasons[framework].length > 0);
|
|
295
|
+
const candidateSet = new Set(candidates);
|
|
296
|
+
if (candidateSet.has("next") && candidateSet.has("react")) {
|
|
297
|
+
// Next.js naturally implies React; treat this pair as Next.js rather than ambiguity.
|
|
298
|
+
candidateSet.delete("react");
|
|
299
|
+
}
|
|
300
|
+
const reducedCandidates = FRAMEWORKS.filter((framework) => candidateSet.has(framework));
|
|
301
|
+
|
|
302
|
+
if (reducedCandidates.length === 0) {
|
|
303
|
+
if (scanLimitSignals.length > 0) {
|
|
304
|
+
return {
|
|
305
|
+
mode: "ambiguous",
|
|
306
|
+
candidates: FRAMEWORKS,
|
|
307
|
+
reasons,
|
|
308
|
+
scanLimitSignals
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
mode: "unknown",
|
|
313
|
+
candidates: [],
|
|
314
|
+
reasons,
|
|
315
|
+
scanLimitSignals
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (reducedCandidates.length === 1) {
|
|
320
|
+
return {
|
|
321
|
+
mode: reducedCandidates[0],
|
|
322
|
+
candidates: reducedCandidates,
|
|
323
|
+
reasons,
|
|
324
|
+
scanLimitSignals
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
mode: "ambiguous",
|
|
330
|
+
candidates: reducedCandidates,
|
|
331
|
+
reasons,
|
|
332
|
+
scanLimitSignals
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function inferTemplateModeFromLanding(landingPath, frameworkDetection) {
|
|
337
|
+
const ext = path.extname(landingPath).toLowerCase();
|
|
338
|
+
const normalizedPath = normalizeRelativePath(landingPath);
|
|
339
|
+
|
|
340
|
+
if (ext === ".vue") {
|
|
341
|
+
return "vue";
|
|
342
|
+
}
|
|
343
|
+
if (ext === ".svelte") {
|
|
344
|
+
return "svelte";
|
|
345
|
+
}
|
|
346
|
+
if (ext === ".html") {
|
|
347
|
+
return "html";
|
|
348
|
+
}
|
|
349
|
+
if (ext === ".tsx" || ext === ".jsx" || ext === ".ts" || ext === ".js") {
|
|
350
|
+
if (/\/app\/.+\/page\.[tj]sx?$/i.test(normalizedPath) || /\/app\/page\.[tj]sx?$/i.test(normalizedPath)) {
|
|
351
|
+
return "next";
|
|
352
|
+
}
|
|
353
|
+
if (frameworkDetection.mode === "next") {
|
|
354
|
+
return "next";
|
|
355
|
+
}
|
|
356
|
+
return "react";
|
|
357
|
+
}
|
|
358
|
+
return "html";
|
|
52
359
|
}
|
|
53
360
|
|
|
54
|
-
function
|
|
361
|
+
function buildHtmlTemplate(mapping, metadata) {
|
|
55
362
|
return [
|
|
56
363
|
"<!doctype html>",
|
|
57
|
-
|
|
364
|
+
"<html lang=\"en\">",
|
|
58
365
|
"<head>",
|
|
59
|
-
|
|
60
|
-
|
|
366
|
+
" <meta charset=\"utf-8\" />",
|
|
367
|
+
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />",
|
|
61
368
|
` <title>${mapping.designPage || "Scaffold Page"}</title>`,
|
|
62
369
|
"</head>",
|
|
63
370
|
"<body>",
|
|
64
371
|
" <!-- TODO(scaffold): Replace placeholder skeleton with production implementation. -->",
|
|
372
|
+
` <!-- template-mode: ${metadata.templateMode} | framework-detection: ${metadata.frameworkMode} | source: ${metadata.source} -->`,
|
|
65
373
|
` <!-- mapping: ${mapping.implementation} -> ${mapping.designPage}${mapping.screenId ? ` (${mapping.screenId})` : ""} -->`,
|
|
66
374
|
" <main>",
|
|
67
375
|
" <section data-scaffold-region=\"hero\">TODO: implement hero region</section>",
|
|
@@ -74,10 +382,76 @@ function buildSkeletonDocument(mapping) {
|
|
|
74
382
|
].join("\n");
|
|
75
383
|
}
|
|
76
384
|
|
|
385
|
+
function buildReactTemplate(mapping, metadata) {
|
|
386
|
+
return [
|
|
387
|
+
"// TODO(scaffold): Replace this reviewable scaffold component with production implementation.",
|
|
388
|
+
`// template-mode: ${metadata.templateMode} | framework-detection: ${metadata.frameworkMode} | source: ${metadata.source}`,
|
|
389
|
+
`// mapping: ${mapping.implementation} -> ${mapping.designPage}${mapping.screenId ? ` (${mapping.screenId})` : ""}`,
|
|
390
|
+
"",
|
|
391
|
+
"export default function ScaffoldPage() {",
|
|
392
|
+
" return (",
|
|
393
|
+
" <main data-scaffold=\"reviewable-only\">",
|
|
394
|
+
" <section data-scaffold-region=\"hero\">TODO(scaffold): implement hero region</section>",
|
|
395
|
+
" <section data-scaffold-region=\"content\">TODO(scaffold): implement content regions</section>",
|
|
396
|
+
" <section data-scaffold-region=\"actions\">TODO(scaffold): implement action regions</section>",
|
|
397
|
+
" </main>",
|
|
398
|
+
" );",
|
|
399
|
+
"}",
|
|
400
|
+
""
|
|
401
|
+
].join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildVueTemplate(mapping, metadata) {
|
|
405
|
+
return [
|
|
406
|
+
"<template>",
|
|
407
|
+
" <main data-scaffold=\"reviewable-only\">",
|
|
408
|
+
" <section data-scaffold-region=\"hero\">TODO(scaffold): implement hero region</section>",
|
|
409
|
+
" <section data-scaffold-region=\"content\">TODO(scaffold): implement content regions</section>",
|
|
410
|
+
" <section data-scaffold-region=\"actions\">TODO(scaffold): implement action regions</section>",
|
|
411
|
+
" </main>",
|
|
412
|
+
"</template>",
|
|
413
|
+
"",
|
|
414
|
+
"<script setup>",
|
|
415
|
+
"// TODO(scaffold): Replace this reviewable scaffold component with production implementation.",
|
|
416
|
+
`// template-mode: ${metadata.templateMode} | framework-detection: ${metadata.frameworkMode} | source: ${metadata.source}`,
|
|
417
|
+
`// mapping: ${mapping.implementation} -> ${mapping.designPage}${mapping.screenId ? ` (${mapping.screenId})` : ""}`,
|
|
418
|
+
"</script>",
|
|
419
|
+
""
|
|
420
|
+
].join("\n");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildSvelteTemplate(mapping, metadata) {
|
|
424
|
+
return [
|
|
425
|
+
"<!-- TODO(scaffold): Replace this reviewable scaffold component with production implementation. -->",
|
|
426
|
+
`<!-- template-mode: ${metadata.templateMode} | framework-detection: ${metadata.frameworkMode} | source: ${metadata.source} -->`,
|
|
427
|
+
`<!-- mapping: ${mapping.implementation} -> ${mapping.designPage}${mapping.screenId ? ` (${mapping.screenId})` : ""} -->`,
|
|
428
|
+
"<main data-scaffold=\"reviewable-only\">",
|
|
429
|
+
" <section data-scaffold-region=\"hero\">TODO(scaffold): implement hero region</section>",
|
|
430
|
+
" <section data-scaffold-region=\"content\">TODO(scaffold): implement content regions</section>",
|
|
431
|
+
" <section data-scaffold-region=\"actions\">TODO(scaffold): implement action regions</section>",
|
|
432
|
+
"</main>",
|
|
433
|
+
""
|
|
434
|
+
].join("\n");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildScaffoldDocument(mapping, metadata) {
|
|
438
|
+
if (metadata.templateMode === "next" || metadata.templateMode === "react") {
|
|
439
|
+
return buildReactTemplate(mapping, metadata);
|
|
440
|
+
}
|
|
441
|
+
if (metadata.templateMode === "vue") {
|
|
442
|
+
return buildVueTemplate(mapping, metadata);
|
|
443
|
+
}
|
|
444
|
+
if (metadata.templateMode === "svelte") {
|
|
445
|
+
return buildSvelteTemplate(mapping, metadata);
|
|
446
|
+
}
|
|
447
|
+
return buildHtmlTemplate(mapping, metadata);
|
|
448
|
+
}
|
|
449
|
+
|
|
77
450
|
function scaffoldFromBindings(projectPathInput, options = {}) {
|
|
78
451
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
79
452
|
const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
|
|
80
453
|
const outputDir = path.resolve(options.outputDir || path.join(projectRoot, ".da-vinci", "scaffold"));
|
|
454
|
+
const frameworkDetection = detectFramework(projectRoot);
|
|
81
455
|
const result = {
|
|
82
456
|
status: STATUS.PASS,
|
|
83
457
|
failures: [],
|
|
@@ -87,7 +461,9 @@ function scaffoldFromBindings(projectPathInput, options = {}) {
|
|
|
87
461
|
changeId: null,
|
|
88
462
|
outputDir,
|
|
89
463
|
files: [],
|
|
90
|
-
|
|
464
|
+
frameworkDetection,
|
|
465
|
+
mvpScope:
|
|
466
|
+
"framework-aware reviewable scaffold templates only; generated output remains TODO-marked and non-final."
|
|
91
467
|
};
|
|
92
468
|
|
|
93
469
|
const resolved = resolveChangeDir(projectRoot, requestedChangeId);
|
|
@@ -113,11 +489,47 @@ function scaffoldFromBindings(projectPathInput, options = {}) {
|
|
|
113
489
|
return result;
|
|
114
490
|
}
|
|
115
491
|
|
|
492
|
+
if (frameworkDetection.mode === "unknown") {
|
|
493
|
+
result.warnings.push("Framework detection is unknown; scaffold falls back to HTML template mode when landing shape is not known.");
|
|
494
|
+
} else if (frameworkDetection.mode === "ambiguous") {
|
|
495
|
+
result.warnings.push(
|
|
496
|
+
`Framework detection is ambiguous (${frameworkDetection.candidates.join(", ")}); scaffold falls back to HTML template mode when landing shape is not known.`
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
result.notes.push(
|
|
500
|
+
`Framework detection selected ${frameworkDetection.mode} (${frameworkDetection.reasons[frameworkDetection.mode].join(", ")}).`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (Array.isArray(frameworkDetection.scanLimitSignals) && frameworkDetection.scanLimitSignals.length > 0) {
|
|
504
|
+
result.warnings.push(
|
|
505
|
+
`Framework detection hit scan limits (${TARGET_SCAN_MAX_FILES} files); unresolved probes: ${frameworkDetection.scanLimitSignals.join(", ")}.`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
116
509
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
117
510
|
const outputRoot = path.resolve(outputDir);
|
|
118
511
|
const outputRootPrefix = outputRoot.endsWith(path.sep) ? outputRoot : `${outputRoot}${path.sep}`;
|
|
119
512
|
for (const mapping of bindings.mappings) {
|
|
120
|
-
const
|
|
513
|
+
const landing = resolveImplementationLanding(projectRoot, mapping.implementation);
|
|
514
|
+
|
|
515
|
+
let templateMode = "html";
|
|
516
|
+
let source = "framework-fallback";
|
|
517
|
+
let targetRelativePath = null;
|
|
518
|
+
if (landing && pathWithinRoot(projectRoot, landing)) {
|
|
519
|
+
targetRelativePath = normalizeRelativePath(path.relative(projectRoot, landing));
|
|
520
|
+
templateMode = inferTemplateModeFromLanding(targetRelativePath, frameworkDetection);
|
|
521
|
+
source = "existing-landing";
|
|
522
|
+
} else {
|
|
523
|
+
if (frameworkDetection.mode === "unknown" || frameworkDetection.mode === "ambiguous") {
|
|
524
|
+
templateMode = "html";
|
|
525
|
+
source = frameworkDetection.mode === "unknown" ? "unknown-fallback" : "ambiguous-fallback";
|
|
526
|
+
} else {
|
|
527
|
+
templateMode = frameworkDetection.mode;
|
|
528
|
+
source = "framework-detected";
|
|
529
|
+
}
|
|
530
|
+
targetRelativePath = deriveFrameworkTargetPath(mapping.implementation, templateMode);
|
|
531
|
+
}
|
|
532
|
+
|
|
121
533
|
if (!targetRelativePath) {
|
|
122
534
|
result.failures.push(
|
|
123
535
|
`Blocked scaffold route \`${mapping.implementation}\`: traversal segments are not allowed.`
|
|
@@ -132,12 +544,22 @@ function scaffoldFromBindings(projectPathInput, options = {}) {
|
|
|
132
544
|
);
|
|
133
545
|
continue;
|
|
134
546
|
}
|
|
547
|
+
|
|
135
548
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
136
|
-
writeFileAtomic(
|
|
549
|
+
writeFileAtomic(
|
|
550
|
+
targetPath,
|
|
551
|
+
buildScaffoldDocument(mapping, {
|
|
552
|
+
templateMode,
|
|
553
|
+
frameworkMode: frameworkDetection.mode,
|
|
554
|
+
source
|
|
555
|
+
})
|
|
556
|
+
);
|
|
137
557
|
result.files.push({
|
|
138
558
|
mapping: mapping.implementation,
|
|
139
559
|
designPage: mapping.designPage,
|
|
140
|
-
path: targetPath
|
|
560
|
+
path: targetPath,
|
|
561
|
+
templateMode,
|
|
562
|
+
source
|
|
141
563
|
});
|
|
142
564
|
}
|
|
143
565
|
|
|
@@ -151,10 +573,10 @@ function scaffoldFromBindings(projectPathInput, options = {}) {
|
|
|
151
573
|
"Run `da-vinci verify-bindings`, `da-vinci verify-implementation`, and `da-vinci verify-structure` before accepting scaffold output."
|
|
152
574
|
);
|
|
153
575
|
result.notes.push(
|
|
154
|
-
"
|
|
576
|
+
"Known implementation landings keep precedence over framework defaults so scaffold targets match existing project shapes."
|
|
155
577
|
);
|
|
156
|
-
result.warnings = unique(result.warnings);
|
|
157
578
|
result.notes = unique(result.notes);
|
|
579
|
+
result.warnings = unique(result.warnings);
|
|
158
580
|
return result;
|
|
159
581
|
}
|
|
160
582
|
|
|
@@ -164,7 +586,8 @@ function formatScaffoldReport(result) {
|
|
|
164
586
|
`Project: ${result.projectRoot}`,
|
|
165
587
|
`Change: ${result.changeId || "(not selected)"}`,
|
|
166
588
|
`Status: ${result.status}`,
|
|
167
|
-
`Output dir: ${result.outputDir}
|
|
589
|
+
`Output dir: ${result.outputDir}`,
|
|
590
|
+
`Framework detection: ${result.frameworkDetection ? result.frameworkDetection.mode : "unknown"}`
|
|
168
591
|
];
|
|
169
592
|
if (result.failures.length > 0) {
|
|
170
593
|
lines.push("", "Failures:");
|
|
@@ -172,10 +595,18 @@ function formatScaffoldReport(result) {
|
|
|
172
595
|
lines.push(`- ${failure}`);
|
|
173
596
|
}
|
|
174
597
|
}
|
|
598
|
+
if (result.warnings.length > 0) {
|
|
599
|
+
lines.push("", "Warnings:");
|
|
600
|
+
for (const warning of result.warnings) {
|
|
601
|
+
lines.push(`- ${warning}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
175
604
|
if (result.files.length > 0) {
|
|
176
605
|
lines.push("", "Generated files:");
|
|
177
606
|
for (const file of result.files) {
|
|
178
|
-
lines.push(
|
|
607
|
+
lines.push(
|
|
608
|
+
`- ${file.path} (${file.mapping} -> ${file.designPage}; mode=${file.templateMode}; source=${file.source})`
|
|
609
|
+
);
|
|
179
610
|
}
|
|
180
611
|
}
|
|
181
612
|
if (result.notes.length > 0) {
|
package/lib/utils.js
CHANGED
|
@@ -28,6 +28,23 @@ function readTextIfExists(targetPath, options = {}) {
|
|
|
28
28
|
return fs.readFileSync(targetPath, encoding);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function normalizeRelativePath(relativePath) {
|
|
32
|
+
return String(relativePath || "")
|
|
33
|
+
.split(path.sep)
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join("/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pathWithinRoot(projectRoot, candidatePath) {
|
|
39
|
+
const root = path.resolve(projectRoot);
|
|
40
|
+
const candidate = path.resolve(candidatePath);
|
|
41
|
+
if (candidate === root) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
45
|
+
return candidate.startsWith(prefix);
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
function uniqueValues(values) {
|
|
32
49
|
return Array.from(new Set((values || []).filter(Boolean)));
|
|
33
50
|
}
|
|
@@ -120,6 +137,8 @@ module.exports = {
|
|
|
120
137
|
escapeRegExp,
|
|
121
138
|
pathExists,
|
|
122
139
|
readTextIfExists,
|
|
140
|
+
normalizeRelativePath,
|
|
141
|
+
pathWithinRoot,
|
|
123
142
|
uniqueValues,
|
|
124
143
|
parseJsonText,
|
|
125
144
|
readJsonFile,
|