@xenonbyte/da-vinci-workflow 0.1.21 → 0.1.23
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 +33 -0
- package/README.md +25 -1
- package/README.zh-CN.md +25 -1
- package/docs/constraint-files.md +109 -0
- package/docs/dv-command-reference.md +17 -0
- package/docs/workflow-examples.md +29 -13
- package/docs/workflow-overview.md +2 -0
- package/docs/zh-CN/constraint-files.md +111 -0
- package/docs/zh-CN/dv-command-reference.md +17 -0
- package/docs/zh-CN/workflow-examples.md +29 -13
- package/docs/zh-CN/workflow-overview.md +2 -0
- package/examples/greenfield-spec-markupflow/DA-VINCI.md +9 -0
- package/examples/greenfield-spec-markupflow/README.md +7 -0
- package/examples/greenfield-spec-markupflow/pencil-design.md +5 -0
- package/lib/audit-parsers.js +452 -0
- package/lib/audit.js +102 -448
- package/lib/cli.js +188 -1
- package/lib/fs-safety.js +116 -0
- package/lib/icon-aliases.js +147 -0
- package/lib/icon-search.js +353 -0
- package/lib/icon-sync.js +361 -0
- package/lib/icon-text.js +27 -0
- package/lib/install.js +18 -10
- package/lib/pencil-preflight.js +167 -18
- package/package.json +6 -2
- package/references/artifact-templates.md +24 -0
- package/references/icon-aliases.example.json +12 -0
- package/scripts/fixtures/mock-pencil.js +49 -0
- package/scripts/test-audit-safety.js +92 -0
- package/scripts/test-icon-aliases.js +96 -0
- package/scripts/test-icon-search.js +77 -0
- package/scripts/test-icon-sync.js +178 -0
- package/scripts/test-pen-persistence.js +7 -3
- package/scripts/test-pencil-preflight.js +16 -0
package/lib/icon-sync.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
MATERIAL_ROUNDED,
|
|
7
|
+
MATERIAL_OUTLINED,
|
|
8
|
+
MATERIAL_SHARP
|
|
9
|
+
} = require("./icon-search");
|
|
10
|
+
|
|
11
|
+
const MATERIAL_METADATA_URL = "https://fonts.google.com/metadata/icons";
|
|
12
|
+
const LUCIDE_TREE_URL = "https://api.github.com/repos/lucide-icons/lucide/git/trees/main?recursive=1";
|
|
13
|
+
const FEATHER_TREE_URL = "https://api.github.com/repos/feathericons/feather/git/trees/main?recursive=1";
|
|
14
|
+
const PHOSPHOR_TREE_URL = "https://api.github.com/repos/phosphor-icons/core/git/trees/main?recursive=1";
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 20000;
|
|
16
|
+
|
|
17
|
+
function toBoolean(value) {
|
|
18
|
+
if (value === true || value === false) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
const normalized = value.trim().toLowerCase();
|
|
23
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return Boolean(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureDir(filePath) {
|
|
34
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeName(name) {
|
|
38
|
+
return String(name || "").trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeIconRecord(family, name) {
|
|
42
|
+
const normalized = normalizeName(name);
|
|
43
|
+
if (!family || !normalized) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
family,
|
|
48
|
+
name: normalized,
|
|
49
|
+
semantic: normalized.replace(/[_-]+/g, " "),
|
|
50
|
+
tags: []
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseGoogleMaterialMetadata(raw) {
|
|
55
|
+
const text = String(raw || "");
|
|
56
|
+
const objectStart = text.indexOf("{");
|
|
57
|
+
if (objectStart < 0) {
|
|
58
|
+
throw new Error("Material metadata payload is not valid JSON.");
|
|
59
|
+
}
|
|
60
|
+
const parsed = JSON.parse(text.slice(objectStart));
|
|
61
|
+
const icons = Array.isArray(parsed.icons) ? parsed.icons : [];
|
|
62
|
+
const roundedFamilyKey = "Material Icons Round";
|
|
63
|
+
const outlinedFamilyKey = "Material Icons Outlined";
|
|
64
|
+
const sharpFamilyKey = "Material Icons Sharp";
|
|
65
|
+
|
|
66
|
+
return icons.flatMap((item) => {
|
|
67
|
+
const name = normalizeName(item && item.name);
|
|
68
|
+
if (!name) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const unsupported = new Set(Array.isArray(item.unsupported_families) ? item.unsupported_families : []);
|
|
72
|
+
const records = [];
|
|
73
|
+
|
|
74
|
+
if (!unsupported.has(roundedFamilyKey)) {
|
|
75
|
+
records.push(makeIconRecord(MATERIAL_ROUNDED, name));
|
|
76
|
+
}
|
|
77
|
+
if (!unsupported.has(outlinedFamilyKey)) {
|
|
78
|
+
records.push(makeIconRecord(MATERIAL_OUTLINED, name));
|
|
79
|
+
}
|
|
80
|
+
if (!unsupported.has(sharpFamilyKey)) {
|
|
81
|
+
records.push(makeIconRecord(MATERIAL_SHARP, name));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return records.filter(Boolean);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseGitHubTreeIcons(raw, options) {
|
|
89
|
+
const { family, prefix, suffix } = options;
|
|
90
|
+
const parsed = JSON.parse(String(raw || ""));
|
|
91
|
+
const tree = Array.isArray(parsed.tree) ? parsed.tree : [];
|
|
92
|
+
|
|
93
|
+
return tree
|
|
94
|
+
.flatMap((node) => {
|
|
95
|
+
const nodePath = normalizeName(node && node.path);
|
|
96
|
+
if (!nodePath || !nodePath.startsWith(prefix) || !nodePath.endsWith(suffix)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const name = nodePath.slice(prefix.length, nodePath.length - suffix.length);
|
|
100
|
+
if (!name || name.includes("/")) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const record = makeIconRecord(family, name);
|
|
104
|
+
return record ? [record] : [];
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function dedupeIconRecords(records) {
|
|
109
|
+
const map = new Map();
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
if (!record) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = `${record.family}::${record.name}`;
|
|
115
|
+
if (!map.has(key)) {
|
|
116
|
+
map.set(key, record);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Array.from(map.values());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function summarizeSourceResults(sourceResults = {}) {
|
|
123
|
+
const entries = Object.values(sourceResults);
|
|
124
|
+
const total = entries.length;
|
|
125
|
+
const errorCount = entries.filter((source) => source && source.status === "error").length;
|
|
126
|
+
const okCount = entries.filter((source) => source && source.status === "ok").length;
|
|
127
|
+
return {
|
|
128
|
+
total,
|
|
129
|
+
okCount,
|
|
130
|
+
errorCount,
|
|
131
|
+
degraded: errorCount > 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fetchText(url, options = {}) {
|
|
136
|
+
const timeoutMs = Number.isFinite(Number(options.timeoutMs))
|
|
137
|
+
? Number(options.timeoutMs)
|
|
138
|
+
: DEFAULT_TIMEOUT_MS;
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const request = https.get(
|
|
142
|
+
url,
|
|
143
|
+
{
|
|
144
|
+
headers: {
|
|
145
|
+
"User-Agent": "da-vinci-workflow/icon-sync",
|
|
146
|
+
Accept: "application/json,text/plain,*/*"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
(response) => {
|
|
150
|
+
let data = "";
|
|
151
|
+
response.setEncoding("utf8");
|
|
152
|
+
response.on("data", (chunk) => {
|
|
153
|
+
data += chunk;
|
|
154
|
+
});
|
|
155
|
+
response.on("end", () => {
|
|
156
|
+
if (response.statusCode && response.statusCode >= 400) {
|
|
157
|
+
reject(new Error(`HTTP ${response.statusCode} for ${url}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolve(data);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
request.on("error", (error) => {
|
|
166
|
+
reject(error);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
request.setTimeout(timeoutMs, () => {
|
|
170
|
+
request.destroy(new Error(`Request timeout after ${timeoutMs}ms for ${url}`));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getDefaultCatalogPath(homeDir) {
|
|
176
|
+
const root = homeDir ? path.resolve(homeDir) : os.homedir();
|
|
177
|
+
return path.join(root, ".da-vinci", "icon-catalog.json");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveCatalogPath(options = {}) {
|
|
181
|
+
if (options.catalogPath) {
|
|
182
|
+
return path.resolve(options.catalogPath);
|
|
183
|
+
}
|
|
184
|
+
if (options.outputPath) {
|
|
185
|
+
return path.resolve(options.outputPath);
|
|
186
|
+
}
|
|
187
|
+
return path.resolve(getDefaultCatalogPath(options.homeDir));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function loadIconCatalog(options = {}) {
|
|
191
|
+
const catalogPath = resolveCatalogPath(options);
|
|
192
|
+
if (!fs.existsSync(catalogPath)) {
|
|
193
|
+
return {
|
|
194
|
+
catalogPath,
|
|
195
|
+
catalog: null
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const parsed = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
200
|
+
if (!parsed || !Array.isArray(parsed.icons)) {
|
|
201
|
+
throw new Error(`Invalid icon catalog format: ${catalogPath}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
catalogPath,
|
|
206
|
+
catalog: parsed
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function syncIconCatalog(options = {}) {
|
|
211
|
+
const catalogPath = resolveCatalogPath(options);
|
|
212
|
+
const timeoutMs = Number.isFinite(Number(options.timeoutMs))
|
|
213
|
+
? Number(options.timeoutMs)
|
|
214
|
+
: DEFAULT_TIMEOUT_MS;
|
|
215
|
+
const strict = toBoolean(options.strict);
|
|
216
|
+
const fetchTextImpl = typeof options.fetchText === "function" ? options.fetchText : fetchText;
|
|
217
|
+
|
|
218
|
+
const sourceSpecs = [
|
|
219
|
+
{
|
|
220
|
+
key: "material",
|
|
221
|
+
url: MATERIAL_METADATA_URL,
|
|
222
|
+
parse: parseGoogleMaterialMetadata
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
key: "lucide",
|
|
226
|
+
url: LUCIDE_TREE_URL,
|
|
227
|
+
parse: (raw) =>
|
|
228
|
+
parseGitHubTreeIcons(raw, {
|
|
229
|
+
family: "lucide",
|
|
230
|
+
prefix: "icons/",
|
|
231
|
+
suffix: ".json"
|
|
232
|
+
})
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
key: "feather",
|
|
236
|
+
url: FEATHER_TREE_URL,
|
|
237
|
+
parse: (raw) =>
|
|
238
|
+
parseGitHubTreeIcons(raw, {
|
|
239
|
+
family: "feather",
|
|
240
|
+
prefix: "icons/",
|
|
241
|
+
suffix: ".svg"
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
key: "phosphor",
|
|
246
|
+
url: PHOSPHOR_TREE_URL,
|
|
247
|
+
parse: (raw) =>
|
|
248
|
+
parseGitHubTreeIcons(raw, {
|
|
249
|
+
family: "phosphor",
|
|
250
|
+
prefix: "assets/regular/",
|
|
251
|
+
suffix: ".svg"
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const sourceResults = {};
|
|
257
|
+
const collected = [];
|
|
258
|
+
|
|
259
|
+
await Promise.all(
|
|
260
|
+
sourceSpecs.map(async (spec) => {
|
|
261
|
+
try {
|
|
262
|
+
const raw = await fetchTextImpl(spec.url, {
|
|
263
|
+
timeoutMs
|
|
264
|
+
});
|
|
265
|
+
const records = spec.parse(raw);
|
|
266
|
+
sourceResults[spec.key] = {
|
|
267
|
+
status: "ok",
|
|
268
|
+
url: spec.url,
|
|
269
|
+
count: records.length
|
|
270
|
+
};
|
|
271
|
+
collected.push(...records);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
sourceResults[spec.key] = {
|
|
274
|
+
status: "error",
|
|
275
|
+
url: spec.url,
|
|
276
|
+
count: 0,
|
|
277
|
+
error: error.message || String(error)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const icons = dedupeIconRecords(collected);
|
|
284
|
+
const sourceSummary = summarizeSourceResults(sourceResults);
|
|
285
|
+
if (icons.length === 0) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
[
|
|
288
|
+
"icon-sync failed: no icon records were fetched.",
|
|
289
|
+
...Object.entries(sourceResults).map(([key, value]) => `${key}: ${value.status}${value.error ? ` (${value.error})` : ""}`)
|
|
290
|
+
].join("\n")
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (strict && sourceSummary.errorCount > 0) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
[
|
|
296
|
+
"icon-sync strict mode failed: one or more upstream sources could not be fetched.",
|
|
297
|
+
...Object.entries(sourceResults).map(([key, value]) => `${key}: ${value.status}${value.error ? ` (${value.error})` : ""}`)
|
|
298
|
+
].join("\n")
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const catalog = {
|
|
303
|
+
schema: 1,
|
|
304
|
+
generatedAt: new Date().toISOString(),
|
|
305
|
+
sourceResults,
|
|
306
|
+
sourceSummary,
|
|
307
|
+
syncStatus: sourceSummary.degraded ? "degraded" : "ok",
|
|
308
|
+
iconCount: icons.length,
|
|
309
|
+
icons
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
ensureDir(catalogPath);
|
|
313
|
+
fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2));
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
catalogPath,
|
|
317
|
+
catalog
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatIconSyncReport(result) {
|
|
322
|
+
const sourceSummary = result.catalog.sourceSummary || summarizeSourceResults(result.catalog.sourceResults || {});
|
|
323
|
+
const statusLine = sourceSummary.degraded
|
|
324
|
+
? `DEGRADED (${sourceSummary.errorCount}/${sourceSummary.total} source failures)`
|
|
325
|
+
: "OK";
|
|
326
|
+
const lines = [
|
|
327
|
+
"Icon Sync",
|
|
328
|
+
`Catalog path: ${result.catalogPath}`,
|
|
329
|
+
`Generated at: ${result.catalog.generatedAt}`,
|
|
330
|
+
`Status: ${statusLine}`,
|
|
331
|
+
`Icon count: ${result.catalog.iconCount}`,
|
|
332
|
+
"Sources:"
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
for (const [key, source] of Object.entries(result.catalog.sourceResults || {})) {
|
|
336
|
+
const base = `- ${key}: ${source.status} (${source.count})`;
|
|
337
|
+
if (source.error) {
|
|
338
|
+
lines.push(`${base} ${source.error}`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
lines.push(base);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return lines.join("\n");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
MATERIAL_METADATA_URL,
|
|
349
|
+
LUCIDE_TREE_URL,
|
|
350
|
+
FEATHER_TREE_URL,
|
|
351
|
+
PHOSPHOR_TREE_URL,
|
|
352
|
+
getDefaultCatalogPath,
|
|
353
|
+
resolveCatalogPath,
|
|
354
|
+
loadIconCatalog,
|
|
355
|
+
syncIconCatalog,
|
|
356
|
+
formatIconSyncReport,
|
|
357
|
+
parseGoogleMaterialMetadata,
|
|
358
|
+
parseGitHubTreeIcons,
|
|
359
|
+
dedupeIconRecords,
|
|
360
|
+
summarizeSourceResults
|
|
361
|
+
};
|
package/lib/icon-text.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function normalizeIconText(text) {
|
|
2
|
+
return String(text || "")
|
|
3
|
+
.normalize("NFKD")
|
|
4
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[_/]+/g, " ")
|
|
7
|
+
.replace(/[^\p{L}\p{N}\s-]+/gu, " ")
|
|
8
|
+
.replace(/\s+/g, " ")
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tokenizeIconText(text) {
|
|
13
|
+
const normalized = normalizeIconText(text);
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return normalized
|
|
19
|
+
.split(/[\s-]+/)
|
|
20
|
+
.map((token) => token.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
normalizeIconText,
|
|
26
|
+
tokenizeIconText
|
|
27
|
+
};
|
package/lib/install.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const os = require("os");
|
|
4
|
+
const { listFilesRecursiveSafe } = require("./fs-safety");
|
|
4
5
|
|
|
5
6
|
const REPO_ROOT = path.resolve(__dirname, "..");
|
|
6
7
|
const PACKAGE_JSON = require(path.join(REPO_ROOT, "package.json"));
|
|
@@ -135,17 +136,24 @@ function tryRemoveEmptyDir(targetPath) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
function listFiles(dirPath) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
144
|
-
if (entry.isDirectory()) {
|
|
145
|
-
return listFiles(fullPath);
|
|
146
|
-
}
|
|
147
|
-
return [fullPath];
|
|
139
|
+
const scan = listFilesRecursiveSafe(dirPath, {
|
|
140
|
+
maxDepth: 24,
|
|
141
|
+
maxEntries: 25000,
|
|
142
|
+
includeDotfiles: false
|
|
148
143
|
});
|
|
144
|
+
|
|
145
|
+
if (scan.readErrors.length > 0) {
|
|
146
|
+
throw new Error(`Unable to enumerate install assets under ${dirPath}: ${scan.readErrors[0].error}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (scan.truncated) {
|
|
150
|
+
const reason = scan.entryLimitHit
|
|
151
|
+
? `file limit ${scan.maxEntries} reached`
|
|
152
|
+
: `depth limit ${scan.maxDepth} reached`;
|
|
153
|
+
throw new Error(`Install asset enumeration exceeded safe traversal limits under ${dirPath}: ${reason}.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return scan.files;
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
function getMissingTargets(homeDir, relativePaths) {
|
package/lib/pencil-preflight.js
CHANGED
|
@@ -9,6 +9,8 @@ const FAIL = "FAIL";
|
|
|
9
9
|
const HARD_BATCH_LIMIT = 25;
|
|
10
10
|
const ANCHOR_BATCH_WARNING_LIMIT = 12;
|
|
11
11
|
const MICRO_BATCH_LIMIT = 6;
|
|
12
|
+
const PREVIEW_VM_TIMEOUT_MS = 750;
|
|
13
|
+
const MAX_OPERATION_SOURCE_BYTES = 80000;
|
|
12
14
|
|
|
13
15
|
const VALID_HEX_COLOR = /^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
14
16
|
const VARIABLE_REFERENCE = /^\$[A-Za-z0-9._-]+$/;
|
|
@@ -36,6 +38,25 @@ const CALLEE_RULES = {
|
|
|
36
38
|
G: { minArgs: 3, maxArgs: 3 }
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
const UNSAFE_OPERATION_PATTERNS = [
|
|
42
|
+
{
|
|
43
|
+
pattern: /\b(?:globalThis|global|process|require|module|exports|constructor|prototype|__proto__)\b/i,
|
|
44
|
+
message: "Operation batch references runtime globals or prototype internals, which are not allowed."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /\b(?:function|class|new|import|export|eval)\b/i,
|
|
48
|
+
message: "Operation batch contains executable code constructs; only declarative operation calls are allowed."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /=>/,
|
|
52
|
+
message: "Operation batch contains arrow functions; only declarative operation calls are allowed."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: /;/,
|
|
56
|
+
message: "Operation batch contains statement separators; keep one operation expression per line."
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
|
|
39
60
|
function relativePath(basePath, targetPath) {
|
|
40
61
|
return path.relative(basePath, targetPath) || ".";
|
|
41
62
|
}
|
|
@@ -63,6 +84,59 @@ function normalizeLines(operations) {
|
|
|
63
84
|
.filter(Boolean);
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
function stripQuotedLiterals(sourceText) {
|
|
88
|
+
const source = String(sourceText || "");
|
|
89
|
+
let result = "";
|
|
90
|
+
let quote = null;
|
|
91
|
+
let escaped = false;
|
|
92
|
+
|
|
93
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
94
|
+
const char = source[index];
|
|
95
|
+
|
|
96
|
+
if (!quote) {
|
|
97
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
98
|
+
quote = char;
|
|
99
|
+
result += " ";
|
|
100
|
+
} else {
|
|
101
|
+
result += char;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (escaped) {
|
|
107
|
+
escaped = false;
|
|
108
|
+
result += " ";
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (char === "\\") {
|
|
113
|
+
escaped = true;
|
|
114
|
+
result += " ";
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (char === quote) {
|
|
119
|
+
quote = null;
|
|
120
|
+
result += " ";
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
result += " ";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function detectUnsafeOperationSource(operations) {
|
|
131
|
+
const sanitizedSource = stripQuotedLiterals(operations);
|
|
132
|
+
for (const rule of UNSAFE_OPERATION_PATTERNS) {
|
|
133
|
+
if (rule.pattern.test(sanitizedSource)) {
|
|
134
|
+
return rule.message;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
|
|
66
140
|
function createIssue(level, message) {
|
|
67
141
|
return { level, message };
|
|
68
142
|
}
|
|
@@ -256,7 +330,7 @@ function simulateOperations(operations) {
|
|
|
256
330
|
let syntheticId = 0;
|
|
257
331
|
|
|
258
332
|
function makeStub(callee) {
|
|
259
|
-
return (...args) => {
|
|
333
|
+
return Object.freeze((...args) => {
|
|
260
334
|
calls.push({ callee, args });
|
|
261
335
|
|
|
262
336
|
if (callee === "I" || callee === "C" || callee === "R") {
|
|
@@ -265,23 +339,68 @@ function simulateOperations(operations) {
|
|
|
265
339
|
}
|
|
266
340
|
|
|
267
341
|
return undefined;
|
|
268
|
-
};
|
|
342
|
+
});
|
|
269
343
|
}
|
|
270
344
|
|
|
271
|
-
const context =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
345
|
+
const context = Object.create(null);
|
|
346
|
+
Object.defineProperties(context, {
|
|
347
|
+
document: {
|
|
348
|
+
value: "document",
|
|
349
|
+
enumerable: true,
|
|
350
|
+
writable: false,
|
|
351
|
+
configurable: false
|
|
352
|
+
},
|
|
353
|
+
I: {
|
|
354
|
+
value: makeStub("I"),
|
|
355
|
+
enumerable: true,
|
|
356
|
+
writable: false,
|
|
357
|
+
configurable: false
|
|
358
|
+
},
|
|
359
|
+
C: {
|
|
360
|
+
value: makeStub("C"),
|
|
361
|
+
enumerable: true,
|
|
362
|
+
writable: false,
|
|
363
|
+
configurable: false
|
|
364
|
+
},
|
|
365
|
+
U: {
|
|
366
|
+
value: makeStub("U"),
|
|
367
|
+
enumerable: true,
|
|
368
|
+
writable: false,
|
|
369
|
+
configurable: false
|
|
370
|
+
},
|
|
371
|
+
R: {
|
|
372
|
+
value: makeStub("R"),
|
|
373
|
+
enumerable: true,
|
|
374
|
+
writable: false,
|
|
375
|
+
configurable: false
|
|
376
|
+
},
|
|
377
|
+
M: {
|
|
378
|
+
value: makeStub("M"),
|
|
379
|
+
enumerable: true,
|
|
380
|
+
writable: false,
|
|
381
|
+
configurable: false
|
|
382
|
+
},
|
|
383
|
+
D: {
|
|
384
|
+
value: makeStub("D"),
|
|
385
|
+
enumerable: true,
|
|
386
|
+
writable: false,
|
|
387
|
+
configurable: false
|
|
388
|
+
},
|
|
389
|
+
G: {
|
|
390
|
+
value: makeStub("G"),
|
|
391
|
+
enumerable: true,
|
|
392
|
+
writable: false,
|
|
393
|
+
configurable: false
|
|
394
|
+
}
|
|
395
|
+
});
|
|
281
396
|
|
|
282
397
|
vm.runInNewContext(operations, context, {
|
|
283
|
-
timeout:
|
|
284
|
-
displayErrors: true
|
|
398
|
+
timeout: PREVIEW_VM_TIMEOUT_MS,
|
|
399
|
+
displayErrors: true,
|
|
400
|
+
contextCodeGeneration: {
|
|
401
|
+
strings: false,
|
|
402
|
+
wasm: false
|
|
403
|
+
}
|
|
285
404
|
});
|
|
286
405
|
|
|
287
406
|
return calls;
|
|
@@ -330,6 +449,7 @@ function preflightPencilBatch(operations, options = {}) {
|
|
|
330
449
|
const notes = [];
|
|
331
450
|
const warnings = [];
|
|
332
451
|
const normalizedOps = String(operations || "");
|
|
452
|
+
const sourceByteLength = Buffer.byteLength(normalizedOps, "utf8");
|
|
333
453
|
const nonEmptyLines = normalizeLines(normalizedOps);
|
|
334
454
|
const opLineCount = nonEmptyLines.length;
|
|
335
455
|
|
|
@@ -356,17 +476,46 @@ function preflightPencilBatch(operations, options = {}) {
|
|
|
356
476
|
}
|
|
357
477
|
|
|
358
478
|
let calls = [];
|
|
359
|
-
|
|
360
|
-
calls = simulateOperations(normalizedOps);
|
|
361
|
-
} catch (error) {
|
|
479
|
+
if (sourceByteLength > MAX_OPERATION_SOURCE_BYTES) {
|
|
362
480
|
issues.push(
|
|
363
481
|
createIssue(
|
|
364
482
|
"FAIL",
|
|
365
|
-
`
|
|
483
|
+
`Batch source is too large (${sourceByteLength} bytes). Keep Pencil operation batches below ${MAX_OPERATION_SOURCE_BYTES} bytes.`
|
|
366
484
|
)
|
|
367
485
|
);
|
|
368
486
|
}
|
|
369
487
|
|
|
488
|
+
const unsafeSourceMessage = detectUnsafeOperationSource(normalizedOps);
|
|
489
|
+
if (unsafeSourceMessage) {
|
|
490
|
+
issues.push(createIssue("FAIL", unsafeSourceMessage));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (issues.length === 0) {
|
|
494
|
+
try {
|
|
495
|
+
calls = simulateOperations(normalizedOps);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
const errorMessage = error && error.message ? error.message : String(error);
|
|
498
|
+
const timeoutHit =
|
|
499
|
+
(error && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") ||
|
|
500
|
+
/Script execution timed out/i.test(errorMessage);
|
|
501
|
+
if (timeoutHit) {
|
|
502
|
+
issues.push(
|
|
503
|
+
createIssue(
|
|
504
|
+
"FAIL",
|
|
505
|
+
"Pencil batch execution timed out in preflight sandbox. Keep operations declarative and avoid loops or computed code."
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
} else {
|
|
509
|
+
issues.push(
|
|
510
|
+
createIssue(
|
|
511
|
+
"FAIL",
|
|
512
|
+
`Pencil batch has invalid JavaScript-like syntax or references: ${errorMessage}`
|
|
513
|
+
)
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
370
519
|
if (calls.length > 0) {
|
|
371
520
|
validateCalls(calls, issues);
|
|
372
521
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenonbyte/da-vinci-workflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Requirement-to-design-to-code workflow skill for Codex, Claude, and Gemini",
|
|
5
5
|
"bin": {
|
|
6
6
|
"da-vinci": "bin/da-vinci.js"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"postinstall": "node scripts/postinstall.js",
|
|
24
24
|
"validate-assets": "node scripts/validate-assets.js",
|
|
25
|
+
"test:audit-safety": "node scripts/test-audit-safety.js",
|
|
25
26
|
"test:audit-context-delta": "node scripts/test-audit-context-delta.js",
|
|
26
27
|
"test:audit-design-supervisor": "node scripts/test-audit-design-supervisor.js",
|
|
27
28
|
"test:pencil-lock": "node scripts/test-pencil-lock.js",
|
|
@@ -30,7 +31,10 @@
|
|
|
30
31
|
"test:persistence-flows": "node scripts/test-persistence-flows.js",
|
|
31
32
|
"test:pencil-session": "node scripts/test-pencil-session.js",
|
|
32
33
|
"test:pencil-preflight": "node scripts/test-pencil-preflight.js",
|
|
33
|
-
"test:pen-persistence": "node scripts/test-pen-persistence.js"
|
|
34
|
+
"test:pen-persistence": "node scripts/test-pen-persistence.js",
|
|
35
|
+
"test:icon-search": "node scripts/test-icon-search.js",
|
|
36
|
+
"test:icon-sync": "node scripts/test-icon-sync.js",
|
|
37
|
+
"test:icon-aliases": "node scripts/test-icon-aliases.js"
|
|
34
38
|
},
|
|
35
39
|
"engines": {
|
|
36
40
|
"node": ">=18"
|