@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/audit.js
CHANGED
|
@@ -2,8 +2,33 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { getStandardPenStatePath, readPenState, hashPenDocument, readPenDocument } = require("./pen-persistence");
|
|
4
4
|
const { getSessionStatePath, readSessionState } = require("./pencil-session");
|
|
5
|
+
const { isPathInside, listFilesRecursiveSafe } = require("./fs-safety");
|
|
6
|
+
const {
|
|
7
|
+
normalizeCheckpointLabel,
|
|
8
|
+
parseCheckpointStatusMap,
|
|
9
|
+
hasContextDeltaExpectationSignals,
|
|
10
|
+
parseSupersedesTokens,
|
|
11
|
+
buildContextDeltaReferenceIndex,
|
|
12
|
+
resolveSupersedesReferenceIndices,
|
|
13
|
+
inspectContextDelta,
|
|
14
|
+
hasConfiguredDesignSupervisorReview,
|
|
15
|
+
isDesignSupervisorReviewRequired,
|
|
16
|
+
inspectDesignSupervisorReview
|
|
17
|
+
} = require("./audit-parsers");
|
|
5
18
|
|
|
6
19
|
const IMAGE_EXPORT_PATTERN = /\.(png|jpe?g|webp|pdf)$/i;
|
|
20
|
+
const AUDIT_SCAN_LIMITS = Object.freeze({
|
|
21
|
+
maxDepth: 24,
|
|
22
|
+
maxEntries: 12000
|
|
23
|
+
});
|
|
24
|
+
const CHANGE_SCAN_LIMITS = Object.freeze({
|
|
25
|
+
maxDepth: 12,
|
|
26
|
+
maxEntries: 6000
|
|
27
|
+
});
|
|
28
|
+
const EXPORT_SCAN_LIMITS = Object.freeze({
|
|
29
|
+
maxDepth: 4,
|
|
30
|
+
maxEntries: 1200
|
|
31
|
+
});
|
|
7
32
|
|
|
8
33
|
function pathExists(targetPath) {
|
|
9
34
|
return fs.existsSync(targetPath);
|
|
@@ -16,17 +41,10 @@ function readTextIfExists(targetPath) {
|
|
|
16
41
|
return fs.readFileSync(targetPath, "utf8");
|
|
17
42
|
}
|
|
18
43
|
|
|
19
|
-
function listFilesRecursive(rootDir) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return fs.readdirSync(rootDir, { withFileTypes: true }).flatMap((entry) => {
|
|
25
|
-
const fullPath = path.join(rootDir, entry.name);
|
|
26
|
-
if (entry.isDirectory()) {
|
|
27
|
-
return listFilesRecursive(fullPath);
|
|
28
|
-
}
|
|
29
|
-
return [fullPath];
|
|
44
|
+
function listFilesRecursive(rootDir, limits = {}) {
|
|
45
|
+
return listFilesRecursiveSafe(rootDir, {
|
|
46
|
+
includeDotfiles: true,
|
|
47
|
+
...limits
|
|
30
48
|
});
|
|
31
49
|
}
|
|
32
50
|
|
|
@@ -45,18 +63,35 @@ function relativeTo(projectRoot, targetPath) {
|
|
|
45
63
|
return path.relative(projectRoot, targetPath) || ".";
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
function escapeRegExp(value) {
|
|
49
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
66
|
function collectRegisteredPenPaths(projectRoot, designRegistryPath) {
|
|
53
67
|
const registryText = readTextIfExists(designRegistryPath);
|
|
54
68
|
const matches = registryText.match(/\.da-vinci\/designs\/[^\s`]+\.pen/g) || [];
|
|
55
|
-
|
|
69
|
+
const validPaths = [];
|
|
70
|
+
const escapedPaths = [];
|
|
71
|
+
|
|
72
|
+
for (const relativePath of [...new Set(matches)]) {
|
|
73
|
+
const resolvedPath = path.resolve(projectRoot, relativePath);
|
|
74
|
+
if (!isPathInside(projectRoot, resolvedPath)) {
|
|
75
|
+
escapedPaths.push({
|
|
76
|
+
relativePath,
|
|
77
|
+
resolvedPath
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
validPaths.push(resolvedPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
validPaths,
|
|
86
|
+
escapedPaths
|
|
87
|
+
};
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
function getNonEmptyChangeDirs(changesDir) {
|
|
59
|
-
return listChildDirs(changesDir).filter((changeDir) =>
|
|
91
|
+
return listChildDirs(changesDir).filter((changeDir) => {
|
|
92
|
+
const scan = listFilesRecursive(changeDir, CHANGE_SCAN_LIMITS);
|
|
93
|
+
return scan.files.length > 0;
|
|
94
|
+
});
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
function getExpectedChangeArtifacts(changeDir) {
|
|
@@ -127,440 +162,37 @@ function pushUnique(targetList, message) {
|
|
|
127
162
|
}
|
|
128
163
|
}
|
|
129
164
|
|
|
130
|
-
function
|
|
131
|
-
if (!
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const escapedHeading = escapeRegExp(heading);
|
|
136
|
-
const headingPattern = new RegExp(`^##\\s+${escapedHeading}\\s*$`, "i");
|
|
137
|
-
const anyHeadingPattern = /^##\s+/;
|
|
138
|
-
const lines = String(text).replace(/\r\n?/g, "\n").split("\n");
|
|
139
|
-
const sectionLines = [];
|
|
140
|
-
let capturing = false;
|
|
141
|
-
|
|
142
|
-
for (const line of lines) {
|
|
143
|
-
if (capturing && anyHeadingPattern.test(line)) {
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (!capturing && headingPattern.test(line)) {
|
|
148
|
-
capturing = true;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (capturing) {
|
|
153
|
-
sectionLines.push(line);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return sectionLines.join("\n").trim();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function normalizeCheckpointLabel(value) {
|
|
161
|
-
return String(value || "")
|
|
162
|
-
.toLowerCase()
|
|
163
|
-
.replace(/`/g, "")
|
|
164
|
-
.replace(/[_-]+/g, " ")
|
|
165
|
-
.replace(/\s+/g, " ")
|
|
166
|
-
.trim();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function parseCheckpointStatusMap(markdownText) {
|
|
170
|
-
const section = getMarkdownSection(markdownText, "Checkpoint Status");
|
|
171
|
-
if (!section) {
|
|
172
|
-
return {};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const statuses = {};
|
|
176
|
-
const matches = section.matchAll(/(?:^|\n)\s*-\s*`?([^`:\n]+?)`?\s*:\s*(PASS|WARN|BLOCK)\b/gi);
|
|
177
|
-
for (const match of matches) {
|
|
178
|
-
const label = normalizeCheckpointLabel(match[1]);
|
|
179
|
-
if (!label) {
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
statuses[label] = String(match[2]).toUpperCase();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return statuses;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function hasContextDeltaExpectationSignals(markdownText) {
|
|
189
|
-
const text = String(markdownText || "");
|
|
190
|
-
return (
|
|
191
|
-
/##\s+(Checkpoint Status|MCP Runtime Gate)\b/i.test(text) ||
|
|
192
|
-
/(?:^|\n)\s*(?:[-*]\s*)?`?Context Delta Required`?\s*:\s*(?:true|yes|on|1)\b/i.test(text)
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function parseSupersedesTokens(value) {
|
|
197
|
-
return String(value || "")
|
|
198
|
-
.split(/[,\n;]/)
|
|
199
|
-
.map((token) => token.trim())
|
|
200
|
-
.filter(Boolean);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function normalizeTimeToken(value) {
|
|
204
|
-
const raw = String(value || "").trim();
|
|
205
|
-
if (!raw) {
|
|
206
|
-
return "";
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const parseCandidates = [];
|
|
210
|
-
const rawWithT = raw.includes(" ") ? raw.replace(/\s+/, "T") : raw;
|
|
211
|
-
const hasExplicitTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(rawWithT);
|
|
212
|
-
|
|
213
|
-
if (!hasExplicitTimezone && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(rawWithT)) {
|
|
214
|
-
parseCandidates.push(`${rawWithT}Z`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
parseCandidates.push(rawWithT);
|
|
218
|
-
parseCandidates.push(raw);
|
|
219
|
-
|
|
220
|
-
for (const candidate of parseCandidates) {
|
|
221
|
-
const timestamp = Date.parse(candidate);
|
|
222
|
-
if (!Number.isFinite(timestamp)) {
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
return new Date(timestamp).toISOString();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return "";
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function getTimeReferenceKeys(value) {
|
|
232
|
-
const raw = String(value || "").trim();
|
|
233
|
-
if (!raw) {
|
|
234
|
-
return [];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const keys = new Set([raw]);
|
|
238
|
-
|
|
239
|
-
if (raw.includes(" ")) {
|
|
240
|
-
keys.add(raw.replace(/\s+/, "T"));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (raw.endsWith(".000Z")) {
|
|
244
|
-
keys.add(raw.replace(".000Z", "Z"));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const normalized = normalizeTimeToken(raw);
|
|
248
|
-
if (normalized) {
|
|
249
|
-
keys.add(normalized);
|
|
250
|
-
if (normalized.endsWith(".000Z")) {
|
|
251
|
-
keys.add(normalized.replace(".000Z", "Z"));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return [...keys];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function buildContextDeltaReferenceIndex(entries) {
|
|
259
|
-
const referenceIndex = new Map();
|
|
260
|
-
|
|
261
|
-
function addReference(key, entryIndex) {
|
|
262
|
-
const normalizedKey = String(key || "").trim();
|
|
263
|
-
if (!normalizedKey) {
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const existing = referenceIndex.get(normalizedKey) || [];
|
|
268
|
-
existing.push(entryIndex);
|
|
269
|
-
referenceIndex.set(normalizedKey, existing);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
entries.forEach((entry, entryIndex) => {
|
|
273
|
-
if (entry.time) {
|
|
274
|
-
for (const timeKey of getTimeReferenceKeys(entry.time)) {
|
|
275
|
-
addReference(timeKey, entryIndex);
|
|
276
|
-
addReference(`time:${timeKey}`, entryIndex);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (entry.checkpointType && entry.time) {
|
|
281
|
-
const normalizedCheckpointType = normalizeCheckpointLabel(entry.checkpointType);
|
|
282
|
-
for (const timeKey of getTimeReferenceKeys(entry.time)) {
|
|
283
|
-
addReference(`${entry.checkpointType}@${timeKey}`, entryIndex);
|
|
284
|
-
addReference(`${normalizedCheckpointType}@${timeKey}`, entryIndex);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
return referenceIndex;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function getSupersedesCandidateKeys(token) {
|
|
293
|
-
const trimmed = String(token || "").trim();
|
|
294
|
-
if (!trimmed) {
|
|
295
|
-
return [];
|
|
165
|
+
function appendTraversalWarnings(projectRoot, rootDir, scan, warnings) {
|
|
166
|
+
if (!scan) {
|
|
167
|
+
return;
|
|
296
168
|
}
|
|
297
169
|
|
|
298
|
-
const
|
|
299
|
-
const prefixedTimeMatch = trimmed.match(/^time\s*:\s*(.+)$/i);
|
|
300
|
-
if (prefixedTimeMatch) {
|
|
301
|
-
for (const timeKey of getTimeReferenceKeys(prefixedTimeMatch[1])) {
|
|
302
|
-
keys.add(timeKey);
|
|
303
|
-
keys.add(`time:${timeKey}`);
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
keys.add(`time:${trimmed}`);
|
|
307
|
-
for (const timeKey of getTimeReferenceKeys(trimmed)) {
|
|
308
|
-
keys.add(timeKey);
|
|
309
|
-
keys.add(`time:${timeKey}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
170
|
+
const scope = relativeTo(projectRoot, rootDir);
|
|
312
171
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const normalizedCheckpointType = normalizeCheckpointLabel(checkpointType);
|
|
319
|
-
for (const timeKey of getTimeReferenceKeys(time)) {
|
|
320
|
-
keys.add(`${checkpointType}@${timeKey}`);
|
|
321
|
-
keys.add(`${normalizedCheckpointType}@${timeKey}`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
172
|
+
if (scan.truncated) {
|
|
173
|
+
const reason = scan.entryLimitHit
|
|
174
|
+
? `file limit ${scan.maxEntries} reached`
|
|
175
|
+
: `depth limit ${scan.maxDepth} reached`;
|
|
176
|
+
pushUnique(warnings, `File scan truncated under ${scope}: ${reason}.`);
|
|
324
177
|
}
|
|
325
178
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (!matches) {
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
for (const entryIndex of matches) {
|
|
337
|
-
indices.add(entryIndex);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return [...indices].sort((a, b) => a - b);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function inspectContextDelta(markdownText) {
|
|
344
|
-
const section = getMarkdownSection(markdownText, "Context Delta");
|
|
345
|
-
if (!section) {
|
|
346
|
-
return {
|
|
347
|
-
found: false,
|
|
348
|
-
hasConcreteEntry: false,
|
|
349
|
-
entries: [],
|
|
350
|
-
incompleteEntryCount: 0
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const lines = String(section).replace(/\r\n?/g, "\n").split("\n");
|
|
355
|
-
const entries = [];
|
|
356
|
-
let current = null;
|
|
357
|
-
|
|
358
|
-
function ensureCurrent() {
|
|
359
|
-
if (!current) {
|
|
360
|
-
current = {};
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function flushCurrent() {
|
|
365
|
-
if (!current) {
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const hasAnyValue = [
|
|
370
|
-
current.time,
|
|
371
|
-
current.checkpointType,
|
|
372
|
-
current.goal,
|
|
373
|
-
current.decision,
|
|
374
|
-
current.constraints,
|
|
375
|
-
current.impact,
|
|
376
|
-
current.status,
|
|
377
|
-
current.nextAction,
|
|
378
|
-
current.supersedes
|
|
379
|
-
].some((value) => Boolean(value));
|
|
380
|
-
|
|
381
|
-
if (hasAnyValue) {
|
|
382
|
-
entries.push(current);
|
|
383
|
-
}
|
|
384
|
-
current = null;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
for (const rawLine of lines) {
|
|
388
|
-
const line = rawLine.trim();
|
|
389
|
-
|
|
390
|
-
if (!line) {
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const timeMatch = line.match(/^-+\s*`?time`?\s*:\s*(.+)$/i);
|
|
395
|
-
if (timeMatch) {
|
|
396
|
-
flushCurrent();
|
|
397
|
-
current = {
|
|
398
|
-
time: timeMatch[1].trim()
|
|
399
|
-
};
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const checkpointTypeMatch = line.match(/^-+\s*`?checkpoint(?:[_ -]?type)?`?\s*:\s*(.+)$/i);
|
|
404
|
-
if (checkpointTypeMatch) {
|
|
405
|
-
ensureCurrent();
|
|
406
|
-
current.checkpointType = checkpointTypeMatch[1].trim();
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const goalMatch = line.match(/^-+\s*`?goal`?\s*:\s*(.+)$/i);
|
|
411
|
-
if (goalMatch) {
|
|
412
|
-
ensureCurrent();
|
|
413
|
-
current.goal = goalMatch[1].trim();
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const decisionMatch = line.match(/^-+\s*`?decision`?\s*:\s*(.+)$/i);
|
|
418
|
-
if (decisionMatch) {
|
|
419
|
-
ensureCurrent();
|
|
420
|
-
current.decision = decisionMatch[1].trim();
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const constraintsMatch = line.match(/^-+\s*`?constraints`?\s*:\s*(.+)$/i);
|
|
425
|
-
if (constraintsMatch) {
|
|
426
|
-
ensureCurrent();
|
|
427
|
-
current.constraints = constraintsMatch[1].trim();
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const impactMatch = line.match(/^-+\s*`?impact`?\s*:\s*(.+)$/i);
|
|
432
|
-
if (impactMatch) {
|
|
433
|
-
ensureCurrent();
|
|
434
|
-
current.impact = impactMatch[1].trim();
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const statusMatch = line.match(/^-+\s*`?status`?\s*:\s*(PASS|WARN|BLOCK)\b/i);
|
|
439
|
-
if (statusMatch) {
|
|
440
|
-
ensureCurrent();
|
|
441
|
-
current.status = String(statusMatch[1]).toUpperCase();
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const nextActionMatch = line.match(/^-+\s*`?next(?:[_ -]?action)?`?\s*:\s*(.+)$/i);
|
|
446
|
-
if (nextActionMatch) {
|
|
447
|
-
ensureCurrent();
|
|
448
|
-
current.nextAction = nextActionMatch[1].trim();
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const supersedesMatch = line.match(/^-+\s*`?supersedes`?\s*:\s*(.+)$/i);
|
|
453
|
-
if (supersedesMatch) {
|
|
454
|
-
ensureCurrent();
|
|
455
|
-
current.supersedes = supersedesMatch[1].trim();
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
flushCurrent();
|
|
461
|
-
|
|
462
|
-
const hasConcreteEntry = entries.some(
|
|
463
|
-
(entry) => entry.time || entry.checkpointType || entry.status || entry.decision || entry.nextAction
|
|
464
|
-
);
|
|
465
|
-
const incompleteEntryCount = entries.filter(
|
|
466
|
-
(entry) => !entry.time || !entry.checkpointType || !entry.status
|
|
467
|
-
).length;
|
|
468
|
-
|
|
469
|
-
return {
|
|
470
|
-
found: true,
|
|
471
|
-
hasConcreteEntry,
|
|
472
|
-
entries,
|
|
473
|
-
incompleteEntryCount
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function getVisualAssistFieldValues(daVinciText, fieldName) {
|
|
478
|
-
const section = getMarkdownSection(daVinciText, "Visual Assist");
|
|
479
|
-
if (!section) {
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const fieldPattern = new RegExp(`^\\s*-\\s*${escapeRegExp(fieldName)}\\s*:\\s*(.*)$`, "i");
|
|
484
|
-
const nestedValuePattern = /^\s{2,}-\s*(.+?)\s*$/;
|
|
485
|
-
const nextFieldPattern = /^\s*-\s+[^:]+:\s*.*$/;
|
|
486
|
-
const values = [];
|
|
487
|
-
let capturing = false;
|
|
488
|
-
|
|
489
|
-
for (const rawLine of String(section).replace(/\r\n?/g, "\n").split("\n")) {
|
|
490
|
-
const fieldMatch = rawLine.match(fieldPattern);
|
|
491
|
-
if (!capturing && fieldMatch) {
|
|
492
|
-
capturing = true;
|
|
493
|
-
const inlineValue = (fieldMatch[1] || "").trim();
|
|
494
|
-
if (inlineValue) {
|
|
495
|
-
values.push(inlineValue);
|
|
496
|
-
}
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (capturing && nextFieldPattern.test(rawLine)) {
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (capturing) {
|
|
505
|
-
const nestedMatch = rawLine.match(nestedValuePattern);
|
|
506
|
-
if (nestedMatch) {
|
|
507
|
-
const value = nestedMatch[1].trim();
|
|
508
|
-
if (value) {
|
|
509
|
-
values.push(value);
|
|
510
|
-
}
|
|
511
|
-
} else if (rawLine.trim() === "") {
|
|
512
|
-
continue;
|
|
513
|
-
} else {
|
|
514
|
-
break;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return values;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function hasConfiguredDesignSupervisorReview(daVinciText) {
|
|
523
|
-
return getVisualAssistFieldValues(daVinciText, "Design-supervisor reviewers").length > 0;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function isDesignSupervisorReviewRequired(daVinciText) {
|
|
527
|
-
return getVisualAssistFieldValues(daVinciText, "Require Supervisor Review").some((value) =>
|
|
528
|
-
/^true$/i.test(String(value).trim())
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function inspectDesignSupervisorReview(pencilDesignText) {
|
|
533
|
-
const section = getMarkdownSection(pencilDesignText, "Design-Supervisor Review");
|
|
534
|
-
if (!section) {
|
|
535
|
-
return {
|
|
536
|
-
found: false,
|
|
537
|
-
status: null,
|
|
538
|
-
acceptedWarn: false,
|
|
539
|
-
hasIssueList: false,
|
|
540
|
-
hasRevisionOutcome: false
|
|
541
|
-
};
|
|
179
|
+
if (scan.skippedSymlinks > 0) {
|
|
180
|
+
pushUnique(
|
|
181
|
+
warnings,
|
|
182
|
+
`File scan skipped ${scan.skippedSymlinks} symbolic link entr${
|
|
183
|
+
scan.skippedSymlinks === 1 ? "y" : "ies"
|
|
184
|
+
} under ${scope}.`
|
|
185
|
+
);
|
|
542
186
|
}
|
|
543
187
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const revisionOutcome = revisionOutcomeMatch ? revisionOutcomeMatch[1].trim() : "";
|
|
551
|
-
const acceptedWarn =
|
|
552
|
-
status === "WARN" &&
|
|
553
|
-
/(accepted|accepted with follow-up|accepted warning|warn accepted|接受|已接受|接受警告)/i.test(
|
|
554
|
-
revisionOutcome
|
|
188
|
+
if (scan.readErrors.length > 0) {
|
|
189
|
+
pushUnique(
|
|
190
|
+
warnings,
|
|
191
|
+
`File scan hit ${scan.readErrors.length} read error${
|
|
192
|
+
scan.readErrors.length === 1 ? "" : "s"
|
|
193
|
+
} under ${scope}.`
|
|
555
194
|
);
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
found: true,
|
|
559
|
-
status,
|
|
560
|
-
acceptedWarn,
|
|
561
|
-
hasIssueList: Boolean(issueListMatch && issueListMatch[1].trim()),
|
|
562
|
-
hasRevisionOutcome: Boolean(revisionOutcome)
|
|
563
|
-
};
|
|
195
|
+
}
|
|
564
196
|
}
|
|
565
197
|
|
|
566
198
|
function auditProject(projectPathInput, options = {}) {
|
|
@@ -645,7 +277,9 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
645
277
|
}
|
|
646
278
|
}
|
|
647
279
|
|
|
648
|
-
const
|
|
280
|
+
const daVinciScan = listFilesRecursive(daVinciDir, AUDIT_SCAN_LIMITS);
|
|
281
|
+
appendTraversalWarnings(projectRoot, daVinciDir, daVinciScan, warnings);
|
|
282
|
+
const daVinciFiles = daVinciScan.files;
|
|
649
283
|
const misplacedExports = daVinciFiles.filter(
|
|
650
284
|
(filePath) => IMAGE_EXPORT_PATTERN.test(filePath) && !isAllowedExportPath(projectRoot, filePath)
|
|
651
285
|
);
|
|
@@ -659,7 +293,9 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
659
293
|
);
|
|
660
294
|
}
|
|
661
295
|
|
|
662
|
-
const
|
|
296
|
+
const designScan = listFilesRecursive(designsDir, AUDIT_SCAN_LIMITS);
|
|
297
|
+
appendTraversalWarnings(projectRoot, designsDir, designScan, warnings);
|
|
298
|
+
const designFiles = designScan.files;
|
|
663
299
|
const penFiles = designFiles.filter((filePath) => filePath.endsWith(".pen"));
|
|
664
300
|
const pollutedDesignFiles = designFiles.filter((filePath) => !filePath.endsWith(".pen"));
|
|
665
301
|
|
|
@@ -687,7 +323,21 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
687
323
|
);
|
|
688
324
|
}
|
|
689
325
|
|
|
690
|
-
const
|
|
326
|
+
const {
|
|
327
|
+
validPaths: registeredPenPaths,
|
|
328
|
+
escapedPaths: escapedRegisteredPenPaths
|
|
329
|
+
} = collectRegisteredPenPaths(projectRoot, designRegistryPath);
|
|
330
|
+
for (const escapedPath of escapedRegisteredPenPaths) {
|
|
331
|
+
const message =
|
|
332
|
+
`Registered design source escapes project root and will be ignored: ${escapedPath.relativePath} ` +
|
|
333
|
+
`(${escapedPath.resolvedPath})`;
|
|
334
|
+
if (mode === "completion") {
|
|
335
|
+
failures.push(message);
|
|
336
|
+
} else {
|
|
337
|
+
warnings.push(message);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
691
341
|
for (const registeredPenPath of registeredPenPaths) {
|
|
692
342
|
if (!pathExists(registeredPenPath)) {
|
|
693
343
|
failures.push(
|
|
@@ -765,7 +415,9 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
765
415
|
: changeDirs;
|
|
766
416
|
|
|
767
417
|
for (const changeDir of changeDirs) {
|
|
768
|
-
const
|
|
418
|
+
const changeScan = listFilesRecursive(changeDir, CHANGE_SCAN_LIMITS);
|
|
419
|
+
appendTraversalWarnings(projectRoot, changeDir, changeScan, warnings);
|
|
420
|
+
const changeFiles = changeScan.files;
|
|
769
421
|
const changeRel = relativeTo(projectRoot, changeDir);
|
|
770
422
|
|
|
771
423
|
if (changeFiles.length === 0) {
|
|
@@ -777,7 +429,9 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
777
429
|
}
|
|
778
430
|
|
|
779
431
|
const exportsDir = path.join(changeDir, "exports");
|
|
780
|
-
const
|
|
432
|
+
const exportsScan = listFilesRecursive(exportsDir, EXPORT_SCAN_LIMITS);
|
|
433
|
+
appendTraversalWarnings(projectRoot, exportsDir, exportsScan, warnings);
|
|
434
|
+
const exportsFiles = exportsScan.files;
|
|
781
435
|
if (exportsFiles.length > 0) {
|
|
782
436
|
notes.push(`Detected review exports under ${relativeTo(projectRoot, exportsDir)}.`);
|
|
783
437
|
}
|