@xenonbyte/da-vinci-workflow 0.2.3 → 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 +32 -0
- package/README.md +32 -7
- package/README.zh-CN.md +151 -7
- package/SKILL.md +45 -704
- package/commands/claude/dv/build.md +5 -0
- package/commands/claude/dv/continue.md +4 -0
- package/commands/claude/dv/tasks.md +6 -0
- package/commands/claude/dv/verify.md +2 -0
- package/commands/codex/prompts/dv-build.md +5 -0
- package/commands/codex/prompts/dv-continue.md +4 -0
- package/commands/codex/prompts/dv-tasks.md +6 -0
- package/commands/codex/prompts/dv-verify.md +2 -0
- package/commands/gemini/dv/build.toml +5 -0
- package/commands/gemini/dv/continue.toml +4 -0
- package/commands/gemini/dv/tasks.toml +6 -0
- package/commands/gemini/dv/verify.toml +2 -0
- package/commands/templates/dv-continue.shared.md +4 -0
- package/docs/discipline-and-orchestration-upgrade.md +83 -0
- package/docs/dv-command-reference.md +33 -5
- package/docs/execution-chain-migration.md +23 -0
- package/docs/prompt-entrypoints.md +6 -0
- package/docs/skill-contract-maintenance.md +14 -0
- package/docs/skill-usage.md +16 -0
- package/docs/workflow-overview.md +17 -0
- package/docs/zh-CN/dv-command-reference.md +31 -5
- package/docs/zh-CN/execution-chain-migration.md +23 -0
- package/docs/zh-CN/prompt-entrypoints.md +6 -0
- package/docs/zh-CN/skill-usage.md +16 -0
- package/docs/zh-CN/workflow-overview.md +17 -0
- package/lib/audit-parsers.js +148 -1
- 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 +123 -145
- package/lib/execution-profile.js +143 -0
- package/lib/execution-signals.js +19 -1
- package/lib/lint-tasks.js +86 -2
- package/lib/planning-parsers.js +263 -19
- package/lib/scaffold.js +454 -23
- package/lib/supervisor-review.js +2 -1
- package/lib/task-execution.js +160 -0
- package/lib/task-review.js +197 -0
- package/lib/utils.js +19 -0
- package/lib/verify.js +1308 -85
- package/lib/workflow-state.js +452 -30
- package/lib/worktree-preflight.js +214 -0
- package/package.json +1 -1
- package/references/artifact-templates.md +56 -6
- package/references/skill-workflow-detail.md +66 -0
package/lib/verify.js
CHANGED
|
@@ -2,7 +2,6 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { STATUS } = require("./workflow-contract");
|
|
4
4
|
const {
|
|
5
|
-
normalizeText,
|
|
6
5
|
unique,
|
|
7
6
|
resolveImplementationLanding,
|
|
8
7
|
resolveChangeDir,
|
|
@@ -13,8 +12,23 @@ const {
|
|
|
13
12
|
readChangeArtifacts,
|
|
14
13
|
readArtifactTexts
|
|
15
14
|
} = require("./planning-parsers");
|
|
15
|
+
const { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
|
|
16
|
+
const { normalizeRelativePath, pathWithinRoot } = require("./utils");
|
|
16
17
|
|
|
17
|
-
const CODE_FILE_EXTENSIONS = new Set([
|
|
18
|
+
const CODE_FILE_EXTENSIONS = new Set([
|
|
19
|
+
".js",
|
|
20
|
+
".jsx",
|
|
21
|
+
".ts",
|
|
22
|
+
".tsx",
|
|
23
|
+
".html",
|
|
24
|
+
".css",
|
|
25
|
+
".scss",
|
|
26
|
+
".vue",
|
|
27
|
+
".svelte"
|
|
28
|
+
]);
|
|
29
|
+
const SYNTAX_AWARE_SCRIPT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
30
|
+
const IMPLEMENTATION_MARKUP_EXTENSIONS = new Set([".html", ".vue", ".svelte"]);
|
|
31
|
+
const STRUCTURE_MARKUP_EXTENSIONS = new Set([".html", ".tsx", ".jsx", ".js", ".vue", ".svelte"]);
|
|
18
32
|
const NON_IMPLEMENTATION_DIR_NAMES = new Set([
|
|
19
33
|
".git",
|
|
20
34
|
".da-vinci",
|
|
@@ -47,6 +61,7 @@ const MAX_SCANNED_FILES = 2000;
|
|
|
47
61
|
const MAX_SCANNED_BYTES_PER_FILE = 512 * 1024;
|
|
48
62
|
const MAX_SCANNED_DIRECTORIES = 10000;
|
|
49
63
|
const MAX_SCAN_DEPTH = 32;
|
|
64
|
+
const DEFAULT_EVIDENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
50
65
|
|
|
51
66
|
function buildEnvelope(name, projectRoot, strict) {
|
|
52
67
|
return {
|
|
@@ -90,6 +105,58 @@ function isNonImplementationFileName(name) {
|
|
|
90
105
|
return NON_IMPLEMENTATION_FILE_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
91
106
|
}
|
|
92
107
|
|
|
108
|
+
function normalizeCoverageText(value) {
|
|
109
|
+
return String(value || "")
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
112
|
+
.replace(/\s+/g, " ")
|
|
113
|
+
.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function tokenizeCoverage(value, minLength = 1) {
|
|
117
|
+
return normalizeCoverageText(value)
|
|
118
|
+
.split(" ")
|
|
119
|
+
.map((token) => token.trim())
|
|
120
|
+
.filter((token) => token.length >= minLength);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function safeRealpathSync(candidatePath) {
|
|
124
|
+
try {
|
|
125
|
+
if (typeof fs.realpathSync.native === "function") {
|
|
126
|
+
return fs.realpathSync.native(candidatePath);
|
|
127
|
+
}
|
|
128
|
+
return fs.realpathSync(candidatePath);
|
|
129
|
+
} catch (_error) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function canonicalizePath(candidatePath) {
|
|
135
|
+
const resolved = path.resolve(candidatePath);
|
|
136
|
+
return safeRealpathSync(resolved) || resolved;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function listSummary(items, max = 5) {
|
|
140
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
const head = items.slice(0, max).join(", ");
|
|
144
|
+
if (items.length <= max) {
|
|
145
|
+
return head;
|
|
146
|
+
}
|
|
147
|
+
return `${head} ... (+${items.length - max} more)`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasExcludedDirectory(relativePath) {
|
|
151
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
152
|
+
if (!normalized) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const segments = normalized.split("/");
|
|
156
|
+
const directorySegments = segments.slice(0, -1);
|
|
157
|
+
return directorySegments.some((segment) => isNonImplementationDirName(segment));
|
|
158
|
+
}
|
|
159
|
+
|
|
93
160
|
function collectCodeFiles(projectRoot) {
|
|
94
161
|
const files = [];
|
|
95
162
|
const scan = {
|
|
@@ -217,13 +284,136 @@ function readCodeFileForScan(filePath) {
|
|
|
217
284
|
}
|
|
218
285
|
}
|
|
219
286
|
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
287
|
+
function safeMtimeMs(filePath) {
|
|
288
|
+
try {
|
|
289
|
+
const stat = fs.statSync(filePath);
|
|
290
|
+
return Number(stat.mtimeMs) || 0;
|
|
291
|
+
} catch (_error) {
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function collectFreshnessBaseline(projectRoot, resolved, artifactPaths) {
|
|
297
|
+
const baselineCandidates = [];
|
|
298
|
+
if (artifactPaths) {
|
|
299
|
+
baselineCandidates.push(
|
|
300
|
+
artifactPaths.proposalPath,
|
|
301
|
+
artifactPaths.tasksPath,
|
|
302
|
+
artifactPaths.bindingsPath,
|
|
303
|
+
artifactPaths.pencilDesignPath,
|
|
304
|
+
artifactPaths.verificationPath
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (resolved && resolved.changeDir) {
|
|
308
|
+
const specsRoot = path.join(resolved.changeDir, "specs");
|
|
309
|
+
if (fs.existsSync(specsRoot)) {
|
|
310
|
+
const stack = [specsRoot];
|
|
311
|
+
while (stack.length > 0) {
|
|
312
|
+
const current = stack.pop();
|
|
313
|
+
let entries = [];
|
|
314
|
+
try {
|
|
315
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
316
|
+
} catch (_error) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
for (const entry of entries) {
|
|
320
|
+
const absolutePath = path.join(current, entry.name);
|
|
321
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
322
|
+
stack.push(absolutePath);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (entry.isFile() && entry.name === "spec.md") {
|
|
326
|
+
baselineCandidates.push(absolutePath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const baselineMs = baselineCandidates.reduce((latest, candidate) => Math.max(latest, safeMtimeMs(candidate)), 0);
|
|
333
|
+
return {
|
|
334
|
+
baselineMs,
|
|
335
|
+
baselineIso: baselineMs > 0 ? new Date(baselineMs).toISOString() : ""
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function collectVerificationFreshness(projectPathInput, options = {}) {
|
|
340
|
+
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
341
|
+
const changeId = options.changeId ? String(options.changeId).trim() : "";
|
|
342
|
+
if (!changeId) {
|
|
343
|
+
return {
|
|
344
|
+
fresh: false,
|
|
345
|
+
changeId: null,
|
|
346
|
+
requiredSurfaces: [],
|
|
347
|
+
surfaces: {},
|
|
348
|
+
staleReasons: ["Missing change id for verification freshness checks."],
|
|
349
|
+
baselineIso: ""
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const requiredSurfaces =
|
|
354
|
+
Array.isArray(options.requiredSurfaces) && options.requiredSurfaces.length > 0
|
|
355
|
+
? options.requiredSurfaces
|
|
356
|
+
: ["verify-bindings", "verify-implementation", "verify-structure", "verify-coverage"];
|
|
357
|
+
const maxAgeMs =
|
|
358
|
+
Number.isFinite(Number(options.maxAgeMs)) && Number(options.maxAgeMs) > 0
|
|
359
|
+
? Number(options.maxAgeMs)
|
|
360
|
+
: DEFAULT_EVIDENCE_MAX_AGE_MS;
|
|
361
|
+
const nowMs = Date.now();
|
|
362
|
+
const signals = readExecutionSignals(projectRoot, { changeId });
|
|
363
|
+
const summary = summarizeSignalsBySurface(signals);
|
|
364
|
+
const baseline = collectFreshnessBaseline(projectRoot, options.resolved, options.artifactPaths);
|
|
365
|
+
const staleReasons = [];
|
|
366
|
+
const surfaces = {};
|
|
367
|
+
|
|
368
|
+
for (const surface of requiredSurfaces) {
|
|
369
|
+
const key = String(surface || "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
370
|
+
const signal = summary[key];
|
|
371
|
+
if (!signal) {
|
|
372
|
+
staleReasons.push(`Missing evidence signal: ${surface}.`);
|
|
373
|
+
surfaces[surface] = {
|
|
374
|
+
present: false,
|
|
375
|
+
stale: true
|
|
376
|
+
};
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const timestampMs = Date.parse(String(signal.timestamp || ""));
|
|
380
|
+
if (!Number.isFinite(timestampMs)) {
|
|
381
|
+
staleReasons.push(`Invalid evidence timestamp for ${surface}.`);
|
|
382
|
+
surfaces[surface] = {
|
|
383
|
+
present: true,
|
|
384
|
+
stale: true,
|
|
385
|
+
timestamp: signal.timestamp || ""
|
|
386
|
+
};
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const olderThanBaseline = baseline.baselineMs > 0 && timestampMs < baseline.baselineMs;
|
|
390
|
+
const olderThanMaxAge = nowMs - timestampMs > maxAgeMs;
|
|
391
|
+
if (olderThanBaseline) {
|
|
392
|
+
staleReasons.push(
|
|
393
|
+
`${surface} evidence is older than current artifact baseline (${new Date(timestampMs).toISOString()} < ${baseline.baselineIso}).`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (olderThanMaxAge) {
|
|
397
|
+
staleReasons.push(
|
|
398
|
+
`${surface} evidence is older than freshness window (${Math.round((nowMs - timestampMs) / 1000)}s).`
|
|
399
|
+
);
|
|
224
400
|
}
|
|
401
|
+
surfaces[surface] = {
|
|
402
|
+
present: true,
|
|
403
|
+
stale: olderThanBaseline || olderThanMaxAge,
|
|
404
|
+
status: signal.status,
|
|
405
|
+
timestamp: signal.timestamp
|
|
406
|
+
};
|
|
225
407
|
}
|
|
226
|
-
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
fresh: staleReasons.length === 0,
|
|
411
|
+
changeId,
|
|
412
|
+
requiredSurfaces,
|
|
413
|
+
surfaces,
|
|
414
|
+
staleReasons: unique(staleReasons),
|
|
415
|
+
baselineIso: baseline.baselineIso
|
|
416
|
+
};
|
|
227
417
|
}
|
|
228
418
|
|
|
229
419
|
function createSharedSetup(projectPathInput, options = {}) {
|
|
@@ -280,6 +470,547 @@ function commonSetup(surface, projectPathInput, options) {
|
|
|
280
470
|
};
|
|
281
471
|
}
|
|
282
472
|
|
|
473
|
+
function collectChangedFileEntries(projectRoot, options = {}) {
|
|
474
|
+
const requested = options.changedFilesProvided === true || Array.isArray(options.changedFiles);
|
|
475
|
+
const rawEntries = Array.isArray(options.changedFiles)
|
|
476
|
+
? options.changedFiles
|
|
477
|
+
: options.changedFiles === undefined
|
|
478
|
+
? []
|
|
479
|
+
: [options.changedFiles];
|
|
480
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
481
|
+
const canonicalRoot = safeRealpathSync(resolvedRoot) || resolvedRoot;
|
|
482
|
+
|
|
483
|
+
const response = {
|
|
484
|
+
requested,
|
|
485
|
+
rawEntries: rawEntries.map((value) => String(value || "").trim()).filter(Boolean),
|
|
486
|
+
entries: [],
|
|
487
|
+
invalidEntries: [],
|
|
488
|
+
duplicateEntries: [],
|
|
489
|
+
missingEntries: [],
|
|
490
|
+
directoryEntries: [],
|
|
491
|
+
unreadableEntries: []
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
if (!requested) {
|
|
495
|
+
return response;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const seenInputs = new Set();
|
|
499
|
+
const seenCanonical = new Set();
|
|
500
|
+
for (const rawEntry of response.rawEntries) {
|
|
501
|
+
const absolutePath = path.isAbsolute(rawEntry)
|
|
502
|
+
? path.resolve(rawEntry)
|
|
503
|
+
: path.resolve(projectRoot, rawEntry);
|
|
504
|
+
|
|
505
|
+
if (!pathWithinRoot(resolvedRoot, absolutePath)) {
|
|
506
|
+
response.invalidEntries.push({
|
|
507
|
+
input: rawEntry,
|
|
508
|
+
reason: "path escapes project root"
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const relativePath = normalizeRelativePath(path.relative(projectRoot, absolutePath));
|
|
514
|
+
const dedupeInputKey = relativePath.toLowerCase();
|
|
515
|
+
if (seenInputs.has(dedupeInputKey)) {
|
|
516
|
+
response.duplicateEntries.push(relativePath || rawEntry);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
seenInputs.add(dedupeInputKey);
|
|
520
|
+
|
|
521
|
+
if (!fs.existsSync(absolutePath)) {
|
|
522
|
+
response.missingEntries.push(relativePath || rawEntry);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
let stat;
|
|
527
|
+
try {
|
|
528
|
+
stat = fs.lstatSync(absolutePath);
|
|
529
|
+
} catch (_error) {
|
|
530
|
+
response.unreadableEntries.push(relativePath || rawEntry);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (stat.isDirectory()) {
|
|
535
|
+
response.directoryEntries.push(relativePath || rawEntry);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const canonicalAbsolutePath = safeRealpathSync(absolutePath);
|
|
540
|
+
if (!canonicalAbsolutePath) {
|
|
541
|
+
response.unreadableEntries.push(relativePath || rawEntry);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (!pathWithinRoot(canonicalRoot, canonicalAbsolutePath)) {
|
|
545
|
+
response.invalidEntries.push({
|
|
546
|
+
input: rawEntry,
|
|
547
|
+
reason: "path escapes project root via symlink"
|
|
548
|
+
});
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const canonicalRelativePath = normalizeRelativePath(path.relative(canonicalRoot, canonicalAbsolutePath));
|
|
553
|
+
const effectiveRelativePath = canonicalRelativePath || relativePath;
|
|
554
|
+
const dedupeCanonicalKey = effectiveRelativePath.toLowerCase();
|
|
555
|
+
if (seenCanonical.has(dedupeCanonicalKey)) {
|
|
556
|
+
response.duplicateEntries.push(effectiveRelativePath || rawEntry);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
seenCanonical.add(dedupeCanonicalKey);
|
|
560
|
+
|
|
561
|
+
response.entries.push({
|
|
562
|
+
input: rawEntry,
|
|
563
|
+
absolutePath,
|
|
564
|
+
canonicalPath: canonicalAbsolutePath,
|
|
565
|
+
relativePath: effectiveRelativePath,
|
|
566
|
+
extension: path.extname(effectiveRelativePath || relativePath).toLowerCase(),
|
|
567
|
+
isSymlink: stat.isSymbolicLink()
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return response;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function buildImplementationChecks(specRecords, tasksArtifact) {
|
|
575
|
+
const stateChecks = [];
|
|
576
|
+
let stateCounter = 0;
|
|
577
|
+
for (const record of specRecords) {
|
|
578
|
+
const states = record.parsed.sections.states.items || [];
|
|
579
|
+
for (const stateItem of states) {
|
|
580
|
+
const stateLabel = String(stateItem || "").split(":")[0];
|
|
581
|
+
const tokens = unique(tokenizeCoverage(stateLabel, 3));
|
|
582
|
+
if (tokens.length === 0) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
stateCounter += 1;
|
|
586
|
+
stateChecks.push(createImplementationCheck({
|
|
587
|
+
id: `state-${stateCounter}`,
|
|
588
|
+
type: "state",
|
|
589
|
+
label: String(stateItem || "").trim(),
|
|
590
|
+
recordPath: record.path,
|
|
591
|
+
tokens
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const taskGroupChecks = [];
|
|
597
|
+
for (const group of tasksArtifact.taskGroups) {
|
|
598
|
+
const tokens = unique(tokenizeCoverage(group.title, 4));
|
|
599
|
+
if (tokens.length === 0) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
taskGroupChecks.push(createImplementationCheck({
|
|
603
|
+
id: `task-group-${group.id}`,
|
|
604
|
+
type: "task-group",
|
|
605
|
+
label: `${group.id}. ${group.title}`,
|
|
606
|
+
groupId: group.id,
|
|
607
|
+
tokens
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
stateChecks,
|
|
613
|
+
taskGroupChecks,
|
|
614
|
+
allChecks: [...stateChecks, ...taskGroupChecks]
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function createImplementationCheck(data) {
|
|
619
|
+
const tokens = Array.isArray(data.tokens) ? data.tokens.filter(Boolean) : [];
|
|
620
|
+
return {
|
|
621
|
+
id: data.id,
|
|
622
|
+
type: data.type,
|
|
623
|
+
label: data.label,
|
|
624
|
+
recordPath: data.recordPath || null,
|
|
625
|
+
groupId: data.groupId || null,
|
|
626
|
+
tokens,
|
|
627
|
+
requiredMatches: computeRequiredMatches(data.type, tokens),
|
|
628
|
+
covered: false,
|
|
629
|
+
evidence: null,
|
|
630
|
+
boundaries: []
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function computeRequiredMatches(type, tokens) {
|
|
635
|
+
if (!Array.isArray(tokens) || tokens.length === 0) {
|
|
636
|
+
return 0;
|
|
637
|
+
}
|
|
638
|
+
if (tokens.length === 1) {
|
|
639
|
+
return 1;
|
|
640
|
+
}
|
|
641
|
+
if (type === "task-group") {
|
|
642
|
+
return Math.min(2, tokens.length);
|
|
643
|
+
}
|
|
644
|
+
return Math.min(2, tokens.length);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function evaluateCheckAgainstTokenSet(check, tokenSet) {
|
|
648
|
+
const matchedTokens = [];
|
|
649
|
+
for (const token of check.tokens) {
|
|
650
|
+
if (tokenSet.has(token)) {
|
|
651
|
+
matchedTokens.push(token);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
matchedTokens,
|
|
656
|
+
covered: matchedTokens.length >= check.requiredMatches
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function addBoundary(check, boundary) {
|
|
661
|
+
if (!check || !boundary || !boundary.type) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const key = `${boundary.type}|${boundary.file || ""}|${boundary.reason || ""}`;
|
|
665
|
+
if (!Array.isArray(check.boundaries)) {
|
|
666
|
+
check.boundaries = [];
|
|
667
|
+
}
|
|
668
|
+
if (check.boundaries.some((item) => `${item.type}|${item.file || ""}|${item.reason || ""}` === key)) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
check.boundaries.push({
|
|
672
|
+
type: boundary.type,
|
|
673
|
+
file: boundary.file || null,
|
|
674
|
+
reason: boundary.reason || ""
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function evidenceScore(evidence) {
|
|
679
|
+
if (!evidence) {
|
|
680
|
+
return 0;
|
|
681
|
+
}
|
|
682
|
+
const confidenceScore =
|
|
683
|
+
evidence.confidence === "high"
|
|
684
|
+
? 30
|
|
685
|
+
: evidence.confidence === "medium"
|
|
686
|
+
? 20
|
|
687
|
+
: evidence.confidence === "low"
|
|
688
|
+
? 10
|
|
689
|
+
: 0;
|
|
690
|
+
const modeScore =
|
|
691
|
+
evidence.mode === "syntax-aware"
|
|
692
|
+
? 4
|
|
693
|
+
: evidence.mode === "markup"
|
|
694
|
+
? 3
|
|
695
|
+
: evidence.mode === "heuristic"
|
|
696
|
+
? 2
|
|
697
|
+
: 1;
|
|
698
|
+
return confidenceScore + modeScore;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function applyEvidence(check, evidence) {
|
|
702
|
+
if (!check || !evidence) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (!check.evidence || evidenceScore(evidence) > evidenceScore(check.evidence)) {
|
|
706
|
+
check.evidence = evidence;
|
|
707
|
+
check.covered = true;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const REGEX_PREFIX_CHARS = new Set([
|
|
712
|
+
"(",
|
|
713
|
+
"[",
|
|
714
|
+
"{",
|
|
715
|
+
",",
|
|
716
|
+
";",
|
|
717
|
+
":",
|
|
718
|
+
"=",
|
|
719
|
+
"!",
|
|
720
|
+
"&",
|
|
721
|
+
"|",
|
|
722
|
+
"?",
|
|
723
|
+
"+",
|
|
724
|
+
"-",
|
|
725
|
+
"*",
|
|
726
|
+
"%",
|
|
727
|
+
"^",
|
|
728
|
+
"~",
|
|
729
|
+
"<",
|
|
730
|
+
">",
|
|
731
|
+
"/"
|
|
732
|
+
]);
|
|
733
|
+
const REGEX_PREFIX_KEYWORD_PATTERN =
|
|
734
|
+
/(?:^|[^\w$])(return|throw|case|delete|void|typeof|instanceof|in|of|new|await|yield)\s*$/;
|
|
735
|
+
|
|
736
|
+
function previousNonWhitespaceChar(text, startIndex) {
|
|
737
|
+
for (let index = startIndex; index >= 0; index -= 1) {
|
|
738
|
+
const char = text[index];
|
|
739
|
+
if (char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f") {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
return char;
|
|
743
|
+
}
|
|
744
|
+
return "";
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function canStartRegexLiteral(text, index) {
|
|
748
|
+
const previousChar = previousNonWhitespaceChar(text, index - 1);
|
|
749
|
+
if (!previousChar) {
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
if (REGEX_PREFIX_CHARS.has(previousChar)) {
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
return REGEX_PREFIX_KEYWORD_PATTERN.test(text.slice(0, index));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function collectScriptEvidenceText(source) {
|
|
759
|
+
const text = String(source || "");
|
|
760
|
+
const out = [];
|
|
761
|
+
let state = "normal";
|
|
762
|
+
let regexInClass = false;
|
|
763
|
+
|
|
764
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
765
|
+
const char = text[index];
|
|
766
|
+
const next = text[index + 1];
|
|
767
|
+
|
|
768
|
+
if (state === "normal") {
|
|
769
|
+
if (char === "/" && next === "/") {
|
|
770
|
+
out.push(" ", " ");
|
|
771
|
+
state = "line-comment";
|
|
772
|
+
index += 1;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (char === "/" && next === "*") {
|
|
776
|
+
out.push(" ", " ");
|
|
777
|
+
state = "block-comment";
|
|
778
|
+
index += 1;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (char === "/" && canStartRegexLiteral(text, index)) {
|
|
782
|
+
out.push(" ");
|
|
783
|
+
state = "regex";
|
|
784
|
+
regexInClass = false;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (char === "'") {
|
|
788
|
+
out.push(" ");
|
|
789
|
+
state = "single-quote";
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (char === '"') {
|
|
793
|
+
out.push(" ");
|
|
794
|
+
state = "double-quote";
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (char === "`") {
|
|
798
|
+
out.push(" ");
|
|
799
|
+
state = "template";
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
out.push(char);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (state === "line-comment") {
|
|
807
|
+
if (char === "\n") {
|
|
808
|
+
out.push("\n");
|
|
809
|
+
state = "normal";
|
|
810
|
+
} else {
|
|
811
|
+
out.push(" ");
|
|
812
|
+
}
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (state === "block-comment") {
|
|
817
|
+
if (char === "*" && next === "/") {
|
|
818
|
+
out.push(" ", " ");
|
|
819
|
+
state = "normal";
|
|
820
|
+
index += 1;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
out.push(char === "\n" ? "\n" : " ");
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (state === "regex") {
|
|
828
|
+
if (char === "\n") {
|
|
829
|
+
out.push("\n");
|
|
830
|
+
state = "normal";
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (char === "\\") {
|
|
834
|
+
out.push(" ");
|
|
835
|
+
if (index + 1 < text.length) {
|
|
836
|
+
out.push(" ");
|
|
837
|
+
index += 1;
|
|
838
|
+
}
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (char === "[" && !regexInClass) {
|
|
842
|
+
out.push(" ");
|
|
843
|
+
regexInClass = true;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (char === "]" && regexInClass) {
|
|
847
|
+
out.push(" ");
|
|
848
|
+
regexInClass = false;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (char === "/" && !regexInClass) {
|
|
852
|
+
out.push(" ");
|
|
853
|
+
state = "regex-flags";
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
out.push(" ");
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (state === "regex-flags") {
|
|
861
|
+
if (/[a-z]/i.test(char)) {
|
|
862
|
+
out.push(" ");
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
state = "normal";
|
|
866
|
+
index -= 1;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (state === "single-quote") {
|
|
871
|
+
if (char === "\\") {
|
|
872
|
+
out.push(" ");
|
|
873
|
+
if (index + 1 < text.length) {
|
|
874
|
+
out.push(" ");
|
|
875
|
+
index += 1;
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (char === "'") {
|
|
880
|
+
out.push(" ");
|
|
881
|
+
state = "normal";
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (char === "\n") {
|
|
885
|
+
return {
|
|
886
|
+
ok: false,
|
|
887
|
+
text: "",
|
|
888
|
+
reason: "unterminated-string-literal"
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
out.push(" ");
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (state === "double-quote") {
|
|
896
|
+
if (char === "\\") {
|
|
897
|
+
out.push(" ");
|
|
898
|
+
if (index + 1 < text.length) {
|
|
899
|
+
out.push(" ");
|
|
900
|
+
index += 1;
|
|
901
|
+
}
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (char === '"') {
|
|
905
|
+
out.push(" ");
|
|
906
|
+
state = "normal";
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (char === "\n") {
|
|
910
|
+
return {
|
|
911
|
+
ok: false,
|
|
912
|
+
text: "",
|
|
913
|
+
reason: "unterminated-string-literal"
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
out.push(" ");
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (state === "template") {
|
|
921
|
+
if (char === "\\") {
|
|
922
|
+
out.push(" ");
|
|
923
|
+
if (index + 1 < text.length) {
|
|
924
|
+
out.push(" ");
|
|
925
|
+
index += 1;
|
|
926
|
+
}
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (char === "`") {
|
|
930
|
+
out.push(" ");
|
|
931
|
+
state = "normal";
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
out.push(char === "\n" ? "\n" : " ");
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (state === "single-quote" || state === "double-quote" || state === "template" || state === "block-comment") {
|
|
940
|
+
return {
|
|
941
|
+
ok: false,
|
|
942
|
+
text: "",
|
|
943
|
+
reason: "unterminated-comment-or-string"
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
ok: true,
|
|
949
|
+
text: out.join(""),
|
|
950
|
+
reason: ""
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function stripMarkupComments(source) {
|
|
955
|
+
return String(source || "").replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function collectStructureEvidenceText(source, extension) {
|
|
959
|
+
const markupFiltered = stripMarkupComments(source);
|
|
960
|
+
if (SYNTAX_AWARE_SCRIPT_EXTENSIONS.has(extension)) {
|
|
961
|
+
const scriptEvidence = collectScriptEvidenceText(markupFiltered);
|
|
962
|
+
if (scriptEvidence.ok) {
|
|
963
|
+
return {
|
|
964
|
+
text: scriptEvidence.text,
|
|
965
|
+
syntaxAvailable: true,
|
|
966
|
+
reason: ""
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
text: "",
|
|
971
|
+
syntaxAvailable: false,
|
|
972
|
+
reason: scriptEvidence.reason || "syntax-unavailable"
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return {
|
|
977
|
+
text: markupFiltered,
|
|
978
|
+
syntaxAvailable: true,
|
|
979
|
+
reason: ""
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function serializeCheck(check, noRelevantFiles = false) {
|
|
984
|
+
const evidence = check.evidence
|
|
985
|
+
? {
|
|
986
|
+
mode: check.evidence.mode,
|
|
987
|
+
confidence: check.evidence.confidence,
|
|
988
|
+
file: check.evidence.file || null,
|
|
989
|
+
reason: check.evidence.reason || "",
|
|
990
|
+
matchedTokens: check.evidence.matchedTokens || [],
|
|
991
|
+
degraded: check.evidence.degraded === true
|
|
992
|
+
}
|
|
993
|
+
: {
|
|
994
|
+
mode: noRelevantFiles ? "not-scanned" : "none",
|
|
995
|
+
confidence: "none",
|
|
996
|
+
file: null,
|
|
997
|
+
reason: noRelevantFiles ? "no-relevant-files-scanned" : "no-qualifying-evidence",
|
|
998
|
+
matchedTokens: [],
|
|
999
|
+
degraded: false
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
return {
|
|
1003
|
+
id: check.id,
|
|
1004
|
+
type: check.type,
|
|
1005
|
+
label: check.label,
|
|
1006
|
+
covered: check.covered,
|
|
1007
|
+
tokens: check.tokens,
|
|
1008
|
+
requiredMatches: check.requiredMatches,
|
|
1009
|
+
evidence,
|
|
1010
|
+
boundaries: Array.isArray(check.boundaries) ? check.boundaries : []
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
|
|
283
1014
|
function verifyBindings(projectPathInput, options = {}) {
|
|
284
1015
|
const setup = commonSetup("verify-bindings", projectPathInput, options);
|
|
285
1016
|
const { result, artifacts } = setup;
|
|
@@ -323,13 +1054,6 @@ function verifyImplementation(projectPathInput, options = {}) {
|
|
|
323
1054
|
return result;
|
|
324
1055
|
}
|
|
325
1056
|
|
|
326
|
-
const codeScan = collectCodeFiles(result.projectRoot);
|
|
327
|
-
const codeFiles = codeScan.files;
|
|
328
|
-
if (codeFiles.length === 0) {
|
|
329
|
-
result.failures.push("No implementation files were found for verify-implementation.");
|
|
330
|
-
return finalize(result);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
1057
|
const specRecords = parseRuntimeSpecs(resolved.changeDir, result.projectRoot);
|
|
334
1058
|
const tasksArtifact = parseTasksArtifact(artifacts.tasks || "");
|
|
335
1059
|
|
|
@@ -338,46 +1062,158 @@ function verifyImplementation(projectPathInput, options = {}) {
|
|
|
338
1062
|
return finalize(result);
|
|
339
1063
|
}
|
|
340
1064
|
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1065
|
+
const checks = buildImplementationChecks(specRecords, tasksArtifact);
|
|
1066
|
+
const allChecks = checks.allChecks;
|
|
1067
|
+
|
|
1068
|
+
const changedFiles = collectChangedFileEntries(result.projectRoot, options);
|
|
1069
|
+
if (changedFiles.requested && changedFiles.invalidEntries.length > 0) {
|
|
1070
|
+
for (const invalidEntry of changedFiles.invalidEntries) {
|
|
1071
|
+
result.failures.push(
|
|
1072
|
+
`Invalid --changed-files entry "${invalidEntry.input}": ${invalidEntry.reason}.`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
return finalize(result);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
let codeFiles = [];
|
|
1079
|
+
let scan = {
|
|
1080
|
+
truncatedByFileLimit: false,
|
|
1081
|
+
truncatedByDirectoryLimit: false,
|
|
1082
|
+
depthLimitHits: 0,
|
|
1083
|
+
skippedSymlinks: 0,
|
|
1084
|
+
readErrors: 0,
|
|
1085
|
+
scannedDirectories: 0
|
|
1086
|
+
};
|
|
1087
|
+
const filteredChanged = {
|
|
1088
|
+
duplicates: changedFiles.duplicateEntries.length,
|
|
1089
|
+
missing: changedFiles.missingEntries.length,
|
|
1090
|
+
directories: changedFiles.directoryEntries.length,
|
|
1091
|
+
unreadable: changedFiles.unreadableEntries.length,
|
|
1092
|
+
unsupported: 0,
|
|
1093
|
+
excluded: 0,
|
|
1094
|
+
symlinks: 0
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
if (changedFiles.requested) {
|
|
1098
|
+
for (const entry of changedFiles.entries) {
|
|
1099
|
+
if (entry.isSymlink) {
|
|
1100
|
+
filteredChanged.symlinks += 1;
|
|
347
1101
|
continue;
|
|
348
1102
|
}
|
|
349
|
-
|
|
350
|
-
|
|
1103
|
+
if (!CODE_FILE_EXTENSIONS.has(entry.extension)) {
|
|
1104
|
+
filteredChanged.unsupported += 1;
|
|
351
1105
|
continue;
|
|
352
1106
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
});
|
|
1107
|
+
if (isNonImplementationFileName(path.basename(entry.relativePath)) || hasExcludedDirectory(entry.relativePath)) {
|
|
1108
|
+
filteredChanged.excluded += 1;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
codeFiles.push(entry.absolutePath);
|
|
359
1112
|
}
|
|
1113
|
+
codeFiles = unique(codeFiles).sort();
|
|
1114
|
+
} else {
|
|
1115
|
+
const codeScan = collectCodeFiles(result.projectRoot);
|
|
1116
|
+
codeFiles = codeScan.files;
|
|
1117
|
+
scan = codeScan.scan;
|
|
360
1118
|
}
|
|
361
1119
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
1120
|
+
let scannedBytes = 0;
|
|
1121
|
+
let skippedLargeFiles = 0;
|
|
1122
|
+
let readErrors = 0;
|
|
1123
|
+
let scannedFiles = 0;
|
|
1124
|
+
const degradedFallbackFiles = new Set();
|
|
1125
|
+
const markupHeuristicFiles = new Set();
|
|
1126
|
+
const unsupportedHeuristicFiles = new Set();
|
|
1127
|
+
|
|
1128
|
+
result.scan = {
|
|
1129
|
+
scanMode: changedFiles.requested ? "incremental" : "full",
|
|
1130
|
+
requestedChangedFiles: changedFiles.rawEntries.length,
|
|
1131
|
+
selectedFileCount: changedFiles.requested ? changedFiles.entries.length : codeFiles.length,
|
|
1132
|
+
relevantFileCount: codeFiles.length,
|
|
1133
|
+
scannedFileCount: 0,
|
|
1134
|
+
filtered: {
|
|
1135
|
+
...filteredChanged,
|
|
1136
|
+
total:
|
|
1137
|
+
filteredChanged.duplicates +
|
|
1138
|
+
filteredChanged.missing +
|
|
1139
|
+
filteredChanged.directories +
|
|
1140
|
+
filteredChanged.unreadable +
|
|
1141
|
+
filteredChanged.unsupported +
|
|
1142
|
+
filteredChanged.excluded +
|
|
1143
|
+
filteredChanged.symlinks
|
|
1144
|
+
},
|
|
1145
|
+
noRelevantFiles: false
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
if (changedFiles.requested) {
|
|
1149
|
+
if (changedFiles.missingEntries.length > 0) {
|
|
1150
|
+
result.notes.push(
|
|
1151
|
+
`Incremental input ignored missing files: ${listSummary(changedFiles.missingEntries)}.`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (changedFiles.directoryEntries.length > 0) {
|
|
1155
|
+
result.notes.push(
|
|
1156
|
+
`Incremental input ignored directory paths: ${listSummary(changedFiles.directoryEntries)}.`
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
if (changedFiles.duplicateEntries.length > 0) {
|
|
1160
|
+
result.notes.push(
|
|
1161
|
+
`Incremental input deduplicated repeated paths: ${listSummary(changedFiles.duplicateEntries)}.`
|
|
1162
|
+
);
|
|
369
1163
|
}
|
|
1164
|
+
if (filteredChanged.unsupported > 0) {
|
|
1165
|
+
result.notes.push(
|
|
1166
|
+
`Incremental input ignored ${filteredChanged.unsupported} unsupported implementation file(s).`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
if (filteredChanged.excluded > 0) {
|
|
1170
|
+
result.notes.push(
|
|
1171
|
+
`Incremental input ignored ${filteredChanged.excluded} excluded test/fixture/spec file(s).`
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
if (filteredChanged.symlinks > 0) {
|
|
1175
|
+
result.notes.push(
|
|
1176
|
+
`Incremental input ignored ${filteredChanged.symlinks} symlink path(s).`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
if (changedFiles.unreadableEntries.length > 0) {
|
|
1180
|
+
result.notes.push(
|
|
1181
|
+
`Incremental input ignored unreadable files: ${listSummary(changedFiles.unreadableEntries)}.`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
370
1185
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
1186
|
+
if (codeFiles.length === 0) {
|
|
1187
|
+
if (changedFiles.requested) {
|
|
1188
|
+
result.scan.noRelevantFiles = true;
|
|
1189
|
+
result.warnings.push(
|
|
1190
|
+
"Incremental verify-implementation scanned zero relevant implementation files; result is partial."
|
|
1191
|
+
);
|
|
1192
|
+
result.summary = {
|
|
1193
|
+
codeFiles: 0,
|
|
1194
|
+
specFiles: specRecords.length,
|
|
1195
|
+
taskGroups: tasksArtifact.taskGroups.length,
|
|
1196
|
+
scannedBytes: 0,
|
|
1197
|
+
scanMode: "incremental"
|
|
1198
|
+
};
|
|
1199
|
+
result.implementation = {
|
|
1200
|
+
stateChecks: checks.stateChecks.length,
|
|
1201
|
+
taskGroupChecks: checks.taskGroupChecks.length,
|
|
1202
|
+
degradedChecks: 0,
|
|
1203
|
+
checks: allChecks.map((check) => serializeCheck(check, true)),
|
|
1204
|
+
evidenceModeCounts: {
|
|
1205
|
+
"syntax-aware": 0,
|
|
1206
|
+
markup: 0,
|
|
1207
|
+
heuristic: 0,
|
|
1208
|
+
none: allChecks.length
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
return finalize(result);
|
|
1212
|
+
}
|
|
1213
|
+
result.failures.push("No implementation files were found for verify-implementation.");
|
|
1214
|
+
return finalize(result);
|
|
376
1215
|
}
|
|
377
1216
|
|
|
378
|
-
let scannedBytes = 0;
|
|
379
|
-
let skippedLargeFiles = 0;
|
|
380
|
-
let readErrors = 0;
|
|
381
1217
|
for (const codeFile of codeFiles) {
|
|
382
1218
|
const read = readCodeFileForScan(codeFile);
|
|
383
1219
|
scannedBytes += read.bytesRead;
|
|
@@ -390,67 +1226,201 @@ function verifyImplementation(projectPathInput, options = {}) {
|
|
|
390
1226
|
continue;
|
|
391
1227
|
}
|
|
392
1228
|
|
|
393
|
-
|
|
394
|
-
|
|
1229
|
+
scannedFiles += 1;
|
|
1230
|
+
const relativeFile = normalizeRelativePath(path.relative(result.projectRoot, codeFile));
|
|
1231
|
+
const extension = path.extname(codeFile).toLowerCase();
|
|
1232
|
+
const source = String(read.text || "");
|
|
1233
|
+
if (!source) {
|
|
395
1234
|
continue;
|
|
396
1235
|
}
|
|
397
1236
|
|
|
398
|
-
|
|
399
|
-
|
|
1237
|
+
const rawTokenSet = new Set(tokenizeCoverage(source, 1));
|
|
1238
|
+
|
|
1239
|
+
if (SYNTAX_AWARE_SCRIPT_EXTENSIONS.has(extension)) {
|
|
1240
|
+
const scriptEvidence = collectScriptEvidenceText(source);
|
|
1241
|
+
if (scriptEvidence.ok) {
|
|
1242
|
+
const syntaxTokenSet = new Set(tokenizeCoverage(scriptEvidence.text, 1));
|
|
1243
|
+
for (const check of allChecks) {
|
|
1244
|
+
if (!check.tokens.length) {
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
const syntaxMatch = evaluateCheckAgainstTokenSet(check, syntaxTokenSet);
|
|
1248
|
+
if (syntaxMatch.covered) {
|
|
1249
|
+
applyEvidence(check, {
|
|
1250
|
+
mode: "syntax-aware",
|
|
1251
|
+
confidence: "high",
|
|
1252
|
+
file: relativeFile,
|
|
1253
|
+
reason: "syntax-structure-match",
|
|
1254
|
+
matchedTokens: syntaxMatch.matchedTokens,
|
|
1255
|
+
degraded: false
|
|
1256
|
+
});
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const rawMatch = evaluateCheckAgainstTokenSet(check, rawTokenSet);
|
|
1261
|
+
if (rawMatch.covered) {
|
|
1262
|
+
addBoundary(check, {
|
|
1263
|
+
type: "comment-or-string-only",
|
|
1264
|
+
file: relativeFile,
|
|
1265
|
+
reason: "raw token appeared only outside executable syntax"
|
|
1266
|
+
});
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (rawMatch.matchedTokens.length > 0 || syntaxMatch.matchedTokens.length > 0) {
|
|
1271
|
+
addBoundary(check, {
|
|
1272
|
+
type: "accidental-token-overlap",
|
|
1273
|
+
file: relativeFile,
|
|
1274
|
+
reason: "token overlap below required threshold"
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
400
1278
|
continue;
|
|
401
1279
|
}
|
|
402
|
-
|
|
1280
|
+
|
|
1281
|
+
degradedFallbackFiles.add(relativeFile);
|
|
1282
|
+
for (const check of allChecks) {
|
|
1283
|
+
if (!check.tokens.length) {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const rawMatch = evaluateCheckAgainstTokenSet(check, rawTokenSet);
|
|
1287
|
+
if (rawMatch.covered) {
|
|
1288
|
+
applyEvidence(check, {
|
|
1289
|
+
mode: "heuristic",
|
|
1290
|
+
confidence: "low",
|
|
1291
|
+
file: relativeFile,
|
|
1292
|
+
reason: `syntax-unavailable:${scriptEvidence.reason}`,
|
|
1293
|
+
matchedTokens: rawMatch.matchedTokens,
|
|
1294
|
+
degraded: true
|
|
1295
|
+
});
|
|
1296
|
+
} else if (rawMatch.matchedTokens.length > 0) {
|
|
1297
|
+
addBoundary(check, {
|
|
1298
|
+
type: "accidental-token-overlap",
|
|
1299
|
+
file: relativeFile,
|
|
1300
|
+
reason: "token overlap below required threshold"
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const mode = IMPLEMENTATION_MARKUP_EXTENSIONS.has(extension) ? "markup" : "heuristic";
|
|
1308
|
+
const confidence = mode === "markup" ? "medium" : "low";
|
|
1309
|
+
if (mode === "markup") {
|
|
1310
|
+
markupHeuristicFiles.add(relativeFile);
|
|
1311
|
+
} else {
|
|
1312
|
+
unsupportedHeuristicFiles.add(relativeFile);
|
|
403
1313
|
}
|
|
404
|
-
for (const check of
|
|
405
|
-
if (check.
|
|
1314
|
+
for (const check of allChecks) {
|
|
1315
|
+
if (!check.tokens.length) {
|
|
406
1316
|
continue;
|
|
407
1317
|
}
|
|
408
|
-
|
|
1318
|
+
const rawMatch = evaluateCheckAgainstTokenSet(check, rawTokenSet);
|
|
1319
|
+
if (rawMatch.covered) {
|
|
1320
|
+
applyEvidence(check, {
|
|
1321
|
+
mode,
|
|
1322
|
+
confidence,
|
|
1323
|
+
file: relativeFile,
|
|
1324
|
+
reason: mode === "markup" ? "markup-heuristic-match" : `unsupported-language:${extension || "(none)"}`,
|
|
1325
|
+
matchedTokens: rawMatch.matchedTokens,
|
|
1326
|
+
degraded: mode !== "markup"
|
|
1327
|
+
});
|
|
1328
|
+
} else if (rawMatch.matchedTokens.length > 0) {
|
|
1329
|
+
addBoundary(check, {
|
|
1330
|
+
type: "accidental-token-overlap",
|
|
1331
|
+
file: relativeFile,
|
|
1332
|
+
reason: "token overlap below required threshold"
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
409
1335
|
}
|
|
1336
|
+
}
|
|
410
1337
|
|
|
411
|
-
|
|
412
|
-
|
|
1338
|
+
result.scan.scannedFileCount = scannedFiles;
|
|
1339
|
+
|
|
1340
|
+
const degradedChecks = [];
|
|
1341
|
+
const evidenceModeCounts = {
|
|
1342
|
+
"syntax-aware": 0,
|
|
1343
|
+
markup: 0,
|
|
1344
|
+
heuristic: 0,
|
|
1345
|
+
none: 0
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
for (const check of allChecks) {
|
|
1349
|
+
if (!check.evidence) {
|
|
1350
|
+
evidenceModeCounts.none += 1;
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
const mode = check.evidence.mode;
|
|
1354
|
+
if (Object.prototype.hasOwnProperty.call(evidenceModeCounts, mode)) {
|
|
1355
|
+
evidenceModeCounts[mode] += 1;
|
|
1356
|
+
}
|
|
1357
|
+
if (check.evidence.degraded) {
|
|
1358
|
+
degradedChecks.push(check);
|
|
413
1359
|
}
|
|
414
1360
|
}
|
|
415
1361
|
|
|
416
|
-
for (const check of stateChecks) {
|
|
1362
|
+
for (const check of checks.stateChecks) {
|
|
417
1363
|
if (!check.covered) {
|
|
418
1364
|
result.warnings.push(
|
|
419
|
-
`State coverage may be missing in implementation: "${check.
|
|
1365
|
+
`State coverage may be missing in implementation: "${check.label}" (${check.recordPath}).`
|
|
420
1366
|
);
|
|
421
1367
|
}
|
|
422
1368
|
}
|
|
423
|
-
for (const check of taskGroupChecks) {
|
|
1369
|
+
for (const check of checks.taskGroupChecks) {
|
|
424
1370
|
if (!check.covered) {
|
|
425
1371
|
result.warnings.push(
|
|
426
|
-
`Task-group intent may be missing in implementation: "${check.
|
|
1372
|
+
`Task-group intent may be missing in implementation: "${check.label}".`
|
|
427
1373
|
);
|
|
428
1374
|
}
|
|
429
1375
|
}
|
|
430
1376
|
|
|
431
|
-
if (
|
|
1377
|
+
if (degradedFallbackFiles.size > 0) {
|
|
1378
|
+
result.warnings.push(
|
|
1379
|
+
`verify-implementation fell back to heuristic mode for unparseable script files: ${listSummary(Array.from(degradedFallbackFiles).sort())}.`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
if (markupHeuristicFiles.size > 0) {
|
|
1383
|
+
result.notes.push(
|
|
1384
|
+
`verify-implementation used markup heuristic evidence for: ${listSummary(Array.from(markupHeuristicFiles).sort())}.`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
if (unsupportedHeuristicFiles.size > 0) {
|
|
1388
|
+
result.notes.push(
|
|
1389
|
+
`verify-implementation used unsupported-language heuristic evidence for: ${listSummary(
|
|
1390
|
+
Array.from(unsupportedHeuristicFiles).sort()
|
|
1391
|
+
)}.`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (result.strict && degradedChecks.length > 0) {
|
|
1396
|
+
result.warnings.push(
|
|
1397
|
+
"Strict mode does not promote degraded heuristic evidence to full-confidence coverage."
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (scan.truncatedByFileLimit) {
|
|
432
1402
|
result.warnings.push(
|
|
433
1403
|
`verify-implementation hit file scan limit (${MAX_SCANNED_FILES}); deep coverage may be incomplete.`
|
|
434
1404
|
);
|
|
435
1405
|
}
|
|
436
|
-
if (
|
|
1406
|
+
if (scan.truncatedByDirectoryLimit) {
|
|
437
1407
|
result.warnings.push(
|
|
438
1408
|
`verify-implementation hit directory scan limit (${MAX_SCANNED_DIRECTORIES}); coverage may be incomplete.`
|
|
439
1409
|
);
|
|
440
1410
|
}
|
|
441
|
-
if (
|
|
1411
|
+
if (scan.readErrors + readErrors > 0) {
|
|
442
1412
|
result.warnings.push(
|
|
443
|
-
`verify-implementation skipped unreadable files/directories (${
|
|
1413
|
+
`verify-implementation skipped unreadable files/directories (${scan.readErrors + readErrors}).`
|
|
444
1414
|
);
|
|
445
1415
|
}
|
|
446
|
-
if (
|
|
1416
|
+
if (scan.depthLimitHits > 0) {
|
|
447
1417
|
result.notes.push(
|
|
448
|
-
`verify-implementation enforced max scan depth (${MAX_SCAN_DEPTH}); skipped deeper paths: ${
|
|
1418
|
+
`verify-implementation enforced max scan depth (${MAX_SCAN_DEPTH}); skipped deeper paths: ${scan.depthLimitHits}.`
|
|
449
1419
|
);
|
|
450
1420
|
}
|
|
451
|
-
if (
|
|
1421
|
+
if (scan.skippedSymlinks > 0) {
|
|
452
1422
|
result.notes.push(
|
|
453
|
-
`verify-implementation skipped symlink entries during scan: ${
|
|
1423
|
+
`verify-implementation skipped symlink entries during scan: ${scan.skippedSymlinks}.`
|
|
454
1424
|
);
|
|
455
1425
|
}
|
|
456
1426
|
if (skippedLargeFiles > 0) {
|
|
@@ -463,8 +1433,17 @@ function verifyImplementation(projectPathInput, options = {}) {
|
|
|
463
1433
|
codeFiles: codeFiles.length,
|
|
464
1434
|
specFiles: specRecords.length,
|
|
465
1435
|
taskGroups: tasksArtifact.taskGroups.length,
|
|
466
|
-
scannedBytes
|
|
1436
|
+
scannedBytes,
|
|
1437
|
+
scanMode: result.scan.scanMode
|
|
1438
|
+
};
|
|
1439
|
+
result.implementation = {
|
|
1440
|
+
stateChecks: checks.stateChecks.length,
|
|
1441
|
+
taskGroupChecks: checks.taskGroupChecks.length,
|
|
1442
|
+
degradedChecks: degradedChecks.length,
|
|
1443
|
+
checks: allChecks.map((check) => serializeCheck(check, false)),
|
|
1444
|
+
evidenceModeCounts
|
|
467
1445
|
};
|
|
1446
|
+
|
|
468
1447
|
return finalize(result);
|
|
469
1448
|
}
|
|
470
1449
|
|
|
@@ -488,6 +1467,32 @@ function verifyStructure(projectPathInput, options = {}) {
|
|
|
488
1467
|
return finalize(result);
|
|
489
1468
|
}
|
|
490
1469
|
|
|
1470
|
+
const changedFiles = collectChangedFileEntries(result.projectRoot, options);
|
|
1471
|
+
if (changedFiles.requested && changedFiles.invalidEntries.length > 0) {
|
|
1472
|
+
for (const invalidEntry of changedFiles.invalidEntries) {
|
|
1473
|
+
result.failures.push(
|
|
1474
|
+
`Invalid --changed-files entry "${invalidEntry.input}": ${invalidEntry.reason}.`
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
return finalize(result);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const changedFileSet = new Set(
|
|
1481
|
+
changedFiles.entries
|
|
1482
|
+
.filter((entry) => !entry.isSymlink)
|
|
1483
|
+
.map((entry) => canonicalizePath(entry.absolutePath))
|
|
1484
|
+
);
|
|
1485
|
+
|
|
1486
|
+
const scannedMappingFiles = new Set();
|
|
1487
|
+
let scannedMappings = 0;
|
|
1488
|
+
let skippedByIncremental = 0;
|
|
1489
|
+
let ignoredSymlinkEntries = 0;
|
|
1490
|
+
for (const entry of changedFiles.entries) {
|
|
1491
|
+
if (entry.isSymlink) {
|
|
1492
|
+
ignoredSymlinkEntries += 1;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
491
1496
|
for (const mapping of bindings.mappings) {
|
|
492
1497
|
const landing = resolveImplementationLanding(result.projectRoot, mapping.implementation);
|
|
493
1498
|
if (!landing) {
|
|
@@ -495,33 +1500,69 @@ function verifyStructure(projectPathInput, options = {}) {
|
|
|
495
1500
|
continue;
|
|
496
1501
|
}
|
|
497
1502
|
|
|
1503
|
+
const normalizedLanding = canonicalizePath(landing);
|
|
1504
|
+
if (changedFiles.requested && !changedFileSet.has(normalizedLanding)) {
|
|
1505
|
+
skippedByIncremental += 1;
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
scannedMappingFiles.add(normalizedLanding);
|
|
1510
|
+
scannedMappings += 1;
|
|
1511
|
+
|
|
498
1512
|
const ext = path.extname(landing).toLowerCase();
|
|
499
1513
|
const source = safeReadFile(landing);
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1514
|
+
const structureEvidence = collectStructureEvidenceText(source, ext);
|
|
1515
|
+
const normalizedSource = normalizeCoverageText(structureEvidence.text);
|
|
1516
|
+
const pageTokens = unique(tokenizeCoverage(mapping.designPage, 3));
|
|
1517
|
+
|
|
1518
|
+
if (STRUCTURE_MARKUP_EXTENSIONS.has(ext)) {
|
|
1519
|
+
const hasMarkupIndicators = /<section|<main|<header|<footer|<div|<template|<article/.test(
|
|
1520
|
+
structureEvidence.text
|
|
1521
|
+
);
|
|
1522
|
+
if (!structureEvidence.syntaxAvailable) {
|
|
1523
|
+
confidence.push({
|
|
1524
|
+
mapping: mapping.implementation,
|
|
1525
|
+
mode: "heuristic",
|
|
1526
|
+
confidence: "medium",
|
|
1527
|
+
file: normalizeRelativePath(path.relative(result.projectRoot, landing))
|
|
1528
|
+
});
|
|
1529
|
+
result.warnings.push(
|
|
1530
|
+
`verify-structure used heuristic mode for "${mapping.implementation}" because syntax parsing was unavailable (${structureEvidence.reason}).`
|
|
1531
|
+
);
|
|
1532
|
+
} else if (hasMarkupIndicators) {
|
|
1533
|
+
confidence.push({
|
|
1534
|
+
mapping: mapping.implementation,
|
|
1535
|
+
mode: "markup",
|
|
1536
|
+
confidence: "high",
|
|
1537
|
+
file: normalizeRelativePath(path.relative(result.projectRoot, landing))
|
|
1538
|
+
});
|
|
509
1539
|
} else {
|
|
510
|
-
confidence.push({
|
|
1540
|
+
confidence.push({
|
|
1541
|
+
mapping: mapping.implementation,
|
|
1542
|
+
mode: "heuristic",
|
|
1543
|
+
confidence: "medium",
|
|
1544
|
+
file: normalizeRelativePath(path.relative(result.projectRoot, landing))
|
|
1545
|
+
});
|
|
511
1546
|
result.warnings.push(
|
|
512
1547
|
`verify-structure used heuristic mode for "${mapping.implementation}" because markup structure was limited.`
|
|
513
1548
|
);
|
|
514
1549
|
}
|
|
515
1550
|
} else {
|
|
516
|
-
confidence.push({
|
|
1551
|
+
confidence.push({
|
|
1552
|
+
mapping: mapping.implementation,
|
|
1553
|
+
mode: "heuristic",
|
|
1554
|
+
confidence: "low",
|
|
1555
|
+
file: normalizeRelativePath(path.relative(result.projectRoot, landing))
|
|
1556
|
+
});
|
|
517
1557
|
result.warnings.push(
|
|
518
1558
|
`verify-structure used heuristic mode for "${mapping.implementation}" due to unsupported file type ${ext || "(none)"}.`
|
|
519
1559
|
);
|
|
520
1560
|
}
|
|
521
1561
|
|
|
522
1562
|
if (pageTokens.length > 0) {
|
|
523
|
-
const
|
|
524
|
-
|
|
1563
|
+
const tokenSet = new Set(tokenizeCoverage(normalizedSource, 1));
|
|
1564
|
+
const matchedCount = pageTokens.filter((token) => tokenSet.has(token)).length;
|
|
1565
|
+
if (matchedCount === 0) {
|
|
525
1566
|
result.warnings.push(
|
|
526
1567
|
`Structural drift suspected: design page "${mapping.designPage}" tokens not found in "${mapping.implementation}".`
|
|
527
1568
|
);
|
|
@@ -529,8 +1570,72 @@ function verifyStructure(projectPathInput, options = {}) {
|
|
|
529
1570
|
}
|
|
530
1571
|
}
|
|
531
1572
|
|
|
1573
|
+
const unmatchedChangedFiles = changedFiles.entries
|
|
1574
|
+
.filter((entry) => !entry.isSymlink)
|
|
1575
|
+
.map((entry) => canonicalizePath(entry.absolutePath))
|
|
1576
|
+
.filter((absolutePath) => !scannedMappingFiles.has(absolutePath));
|
|
1577
|
+
|
|
1578
|
+
result.scan = {
|
|
1579
|
+
scanMode: changedFiles.requested ? "incremental" : "full",
|
|
1580
|
+
requestedChangedFiles: changedFiles.rawEntries.length,
|
|
1581
|
+
selectedFileCount: changedFiles.requested ? changedFiles.entries.length : bindings.mappings.length,
|
|
1582
|
+
scannedMappingCount: scannedMappings,
|
|
1583
|
+
skippedMappings: skippedByIncremental,
|
|
1584
|
+
filtered: {
|
|
1585
|
+
duplicates: changedFiles.duplicateEntries.length,
|
|
1586
|
+
missing: changedFiles.missingEntries.length,
|
|
1587
|
+
directories: changedFiles.directoryEntries.length,
|
|
1588
|
+
unreadable: changedFiles.unreadableEntries.length,
|
|
1589
|
+
symlinks: ignoredSymlinkEntries,
|
|
1590
|
+
noBindingMatch: unmatchedChangedFiles.length,
|
|
1591
|
+
total:
|
|
1592
|
+
changedFiles.duplicateEntries.length +
|
|
1593
|
+
changedFiles.missingEntries.length +
|
|
1594
|
+
changedFiles.directoryEntries.length +
|
|
1595
|
+
changedFiles.unreadableEntries.length +
|
|
1596
|
+
ignoredSymlinkEntries +
|
|
1597
|
+
unmatchedChangedFiles.length
|
|
1598
|
+
},
|
|
1599
|
+
noRelevantFiles: changedFiles.requested && scannedMappings === 0
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
if (changedFiles.requested) {
|
|
1603
|
+
if (changedFiles.missingEntries.length > 0) {
|
|
1604
|
+
result.notes.push(
|
|
1605
|
+
`Incremental input ignored missing files: ${listSummary(changedFiles.missingEntries)}.`
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
if (changedFiles.directoryEntries.length > 0) {
|
|
1609
|
+
result.notes.push(
|
|
1610
|
+
`Incremental input ignored directory paths: ${listSummary(changedFiles.directoryEntries)}.`
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
if (changedFiles.duplicateEntries.length > 0) {
|
|
1614
|
+
result.notes.push(
|
|
1615
|
+
`Incremental input deduplicated repeated paths: ${listSummary(changedFiles.duplicateEntries)}.`
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
if (unmatchedChangedFiles.length > 0) {
|
|
1619
|
+
const unmatchedRelative = unmatchedChangedFiles
|
|
1620
|
+
.map((absolutePath) => normalizeRelativePath(path.relative(result.projectRoot, absolutePath)))
|
|
1621
|
+
.sort();
|
|
1622
|
+
result.notes.push(
|
|
1623
|
+
`Incremental input included files without binding coverage: ${listSummary(unmatchedRelative)}.`
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
if (ignoredSymlinkEntries > 0) {
|
|
1627
|
+
result.notes.push(`Incremental input ignored ${ignoredSymlinkEntries} symlink path(s).`);
|
|
1628
|
+
}
|
|
1629
|
+
if (scannedMappings === 0) {
|
|
1630
|
+
result.warnings.push(
|
|
1631
|
+
"Incremental verify-structure scanned zero relevant implementation mappings; result is partial."
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
532
1636
|
result.structure = {
|
|
533
|
-
confidence
|
|
1637
|
+
confidence,
|
|
1638
|
+
scan: result.scan
|
|
534
1639
|
};
|
|
535
1640
|
return finalize(result);
|
|
536
1641
|
}
|
|
@@ -539,8 +1644,17 @@ function verifyCoverage(projectPathInput, options = {}) {
|
|
|
539
1644
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
540
1645
|
const strict = options.strict === true;
|
|
541
1646
|
const changeId = options.changeId ? String(options.changeId).trim() : "";
|
|
1647
|
+
const changedFilesRequested = options.changedFilesProvided === true || Array.isArray(options.changedFiles);
|
|
1648
|
+
const changedFiles = Array.isArray(options.changedFiles) ? options.changedFiles : undefined;
|
|
1649
|
+
|
|
542
1650
|
const sharedSetup = createSharedSetup(projectRoot, { changeId, strict });
|
|
543
|
-
const sharedOptions = {
|
|
1651
|
+
const sharedOptions = {
|
|
1652
|
+
changeId,
|
|
1653
|
+
strict,
|
|
1654
|
+
sharedSetup,
|
|
1655
|
+
changedFiles,
|
|
1656
|
+
changedFilesProvided: changedFilesRequested
|
|
1657
|
+
};
|
|
544
1658
|
const bindingsResult = verifyBindings(projectRoot, sharedOptions);
|
|
545
1659
|
const implementationResult = verifyImplementation(projectRoot, sharedOptions);
|
|
546
1660
|
const structureResult = verifyStructure(projectRoot, sharedOptions);
|
|
@@ -581,6 +1695,30 @@ function verifyCoverage(projectPathInput, options = {}) {
|
|
|
581
1695
|
result.warnings.push("Missing `verification.md`; coverage evidence is incomplete.");
|
|
582
1696
|
}
|
|
583
1697
|
|
|
1698
|
+
const incrementalSurfaces = [];
|
|
1699
|
+
const partialSurfaces = [];
|
|
1700
|
+
if (implementationResult.scan && implementationResult.scan.scanMode === "incremental") {
|
|
1701
|
+
incrementalSurfaces.push("verify-implementation");
|
|
1702
|
+
if (implementationResult.scan.noRelevantFiles) {
|
|
1703
|
+
partialSurfaces.push("verify-implementation:no-relevant-files");
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (structureResult.scan && structureResult.scan.scanMode === "incremental") {
|
|
1707
|
+
incrementalSurfaces.push("verify-structure");
|
|
1708
|
+
if (structureResult.scan.noRelevantFiles) {
|
|
1709
|
+
partialSurfaces.push("verify-structure:no-relevant-files");
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (incrementalSurfaces.length > 0) {
|
|
1714
|
+
result.warnings.push(
|
|
1715
|
+
`verify-coverage aggregated incremental upstream verification (${incrementalSurfaces.join(", ")}); treat as partial freshness.`
|
|
1716
|
+
);
|
|
1717
|
+
result.notes.push(
|
|
1718
|
+
"Incremental verification scopes are useful for changed-file checks but do not replace full-project freshness."
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
584
1722
|
result.components = {
|
|
585
1723
|
bindings: {
|
|
586
1724
|
status: bindingsResult.status,
|
|
@@ -590,19 +1728,85 @@ function verifyCoverage(projectPathInput, options = {}) {
|
|
|
590
1728
|
implementation: {
|
|
591
1729
|
status: implementationResult.status,
|
|
592
1730
|
failures: implementationResult.failures,
|
|
593
|
-
warnings: implementationResult.warnings
|
|
1731
|
+
warnings: implementationResult.warnings,
|
|
1732
|
+
scan: implementationResult.scan || null,
|
|
1733
|
+
evidenceModeCounts:
|
|
1734
|
+
implementationResult.implementation && implementationResult.implementation.evidenceModeCounts
|
|
1735
|
+
? implementationResult.implementation.evidenceModeCounts
|
|
1736
|
+
: {}
|
|
594
1737
|
},
|
|
595
1738
|
structure: {
|
|
596
1739
|
status: structureResult.status,
|
|
597
1740
|
failures: structureResult.failures,
|
|
598
1741
|
warnings: structureResult.warnings,
|
|
599
|
-
confidence: structureResult.structure ? structureResult.structure.confidence : []
|
|
1742
|
+
confidence: structureResult.structure ? structureResult.structure.confidence : [],
|
|
1743
|
+
scan: structureResult.scan || null
|
|
600
1744
|
}
|
|
601
1745
|
};
|
|
602
1746
|
|
|
1747
|
+
result.scan = {
|
|
1748
|
+
scanMode: incrementalSurfaces.length > 0 ? "incremental" : "full",
|
|
1749
|
+
incrementalSurfaces,
|
|
1750
|
+
partialSurfaces,
|
|
1751
|
+
changedFilesRequested: changedFilesRequested
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
const freshness = collectVerificationFreshness(projectRoot, {
|
|
1755
|
+
changeId: result.changeId,
|
|
1756
|
+
resolved: sharedSetup.resolved,
|
|
1757
|
+
artifactPaths: sharedSetup.artifactPaths,
|
|
1758
|
+
requiredSurfaces: ["verify-bindings", "verify-implementation", "verify-structure"]
|
|
1759
|
+
});
|
|
1760
|
+
result.freshness = freshness;
|
|
1761
|
+
if (!freshness.fresh) {
|
|
1762
|
+
result.warnings.push(
|
|
1763
|
+
"Verification freshness is stale for completion-facing claims; re-run verify surfaces before completion wording."
|
|
1764
|
+
);
|
|
1765
|
+
for (const reason of freshness.staleReasons) {
|
|
1766
|
+
result.warnings.push(reason);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
603
1770
|
return finalize(result);
|
|
604
1771
|
}
|
|
605
1772
|
|
|
1773
|
+
function formatBoundarySummary(boundaries) {
|
|
1774
|
+
if (!Array.isArray(boundaries) || boundaries.length === 0) {
|
|
1775
|
+
return "";
|
|
1776
|
+
}
|
|
1777
|
+
const compact = boundaries.map((item) => item.type).filter(Boolean);
|
|
1778
|
+
if (compact.length === 0) {
|
|
1779
|
+
return "";
|
|
1780
|
+
}
|
|
1781
|
+
return ` [boundaries: ${unique(compact).join(", ")}]`;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function appendScanLines(lines, scan) {
|
|
1785
|
+
if (!scan) {
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
lines.push("", "Scan:");
|
|
1789
|
+
lines.push(`- mode: ${scan.scanMode || "full"}`);
|
|
1790
|
+
if (Number.isFinite(scan.selectedFileCount)) {
|
|
1791
|
+
lines.push(`- selected files: ${scan.selectedFileCount}`);
|
|
1792
|
+
}
|
|
1793
|
+
if (Number.isFinite(scan.relevantFileCount)) {
|
|
1794
|
+
lines.push(`- relevant files: ${scan.relevantFileCount}`);
|
|
1795
|
+
}
|
|
1796
|
+
if (Number.isFinite(scan.scannedFileCount)) {
|
|
1797
|
+
lines.push(`- scanned files: ${scan.scannedFileCount}`);
|
|
1798
|
+
}
|
|
1799
|
+
if (Number.isFinite(scan.scannedMappingCount)) {
|
|
1800
|
+
lines.push(`- scanned mappings: ${scan.scannedMappingCount}`);
|
|
1801
|
+
}
|
|
1802
|
+
if (scan.filtered && Number.isFinite(scan.filtered.total)) {
|
|
1803
|
+
lines.push(`- filtered entries: ${scan.filtered.total}`);
|
|
1804
|
+
}
|
|
1805
|
+
if (scan.noRelevantFiles) {
|
|
1806
|
+
lines.push("- no relevant files: yes");
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
606
1810
|
function formatVerifyReport(result, title = "Da Vinci verify") {
|
|
607
1811
|
const lines = [
|
|
608
1812
|
title,
|
|
@@ -616,6 +1820,9 @@ function formatVerifyReport(result, title = "Da Vinci verify") {
|
|
|
616
1820
|
lines.push(`${key}: ${value}`);
|
|
617
1821
|
}
|
|
618
1822
|
}
|
|
1823
|
+
|
|
1824
|
+
appendScanLines(lines, result.scan);
|
|
1825
|
+
|
|
619
1826
|
if (result.failures.length > 0) {
|
|
620
1827
|
lines.push("", "Failures:");
|
|
621
1828
|
for (const failure of result.failures) {
|
|
@@ -634,12 +1841,27 @@ function formatVerifyReport(result, title = "Da Vinci verify") {
|
|
|
634
1841
|
lines.push(`- ${note}`);
|
|
635
1842
|
}
|
|
636
1843
|
}
|
|
1844
|
+
|
|
1845
|
+
if (result.implementation && Array.isArray(result.implementation.checks) && result.implementation.checks.length > 0) {
|
|
1846
|
+
lines.push("", "Implementation evidence:");
|
|
1847
|
+
for (const check of result.implementation.checks) {
|
|
1848
|
+
const evidence = check.evidence || {};
|
|
1849
|
+
const state = check.covered ? "covered" : "missing";
|
|
1850
|
+
const location = evidence.file ? ` @ ${evidence.file}` : "";
|
|
1851
|
+
lines.push(
|
|
1852
|
+
`- ${check.type} "${check.label}": ${state} via ${evidence.mode || "none"} (${evidence.confidence || "none"})${location}${formatBoundarySummary(check.boundaries)}`
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
637
1857
|
if (result.structure && Array.isArray(result.structure.confidence) && result.structure.confidence.length > 0) {
|
|
638
1858
|
lines.push("", "Structure confidence:");
|
|
639
1859
|
for (const item of result.structure.confidence) {
|
|
640
|
-
|
|
1860
|
+
const location = item.file ? ` @ ${item.file}` : "";
|
|
1861
|
+
lines.push(`- ${item.mapping}: ${item.mode} (${item.confidence})${location}`);
|
|
641
1862
|
}
|
|
642
1863
|
}
|
|
1864
|
+
|
|
643
1865
|
return lines.join("\n");
|
|
644
1866
|
}
|
|
645
1867
|
|
|
@@ -648,5 +1870,6 @@ module.exports = {
|
|
|
648
1870
|
verifyImplementation,
|
|
649
1871
|
verifyStructure,
|
|
650
1872
|
verifyCoverage,
|
|
651
|
-
formatVerifyReport
|
|
1873
|
+
formatVerifyReport,
|
|
1874
|
+
collectVerificationFreshness
|
|
652
1875
|
};
|