flowseeker 0.1.8 → 0.1.9

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.
@@ -36,8 +36,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadOrBuildWorkspaceIndex = loadOrBuildWorkspaceIndex;
37
37
  exports.selectIndexCandidates = selectIndexCandidates;
38
38
  exports.clearNodeWorkspaceIndex = clearNodeWorkspaceIndex;
39
+ exports.computeChunkStats = computeChunkStats;
40
+ exports.scoreChunksForTask = scoreChunksForTask;
39
41
  const crypto = __importStar(require("crypto"));
40
42
  const fs = __importStar(require("fs/promises"));
43
+ const fssync = __importStar(require("fs"));
41
44
  const path = __importStar(require("path"));
42
45
  const dependencyExtractor_1 = require("./dependencyExtractor");
43
46
  const fileDiscovery_1 = require("./fileDiscovery");
@@ -52,7 +55,7 @@ const subsystem_1 = require("../pipeline/subsystem");
52
55
  const indexVersion = 7;
53
56
  const regexExtractorVersion = "structured-regex-mvp-1";
54
57
  const treeSitterExtractorVersion = "tree-sitter-mvp-1";
55
- const chunkerVersion = "symbol-range-mvp-1";
58
+ const chunkerVersion = "chunk-record-mvp-1";
56
59
  const graphVersion = "indexed-edges-mvp-1";
57
60
  const tokenLimitPerFile = 2500;
58
61
  async function loadOrBuildWorkspaceIndex(rootPath, config, options = {}) {
@@ -513,7 +516,7 @@ async function indexFile(discovered, prior, config) {
513
516
  const calls = extractCalls(structuredNodes);
514
517
  const subsystemTerms = (0, subsystem_1.inferSubsystemTerms)(discovered.relativePath, symbols.map((symbol) => symbol.name));
515
518
  const imports = (0, dependencyExtractor_1.extractImports)(content).map((item) => item.source).slice(0, 50);
516
- const chunks = buildIndexedChunks(discovered.relativePath, contentHash, structuredNodes);
519
+ const chunks = buildIndexedChunks(discovered.relativePath, contentHash, structuredNodes, language ?? "unknown", content, imports, calls, effectiveExtractorVersion, config);
517
520
  const edges = buildIndexedEdges(discovered.relativePath, imports, symbols, calls);
518
521
  return {
519
522
  reused: false,
@@ -751,38 +754,180 @@ function extractStructuredNodesForIndex(relativePath, content) {
751
754
  }
752
755
  return (0, structuredExtractor_1.extractStructuredNodes)(content.slice(0, 200000), language);
753
756
  }
754
- function buildIndexedChunks(relativePath, contentHash, nodes) {
755
- const symbolChunks = nodes
756
- .filter((node) => node.kind === "declaration" && node.name)
757
- .slice(0, 120)
758
- .map((node) => ({
759
- id: `${relativePath}:${node.range.startLine}-${node.range.endLine}:${node.name}`,
760
- relativePath,
761
- range: node.range,
762
- kind: "symbol",
763
- symbolFqn: node.fqn ?? node.name,
764
- textHash: contentHash
765
- }));
766
- if (symbolChunks.length > 0) {
767
- return symbolChunks;
757
+ function buildIndexedChunks(relativePath, contentHash, nodes, language, content, allImports, allCalls, parserVer, config) {
758
+ const declNodes = nodes.filter((node) => node.kind === "declaration" && node.name).slice(0, 120);
759
+ if (declNodes.length > 0) {
760
+ return declNodes.map((node) => {
761
+ const snippet = getSnippet(content, node.range.startLine, node.range.endLine);
762
+ const chunkKind = symbolKindToChunkKind(node.symbolKind, relativePath);
763
+ const chunkSlot = inferChunkSlot(relativePath, chunkKind);
764
+ const lineRange = node.range;
765
+ return {
766
+ id: `${relativePath}:${lineRange.startLine}-${lineRange.endLine}:${node.name}`,
767
+ // Public ChunkRecord fields
768
+ filePath: relativePath,
769
+ startLine: lineRange.startLine,
770
+ endLine: lineRange.endLine,
771
+ language,
772
+ kind: chunkKind,
773
+ slot: chunkSlot,
774
+ symbolName: node.name ?? "",
775
+ parentSymbol: node.parentFqn ?? "",
776
+ textHash: contentHash,
777
+ tokenCount: (0, text_1.fastTokenEstimate)(snippet),
778
+ terms: extractChunkTerms(node.name ?? "", snippet),
779
+ routes: extractRouteHints(snippet, relativePath),
780
+ annotations: extractChunkAnnotations(nodes, lineRange),
781
+ parserKind: node.extractorKind ?? "structured-regex",
782
+ parserVersion: parserVer,
783
+ contentPreview: snippet.slice(0, 300),
784
+ imports: allImports.slice(0, 10),
785
+ calls: allCalls.slice(0, 10),
786
+ // Compatibility fields
787
+ relativePath,
788
+ range: lineRange,
789
+ symbolFqn: node.fqn ?? node.name,
790
+ };
791
+ });
768
792
  }
769
- return [{
770
- id: `${relativePath}:file`,
793
+ // Fallback: line window chunks for symbol-less files
794
+ return buildFallbackChunks(relativePath, contentHash, language, content, allImports, allCalls, parserVer, config);
795
+ }
796
+ function buildFallbackChunks(relativePath, contentHash, language, content, allImports, allCalls, parserVer, config) {
797
+ const lines = content.split(/\r?\n/);
798
+ const chunkSize = Math.max(5, config.index.chunkSizeLines || 80);
799
+ const overlap = Math.max(0, config.index.chunkOverlapLines ?? 12);
800
+ const step = Math.max(1, chunkSize - overlap);
801
+ const maxChunks = 50;
802
+ const chunks = [];
803
+ for (let start = 0; start < lines.length && chunks.length < maxChunks; start += step) {
804
+ const end = Math.min(lines.length, start + chunkSize);
805
+ const snippet = lines.slice(start, end).join("\n");
806
+ const chunkKind = inferFileChunkKind(relativePath);
807
+ const chunkSlot = inferChunkSlot(relativePath, chunkKind);
808
+ const firstLine = start + 1;
809
+ const lastLine = end;
810
+ chunks.push({
811
+ id: `${relativePath}:L${firstLine}-${lastLine}`,
812
+ // Public ChunkRecord fields
813
+ filePath: relativePath,
814
+ startLine: firstLine,
815
+ endLine: lastLine,
816
+ language,
817
+ kind: chunkKind,
818
+ slot: chunkSlot,
819
+ symbolName: "",
820
+ parentSymbol: "",
821
+ textHash: contentHash,
822
+ tokenCount: (0, text_1.fastTokenEstimate)(snippet),
823
+ terms: extractChunkTerms("", snippet),
824
+ routes: extractRouteHints(snippet, relativePath),
825
+ annotations: [],
826
+ parserKind: "unknown",
827
+ parserVersion: parserVer,
828
+ contentPreview: snippet.slice(0, 200),
829
+ imports: allImports.slice(0, 10),
830
+ calls: allCalls.slice(0, 10),
831
+ // Compatibility fields
771
832
  relativePath,
772
- range: { startLine: 1, endLine: 1 },
773
- kind: inferChunkKind(relativePath),
774
- textHash: contentHash
775
- }];
833
+ range: { startLine: firstLine, endLine: lastLine },
834
+ });
835
+ }
836
+ return chunks;
837
+ }
838
+ function getSnippet(content, startLine, endLine) {
839
+ const lines = content.split(/\r?\n/);
840
+ const start = Math.max(0, startLine - 1);
841
+ const end = Math.min(lines.length, endLine + 2);
842
+ return lines.slice(start, end).join("\n");
843
+ }
844
+ function symbolKindToChunkKind(symbolKind, relativePath) {
845
+ const lower = (relativePath || "").toLowerCase();
846
+ if (lower.includes("/routes/") || lower.includes("/router/") || lower.includes("/urls/"))
847
+ return "route";
848
+ if (symbolKind === "class" || symbolKind === "interface") {
849
+ if (lower.includes("/controller") || lower.includes("/handler") || lower.includes("/view"))
850
+ return "controller_action";
851
+ if (lower.includes("/service") || lower.includes("/usecase") || lower.includes("/action") || lower.includes("/export"))
852
+ return "service_method";
853
+ if (lower.includes("/model") || lower.includes("/entity") || lower.includes("/repository"))
854
+ return "model_entity";
855
+ if (lower.includes("/middleware") || lower.includes("/guard") || lower.includes("/auth"))
856
+ return "middleware";
857
+ if (lower.includes("/job") || lower.includes("/event") || lower.includes("/listener"))
858
+ return "job";
859
+ return "class";
860
+ }
861
+ if (symbolKind === "method")
862
+ return "method";
863
+ if (symbolKind === "function")
864
+ return "function";
865
+ if (/(^|\/)(tests?|specs?|__tests__)(\/|$)/.test(lower))
866
+ return "test_case";
867
+ if (/(^|\/)(config|configs?|settings?)(\/|$)|\.(json|ya?ml|toml|ini|env)$/.test(lower))
868
+ return "config_block";
869
+ if (/(^|\/)(migrations?|schema|schemas)(\/|$)/.test(lower))
870
+ return "schema";
871
+ return "unknown";
872
+ }
873
+ function inferChunkSlot(relativePath, chunkKind) {
874
+ const lower = relativePath.toLowerCase().replace(/\\/g, "/");
875
+ if (/(^|\/)(routes?|router|urls)(\/|$)/.test(lower))
876
+ return "entry";
877
+ if (/(^|\/)(controllers?|handlers?)(\/|$)/.test(lower))
878
+ return "handler";
879
+ if (/(^|\/)(services?|usecases?|actions?|exports?|domain)(\/|$)/.test(lower))
880
+ return "domain";
881
+ if (/(^|\/)(models?|entities?|repositories?)(\/|$)/.test(lower))
882
+ return "data";
883
+ if (/(^|\/)(tests?|specs?|__tests__)(\/|$)/.test(lower))
884
+ return "tests";
885
+ if (/(^|\/)(config|configs?|settings?)(\/|$)/.test(lower))
886
+ return "config";
887
+ if (/(^|\/)(middleware|guards?|policies?|auth)(\/|$)/.test(lower))
888
+ return "permission";
889
+ if (/(^|\/)(jobs?|events?|listeners?|mail|notifications?)(\/|$)/.test(lower))
890
+ return "side_effect";
891
+ return "unknown";
776
892
  }
777
- function inferChunkKind(relativePath) {
893
+ function extractChunkTerms(symbolName, snippet) {
894
+ const words = new Set();
895
+ for (const w of symbolName.split(/(?=[A-Z])|_|-|\./)) {
896
+ const t = w.toLowerCase().trim();
897
+ if (t && t.length > 1)
898
+ words.add(t);
899
+ }
900
+ const textWords = snippet.toLowerCase().match(/\b[a-z_]{3,20}\b/g);
901
+ if (textWords)
902
+ for (const w of textWords)
903
+ words.add(w);
904
+ return Array.from(words).slice(0, 30);
905
+ }
906
+ function extractRouteHints(snippet, relativePath) {
907
+ const routes = [];
908
+ const lower = relativePath.toLowerCase();
909
+ if (lower.includes("route") || lower.includes("url")) {
910
+ const matches = snippet.match(/['"`]([\/][^'"`\s]{2,80})['"`]/g);
911
+ if (matches)
912
+ for (const m of matches)
913
+ routes.push(m.replace(/['"`]/g, ""));
914
+ }
915
+ return routes.slice(0, 5);
916
+ }
917
+ function extractChunkAnnotations(nodes, range) {
918
+ return nodes
919
+ .filter((n) => n.kind === "annotation" && n.range.startLine >= range.startLine - 2 && n.range.endLine <= range.startLine)
920
+ .map((n) => n.name ?? "")
921
+ .filter(Boolean)
922
+ .slice(0, 5);
923
+ }
924
+ function inferFileChunkKind(relativePath) {
778
925
  const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
779
- if (/\.(md|mdx|txt|rst)$/.test(normalized)) {
926
+ if (/\.(md|mdx|txt|rst)$/.test(normalized))
780
927
  return "doc";
781
- }
782
- if (/(^|\/)(config|configs?|settings?)(\/|$)|\.(json|ya?ml|toml|ini|env)$/.test(normalized)) {
783
- return "config";
784
- }
785
- return "file_region";
928
+ if (/(^|\/)(config|configs?|settings?)(\/|$)|\.(json|ya?ml|toml|ini|env)$/.test(normalized))
929
+ return "config_block";
930
+ return "unknown";
786
931
  }
787
932
  function buildIndexedEdges(relativePath, imports, symbols, calls) {
788
933
  const edges = [];
@@ -865,6 +1010,171 @@ function formatProgressPath(relativePath) {
865
1010
  function yieldToHost() {
866
1011
  return new Promise((resolve) => setTimeout(resolve, 0));
867
1012
  }
1013
+ function computeChunkStats(index) {
1014
+ const chunksByKind = {};
1015
+ const parserKindCounts = {};
1016
+ let totalChunks = 0;
1017
+ let filesWithChunks = 0;
1018
+ for (const file of index.files) {
1019
+ if (file.chunks && file.chunks.length > 0) {
1020
+ filesWithChunks++;
1021
+ totalChunks += file.chunks.length;
1022
+ for (const chunk of file.chunks) {
1023
+ const kind = chunk.kind || "unknown";
1024
+ chunksByKind[kind] = (chunksByKind[kind] || 0) + 1;
1025
+ const pk = chunk.parserKind || "unknown";
1026
+ parserKindCounts[pk] = (parserKindCounts[pk] || 0) + 1;
1027
+ }
1028
+ }
1029
+ }
1030
+ return { totalChunks, chunksByKind, filesWithChunks, parserKindCounts };
1031
+ }
1032
+ function scoreChunksForTask(filePaths, profile, workspaceRoot, options = {}) {
1033
+ const maxPerFile = options.maxChunksPerFile ?? 3;
1034
+ const maxTotal = options.maxTotalChunks ?? 30;
1035
+ // Load chunks from cache — match files by normalized path
1036
+ const normalizeFP = (p) => p.replace(/\\/g, "/");
1037
+ const normalizedTargets = filePaths.map(normalizeFP);
1038
+ let allChunks = [];
1039
+ try {
1040
+ const cacheFile = cachePath(workspaceRoot);
1041
+ if (fssync.existsSync(cacheFile)) {
1042
+ const raw = JSON.parse(fssync.readFileSync(cacheFile, "utf8"));
1043
+ allChunks = raw.files
1044
+ .filter((f) => {
1045
+ const nf = normalizeFP(f.relativePath);
1046
+ return normalizedTargets.some((fp) => nf === fp || nf.endsWith("/" + fp) || fp.endsWith("/" + nf));
1047
+ })
1048
+ .flatMap((f) => f.chunks || []);
1049
+ }
1050
+ }
1051
+ catch {
1052
+ return [];
1053
+ }
1054
+ if (allChunks.length === 0)
1055
+ return [];
1056
+ const taskTerms = new Set(profile.keywords.map((k) => k.toLowerCase()));
1057
+ const taskConcepts = new Set(profile.concepts.map((c) => c.toLowerCase()));
1058
+ const requiredSlots = new Set(profile.blueprint.requiredSlots);
1059
+ const positiveTerms = [...taskTerms, ...taskConcepts];
1060
+ const scoreChunk = (chunk) => {
1061
+ let score = 0;
1062
+ const reasons = [];
1063
+ // Term matching against chunk body
1064
+ for (const term of taskTerms) {
1065
+ if (chunk.contentPreview.toLowerCase().includes(term)) {
1066
+ score += 3;
1067
+ if (reasons.length < 4)
1068
+ reasons.push(`matched task term: ${term}`);
1069
+ }
1070
+ for (const ct of chunk.terms) {
1071
+ if (ct.includes(term) || term.includes(ct)) {
1072
+ score += 2;
1073
+ break;
1074
+ }
1075
+ }
1076
+ }
1077
+ // Symbol name matching
1078
+ if (chunk.symbolName) {
1079
+ const sym = chunk.symbolName.toLowerCase();
1080
+ for (const term of positiveTerms) {
1081
+ if (sym.includes(term) || term.includes(sym)) {
1082
+ score += 4;
1083
+ if (reasons.length < 4)
1084
+ reasons.push(`matched symbol name: ${chunk.symbolName}`);
1085
+ break;
1086
+ }
1087
+ }
1088
+ }
1089
+ // Slot / context matching
1090
+ if (requiredSlots.has(chunk.slot)) {
1091
+ score += 2;
1092
+ if (reasons.length < 4)
1093
+ reasons.push(`matched context slot: ${chunk.slot}`);
1094
+ }
1095
+ // Route evidence
1096
+ if (chunk.routes.length > 0) {
1097
+ for (const route of chunk.routes) {
1098
+ for (const term of taskTerms) {
1099
+ if (route.toLowerCase().includes(term)) {
1100
+ score += 3;
1101
+ if (reasons.length < 4)
1102
+ reasons.push(`matched route evidence: ${route}`);
1103
+ break;
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ // Annotation matching
1109
+ for (const ann of chunk.annotations) {
1110
+ for (const term of positiveTerms) {
1111
+ if (ann.toLowerCase().includes(term)) {
1112
+ score += 1;
1113
+ break;
1114
+ }
1115
+ }
1116
+ }
1117
+ // Path evidence
1118
+ const pathLower = (chunk.filePath || chunk.relativePath || "").toLowerCase();
1119
+ for (const term of taskTerms) {
1120
+ if (pathLower.includes(term)) {
1121
+ score += 1;
1122
+ break;
1123
+ }
1124
+ }
1125
+ // Import/call matching
1126
+ for (const imp of chunk.imports) {
1127
+ for (const term of positiveTerms) {
1128
+ if (imp.toLowerCase().includes(term)) {
1129
+ score += 1;
1130
+ break;
1131
+ }
1132
+ }
1133
+ }
1134
+ for (const call of chunk.calls) {
1135
+ for (const term of positiveTerms) {
1136
+ if (call.toLowerCase().includes(term)) {
1137
+ score += 1;
1138
+ break;
1139
+ }
1140
+ }
1141
+ }
1142
+ if (reasons.length === 0 && score > 0)
1143
+ reasons.push("weak term match");
1144
+ return { score, reasons };
1145
+ };
1146
+ const matches = [];
1147
+ const seenFiles = new Set();
1148
+ for (const fp of filePaths) {
1149
+ if (seenFiles.has(fp))
1150
+ continue;
1151
+ seenFiles.add(fp);
1152
+ const fileChunks = allChunks.filter((c) => {
1153
+ const cp = c.filePath || c.relativePath || "";
1154
+ return cp === fp || fp.endsWith(cp) || cp.endsWith(fp);
1155
+ });
1156
+ if (fileChunks.length === 0)
1157
+ continue;
1158
+ const scored = fileChunks.map((c) => ({ chunk: c, ...scoreChunk(c) }));
1159
+ scored.sort((a, b) => b.score - a.score);
1160
+ const top = scored.slice(0, maxPerFile).filter((s) => s.score > 0);
1161
+ for (const s of top) {
1162
+ matches.push({
1163
+ filePath: s.chunk.filePath || s.chunk.relativePath || fp,
1164
+ startLine: s.chunk.startLine ?? s.chunk.range?.startLine ?? 1,
1165
+ endLine: s.chunk.endLine ?? s.chunk.range?.endLine ?? 1,
1166
+ kind: s.chunk.kind,
1167
+ slot: s.chunk.slot,
1168
+ symbolName: s.chunk.symbolName || s.chunk.symbolFqn || null,
1169
+ score: s.score,
1170
+ reasons: s.reasons,
1171
+ contentPreview: s.chunk.contentPreview.slice(0, 200),
1172
+ });
1173
+ }
1174
+ }
1175
+ matches.sort((a, b) => b.score - a.score);
1176
+ return matches.slice(0, maxTotal);
1177
+ }
868
1178
  async function readIndex(rootPath) {
869
1179
  const filePath = cachePath(rootPath);
870
1180
  try {