@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +32 -7
  3. package/README.zh-CN.md +151 -7
  4. package/SKILL.md +45 -704
  5. package/commands/claude/dv/build.md +5 -0
  6. package/commands/claude/dv/continue.md +4 -0
  7. package/commands/claude/dv/tasks.md +6 -0
  8. package/commands/claude/dv/verify.md +2 -0
  9. package/commands/codex/prompts/dv-build.md +5 -0
  10. package/commands/codex/prompts/dv-continue.md +4 -0
  11. package/commands/codex/prompts/dv-tasks.md +6 -0
  12. package/commands/codex/prompts/dv-verify.md +2 -0
  13. package/commands/gemini/dv/build.toml +5 -0
  14. package/commands/gemini/dv/continue.toml +4 -0
  15. package/commands/gemini/dv/tasks.toml +6 -0
  16. package/commands/gemini/dv/verify.toml +2 -0
  17. package/commands/templates/dv-continue.shared.md +4 -0
  18. package/docs/discipline-and-orchestration-upgrade.md +83 -0
  19. package/docs/dv-command-reference.md +33 -5
  20. package/docs/execution-chain-migration.md +23 -0
  21. package/docs/prompt-entrypoints.md +6 -0
  22. package/docs/skill-contract-maintenance.md +14 -0
  23. package/docs/skill-usage.md +16 -0
  24. package/docs/workflow-overview.md +17 -0
  25. package/docs/zh-CN/dv-command-reference.md +31 -5
  26. package/docs/zh-CN/execution-chain-migration.md +23 -0
  27. package/docs/zh-CN/prompt-entrypoints.md +6 -0
  28. package/docs/zh-CN/skill-usage.md +16 -0
  29. package/docs/zh-CN/workflow-overview.md +17 -0
  30. package/lib/audit-parsers.js +148 -1
  31. package/lib/cli/helpers.js +43 -0
  32. package/lib/cli/lint-family.js +56 -0
  33. package/lib/cli/verify-family.js +79 -0
  34. package/lib/cli.js +123 -145
  35. package/lib/execution-profile.js +143 -0
  36. package/lib/execution-signals.js +19 -1
  37. package/lib/lint-tasks.js +86 -2
  38. package/lib/planning-parsers.js +263 -19
  39. package/lib/scaffold.js +454 -23
  40. package/lib/supervisor-review.js +2 -1
  41. package/lib/task-execution.js +160 -0
  42. package/lib/task-review.js +197 -0
  43. package/lib/utils.js +19 -0
  44. package/lib/verify.js +1308 -85
  45. package/lib/workflow-state.js +452 -30
  46. package/lib/worktree-preflight.js +214 -0
  47. package/package.json +1 -1
  48. package/references/artifact-templates.md +56 -6
  49. 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([".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".scss"]);
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 allCovered(checks) {
221
- for (const check of checks) {
222
- if (!check.covered) {
223
- return false;
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
- return true;
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 stateChecks = [];
342
- for (const record of specRecords) {
343
- const states = record.parsed.sections.states.items || [];
344
- for (const stateItem of states) {
345
- const state = normalizeText(String(stateItem || "").split(":")[0]);
346
- if (!state) {
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
- const stateTokens = state.split(" ").filter((token) => token.length >= 3);
350
- if (stateTokens.length === 0) {
1103
+ if (!CODE_FILE_EXTENSIONS.has(entry.extension)) {
1104
+ filteredChanged.unsupported += 1;
351
1105
  continue;
352
1106
  }
353
- stateChecks.push({
354
- recordPath: record.path,
355
- stateItem,
356
- tokens: stateTokens,
357
- covered: false
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
- const taskGroupChecks = [];
363
- for (const group of tasksArtifact.taskGroups) {
364
- const titleTokens = normalizeText(group.title)
365
- .split(" ")
366
- .filter((token) => token.length >= 4);
367
- if (titleTokens.length === 0) {
368
- continue;
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
- taskGroupChecks.push({
372
- group,
373
- tokens: titleTokens,
374
- covered: false
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
- const lower = String(read.text || "").toLowerCase();
394
- if (!lower) {
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
- for (const check of stateChecks) {
399
- if (check.covered) {
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
- check.covered = check.tokens.some((token) => lower.includes(token));
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 taskGroupChecks) {
405
- if (check.covered) {
1314
+ for (const check of allChecks) {
1315
+ if (!check.tokens.length) {
406
1316
  continue;
407
1317
  }
408
- check.covered = check.tokens.some((token) => lower.includes(token));
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
- if (allCovered(stateChecks) && allCovered(taskGroupChecks)) {
412
- break;
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.stateItem}" (${check.recordPath}).`
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.group.id}. ${check.group.title}".`
1372
+ `Task-group intent may be missing in implementation: "${check.label}".`
427
1373
  );
428
1374
  }
429
1375
  }
430
1376
 
431
- if (codeScan.scan.truncatedByFileLimit) {
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 (codeScan.scan.truncatedByDirectoryLimit) {
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 (codeScan.scan.readErrors + readErrors > 0) {
1411
+ if (scan.readErrors + readErrors > 0) {
442
1412
  result.warnings.push(
443
- `verify-implementation skipped unreadable files/directories (${codeScan.scan.readErrors + readErrors}).`
1413
+ `verify-implementation skipped unreadable files/directories (${scan.readErrors + readErrors}).`
444
1414
  );
445
1415
  }
446
- if (codeScan.scan.depthLimitHits > 0) {
1416
+ if (scan.depthLimitHits > 0) {
447
1417
  result.notes.push(
448
- `verify-implementation enforced max scan depth (${MAX_SCAN_DEPTH}); skipped deeper paths: ${codeScan.scan.depthLimitHits}.`
1418
+ `verify-implementation enforced max scan depth (${MAX_SCAN_DEPTH}); skipped deeper paths: ${scan.depthLimitHits}.`
449
1419
  );
450
1420
  }
451
- if (codeScan.scan.skippedSymlinks > 0) {
1421
+ if (scan.skippedSymlinks > 0) {
452
1422
  result.notes.push(
453
- `verify-implementation skipped symlink entries during scan: ${codeScan.scan.skippedSymlinks}.`
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 normalizedSource = normalizeText(source);
501
- const pageTokens = normalizeText(mapping.designPage)
502
- .split(" ")
503
- .filter((token) => token.length >= 3);
504
-
505
- if (ext === ".html" || ext === ".tsx" || ext === ".jsx" || ext === ".js") {
506
- const hasMarkupIndicators = /<section|<main|<header|<footer|<div/.test(source);
507
- if (hasMarkupIndicators) {
508
- confidence.push({ mapping: mapping.implementation, mode: "markup", confidence: "high" });
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({ mapping: mapping.implementation, mode: "heuristic", confidence: "medium" });
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({ mapping: mapping.implementation, mode: "heuristic", confidence: "low" });
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 covered = pageTokens.some((token) => normalizedSource.includes(token));
524
- if (!covered) {
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 = { changeId, strict, sharedSetup };
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
- lines.push(`- ${item.mapping}: ${item.mode} (${item.confidence})`);
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
  };