@xenonbyte/da-vinci-workflow 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,
@@ -14,8 +13,22 @@ const {
14
13
  readArtifactTexts
15
14
  } = require("./planning-parsers");
16
15
  const { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
16
+ const { normalizeRelativePath, pathWithinRoot } = require("./utils");
17
17
 
18
- 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"]);
19
32
  const NON_IMPLEMENTATION_DIR_NAMES = new Set([
20
33
  ".git",
21
34
  ".da-vinci",
@@ -92,6 +105,58 @@ function isNonImplementationFileName(name) {
92
105
  return NON_IMPLEMENTATION_FILE_PATTERNS.some((pattern) => pattern.test(normalized));
93
106
  }
94
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
+
95
160
  function collectCodeFiles(projectRoot) {
96
161
  const files = [];
97
162
  const scan = {
@@ -219,15 +284,6 @@ function readCodeFileForScan(filePath) {
219
284
  }
220
285
  }
221
286
 
222
- function allCovered(checks) {
223
- for (const check of checks) {
224
- if (!check.covered) {
225
- return false;
226
- }
227
- }
228
- return true;
229
- }
230
-
231
287
  function safeMtimeMs(filePath) {
232
288
  try {
233
289
  const stat = fs.statSync(filePath);
@@ -414,6 +470,547 @@ function commonSetup(surface, projectPathInput, options) {
414
470
  };
415
471
  }
416
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
+
417
1014
  function verifyBindings(projectPathInput, options = {}) {
418
1015
  const setup = commonSetup("verify-bindings", projectPathInput, options);
419
1016
  const { result, artifacts } = setup;
@@ -457,13 +1054,6 @@ function verifyImplementation(projectPathInput, options = {}) {
457
1054
  return result;
458
1055
  }
459
1056
 
460
- const codeScan = collectCodeFiles(result.projectRoot);
461
- const codeFiles = codeScan.files;
462
- if (codeFiles.length === 0) {
463
- result.failures.push("No implementation files were found for verify-implementation.");
464
- return finalize(result);
465
- }
466
-
467
1057
  const specRecords = parseRuntimeSpecs(resolved.changeDir, result.projectRoot);
468
1058
  const tasksArtifact = parseTasksArtifact(artifacts.tasks || "");
469
1059
 
@@ -472,46 +1062,158 @@ function verifyImplementation(projectPathInput, options = {}) {
472
1062
  return finalize(result);
473
1063
  }
474
1064
 
475
- const stateChecks = [];
476
- for (const record of specRecords) {
477
- const states = record.parsed.sections.states.items || [];
478
- for (const stateItem of states) {
479
- const state = normalizeText(String(stateItem || "").split(":")[0]);
480
- 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;
481
1101
  continue;
482
1102
  }
483
- const stateTokens = state.split(" ").filter((token) => token.length >= 3);
484
- if (stateTokens.length === 0) {
1103
+ if (!CODE_FILE_EXTENSIONS.has(entry.extension)) {
1104
+ filteredChanged.unsupported += 1;
485
1105
  continue;
486
1106
  }
487
- stateChecks.push({
488
- recordPath: record.path,
489
- stateItem,
490
- tokens: stateTokens,
491
- covered: false
492
- });
1107
+ if (isNonImplementationFileName(path.basename(entry.relativePath)) || hasExcludedDirectory(entry.relativePath)) {
1108
+ filteredChanged.excluded += 1;
1109
+ continue;
1110
+ }
1111
+ codeFiles.push(entry.absolutePath);
493
1112
  }
1113
+ codeFiles = unique(codeFiles).sort();
1114
+ } else {
1115
+ const codeScan = collectCodeFiles(result.projectRoot);
1116
+ codeFiles = codeScan.files;
1117
+ scan = codeScan.scan;
494
1118
  }
495
1119
 
496
- const taskGroupChecks = [];
497
- for (const group of tasksArtifact.taskGroups) {
498
- const titleTokens = normalizeText(group.title)
499
- .split(" ")
500
- .filter((token) => token.length >= 4);
501
- if (titleTokens.length === 0) {
502
- 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
+ );
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
+ );
503
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
+ }
504
1185
 
505
- taskGroupChecks.push({
506
- group,
507
- tokens: titleTokens,
508
- covered: false
509
- });
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);
510
1215
  }
511
1216
 
512
- let scannedBytes = 0;
513
- let skippedLargeFiles = 0;
514
- let readErrors = 0;
515
1217
  for (const codeFile of codeFiles) {
516
1218
  const read = readCodeFileForScan(codeFile);
517
1219
  scannedBytes += read.bytesRead;
@@ -524,67 +1226,201 @@ function verifyImplementation(projectPathInput, options = {}) {
524
1226
  continue;
525
1227
  }
526
1228
 
527
- const lower = String(read.text || "").toLowerCase();
528
- 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) {
529
1234
  continue;
530
1235
  }
531
1236
 
532
- for (const check of stateChecks) {
533
- 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
+ }
534
1278
  continue;
535
1279
  }
536
- 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);
537
1313
  }
538
- for (const check of taskGroupChecks) {
539
- if (check.covered) {
1314
+ for (const check of allChecks) {
1315
+ if (!check.tokens.length) {
540
1316
  continue;
541
1317
  }
542
- 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
+ }
543
1335
  }
1336
+ }
544
1337
 
545
- if (allCovered(stateChecks) && allCovered(taskGroupChecks)) {
546
- 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);
547
1359
  }
548
1360
  }
549
1361
 
550
- for (const check of stateChecks) {
1362
+ for (const check of checks.stateChecks) {
551
1363
  if (!check.covered) {
552
1364
  result.warnings.push(
553
- `State coverage may be missing in implementation: "${check.stateItem}" (${check.recordPath}).`
1365
+ `State coverage may be missing in implementation: "${check.label}" (${check.recordPath}).`
554
1366
  );
555
1367
  }
556
1368
  }
557
- for (const check of taskGroupChecks) {
1369
+ for (const check of checks.taskGroupChecks) {
558
1370
  if (!check.covered) {
559
1371
  result.warnings.push(
560
- `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}".`
561
1373
  );
562
1374
  }
563
1375
  }
564
1376
 
565
- 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) {
566
1402
  result.warnings.push(
567
1403
  `verify-implementation hit file scan limit (${MAX_SCANNED_FILES}); deep coverage may be incomplete.`
568
1404
  );
569
1405
  }
570
- if (codeScan.scan.truncatedByDirectoryLimit) {
1406
+ if (scan.truncatedByDirectoryLimit) {
571
1407
  result.warnings.push(
572
1408
  `verify-implementation hit directory scan limit (${MAX_SCANNED_DIRECTORIES}); coverage may be incomplete.`
573
1409
  );
574
1410
  }
575
- if (codeScan.scan.readErrors + readErrors > 0) {
1411
+ if (scan.readErrors + readErrors > 0) {
576
1412
  result.warnings.push(
577
- `verify-implementation skipped unreadable files/directories (${codeScan.scan.readErrors + readErrors}).`
1413
+ `verify-implementation skipped unreadable files/directories (${scan.readErrors + readErrors}).`
578
1414
  );
579
1415
  }
580
- if (codeScan.scan.depthLimitHits > 0) {
1416
+ if (scan.depthLimitHits > 0) {
581
1417
  result.notes.push(
582
- `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}.`
583
1419
  );
584
1420
  }
585
- if (codeScan.scan.skippedSymlinks > 0) {
1421
+ if (scan.skippedSymlinks > 0) {
586
1422
  result.notes.push(
587
- `verify-implementation skipped symlink entries during scan: ${codeScan.scan.skippedSymlinks}.`
1423
+ `verify-implementation skipped symlink entries during scan: ${scan.skippedSymlinks}.`
588
1424
  );
589
1425
  }
590
1426
  if (skippedLargeFiles > 0) {
@@ -597,8 +1433,17 @@ function verifyImplementation(projectPathInput, options = {}) {
597
1433
  codeFiles: codeFiles.length,
598
1434
  specFiles: specRecords.length,
599
1435
  taskGroups: tasksArtifact.taskGroups.length,
600
- scannedBytes
1436
+ scannedBytes,
1437
+ scanMode: result.scan.scanMode
601
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
1445
+ };
1446
+
602
1447
  return finalize(result);
603
1448
  }
604
1449
 
@@ -622,6 +1467,32 @@ function verifyStructure(projectPathInput, options = {}) {
622
1467
  return finalize(result);
623
1468
  }
624
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
+
625
1496
  for (const mapping of bindings.mappings) {
626
1497
  const landing = resolveImplementationLanding(result.projectRoot, mapping.implementation);
627
1498
  if (!landing) {
@@ -629,33 +1500,69 @@ function verifyStructure(projectPathInput, options = {}) {
629
1500
  continue;
630
1501
  }
631
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
+
632
1512
  const ext = path.extname(landing).toLowerCase();
633
1513
  const source = safeReadFile(landing);
634
- const normalizedSource = normalizeText(source);
635
- const pageTokens = normalizeText(mapping.designPage)
636
- .split(" ")
637
- .filter((token) => token.length >= 3);
638
-
639
- if (ext === ".html" || ext === ".tsx" || ext === ".jsx" || ext === ".js") {
640
- const hasMarkupIndicators = /<section|<main|<header|<footer|<div/.test(source);
641
- if (hasMarkupIndicators) {
642
- 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
+ });
643
1539
  } else {
644
- 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
+ });
645
1546
  result.warnings.push(
646
1547
  `verify-structure used heuristic mode for "${mapping.implementation}" because markup structure was limited.`
647
1548
  );
648
1549
  }
649
1550
  } else {
650
- 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
+ });
651
1557
  result.warnings.push(
652
1558
  `verify-structure used heuristic mode for "${mapping.implementation}" due to unsupported file type ${ext || "(none)"}.`
653
1559
  );
654
1560
  }
655
1561
 
656
1562
  if (pageTokens.length > 0) {
657
- const covered = pageTokens.some((token) => normalizedSource.includes(token));
658
- if (!covered) {
1563
+ const tokenSet = new Set(tokenizeCoverage(normalizedSource, 1));
1564
+ const matchedCount = pageTokens.filter((token) => tokenSet.has(token)).length;
1565
+ if (matchedCount === 0) {
659
1566
  result.warnings.push(
660
1567
  `Structural drift suspected: design page "${mapping.designPage}" tokens not found in "${mapping.implementation}".`
661
1568
  );
@@ -663,8 +1570,72 @@ function verifyStructure(projectPathInput, options = {}) {
663
1570
  }
664
1571
  }
665
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
+
666
1636
  result.structure = {
667
- confidence
1637
+ confidence,
1638
+ scan: result.scan
668
1639
  };
669
1640
  return finalize(result);
670
1641
  }
@@ -673,8 +1644,17 @@ function verifyCoverage(projectPathInput, options = {}) {
673
1644
  const projectRoot = path.resolve(projectPathInput || process.cwd());
674
1645
  const strict = options.strict === true;
675
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
+
676
1650
  const sharedSetup = createSharedSetup(projectRoot, { changeId, strict });
677
- const sharedOptions = { changeId, strict, sharedSetup };
1651
+ const sharedOptions = {
1652
+ changeId,
1653
+ strict,
1654
+ sharedSetup,
1655
+ changedFiles,
1656
+ changedFilesProvided: changedFilesRequested
1657
+ };
678
1658
  const bindingsResult = verifyBindings(projectRoot, sharedOptions);
679
1659
  const implementationResult = verifyImplementation(projectRoot, sharedOptions);
680
1660
  const structureResult = verifyStructure(projectRoot, sharedOptions);
@@ -715,6 +1695,30 @@ function verifyCoverage(projectPathInput, options = {}) {
715
1695
  result.warnings.push("Missing `verification.md`; coverage evidence is incomplete.");
716
1696
  }
717
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
+
718
1722
  result.components = {
719
1723
  bindings: {
720
1724
  status: bindingsResult.status,
@@ -724,16 +1728,29 @@ function verifyCoverage(projectPathInput, options = {}) {
724
1728
  implementation: {
725
1729
  status: implementationResult.status,
726
1730
  failures: implementationResult.failures,
727
- 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
+ : {}
728
1737
  },
729
1738
  structure: {
730
1739
  status: structureResult.status,
731
1740
  failures: structureResult.failures,
732
1741
  warnings: structureResult.warnings,
733
- confidence: structureResult.structure ? structureResult.structure.confidence : []
1742
+ confidence: structureResult.structure ? structureResult.structure.confidence : [],
1743
+ scan: structureResult.scan || null
734
1744
  }
735
1745
  };
736
1746
 
1747
+ result.scan = {
1748
+ scanMode: incrementalSurfaces.length > 0 ? "incremental" : "full",
1749
+ incrementalSurfaces,
1750
+ partialSurfaces,
1751
+ changedFilesRequested: changedFilesRequested
1752
+ };
1753
+
737
1754
  const freshness = collectVerificationFreshness(projectRoot, {
738
1755
  changeId: result.changeId,
739
1756
  resolved: sharedSetup.resolved,
@@ -753,6 +1770,43 @@ function verifyCoverage(projectPathInput, options = {}) {
753
1770
  return finalize(result);
754
1771
  }
755
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
+
756
1810
  function formatVerifyReport(result, title = "Da Vinci verify") {
757
1811
  const lines = [
758
1812
  title,
@@ -766,6 +1820,9 @@ function formatVerifyReport(result, title = "Da Vinci verify") {
766
1820
  lines.push(`${key}: ${value}`);
767
1821
  }
768
1822
  }
1823
+
1824
+ appendScanLines(lines, result.scan);
1825
+
769
1826
  if (result.failures.length > 0) {
770
1827
  lines.push("", "Failures:");
771
1828
  for (const failure of result.failures) {
@@ -784,12 +1841,27 @@ function formatVerifyReport(result, title = "Da Vinci verify") {
784
1841
  lines.push(`- ${note}`);
785
1842
  }
786
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
+
787
1857
  if (result.structure && Array.isArray(result.structure.confidence) && result.structure.confidence.length > 0) {
788
1858
  lines.push("", "Structure confidence:");
789
1859
  for (const item of result.structure.confidence) {
790
- 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}`);
791
1862
  }
792
1863
  }
1864
+
793
1865
  return lines.join("\n");
794
1866
  }
795
1867