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.
@@ -8,6 +8,39 @@
8
8
  *
9
9
  * Tools run the headless pipeline (no VS Code dependency) and return structured results.
10
10
  */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
11
44
  Object.defineProperty(exports, "__esModule", { value: true });
12
45
  exports.MCP_PROMPT_DEFINITIONS = exports.TOOL_DEFINITIONS = void 0;
13
46
  exports.handleToolCall = handleToolCall;
@@ -18,6 +51,11 @@ const solvePacket_1 = require("../pipeline/solvePacket");
18
51
  const agentPromptHeadless_1 = require("../pipeline/agentPromptHeadless");
19
52
  const fileGroups_1 = require("../pipeline/fileGroups");
20
53
  const tokenSavings_1 = require("../pipeline/tokenSavings");
54
+ const path = __importStar(require("path"));
55
+ const fs = __importStar(require("fs"));
56
+ const evaluationMetrics_1 = require("../pipeline/evaluationMetrics");
57
+ const embeddingProviders_1 = require("../gateway/embeddingProviders");
58
+ const workspaceIndex_1 = require("../index/workspaceIndex");
21
59
  exports.TOOL_DEFINITIONS = [
22
60
  {
23
61
  name: "flowseeker_retrieve",
@@ -98,6 +136,142 @@ exports.TOOL_DEFINITIONS = [
98
136
  },
99
137
  required: ["task"]
100
138
  }
139
+ },
140
+ {
141
+ name: "flowseeker_validate_plan",
142
+ description: "Validate a proposed edit plan against FlowSeeker retrieval evidence. Checks whether proposed edit files match primary edit candidates, warns if files should be read-only, and flags missing required slots. Returns a verdict (safe/caution/risky/insufficient_evidence) with suggested corrections.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ task: {
147
+ type: "string",
148
+ description: "Natural-language coding task to validate the plan against."
149
+ },
150
+ workspaceRoot: {
151
+ type: "string",
152
+ description: "Workspace root path. Defaults to current working directory."
153
+ },
154
+ proposedEditFiles: {
155
+ type: "array",
156
+ items: { type: "string" },
157
+ description: "Files the agent plans to edit (workspace-relative paths)."
158
+ },
159
+ proposedReadFiles: {
160
+ type: "array",
161
+ items: { type: "string" },
162
+ description: "Files the agent plans to read (workspace-relative paths)."
163
+ },
164
+ proposedTestFiles: {
165
+ type: "array",
166
+ items: { type: "string" },
167
+ description: "Files the agent plans to use for testing/verification."
168
+ },
169
+ riskMode: {
170
+ type: "string",
171
+ enum: ["strict", "balanced", "broad"],
172
+ description: "Risk tolerance. strict: unsupported edits are risky. balanced: caution unless clearly dangerous. broad: permissive but warns on noise. Default: balanced."
173
+ }
174
+ },
175
+ required: ["task", "proposedEditFiles"]
176
+ }
177
+ },
178
+ {
179
+ name: "flowseeker_context",
180
+ description: "Canonical FlowSeeker context tool. Returns task-relevant files, roles, slots, and evidence in the requested output profile (json/agent/human/compact). Replaces the need to choose between retrieve/guide/auto/files.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ task: { type: "string", description: "Natural-language coding task to find context for." },
185
+ workspaceRoot: { type: "string", description: "Workspace root path. Defaults to current working directory." },
186
+ workspace: { type: "string", description: "Alias for workspaceRoot." },
187
+ outputProfile: { type: "string", enum: ["json", "agent", "human", "compact"], description: "Output format. json: structured JSON. agent: guidance prompt. human: readable Solve Packet. compact: top files only. Default: json." },
188
+ mode: { type: "string", enum: ["auto", "deterministic", "hybrid-safe", "hybrid-aggressive", "semantic-only"], description: "Retrieval mode. Currently only deterministic is fully supported. Default: auto -> deterministic." },
189
+ depth: { type: "string", enum: ["quick", "standard", "deep"], description: "Scan depth. Currently only standard is fully supported. Default: standard." },
190
+ tokenBudget: { type: "number", description: "Approximate token budget for context. Acceptable, currently report-only." },
191
+ maxFiles: { type: "number", description: "Maximum files to scan. Default: 5000." },
192
+ includeSnippets: { type: "boolean", description: "Include code snippets in output. Default: true." }
193
+ },
194
+ required: ["task"]
195
+ }
196
+ },
197
+ {
198
+ name: "flowseeker_inspect",
199
+ description: "Inspect specific files against FlowSeeker retrieval evidence. Returns roles, scores, evidence chunks, snippets, retrieval trace, and honest edit guidance per file.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: {
203
+ task: { type: "string", description: "Natural-language coding task to retrieve context for." },
204
+ workspaceRoot: { type: "string", description: "Workspace root path. Defaults to current working directory." },
205
+ workspace: { type: "string", description: "Alias for workspaceRoot." },
206
+ files: { type: "array", items: { type: "string" }, description: "Workspace-relative file paths to inspect." },
207
+ maxSnippetsPerFile: { type: "number", description: "Max snippets to include per file. Default: 3." },
208
+ includeGraphNeighbors: { type: "boolean", description: "Include graph neighbor hints when available. Currently report-only." },
209
+ outputProfile: { type: "string", enum: ["json", "human"], description: "Output format. json: structured JSON. human: readable inspection report. Default: json." }
210
+ },
211
+ required: ["task", "files"]
212
+ }
213
+ },
214
+ {
215
+ name: "flowseeker_expand",
216
+ description: "Expand context for a specific slot. Given a task and a target slot (e.g. side_effect, domain, permission), returns files that can fill that missing context. Use anchor files to guide expansion.",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ task: { type: "string", description: "Natural-language coding task to find context for." },
221
+ slot: { type: "string", description: "Target context slot to expand (e.g. side_effect, domain, permission, validation, handler, data, config, tests)." },
222
+ workspaceRoot: { type: "string", description: "Workspace root path. Defaults to current working directory." },
223
+ workspace: { type: "string", description: "Alias for workspaceRoot." },
224
+ anchorFiles: { type: "array", items: { type: "string" }, description: "Files the agent already has context for. Used to prefer connected candidates." },
225
+ maxFiles: { type: "number", description: "Max expansion candidates to return. Default: 15." },
226
+ outputProfile: { type: "string", enum: ["json", "human"], description: "Output format. json: structured JSON. human: readable expansion report. Default: json." }
227
+ },
228
+ required: ["task", "slot"]
229
+ }
230
+ },
231
+ {
232
+ name: "flowseeker_status",
233
+ description: "Check FlowSeeker readiness: version, workspace config, deterministic/semantic/local model status, index/cache state, and next actions.",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: {
237
+ workspaceRoot: { type: "string", description: "Workspace root path. Defaults to current working directory." },
238
+ workspace: { type: "string", description: "Alias for workspaceRoot." },
239
+ outputProfile: { type: "string", enum: ["json", "human"], description: "Output format. json: structured JSON. human: readable report. Default: json." }
240
+ },
241
+ required: []
242
+ }
243
+ },
244
+ {
245
+ name: "flowseeker_index",
246
+ description: "Check or build the FlowSeeker workspace index. action=status reports index state. action=build runs safe indexing. Destructive actions (clean, rebuild, delete, reset, remove, purge) are rejected.",
247
+ inputSchema: {
248
+ type: "object",
249
+ properties: {
250
+ action: { type: "string", enum: ["status", "build"], description: "Action: status (read-only) or build (safe indexing)." },
251
+ workspaceRoot: { type: "string", description: "Workspace root path. Defaults to current working directory." },
252
+ workspace: { type: "string", description: "Alias for workspaceRoot." },
253
+ outputProfile: { type: "string", enum: ["json", "human"], description: "Output format. json: structured JSON. human: readable report. Default: json." },
254
+ maxFiles: { type: "number", description: "Max files to index (for build). Default: 5000." }
255
+ },
256
+ required: ["action"]
257
+ }
258
+ },
259
+ {
260
+ name: "flowseeker_feedback",
261
+ description: "Append lightweight labels to FlowSeeker feedback for future golden replay and accuracy improvement. Labels: useful, wrong, missing, edited, test_used, should_not_edit.",
262
+ inputSchema: {
263
+ type: "object",
264
+ properties: {
265
+ task: { type: "string", description: "The coding task this feedback applies to." },
266
+ label: { type: "string", enum: ["useful", "wrong", "missing", "edited", "test_used", "should_not_edit"], description: "Feedback label." },
267
+ files: { type: "array", items: { type: "string" }, description: "Workspace-relative file paths related to this feedback." },
268
+ notes: { type: "string", description: "Optional human-readable notes about this feedback." },
269
+ workspaceRoot: { type: "string", description: "Workspace root path. Defaults to current working directory." },
270
+ workspace: { type: "string", description: "Alias for workspaceRoot." },
271
+ outputProfile: { type: "string", enum: ["json", "human"], description: "Output format. Default: json." }
272
+ },
273
+ required: ["task", "label"]
274
+ }
101
275
  }
102
276
  ];
103
277
  exports.MCP_PROMPT_DEFINITIONS = [
@@ -141,10 +315,164 @@ exports.MCP_PROMPT_DEFINITIONS = [
141
315
  { name: "workspace", description: "Optional workspace root path." },
142
316
  { name: "limit", description: "Optional maximum number of files to return." }
143
317
  ]
318
+ },
319
+ {
320
+ name: "validate-plan",
321
+ description: "Validate a proposed edit plan against FlowSeeker retrieval evidence.",
322
+ toolName: "flowseeker_validate_plan",
323
+ toolPurpose: "return a safety verdict with issues, suggested edit/read/test files, and missing slots",
324
+ arguments: [
325
+ { name: "task", description: "Natural-language coding task.", required: true },
326
+ { name: "proposedEditFiles", description: "Files the agent plans to edit.", required: true },
327
+ { name: "workspaceRoot", description: "Optional workspace root path." },
328
+ { name: "proposedReadFiles", description: "Optional files the agent plans to read." },
329
+ { name: "proposedTestFiles", description: "Optional files for testing/verification." },
330
+ { name: "riskMode", description: "Risk tolerance: strict, balanced, or broad. Default: balanced." }
331
+ ]
332
+ },
333
+ {
334
+ name: "context",
335
+ description: "Canonical FlowSeeker context tool -- structured JSON, agent guidance, human report, or compact top-files list.",
336
+ toolName: "flowseeker_context",
337
+ toolPurpose: "return task context in the requested output profile (json/agent/human/compact)",
338
+ arguments: [
339
+ { name: "task", description: "Natural-language coding task.", required: true },
340
+ { name: "outputProfile", description: "Output format: json, agent, human, or compact. Default: json." },
341
+ { name: "workspaceRoot", description: "Optional workspace root path." },
342
+ { name: "mode", description: "Retrieval mode. Default: auto -> deterministic." },
343
+ { name: "depth", description: "Scan depth. Default: standard." },
344
+ { name: "tokenBudget", description: "Optional token budget." },
345
+ { name: "maxFiles", description: "Optional max files." },
346
+ { name: "includeSnippets", description: "Include snippets. Default: true." }
347
+ ]
348
+ },
349
+ {
350
+ name: "inspect",
351
+ description: "Inspect specific files against FlowSeeker evidence -- roles, scores, chunks, snippets, and edit guidance.",
352
+ toolName: "flowseeker_inspect",
353
+ toolPurpose: "return per-file inspection with roles, evidence, and edit guidance (json/human)",
354
+ arguments: [
355
+ { name: "task", description: "Natural-language coding task.", required: true },
356
+ { name: "files", description: "Workspace-relative file paths to inspect.", required: true },
357
+ { name: "workspaceRoot", description: "Optional workspace root path." },
358
+ { name: "maxSnippetsPerFile", description: "Max snippets per file. Default: 3." },
359
+ { name: "includeGraphNeighbors", description: "Include graph neighbor hints. Report-only in this sprint." },
360
+ { name: "outputProfile", description: "Output format: json or human. Default: json." }
361
+ ]
362
+ },
363
+ {
364
+ name: "expand",
365
+ description: "Expand context for a specific slot -- returns files to read next to close missing context.",
366
+ toolName: "flowseeker_expand",
367
+ toolPurpose: "return slot-specific expansion candidates with reasons and edit guidance (json/human)",
368
+ arguments: [
369
+ { name: "task", description: "Natural-language coding task.", required: true },
370
+ { name: "slot", description: "Target context slot to expand.", required: true },
371
+ { name: "workspaceRoot", description: "Optional workspace root path." },
372
+ { name: "anchorFiles", description: "Files the agent already has context for." },
373
+ { name: "maxFiles", description: "Max expansion candidates. Default: 15." },
374
+ { name: "outputProfile", description: "Output format: json or human. Default: json." }
375
+ ]
376
+ },
377
+ {
378
+ name: "status",
379
+ description: "Check FlowSeeker readiness: version, config, deterministic/semantic/local model status, index state, next actions.",
380
+ toolName: "flowseeker_status",
381
+ toolPurpose: "return workspace-level FlowSeeker readiness report (json/human)",
382
+ arguments: [
383
+ { name: "workspaceRoot", description: "Optional workspace root path." },
384
+ { name: "outputProfile", description: "Output format: json or human. Default: json." }
385
+ ]
386
+ },
387
+ {
388
+ name: "index",
389
+ description: "Check or build the FlowSeeker workspace index safely.",
390
+ toolName: "flowseeker_index",
391
+ toolPurpose: "return index status or trigger safe build (json/human)",
392
+ arguments: [
393
+ { name: "action", description: "Action: status (read-only) or build (safe indexing).", required: true },
394
+ { name: "workspaceRoot", description: "Optional workspace root path." },
395
+ { name: "maxFiles", description: "Max files to index (for build). Default: 5000." },
396
+ { name: "outputProfile", description: "Output format: json or human. Default: json." }
397
+ ]
398
+ },
399
+ {
400
+ name: "feedback",
401
+ description: "Append labels to FlowSeeker feedback for future accuracy improvement.",
402
+ toolName: "flowseeker_feedback",
403
+ toolPurpose: "append a lightweight labeled record to .flowseeker/feedback/ (json/human)",
404
+ arguments: [
405
+ { name: "task", description: "The coding task this feedback applies to.", required: true },
406
+ { name: "label", description: "Feedback label.", required: true },
407
+ { name: "files", description: "Related workspace-relative file paths." },
408
+ { name: "notes", description: "Optional notes." },
409
+ { name: "workspaceRoot", description: "Optional workspace root path." },
410
+ { name: "outputProfile", description: "Output format: json or human. Default: json." }
411
+ ]
144
412
  }
145
413
  ];
146
414
  async function handleToolCall(toolName, input, defaultWorkspace) {
147
415
  const workspace = input.workspace ?? defaultWorkspace;
416
+ // Route validate_plan before generic task validation -- it has its own
417
+ // input validation that returns structured JSON for bad task input.
418
+ if (toolName === "flowseeker_validate_plan") {
419
+ try {
420
+ return await handleValidatePlan(input, workspace);
421
+ }
422
+ catch (error) {
423
+ const message = error instanceof Error ? error.message : String(error);
424
+ return errorResult(`FlowSeeker failed: ${message}`);
425
+ }
426
+ }
427
+ // Route inspect before generic task validation -- it validates files separately.
428
+ if (toolName === "flowseeker_inspect") {
429
+ try {
430
+ return await handleInspect(input, workspace);
431
+ }
432
+ catch (error) {
433
+ const message = error instanceof Error ? error.message : String(error);
434
+ return errorResult(`FlowSeeker failed: ${message}`);
435
+ }
436
+ }
437
+ // Route expand before generic task validation -- it validates slot separately.
438
+ if (toolName === "flowseeker_expand") {
439
+ try {
440
+ return await handleExpand(input, workspace);
441
+ }
442
+ catch (error) {
443
+ const message = error instanceof Error ? error.message : String(error);
444
+ return errorResult(`FlowSeeker failed: ${message}`);
445
+ }
446
+ }
447
+ // Route status/index before generic task validation -- they don't need task.
448
+ if (toolName === "flowseeker_status") {
449
+ try {
450
+ return await handleStatus(input, workspace);
451
+ }
452
+ catch (error) {
453
+ const message = error instanceof Error ? error.message : String(error);
454
+ return errorResult(`FlowSeeker failed: ${message}`);
455
+ }
456
+ }
457
+ if (toolName === "flowseeker_index") {
458
+ try {
459
+ return await handleIndex(input, workspace);
460
+ }
461
+ catch (error) {
462
+ const message = error instanceof Error ? error.message : String(error);
463
+ return errorResult(`FlowSeeker failed: ${message}`);
464
+ }
465
+ }
466
+ // Route feedback before generic task validation -- it validates label separately.
467
+ if (toolName === "flowseeker_feedback") {
468
+ try {
469
+ return await handleFeedback(input, workspace);
470
+ }
471
+ catch (error) {
472
+ const message = error instanceof Error ? error.message : String(error);
473
+ return errorResult(`FlowSeeker failed: ${message}`);
474
+ }
475
+ }
148
476
  const task = input.task?.trim();
149
477
  if (!task) {
150
478
  return errorResult("A task description is required.");
@@ -159,6 +487,8 @@ async function handleToolCall(toolName, input, defaultWorkspace) {
159
487
  return await handleAuto(task, workspace);
160
488
  case "flowseeker_files":
161
489
  return await handleFiles(task, workspace, input.limit);
490
+ case "flowseeker_context":
491
+ return await handleContext(input, workspace);
162
492
  default:
163
493
  return errorResult(`Unknown tool: ${toolName}`);
164
494
  }
@@ -168,7 +498,7 @@ async function handleToolCall(toolName, input, defaultWorkspace) {
168
498
  return errorResult(`FlowSeeker failed: ${message}`);
169
499
  }
170
500
  }
171
- // ── Individual Handlers ───────────────────────────────────────────────
501
+ // -- Individual Handlers -----------------------------------------------
172
502
  async function handleRetrieve(task, workspace, maxFiles) {
173
503
  const result = await runPipeline(task, workspace, maxFiles);
174
504
  const packet = result.solvePacket
@@ -228,7 +558,7 @@ async function handleFiles(task, workspace, limit) {
228
558
  }
229
559
  return textResult(lines.join("\n"));
230
560
  }
231
- // ── Helpers ───────────────────────────────────────────────────────────
561
+ // -- Helpers -----------------------------------------------------------
232
562
  async function runPipeline(task, workspace, maxFiles) {
233
563
  const baseConfig = await (0, loadConfigFromPath_1.loadConfigFromPath)(workspace);
234
564
  const config = maxFiles && maxFiles > 0
@@ -257,10 +587,1305 @@ function formatResultSummary(result) {
257
587
  }
258
588
  return parts.join("\n");
259
589
  }
590
+ async function handleContext(input, defaultWorkspace) {
591
+ var task = (input.task || "").trim();
592
+ var workspace = input.workspaceRoot || input.workspace || defaultWorkspace;
593
+ var profile = input.outputProfile || "json";
594
+ var mode = input.mode || "auto";
595
+ var depth = input.depth || "standard";
596
+ var includeSnippets = input.includeSnippets !== false;
597
+ if (!task) {
598
+ return errorResult("task must be a non-empty string");
599
+ }
600
+ // Validate inputs
601
+ var validProfiles = ["json", "agent", "human", "compact"];
602
+ var validModes = ["auto", "deterministic", "hybrid-safe", "hybrid-aggressive", "semantic-only"];
603
+ var validDepths = ["quick", "standard", "deep"];
604
+ if (validProfiles.indexOf(profile) < 0) {
605
+ return errorResult("invalid outputProfile: " + profile + ". Must be one of: " + validProfiles.join(", "));
606
+ }
607
+ if (validModes.indexOf(mode) < 0) {
608
+ return errorResult("invalid mode: " + mode + ". Must be one of: " + validModes.join(", "));
609
+ }
610
+ if (validDepths.indexOf(depth) < 0) {
611
+ return errorResult("invalid depth: " + depth + ". Must be one of: " + validDepths.join(", "));
612
+ }
613
+ // Resolve effective mode/depth
614
+ var effectiveMode = mode === "auto" ? "deterministic" : mode;
615
+ var effectiveDepth = depth;
616
+ var warnings = [];
617
+ if (effectiveMode !== "deterministic") {
618
+ warnings.push("mode=" + effectiveMode + " is accepted but report-only; only deterministic retrieval is active");
619
+ }
620
+ if (effectiveDepth !== "standard") {
621
+ warnings.push("depth=" + effectiveDepth + " is accepted but report-only; only standard depth is active");
622
+ }
623
+ if (input.tokenBudget !== undefined) {
624
+ warnings.push("tokenBudget is accepted but report-only");
625
+ }
626
+ if (!includeSnippets) {
627
+ warnings.push("includeSnippets=false: snippets omitted");
628
+ }
629
+ // Run retrieval
630
+ var result = await runPipeline(task, workspace, input.maxFiles);
631
+ var packet = result.solvePacket;
632
+ var groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units);
633
+ if (profile === "json") {
634
+ var modeDiagWarnings = [];
635
+ if (effectiveMode === "semantic-only") {
636
+ modeDiagWarnings.push("semantic-only is diagnostic/evaluation only; deterministic retrieval remains the active production path");
637
+ }
638
+ var modeDiagnostic = {
639
+ requestedMode: mode,
640
+ effectiveMode: effectiveMode,
641
+ status: effectiveMode === "semantic-only" ? "diagnostic_only" : effectiveMode === "deterministic" ? "active" : "report_only",
642
+ reportOnly: effectiveMode !== "deterministic",
643
+ rankingChanged: false,
644
+ primaryCandidatesChanged: false,
645
+ reason: effectiveMode === "semantic-only"
646
+ ? "semantic-only is diagnostic/evaluation only; firstFiles and primaryEditCandidates remain deterministic"
647
+ : effectiveMode === "deterministic"
648
+ ? "deterministic mode active; no semantic retrieval configured by default"
649
+ : "non-deterministic mode requested; output remains deterministic until hybrid-safe is implemented",
650
+ warnings: modeDiagWarnings,
651
+ };
652
+ var output = {
653
+ schemaVersion: "2.0.0",
654
+ tool: "flowseeker_context",
655
+ task: task,
656
+ workspaceRoot: workspace,
657
+ outputProfile: profile,
658
+ requestedMode: mode,
659
+ effectiveMode: effectiveMode,
660
+ requestedDepth: depth,
661
+ effectiveDepth: effectiveDepth,
662
+ modeDiagnostic: modeDiagnostic,
663
+ summary: formatResultSummary(result),
664
+ confidence: result.profile.intentConfidence,
665
+ firstFiles: packet?.firstThreeFiles || [],
666
+ primaryEditCandidates: (packet?.primaryEditCandidates || []).slice(0, 7).map(function (f) { return { path: f.relativePath, role: f.role, kind: f.kind, slot: f.slot, score: f.score, tier: f.tier, reasons: f.reasons }; }),
667
+ readOnlyContext: (packet?.readOnlyContext || []).slice(0, 7).map(function (f) { return { path: f.relativePath, role: f.role, slot: f.slot, score: f.score }; }),
668
+ verificationTargets: (packet?.verificationTargets || []).slice(0, 5).map(function (f) { return { path: f.relativePath, role: f.role, slot: f.slot, score: f.score }; }),
669
+ doNotEditYet: packet?.doNotEditYet || [],
670
+ missingSlots: result.contextCoverage?.missingRequiredSlots || [],
671
+ semanticDiagnostic: result.stats.semanticRetrievalStatus || "disabled",
672
+ semanticChunkDiagnostic: result.stats.semanticChunkDiagnostic || null,
673
+ roleRefinement: result.stats.roleRefinementMode || "off",
674
+ warnings: warnings,
675
+ };
676
+ return textResult(JSON.stringify(output, null, 2));
677
+ }
678
+ if (profile === "agent") {
679
+ var agentPrompt = require("../pipeline/agentPromptHeadless").createAgentSolvePromptHeadless(result);
680
+ var agentLines = [
681
+ "# FlowSeeker Agent Context",
682
+ "",
683
+ "Task: " + task,
684
+ "Intent: " + result.profile.intent + " | Confidence: " + result.profile.intentConfidence,
685
+ "",
686
+ agentPrompt,
687
+ ];
688
+ if (warnings.length > 0) {
689
+ agentLines.push("", "## Warnings", "");
690
+ for (var w = 0; w < warnings.length; w++)
691
+ agentLines.push("- " + warnings[w]);
692
+ }
693
+ return textResult(agentLines.join("\n"));
694
+ }
695
+ if (profile === "human") {
696
+ var packetMd = packet ? (0, solvePacket_1.renderSolvePacketMarkdown)(packet) : result.contextPack;
697
+ var summary = formatResultSummary(result);
698
+ var humanLines = [summary, "", packetMd];
699
+ if (effectiveMode === "semantic-only") {
700
+ humanLines.push("", "> semantic-only is diagnostic/evaluation only -- deterministic retrieval remains the active production path.");
701
+ }
702
+ else if (warnings.length > 0) {
703
+ humanLines.push("", "## Warnings", "");
704
+ for (var hw = 0; hw < warnings.length; hw++)
705
+ humanLines.push("- " + warnings[hw]);
706
+ }
707
+ return textResult(humanLines.join("\n"));
708
+ }
709
+ // compact
710
+ var compactLines = ["# FlowSeeker Context (compact)", "", "Task: " + task];
711
+ var topFiles = groups.slice(0, 15);
712
+ for (var tf = 0; tf < topFiles.length; tf++) {
713
+ var g = topFiles[tf];
714
+ compactLines.push((tf + 1) + ". " + g.relativePath + " role=" + g.role + " slot=" + (g.slot || "unknown") + " score=" + g.score.toFixed(1) + " reasons=" + g.reasons.join(","));
715
+ }
716
+ if (warnings.length > 0) {
717
+ compactLines.push("", "Warnings:");
718
+ for (var w2 = 0; w2 < warnings.length; w2++)
719
+ compactLines.push("- " + warnings[w2]);
720
+ }
721
+ return textResult(compactLines.join("\n"));
722
+ }
723
+ // -- Inspect Tool -----------------------------------------------------
724
+ function editGuidanceForRole(role) {
725
+ switch (role) {
726
+ case "edit_candidate": return ["candidate_edit"];
727
+ case "read_context": return ["read_first", "do_not_edit_without_reason"];
728
+ case "impact": return ["read_first"];
729
+ case "test":
730
+ case "verify": return ["verification_target"];
731
+ default: return ["inspect_manually"];
732
+ }
733
+ }
734
+ async function handleInspect(input, defaultWorkspace) {
735
+ var task = (input.task || "").trim();
736
+ var workspace = input.workspaceRoot || input.workspace || defaultWorkspace;
737
+ var files = input.files || [];
738
+ var maxSnippets = input.maxSnippetsPerFile || 3;
739
+ var includeGraphNeighbors = !!input.includeGraphNeighbors;
740
+ var profile = input.outputProfile || "json";
741
+ // Validate task
742
+ if (!task) {
743
+ return errorResult("task must be a non-empty string");
744
+ }
745
+ // Validate files
746
+ if (!Array.isArray(files) || files.length === 0) {
747
+ return errorResult("files must be a non-empty array of non-empty strings");
748
+ }
749
+ for (var fi = 0; fi < files.length; fi++) {
750
+ if (typeof files[fi] !== "string" || !files[fi].trim()) {
751
+ return errorResult("files[" + fi + "] must be a non-empty string");
752
+ }
753
+ }
754
+ // Validate outputProfile
755
+ if (profile !== "json" && profile !== "human") {
756
+ return errorResult("invalid outputProfile: " + profile + ". Must be json or human");
757
+ }
758
+ // Validate maxSnippetsPerFile
759
+ if (input.maxSnippetsPerFile !== undefined && (typeof input.maxSnippetsPerFile !== "number" || input.maxSnippetsPerFile <= 0)) {
760
+ return errorResult("maxSnippetsPerFile must be a positive number");
761
+ }
762
+ // Path safety: classify each requested file
763
+ var rejectedPaths = [];
764
+ var validFiles = [];
765
+ for (var fi2 = 0; fi2 < files.length; fi2++) {
766
+ var raw = files[fi2].trim();
767
+ if (isOutsideWorkspace(raw, workspace)) {
768
+ rejectedPaths.push({ path: raw, reason: "outside workspace or sibling escape" });
769
+ }
770
+ else {
771
+ validFiles.push(raw);
772
+ }
773
+ }
774
+ // Run retrieval
775
+ var result = await runPipeline(task, workspace, input.maxFiles);
776
+ var groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units);
777
+ // Index groups by relativePath
778
+ var groupByPath = {};
779
+ for (var gi = 0; gi < groups.length; gi++) {
780
+ groupByPath[groups[gi].relativePath] = groups[gi];
781
+ }
782
+ var warnings = [];
783
+ if (includeGraphNeighbors) {
784
+ warnings.push("includeGraphNeighbors is accepted but report-only; graph neighbor extraction is not yet implemented");
785
+ }
786
+ // Index noiseRisk and doNotEditYet for guidance truth
787
+ var noiseRiskPaths = [];
788
+ var noiseRiskByPath = {}; // reasons from noiseRisk entry
789
+ if (result.solvePacket && result.solvePacket.noiseRisk) {
790
+ for (var nr = 0; nr < result.solvePacket.noiseRisk.length; nr++) {
791
+ var nf = result.solvePacket.noiseRisk[nr];
792
+ noiseRiskPaths.push(nf.relativePath);
793
+ noiseRiskByPath[nf.relativePath] = nf.reasons || [];
794
+ }
795
+ }
796
+ var doNotEditPaths = result.solvePacket?.doNotEditYet || [];
797
+ function isDoNotEdit(filePath) {
798
+ var reasons = [];
799
+ // Check noiseRisk
800
+ if (noiseRiskByPath[filePath]) {
801
+ reasons = reasons.concat(noiseRiskByPath[filePath]);
802
+ }
803
+ // Check doNotEditYet entries that reference this file
804
+ for (var di = 0; di < doNotEditPaths.length; di++) {
805
+ if (doNotEditPaths[di].indexOf(filePath) === 0) {
806
+ reasons.push(doNotEditPaths[di]);
807
+ }
808
+ }
809
+ return { isNoise: noiseRiskPaths.indexOf(filePath) >= 0 || reasons.length > 0, reasons: reasons };
810
+ }
811
+ var inspectedFiles = [];
812
+ var missingRequestedFiles = [];
813
+ for (var vf = 0; vf < validFiles.length; vf++) {
814
+ var requestedPath = validFiles[vf];
815
+ var group = groupByPath[requestedPath];
816
+ if (!group) {
817
+ missingRequestedFiles.push(requestedPath);
818
+ continue;
819
+ }
820
+ // Collect top chunks from units
821
+ var sortedUnits = group.units.slice().sort(function (a, b) { return b.score - a.score; });
822
+ var topChunks = [];
823
+ var snippets = [];
824
+ var retrievalTrace = [];
825
+ for (var ui = 0; ui < sortedUnits.length && ui < maxSnippets; ui++) {
826
+ var unit = sortedUnits[ui];
827
+ var chunk = {
828
+ kind: unit.kind,
829
+ role: unit.role,
830
+ slot: unit.slot,
831
+ score: unit.score,
832
+ confidence: unit.confidence,
833
+ tier: unit.tier,
834
+ reasons: unit.reasons,
835
+ range: unit.range || null,
836
+ };
837
+ topChunks.push(chunk);
838
+ if (unit.snippet) {
839
+ snippets.push(unit.snippet);
840
+ }
841
+ if (unit.retrievalTrace && unit.retrievalTrace.length > 0) {
842
+ for (var rt = 0; rt < unit.retrievalTrace.length; rt++) {
843
+ if (retrievalTrace.indexOf(unit.retrievalTrace[rt]) < 0) {
844
+ retrievalTrace.push(unit.retrievalTrace[rt]);
845
+ }
846
+ }
847
+ }
848
+ }
849
+ // Determine edit guidance: check noiseRisk/doNotEditYet first
850
+ var doNotEditCheck = isDoNotEdit(group.relativePath);
851
+ var guidance;
852
+ var doNotEditReason;
853
+ if (doNotEditCheck.isNoise || doNotEditCheck.reasons.length > 0) {
854
+ guidance = ["avoid_edit"];
855
+ doNotEditReason = doNotEditCheck.reasons.length > 0
856
+ ? doNotEditCheck.reasons.join("; ")
857
+ : "listed in noiseRisk or doNotEditYet";
858
+ }
859
+ else {
860
+ guidance = editGuidanceForRole(group.role);
861
+ }
862
+ inspectedFiles.push({
863
+ path: group.relativePath,
864
+ found: true,
865
+ role: group.role,
866
+ slot: group.slot,
867
+ score: group.score,
868
+ confidence: group.confidence,
869
+ tier: group.tier,
870
+ reasons: group.reasons,
871
+ topChunks: topChunks,
872
+ snippets: snippets,
873
+ retrievalTrace: retrievalTrace.length > 0 ? retrievalTrace : undefined,
874
+ editGuidance: guidance,
875
+ doNotEditReason: doNotEditReason,
876
+ });
877
+ }
878
+ if (profile === "human") {
879
+ var humanLines = [
880
+ "# FlowSeeker Inspect Report",
881
+ "",
882
+ "Task: " + task,
883
+ "Workspace: " + workspace,
884
+ "",
885
+ ];
886
+ if (inspectedFiles.length > 0) {
887
+ humanLines.push("## Inspected Files");
888
+ humanLines.push("");
889
+ for (var hf = 0; hf < inspectedFiles.length; hf++) {
890
+ var f = inspectedFiles[hf];
891
+ humanLines.push("### " + f.path);
892
+ humanLines.push("- Role: " + f.role + " | Slot: " + (f.slot || "unknown") + " | Score: " + f.score.toFixed(1) + " | Tier: " + f.tier);
893
+ humanLines.push("- Edit Guidance: " + f.editGuidance.join(", "));
894
+ if (f.doNotEditReason) {
895
+ humanLines.push("- Do Not Edit Reason: " + f.doNotEditReason);
896
+ }
897
+ humanLines.push("- Reasons: " + f.reasons.join("; "));
898
+ if (f.snippets.length > 0) {
899
+ humanLines.push("- Snippets (" + f.snippets.length + "):");
900
+ for (var sn = 0; sn < f.snippets.length; sn++) {
901
+ humanLines.push(" ```");
902
+ humanLines.push(" " + f.snippets[sn].split("\n").join("\n "));
903
+ humanLines.push(" ```");
904
+ }
905
+ }
906
+ humanLines.push("");
907
+ }
908
+ }
909
+ if (missingRequestedFiles.length > 0) {
910
+ humanLines.push("## Missing Files");
911
+ humanLines.push("");
912
+ for (var mf = 0; mf < missingRequestedFiles.length; mf++) {
913
+ humanLines.push("- " + missingRequestedFiles[mf]);
914
+ }
915
+ humanLines.push("");
916
+ }
917
+ if (rejectedPaths.length > 0) {
918
+ humanLines.push("## Rejected Paths");
919
+ humanLines.push("");
920
+ for (var rp = 0; rp < rejectedPaths.length; rp++) {
921
+ humanLines.push("- " + rejectedPaths[rp].path + ": " + rejectedPaths[rp].reason);
922
+ }
923
+ humanLines.push("");
924
+ }
925
+ if (warnings.length > 0) {
926
+ humanLines.push("## Warnings");
927
+ humanLines.push("");
928
+ for (var hw = 0; hw < warnings.length; hw++) {
929
+ humanLines.push("- " + warnings[hw]);
930
+ }
931
+ humanLines.push("");
932
+ }
933
+ return textResult(humanLines.join("\n"));
934
+ }
935
+ // json profile
936
+ var jsonOutput = {
937
+ schemaVersion: "2.0.0",
938
+ tool: "flowseeker_inspect",
939
+ task: task,
940
+ workspaceRoot: workspace,
941
+ inspectedFiles: inspectedFiles,
942
+ missingRequestedFiles: missingRequestedFiles,
943
+ rejectedPaths: rejectedPaths,
944
+ warnings: warnings,
945
+ };
946
+ return textResult(JSON.stringify(jsonOutput, null, 2));
947
+ }
948
+ // -- Expand Tool ------------------------------------------------------
949
+ async function handleExpand(input, defaultWorkspace) {
950
+ var task = (input.task || "").trim();
951
+ var slot = (input.slot || "").trim();
952
+ var workspace = input.workspaceRoot || input.workspace || defaultWorkspace;
953
+ var anchorFiles = input.anchorFiles || [];
954
+ var maxCandidates = input.maxFiles || 15;
955
+ var profile = input.outputProfile || "json";
956
+ // Validate task
957
+ if (!task) {
958
+ return errorResult("task must be a non-empty string");
959
+ }
960
+ // Validate slot
961
+ if (!slot) {
962
+ return errorResult("slot must be a non-empty string");
963
+ }
964
+ // Validate outputProfile
965
+ if (profile !== "json" && profile !== "human") {
966
+ return errorResult("invalid outputProfile: " + profile + ". Must be json or human");
967
+ }
968
+ // Validate maxFiles
969
+ if (input.maxFiles !== undefined && (typeof input.maxFiles !== "number" || input.maxFiles <= 0)) {
970
+ return errorResult("maxFiles must be a positive number");
971
+ }
972
+ // Validate anchorFiles if provided
973
+ if (!Array.isArray(anchorFiles)) {
974
+ return errorResult("anchorFiles must be an array of non-empty strings when provided");
975
+ }
976
+ for (var ai = 0; ai < anchorFiles.length; ai++) {
977
+ if (typeof anchorFiles[ai] !== "string" || !anchorFiles[ai].trim()) {
978
+ return errorResult("anchorFiles[" + ai + "] must be a non-empty string");
979
+ }
980
+ }
981
+ // Path safety for anchorFiles
982
+ var rejectedPaths = [];
983
+ var safeAnchors = [];
984
+ for (var ai2 = 0; ai2 < anchorFiles.length; ai2++) {
985
+ var raw = anchorFiles[ai2].trim();
986
+ if (isOutsideWorkspace(raw, workspace)) {
987
+ rejectedPaths.push({ path: raw, reason: "outside workspace or sibling escape" });
988
+ }
989
+ else {
990
+ safeAnchors.push(raw);
991
+ }
992
+ }
993
+ // Run retrieval
994
+ var result = await runPipeline(task, workspace, input.maxFiles);
995
+ var groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units);
996
+ // Index groups by relativePath before anchor extraction
997
+ var groupByPath = {};
998
+ for (var gi2 = 0; gi2 < groups.length; gi2++) {
999
+ groupByPath[groups[gi2].relativePath] = groups[gi2];
1000
+ }
1001
+ // -- Token classification --
1002
+ // Strong: domain-specific domain/entity tokens that signal real connection
1003
+ var strongTokenSet = {};
1004
+ ["payment", "invoice", "user", "order", "course", "enrollment", "notification",
1005
+ "email", "auth", "gateway", "transaction", "billing", "subscription", "receipt",
1006
+ "checkout", "cart", "product", "pricing", "tax", "shipping", "coupon", "wallet",
1007
+ "credit", "debit", "balance", "transfer", "refund", "charge", "fee", "payout",
1008
+ "customer", "merchant", "account", "ledger", "report", "dashboard", "analytics",
1009
+ "booking", "reservation", "appointment", "schedule", "calendar", "reminder",
1010
+ "alert", "message", "sms", "push", "webhook", "callback", "listener", "observer",
1011
+ "repository", "factory", "builder", "handle", "exec", "queue", "worker",
1012
+ "cron", "batch", "process", "pipeline", "mutator", "accessor", "trait",
1013
+ "interface", "abstract", "contract", "binding", "facade", "middleware",
1014
+ "guard", "policy", "role", "permission", "profile", "token", "session",
1015
+ "login", "register", "logout", "reset", "verify", "confirm", "upload",
1016
+ "download", "export", "import", "filter", "search", "sort", "paginate",
1017
+ "validate", "sanitize", "encrypt", "decrypt", "hash", "sign", "verify",
1018
+ "approve", "reject", "cancel", "complete", "process", "dispatch", "fire",
1019
+ "notify"].forEach(function (t) { strongTokenSet[t] = true; });
1020
+ // Weak: generic framework/structural tokens that appear everywhere
1021
+ var weakTokenSet = {};
1022
+ ["service", "services", "model", "models", "controller", "controllers",
1023
+ "config", "http", "jobs", "mail", "listeners", "events", "console", "commands",
1024
+ "routes", "providers", "exceptions", "helpers", "support", "resources",
1025
+ "views", "lang", "storage", "database", "bootstrap", "public", "tests",
1026
+ "seeders", "migrations", "factories", "middleware", "requests", "responses",
1027
+ "app", "src", "lib", "vendor", "php"].forEach(function (t) { weakTokenSet[t] = true; });
1028
+ // Pure noise: tokens that should never appear in anchor connections
1029
+ var noiseTokenSet = {};
1030
+ ["path", "paths", "match", "matches", "fix", "line", "file", "files",
1031
+ "service", "services", "model", "models", "config",
1032
+ "code", "use", "get", "set", "new", "int", "str", "bool", "array", "null",
1033
+ "true", "false", "return", "function", "class", "public", "private",
1034
+ "protected", "static", "final", "abstract", "extends", "implements",
1035
+ "interface", "namespace", "require", "include", "echo", "print", "var",
1036
+ "let", "const", "this", "self", "parent", "php", "app", "src", "lib",
1037
+ "vendor"].forEach(function (t) { noiseTokenSet[t] = true; });
1038
+ function extractTokens(str) {
1039
+ var lower = str.toLowerCase();
1040
+ var parts = lower.replace(/[\\/]/g, " ").replace(/[^a-z0-9_]/g, " ").split(/\s+/);
1041
+ var tokens = [];
1042
+ for (var pt = 0; pt < parts.length; pt++) {
1043
+ var t = parts[pt].trim();
1044
+ if (t.length >= 3 && !noiseTokenSet[t]) {
1045
+ tokens.push(t);
1046
+ }
1047
+ }
1048
+ return tokens;
1049
+ }
1050
+ // -- Extract anchor signals --
1051
+ var anchorStrongTokens = {};
1052
+ var anchorWeakTokens = {};
1053
+ for (var sa2 = 0; sa2 < safeAnchors.length; sa2++) {
1054
+ var anchorPath = safeAnchors[sa2];
1055
+ var pathTokens = extractTokens(anchorPath);
1056
+ for (var pt2 = 0; pt2 < pathTokens.length; pt2++) {
1057
+ var at = pathTokens[pt2];
1058
+ if (strongTokenSet[at]) {
1059
+ anchorStrongTokens[at] = true;
1060
+ }
1061
+ else if (weakTokenSet[at]) {
1062
+ anchorWeakTokens[at] = true;
1063
+ }
1064
+ }
1065
+ var anchorGroup = groupByPath[anchorPath];
1066
+ if (anchorGroup) {
1067
+ for (var ar = 0; ar < anchorGroup.reasons.length; ar++) {
1068
+ var reasonTokens = extractTokens(anchorGroup.reasons[ar]);
1069
+ for (var rt2 = 0; rt2 < reasonTokens.length; rt2++) {
1070
+ var rt2t = reasonTokens[rt2];
1071
+ if (strongTokenSet[rt2t]) {
1072
+ anchorStrongTokens[rt2t] = true;
1073
+ }
1074
+ else if (weakTokenSet[rt2t]) {
1075
+ anchorWeakTokens[rt2t] = true;
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ // Build expansion candidates prioritizing the target slot
1082
+ var noisePaths = (result.solvePacket?.noiseRisk || []).map(function (f) { return f.relativePath; });
1083
+ var doNotEditSet = {};
1084
+ var doNotEditList = result.solvePacket?.doNotEditYet || [];
1085
+ for (var di = 0; di < doNotEditList.length; di++) {
1086
+ doNotEditSet[doNotEditList[di]] = true;
1087
+ }
1088
+ var anchorSet = {};
1089
+ for (var sa = 0; sa < safeAnchors.length; sa++) {
1090
+ anchorSet[safeAnchors[sa]] = true;
1091
+ }
1092
+ // -- Compute anchor connection scores with strong/weak split --
1093
+ // Config/* files: only path: and symbol: prefixes count as strong.
1094
+ // reason: and unit: matches on config files are always weak (aggregate reason leakage).
1095
+ function computeAnchorScore(g) {
1096
+ var strongMatches = {};
1097
+ var weakMatches = {};
1098
+ var isConfig = g.relativePath.indexOf("config/") === 0;
1099
+ // Check path tokens
1100
+ var pathTokens = extractTokens(g.relativePath);
1101
+ for (var t1 = 0; t1 < pathTokens.length; t1++) {
1102
+ var ptt = pathTokens[t1];
1103
+ if (anchorStrongTokens[ptt] && !strongMatches[ptt]) {
1104
+ strongMatches["path:" + ptt] = true;
1105
+ }
1106
+ else if (anchorWeakTokens[ptt] && !weakMatches[ptt]) {
1107
+ weakMatches["path:" + ptt] = true;
1108
+ }
1109
+ }
1110
+ // Check reasons -- for config files, these are always weak
1111
+ for (var r1 = 0; r1 < g.reasons.length; r1++) {
1112
+ var reasonTokens = extractTokens(g.reasons[r1]);
1113
+ for (var t2 = 0; t2 < reasonTokens.length; t2++) {
1114
+ var rtt = reasonTokens[t2];
1115
+ if (!isConfig && anchorStrongTokens[rtt] && !strongMatches["reason:" + rtt]) {
1116
+ strongMatches["reason:" + rtt] = true;
1117
+ }
1118
+ else if ((isConfig || anchorWeakTokens[rtt] || !anchorStrongTokens[rtt]) && !weakMatches["reason:" + rtt]) {
1119
+ weakMatches["reason:" + rtt] = true;
1120
+ }
1121
+ }
1122
+ }
1123
+ // Check unit-level symbols/reasons
1124
+ for (var u1 = 0; u1 < g.units.length && u1 < 5; u1++) {
1125
+ var unit = g.units[u1];
1126
+ if (unit.symbols) {
1127
+ for (var s1 = 0; s1 < unit.symbols.length; s1++) {
1128
+ var st = unit.symbols[s1].toLowerCase();
1129
+ if (anchorStrongTokens[st] && !strongMatches["symbol:" + st]) {
1130
+ strongMatches["symbol:" + st] = true;
1131
+ }
1132
+ else if (anchorWeakTokens[st] && !weakMatches["symbol:" + st]) {
1133
+ weakMatches["symbol:" + st] = true;
1134
+ }
1135
+ }
1136
+ }
1137
+ if (unit.reasons) {
1138
+ for (var r2 = 0; r2 < unit.reasons.length; r2++) {
1139
+ var urt = extractTokens(unit.reasons[r2]);
1140
+ for (var t3 = 0; t3 < urt.length; t3++) {
1141
+ var utt = urt[t3];
1142
+ // For config files, unit: matches are always weak
1143
+ if (!isConfig && anchorStrongTokens[utt] && !strongMatches["unit:" + utt]) {
1144
+ strongMatches["unit:" + utt] = true;
1145
+ }
1146
+ else if ((isConfig || anchorWeakTokens[utt] || !anchorStrongTokens[utt]) && !weakMatches["unit:" + utt]) {
1147
+ weakMatches["unit:" + utt] = true;
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+ var strongList = Object.keys(strongMatches);
1154
+ var weakList = Object.keys(weakMatches);
1155
+ var totalScore = Math.min(strongList.length * 4 + weakList.length, 20);
1156
+ return { score: totalScore, strongConnections: strongList, weakConnections: weakList };
1157
+ }
1158
+ function isGenericConfig(g) {
1159
+ return g.relativePath.indexOf("config/") === 0;
1160
+ }
1161
+ var tier1 = [];
1162
+ var tier2 = [];
1163
+ var tier3 = [];
1164
+ for (var gi = 0; gi < groups.length; gi++) {
1165
+ var g = groups[gi];
1166
+ var isNoise = noisePaths.indexOf(g.relativePath) >= 0 || doNotEditSet[g.relativePath];
1167
+ if (isNoise)
1168
+ continue;
1169
+ if (anchorSet[g.relativePath])
1170
+ continue;
1171
+ if (g.slot === slot) {
1172
+ tier1.push(g);
1173
+ }
1174
+ else if (g.role === "read_context" || g.role === "impact" || g.role === "verify") {
1175
+ tier2.push(g);
1176
+ }
1177
+ else {
1178
+ tier3.push(g);
1179
+ }
1180
+ }
1181
+ // -- Rank: strong connections first, generic configs demoted --
1182
+ function effectiveRank(g) {
1183
+ var conn = computeAnchorScore(g);
1184
+ var strongCount = conn.strongConnections.length;
1185
+ var weakCount = conn.weakConnections.length;
1186
+ var isConfig = isGenericConfig(g);
1187
+ // Config files without strong path-level connections get heavy penalty
1188
+ var configPenalty = (isConfig && strongCount === 0) ? 5000 : (isConfig ? 1000 : 0);
1189
+ var base = strongCount * 200 + weakCount * 3 + g.score - configPenalty;
1190
+ return base;
1191
+ }
1192
+ var hasSafeAnchors = safeAnchors.length > 0;
1193
+ var allGroups = [];
1194
+ for (var t1 = 0; t1 < tier1.length; t1++) {
1195
+ allGroups.push(tier1[t1]);
1196
+ }
1197
+ for (var t2 = 0; t2 < tier2.length; t2++) {
1198
+ allGroups.push(tier2[t2]);
1199
+ }
1200
+ for (var t3 = 0; t3 < tier3.length; t3++) {
1201
+ allGroups.push(tier3[t3]);
1202
+ }
1203
+ if (hasSafeAnchors) {
1204
+ allGroups.sort(function (a, b) {
1205
+ var ra = effectiveRank(a);
1206
+ var rb = effectiveRank(b);
1207
+ if (ra !== rb)
1208
+ return rb - ra;
1209
+ return b.score - a.score;
1210
+ });
1211
+ }
1212
+ else {
1213
+ allGroups.sort(function (a, b) { return b.score - a.score; });
1214
+ }
1215
+ // -- Split into expansion (strong) and fallback (weak/generic) --
1216
+ var expansionCandidates = [];
1217
+ var fallbackCandidates = [];
1218
+ var configCount = 0;
1219
+ function buildCandidate(g) {
1220
+ var conn = computeAnchorScore(g);
1221
+ var cGuidance = editGuidanceForRole(g.role);
1222
+ var topChunks = [];
1223
+ for (var ui = 0; ui < g.units.length && ui < 3; ui++) {
1224
+ var u = g.units[ui];
1225
+ topChunks.push({
1226
+ kind: u.kind, role: u.role, slot: u.slot, score: u.score,
1227
+ reasons: u.reasons, range: u.range || null,
1228
+ });
1229
+ }
1230
+ return {
1231
+ path: g.relativePath, role: g.role, slot: g.slot, score: g.score, tier: g.tier,
1232
+ reasons: g.reasons, topChunks: topChunks, editGuidance: cGuidance,
1233
+ anchorConnectionScore: conn.score,
1234
+ strongAnchorConnections: conn.strongConnections,
1235
+ weakAnchorConnections: conn.weakConnections,
1236
+ };
1237
+ }
1238
+ for (var ci = 0; ci < allGroups.length; ci++) {
1239
+ var g = allGroups[ci];
1240
+ var conn = computeAnchorScore(g);
1241
+ // Expansion: non-config OR config with strong connections, limit to 8
1242
+ if ((!isGenericConfig(g) || conn.strongConnections.length > 0) && expansionCandidates.length < 8) {
1243
+ expansionCandidates.push(buildCandidate(g));
1244
+ }
1245
+ else {
1246
+ fallbackCandidates.push(buildCandidate(g));
1247
+ }
1248
+ }
1249
+ // NO fallback promotion -- weak evidence stays in fallbackCandidates.
1250
+ // -- Compute anchorInfluence truth --
1251
+ var anyStrong = false;
1252
+ var anyWeak = false;
1253
+ for (var cc = 0; cc < expansionCandidates.length; cc++) {
1254
+ if (expansionCandidates[cc].strongAnchorConnections.length > 0)
1255
+ anyStrong = true;
1256
+ if (expansionCandidates[cc].weakAnchorConnections.length > 0)
1257
+ anyWeak = true;
1258
+ }
1259
+ var influenceStatus = "no_safe_anchors";
1260
+ if (hasSafeAnchors) {
1261
+ if (anyStrong)
1262
+ influenceStatus = "active_strong";
1263
+ else if (anyWeak)
1264
+ influenceStatus = "active_weak";
1265
+ else
1266
+ influenceStatus = "no_connected_evidence";
1267
+ }
1268
+ // If no connected evidence, zero all fallback anchor scores/connections
1269
+ if (influenceStatus === "no_connected_evidence") {
1270
+ for (var fb = 0; fb < expansionCandidates.length; fb++) {
1271
+ expansionCandidates[fb].anchorConnectionScore = 0;
1272
+ expansionCandidates[fb].strongAnchorConnections = [];
1273
+ expansionCandidates[fb].weakAnchorConnections = [];
1274
+ }
1275
+ for (var fb2 = 0; fb2 < fallbackCandidates.length; fb2++) {
1276
+ fallbackCandidates[fb2].anchorConnectionScore = 0;
1277
+ fallbackCandidates[fb2].strongAnchorConnections = [];
1278
+ fallbackCandidates[fb2].weakAnchorConnections = [];
1279
+ }
1280
+ }
1281
+ var anchorInfluence = {
1282
+ status: influenceStatus,
1283
+ safeAnchorCount: safeAnchors.length,
1284
+ rejectedAnchorCount: rejectedPaths.length,
1285
+ strategy: "deterministic_strong_weak_with_config_guard",
1286
+ };
1287
+ var warnings = [];
1288
+ warnings.push("deterministic-only: expansion uses deterministic retrieval only; semantic and graph expansion are not active");
1289
+ if (influenceStatus === "active_weak") {
1290
+ warnings.push("anchor influence is weak: primary expansion is unavailable; fallbackCandidates carries weak/potential context only");
1291
+ }
1292
+ if (hasSafeAnchors && influenceStatus === "no_connected_evidence") {
1293
+ warnings.push("anchor influence: " + safeAnchors.length + " safe anchor(s) provided but no candidate had anchor-connected evidence; fallbackCandidates are generic context only, not anchored");
1294
+ }
1295
+ var missingSlotStatus = "ok";
1296
+ if (expansionCandidates.length === 0) {
1297
+ missingSlotStatus = "no_candidates";
1298
+ }
1299
+ else if (expansionCandidates.length < 3) {
1300
+ missingSlotStatus = "low_coverage";
1301
+ }
1302
+ if (profile === "human") {
1303
+ var humanLines = [
1304
+ "# FlowSeeker Expand Report",
1305
+ "",
1306
+ "Task: " + task,
1307
+ "Target Slot: " + slot,
1308
+ "Workspace: " + workspace,
1309
+ "",
1310
+ ];
1311
+ if (safeAnchors.length > 0) {
1312
+ humanLines.push("## Anchor Files");
1313
+ humanLines.push("");
1314
+ for (var ah = 0; ah < safeAnchors.length; ah++) {
1315
+ humanLines.push("- " + safeAnchors[ah]);
1316
+ }
1317
+ humanLines.push("");
1318
+ }
1319
+ if (rejectedPaths.length > 0) {
1320
+ humanLines.push("## Rejected Paths");
1321
+ humanLines.push("");
1322
+ for (var rh = 0; rh < rejectedPaths.length; rh++) {
1323
+ humanLines.push("- " + rejectedPaths[rh].path + ": " + rejectedPaths[rh].reason);
1324
+ }
1325
+ humanLines.push("");
1326
+ }
1327
+ humanLines.push("## Expansion Candidates (" + expansionCandidates.length + ")");
1328
+ humanLines.push("");
1329
+ humanLines.push("Slot Coverage: " + missingSlotStatus);
1330
+ humanLines.push("Anchor Influence: " + anchorInfluence.status);
1331
+ humanLines.push("");
1332
+ for (var ch = 0; ch < expansionCandidates.length; ch++) {
1333
+ var ec = expansionCandidates[ch];
1334
+ humanLines.push((ch + 1) + ". **" + ec.path + "**");
1335
+ humanLines.push(" Role: " + ec.role + " | Slot: " + (ec.slot || "unknown") + " | Score: " + ec.score.toFixed(1) + " | Tier: " + ec.tier);
1336
+ humanLines.push(" Guidance: " + ec.editGuidance.join(", "));
1337
+ if (ec.anchorConnectionScore > 0) {
1338
+ humanLines.push(" Anchor: score=" + ec.anchorConnectionScore);
1339
+ if (ec.strongAnchorConnections && ec.strongAnchorConnections.length > 0) {
1340
+ humanLines.push(" Strong: " + ec.strongAnchorConnections.join(", "));
1341
+ }
1342
+ if (ec.weakAnchorConnections && ec.weakAnchorConnections.length > 0) {
1343
+ humanLines.push(" Weak: " + ec.weakAnchorConnections.join(", "));
1344
+ }
1345
+ }
1346
+ humanLines.push(" Reasons: " + ec.reasons.join("; "));
1347
+ humanLines.push("");
1348
+ }
1349
+ if (fallbackCandidates.length > 0) {
1350
+ humanLines.push("## Fallback Candidates (" + fallbackCandidates.length + ")");
1351
+ humanLines.push("");
1352
+ humanLines.push("These are weak/generic/context files surfaced when strong expansion is limited.");
1353
+ humanLines.push("");
1354
+ for (var fh = 0; fh < fallbackCandidates.length; fh++) {
1355
+ var fc = fallbackCandidates[fh];
1356
+ humanLines.push((fh + 1) + ". **" + fc.path + "**");
1357
+ humanLines.push(" Role: " + fc.role + " | Slot: " + (fc.slot || "unknown") + " | Score: " + fc.score.toFixed(1));
1358
+ humanLines.push("");
1359
+ }
1360
+ }
1361
+ if (warnings.length > 0) {
1362
+ humanLines.push("## Warnings");
1363
+ humanLines.push("");
1364
+ for (var wh = 0; wh < warnings.length; wh++) {
1365
+ humanLines.push("- " + warnings[wh]);
1366
+ }
1367
+ humanLines.push("");
1368
+ }
1369
+ return textResult(humanLines.join("\n"));
1370
+ }
1371
+ // json profile
1372
+ var jsonOutput = {
1373
+ schemaVersion: "2.0.0",
1374
+ tool: "flowseeker_expand",
1375
+ task: task,
1376
+ workspaceRoot: workspace,
1377
+ targetSlot: slot,
1378
+ anchorFiles: safeAnchors,
1379
+ rejectedPaths: rejectedPaths,
1380
+ anchorInfluence: anchorInfluence,
1381
+ expansionCandidates: expansionCandidates,
1382
+ fallbackCandidates: fallbackCandidates,
1383
+ missingSlotStatus: missingSlotStatus,
1384
+ warnings: warnings,
1385
+ };
1386
+ return textResult(JSON.stringify(jsonOutput, null, 2));
1387
+ }
1388
+ // -- Status Tool -------------------------------------------------------
1389
+ async function handleStatus(input, defaultWorkspace) {
1390
+ var workspace = input.workspaceRoot || input.workspace || defaultWorkspace;
1391
+ var profile = input.outputProfile || "json";
1392
+ if (profile !== "json" && profile !== "human") {
1393
+ return errorResult("invalid outputProfile: " + profile + ". Must be json or human");
1394
+ }
1395
+ var config = {};
1396
+ try {
1397
+ config = await (0, loadConfigFromPath_1.loadConfigFromPath)(workspace);
1398
+ }
1399
+ catch (e) { }
1400
+ // Deterministic always ok
1401
+ var deterministic = { status: "ok", retrieval: "available", note: "No embedding required. Code-structure analysis is first-class." };
1402
+ // Semantic
1403
+ var semanticEnabled = config?.index?.semanticEnabled === true;
1404
+ var semanticProvider = config?.index?.semanticProvider || "none";
1405
+ var semanticModel = config?.index?.semanticModel || "";
1406
+ var semanticResult = semanticEnabled ? "configured" : "disabled";
1407
+ // Local model readiness
1408
+ var localReadiness = null;
1409
+ try {
1410
+ localReadiness = (0, embeddingProviders_1.getLocalProviderReadiness)(workspace, {
1411
+ semanticModel: config?.index?.semanticModel || "",
1412
+ semanticLocalModelPath: config?.index?.semanticLocalModelPath,
1413
+ semanticLocalModelId: config?.index?.semanticLocalModelId,
1414
+ semanticDimensions: config?.index?.semanticDimensions,
1415
+ });
1416
+ }
1417
+ catch (e) { }
1418
+ // Index state
1419
+ var indexStatus = "unknown";
1420
+ var cachePath = path.join(workspace, ".flowseeker", "cache");
1421
+ var workspaceIndexPath = path.join(cachePath, "workspace-index.json");
1422
+ try {
1423
+ if (fs.existsSync(workspaceIndexPath)) {
1424
+ var indexStat = fs.statSync(workspaceIndexPath);
1425
+ var indexAgeS = Math.floor((Date.now() - indexStat.mtimeMs) / 1000);
1426
+ indexStatus = "exists";
1427
+ }
1428
+ else {
1429
+ indexStatus = "not_built";
1430
+ }
1431
+ }
1432
+ catch (e) { }
1433
+ // Config
1434
+ var configPath = path.join(workspace, ".flowseeker", "config.json");
1435
+ var configExists = fs.existsSync(configPath);
1436
+ // Warnings
1437
+ var warnings = [];
1438
+ var nextActions = [];
1439
+ if (semanticResult === "disabled") {
1440
+ warnings.push("semantic retrieval is disabled");
1441
+ nextActions.push("enable semantic retrieval in .flowseeker/config.json");
1442
+ }
1443
+ if (indexStatus === "not_built") {
1444
+ warnings.push("workspace index not built yet");
1445
+ nextActions.push("run flowseeker_index with action=build");
1446
+ }
1447
+ if (!configExists) {
1448
+ warnings.push("no .flowseeker/config.json found; using defaults");
1449
+ nextActions.push("run FlowSeeker at least once to generate config");
1450
+ }
1451
+ if (localReadiness && localReadiness.status !== "local_ready" && localReadiness.status !== "bundled_package_ready") {
1452
+ warnings.push("local model not ready");
1453
+ nextActions.push("run flowseeker semantic local setup && flowseeker semantic local download --yes");
1454
+ }
1455
+ if (profile === "human") {
1456
+ var lines = [
1457
+ "# FlowSeeker Status",
1458
+ "",
1459
+ "Version: 0.1.8",
1460
+ "Workspace: " + workspace,
1461
+ "",
1462
+ "Deterministic: " + deterministic.status + " (" + deterministic.note + ")",
1463
+ "Semantic: " + semanticResult,
1464
+ "Local Model: " + (localReadiness ? localReadiness.status : "unknown"),
1465
+ "Index: " + indexStatus,
1466
+ "Config: " + (configExists ? "present" : "not found"),
1467
+ "MCP Tools: " + exports.TOOL_DEFINITIONS.length + " (" + exports.TOOL_DEFINITIONS.map(function (t) { return t.name; }).join(", ") + ")",
1468
+ "",
1469
+ ];
1470
+ if (warnings.length > 0) {
1471
+ lines.push("## Warnings");
1472
+ for (var w = 0; w < warnings.length; w++) {
1473
+ lines.push("- " + warnings[w]);
1474
+ }
1475
+ lines.push("");
1476
+ }
1477
+ if (nextActions.length > 0) {
1478
+ lines.push("## Next Actions");
1479
+ for (var n = 0; n < nextActions.length; n++) {
1480
+ lines.push("- " + nextActions[n]);
1481
+ }
1482
+ lines.push("");
1483
+ }
1484
+ return textResult(lines.join("\n"));
1485
+ }
1486
+ var output = {
1487
+ schemaVersion: "2.0.0",
1488
+ tool: "flowseeker_status",
1489
+ version: "0.1.8",
1490
+ workspaceRoot: workspace,
1491
+ deterministic: deterministic,
1492
+ semantic: {
1493
+ enabled: semanticEnabled,
1494
+ provider: semanticProvider,
1495
+ model: semanticModel,
1496
+ result: semanticResult,
1497
+ },
1498
+ localModel: localReadiness ? {
1499
+ status: localReadiness.status,
1500
+ runtimeAvailable: localReadiness.runtimeAvailable === true,
1501
+ modelStatus: localReadiness.modelStatus || "unknown",
1502
+ nextAction: localReadiness.nextAction || "",
1503
+ } : undefined,
1504
+ index: {
1505
+ status: indexStatus,
1506
+ path: cachePath,
1507
+ },
1508
+ mcp: {
1509
+ toolsAvailable: exports.TOOL_DEFINITIONS.length,
1510
+ toolNames: exports.TOOL_DEFINITIONS.map(function (t) { return t.name; }),
1511
+ },
1512
+ warnings: warnings,
1513
+ nextActions: nextActions,
1514
+ };
1515
+ return textResult(JSON.stringify(output, null, 2));
1516
+ }
1517
+ // -- Index Tool --------------------------------------------------------
1518
+ var DESTRUCTIVE_ACTIONS = ["clean", "rebuild", "delete", "reset", "remove", "purge"];
1519
+ async function handleIndex(input, defaultWorkspace) {
1520
+ var action = (input.action || "").trim().toLowerCase();
1521
+ var workspace = input.workspaceRoot || input.workspace || defaultWorkspace;
1522
+ var profile = input.outputProfile || "json";
1523
+ var maxFiles = input.maxFiles || 5000;
1524
+ if (DESTRUCTIVE_ACTIONS.indexOf(action) >= 0) {
1525
+ return errorResult("unsupported action: " + action + ". Allowed: status, build. Destructive actions are rejected.");
1526
+ }
1527
+ if (profile !== "json" && profile !== "human") {
1528
+ return errorResult("invalid outputProfile: " + profile + ". Must be json or human");
1529
+ }
1530
+ if (maxFiles <= 0) {
1531
+ return errorResult("maxFiles must be a positive number");
1532
+ }
1533
+ if (action === "status") {
1534
+ var cachePath = path.join(workspace, ".flowseeker", "cache");
1535
+ var indexPath = path.join(cachePath, "workspace-index.json");
1536
+ var exists = false;
1537
+ var entryCount = 0;
1538
+ try {
1539
+ if (fs.existsSync(indexPath)) {
1540
+ exists = true;
1541
+ var raw = fs.readFileSync(indexPath, "utf8");
1542
+ entryCount = Object.keys(JSON.parse(raw)).length;
1543
+ }
1544
+ }
1545
+ catch (e) { }
1546
+ if (profile === "human") {
1547
+ var slines = [
1548
+ "# FlowSeeker Index Status",
1549
+ "",
1550
+ "Workspace: " + workspace,
1551
+ "Index exists: " + exists,
1552
+ "Entries: " + entryCount,
1553
+ "Path: " + indexPath,
1554
+ ];
1555
+ return textResult(slines.join("\n"));
1556
+ }
1557
+ return textResult(JSON.stringify({
1558
+ schemaVersion: "2.0.0",
1559
+ tool: "flowseeker_index",
1560
+ action: "status",
1561
+ workspaceRoot: workspace,
1562
+ indexExists: exists,
1563
+ entries: entryCount,
1564
+ path: indexPath,
1565
+ }, null, 2));
1566
+ }
1567
+ if (action === "build") {
1568
+ // Run safe indexing
1569
+ var config = {};
1570
+ try {
1571
+ config = await (0, loadConfigFromPath_1.loadConfigFromPath)(workspace);
1572
+ }
1573
+ catch (e) { }
1574
+ var buildResult = null;
1575
+ try {
1576
+ var mergedCfg = maxFiles && maxFiles > 0
1577
+ ? (0, defaultConfig_1.mergeConfig)(config, { pipeline: { ...config?.pipeline, maxFiles } })
1578
+ : (config || await (0, loadConfigFromPath_1.loadConfigFromPath)(workspace));
1579
+ buildResult = await (0, workspaceIndex_1.loadOrBuildWorkspaceIndex)(workspace, mergedCfg, { readOnly: false });
1580
+ }
1581
+ catch (e) {
1582
+ return errorResult("Index build failed: " + (e?.message || String(e)));
1583
+ }
1584
+ // Verify source files still exist (safety check)
1585
+ var fixtureFiles = ["app/Services/PaymentService.php", "app/Models/Payment.php"];
1586
+ var missingFiles = [];
1587
+ for (var ff = 0; ff < fixtureFiles.length; ff++) {
1588
+ if (!fs.existsSync(path.join(workspace, fixtureFiles[ff]))) {
1589
+ missingFiles.push(fixtureFiles[ff]);
1590
+ }
1591
+ }
1592
+ if (missingFiles.length > 0) {
1593
+ return errorResult("Build may have deleted source files: " + missingFiles.join(", "));
1594
+ }
1595
+ if (profile === "human") {
1596
+ var blines = [
1597
+ "# FlowSeeker Index Build",
1598
+ "",
1599
+ "Workspace: " + workspace,
1600
+ "Status: complete",
1601
+ "Discovered: " + (buildResult?.discoveredFiles ?? "?"),
1602
+ "Reused: " + (buildResult?.reusedFiles ?? "?"),
1603
+ "Updated: " + (buildResult?.updatedFiles ?? "?"),
1604
+ "Deleted: " + (buildResult?.deletedFiles ?? "0"),
1605
+ "Source files intact: yes",
1606
+ ];
1607
+ return textResult(blines.join("\n"));
1608
+ }
1609
+ return textResult(JSON.stringify({
1610
+ schemaVersion: "2.0.0",
1611
+ tool: "flowseeker_index",
1612
+ action: "build",
1613
+ workspaceRoot: workspace,
1614
+ status: "complete",
1615
+ discoveredFiles: buildResult?.discoveredFiles,
1616
+ reusedFiles: buildResult?.reusedFiles,
1617
+ updatedFiles: buildResult?.updatedFiles,
1618
+ deletedFiles: buildResult?.deletedFiles || 0,
1619
+ sourceFilesIntact: true,
1620
+ }, null, 2));
1621
+ }
1622
+ return errorResult("unknown action: " + action);
1623
+ }
1624
+ // -- Feedback Tool ----------------------------------------------------
1625
+ var VALID_LABELS = ["useful", "wrong", "missing", "edited", "test_used", "should_not_edit"];
1626
+ var MAX_NOTES_LENGTH = 500;
1627
+ function redactNotes(notes) {
1628
+ var redacted = notes.replace(/(sk-[a-z0-9\-]+)/gi, "***REDACTED_KEY***");
1629
+ redacted = redacted.replace(/AIza[a-zA-Z0-9\-_]{15,}/g, "***REDACTED_KEY***");
1630
+ return redacted.substring(0, MAX_NOTES_LENGTH);
1631
+ }
1632
+ async function handleFeedback(input, defaultWorkspace) {
1633
+ var task = (input.task || "").trim();
1634
+ var label = (input.label || "").trim();
1635
+ var workspace = input.workspaceRoot || input.workspace || defaultWorkspace;
1636
+ var profile = input.outputProfile || "json";
1637
+ // Validate task
1638
+ if (!task) {
1639
+ return errorResult("task must be a non-empty string");
1640
+ }
1641
+ // Validate label
1642
+ if (VALID_LABELS.indexOf(label) < 0) {
1643
+ return errorResult("invalid label: " + label + ". Must be one of: " + VALID_LABELS.join(", "));
1644
+ }
1645
+ // Validate outputProfile
1646
+ if (profile !== "json" && profile !== "human") {
1647
+ return errorResult("invalid outputProfile: " + profile + ". Must be json or human");
1648
+ }
1649
+ // Validate files -- must be an array if provided
1650
+ if (input.files !== undefined && !Array.isArray(input.files)) {
1651
+ return errorResult("files must be an array of non-empty workspace-relative paths");
1652
+ }
1653
+ var files = input.files !== undefined ? input.files : [];
1654
+ var workspaceRoot = path.resolve(workspace);
1655
+ for (var fi = 0; fi < files.length; fi++) {
1656
+ if (typeof files[fi] !== "string" || !files[fi].trim()) {
1657
+ return errorResult("files[" + fi + "] must be a non-empty string");
1658
+ }
1659
+ // Reject absolute paths -- files must be workspace-relative
1660
+ if (path.isAbsolute(files[fi])) {
1661
+ return errorResult("files[" + fi + "] must be a workspace-relative path, got absolute: " + files[fi]);
1662
+ }
1663
+ // Reject sibling escapes using robust path.relative
1664
+ var resolved = path.resolve(workspaceRoot, files[fi]);
1665
+ var relative = path.relative(workspaceRoot, resolved);
1666
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
1667
+ return errorResult("files[" + fi + "] is outside workspace: " + files[fi]);
1668
+ }
1669
+ }
1670
+ // Validate notes -- must be a string if provided
1671
+ if (input.notes !== undefined && typeof input.notes !== "string") {
1672
+ return errorResult("notes must be a string when provided");
1673
+ }
1674
+ var rawNotes = input.notes !== undefined ? input.notes : "";
1675
+ var safeNotes = rawNotes ? redactNotes(rawNotes) : "";
1676
+ // Append to JSONL
1677
+ var feedbackDir = path.join(workspace, ".flowseeker", "feedback");
1678
+ fs.mkdirSync(feedbackDir, { recursive: true });
1679
+ var record = {
1680
+ schemaVersion: "2.0.0",
1681
+ tool: "flowseeker_feedback",
1682
+ timestamp: new Date().toISOString(),
1683
+ task: task,
1684
+ label: label,
1685
+ files: files,
1686
+ notes: safeNotes,
1687
+ workspaceRoot: workspace,
1688
+ };
1689
+ var line = JSON.stringify(record) + "\n";
1690
+ var filePath = path.join(feedbackDir, "feedback.jsonl");
1691
+ fs.appendFileSync(filePath, line, "utf8");
1692
+ if (profile === "human") {
1693
+ return textResult("# Feedback Recorded\n\nLabel: " + label + "\nTask: " + task + "\nFiles: " + (files.length ? files.join(", ") : "none") + "\nStored: " + filePath);
1694
+ }
1695
+ return textResult(JSON.stringify(record, null, 2));
1696
+ }
260
1697
  function textResult(text) {
261
1698
  return { content: [{ type: "text", text }] };
262
1699
  }
263
1700
  function errorResult(message) {
264
1701
  return { content: [{ type: "text", text: message }], isError: true };
265
1702
  }
1703
+ function emptyValidatePlanOutput(riskMode, confidence) {
1704
+ return {
1705
+ verdict: "insufficient_evidence",
1706
+ issues: [],
1707
+ suggestedEditFiles: [],
1708
+ suggestedReadFiles: [],
1709
+ suggestedTestFiles: [],
1710
+ missingSlots: [],
1711
+ doNotEditYet: [],
1712
+ rejectedPaths: [],
1713
+ evidenceSummary: "",
1714
+ riskMode,
1715
+ confidence,
1716
+ matchedProposedEditFiles: [],
1717
+ unsupportedProposedEditFiles: [],
1718
+ proposedEditFilesOnlySeenAsReadContext: [],
1719
+ topFlowSeekerEditCandidates: [],
1720
+ };
1721
+ }
1722
+ function normalizeRelPath(p) {
1723
+ return (p || "").replace(/\\/g, "/").replace(/^\/+/, "");
1724
+ }
1725
+ function isOutsideWorkspace(relPath, workspace) {
1726
+ const resolved = path.resolve(workspace, relPath);
1727
+ const relative = path.relative(workspace, resolved);
1728
+ return relative.startsWith("..") || path.isAbsolute(relative);
1729
+ }
1730
+ function anyPathMatches(target, candidates) {
1731
+ return candidates.some((c) => (0, evaluationMetrics_1.pathMatches)(target, c));
1732
+ }
1733
+ function validatePlanInput(input) {
1734
+ const issues = [];
1735
+ if (typeof input.task !== "string" || !input.task.trim()) {
1736
+ issues.push("task must be a non-empty string");
1737
+ }
1738
+ if (!Array.isArray(input.proposedEditFiles)) {
1739
+ issues.push("proposedEditFiles must be an array of non-empty strings");
1740
+ }
1741
+ else {
1742
+ for (let i = 0; i < input.proposedEditFiles.length; i++) {
1743
+ if (typeof input.proposedEditFiles[i] !== "string" || !input.proposedEditFiles[i].trim()) {
1744
+ issues.push(`proposedEditFiles[${i}] must be a non-empty string`);
1745
+ }
1746
+ }
1747
+ }
1748
+ if (input.proposedReadFiles !== undefined && !Array.isArray(input.proposedReadFiles)) {
1749
+ issues.push("proposedReadFiles must be an array of non-empty strings");
1750
+ }
1751
+ else if (Array.isArray(input.proposedReadFiles)) {
1752
+ for (let i = 0; i < input.proposedReadFiles.length; i++) {
1753
+ if (typeof input.proposedReadFiles[i] !== "string" || !input.proposedReadFiles[i].trim()) {
1754
+ issues.push(`proposedReadFiles[${i}] must be a non-empty string`);
1755
+ }
1756
+ }
1757
+ }
1758
+ if (input.proposedTestFiles !== undefined && !Array.isArray(input.proposedTestFiles)) {
1759
+ issues.push("proposedTestFiles must be an array of non-empty strings");
1760
+ }
1761
+ else if (Array.isArray(input.proposedTestFiles)) {
1762
+ for (let i = 0; i < input.proposedTestFiles.length; i++) {
1763
+ if (typeof input.proposedTestFiles[i] !== "string" || !input.proposedTestFiles[i].trim()) {
1764
+ issues.push(`proposedTestFiles[${i}] must be a non-empty string`);
1765
+ }
1766
+ }
1767
+ }
1768
+ if (input.riskMode !== undefined && input.riskMode !== "strict" && input.riskMode !== "balanced" && input.riskMode !== "broad") {
1769
+ issues.push(`riskMode must be "strict", "balanced", or "broad" (got: "${input.riskMode}")`);
1770
+ }
1771
+ return issues;
1772
+ }
1773
+ async function handleValidatePlan(input, defaultWorkspace) {
1774
+ const workspace = path.resolve(input.workspaceRoot ?? defaultWorkspace);
1775
+ // Validate input before any string method calls that could throw
1776
+ const validationIssues = validatePlanInput(input);
1777
+ const task = typeof input.task === "string" ? input.task.trim() : "";
1778
+ const riskMode = (input.riskMode === "strict" || input.riskMode === "balanced" || input.riskMode === "broad") ? input.riskMode : "balanced";
1779
+ if (validationIssues.length > 0 || !task) {
1780
+ const errors = !task ? ["task must be a non-empty string", ...validationIssues] : validationIssues;
1781
+ const out = emptyValidatePlanOutput(riskMode, "low");
1782
+ out.verdict = "insufficient_evidence";
1783
+ out.issues = errors;
1784
+ out.evidenceSummary = "Invalid input: " + errors.join("; ");
1785
+ return textResult(JSON.stringify(out, null, 2));
1786
+ }
1787
+ // Safe to normalize now -- validation confirmed arrays of strings
1788
+ const proposedEditFiles = (input.proposedEditFiles ?? []).map(normalizeRelPath);
1789
+ const proposedReadFiles = (input.proposedReadFiles ?? []).map(normalizeRelPath);
1790
+ const proposedTestFiles = (input.proposedTestFiles ?? []).map(normalizeRelPath);
1791
+ // Check for paths outside workspace
1792
+ const outsidePaths = [];
1793
+ for (const f of [...proposedEditFiles, ...proposedReadFiles, ...proposedTestFiles]) {
1794
+ if (isOutsideWorkspace(f, workspace)) {
1795
+ outsidePaths.push(f);
1796
+ }
1797
+ }
1798
+ // Run FlowSeeker retrieval
1799
+ const result = await runPipeline(task, workspace);
1800
+ const groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units);
1801
+ const topFiles = groups.map((g) => g.relativePath);
1802
+ // FlowSeeker's view of the world
1803
+ const fsEditCandidates = groups.filter((g) => g.role === "edit_candidate").map((g) => g.relativePath);
1804
+ const fsReadContext = groups.filter((g) => g.role === "read_context").map((g) => g.relativePath);
1805
+ const fsTests = groups.filter((g) => g.role === "test" || g.role === "verify").map((g) => g.relativePath);
1806
+ const fsNoise = result.solvePacket?.noiseRisk.map((f) => f.relativePath) ?? [];
1807
+ const fsDoNotEdit = result.solvePacket?.doNotEditYet ?? [];
1808
+ const fsCoverage = result.contextCoverage;
1809
+ const fsMissingSlots = fsCoverage?.missingRequiredSlots.map(String) ?? [];
1810
+ const issues = [];
1811
+ const matchedEdit = [];
1812
+ const unsupportedEdit = [];
1813
+ const readContextOnly = [];
1814
+ // Compare proposed edits against FlowSeeker results
1815
+ for (const pf of proposedEditFiles) {
1816
+ if (anyPathMatches(pf, fsEditCandidates)) {
1817
+ matchedEdit.push(pf);
1818
+ }
1819
+ else {
1820
+ unsupportedEdit.push(pf);
1821
+ if (anyPathMatches(pf, fsReadContext)) {
1822
+ readContextOnly.push(pf);
1823
+ issues.push(`"${pf}" is only in read-only context -- consider moving to proposedReadFiles`);
1824
+ }
1825
+ else if (anyPathMatches(pf, fsNoise) || anyPathMatches(pf, fsDoNotEdit)) {
1826
+ issues.push(`"${pf}" is flagged as noise/do-not-edit by FlowSeeker`);
1827
+ }
1828
+ else {
1829
+ issues.push(`"${pf}" is not a FlowSeeker primary edit candidate`);
1830
+ }
1831
+ }
1832
+ }
1833
+ // Check if strong edit candidates are only in proposedReadFiles
1834
+ for (const ec of fsEditCandidates.slice(0, 5)) {
1835
+ if (!anyPathMatches(ec, proposedEditFiles) && anyPathMatches(ec, proposedReadFiles)) {
1836
+ issues.push(`"${ec}" is a strong edit candidate but is only listed as proposedReadFiles`);
1837
+ }
1838
+ }
1839
+ // Missing slots
1840
+ const combinedMissingSlots = [
1841
+ ...fsMissingSlots.filter((s) => !fsCoverage?.foundRequiredSlots.some((f) => f === s)),
1842
+ ];
1843
+ // Outside workspace paths
1844
+ for (const op of outsidePaths) {
1845
+ issues.push(`"${op}" is outside workspace -- rejected`);
1846
+ }
1847
+ // Determine verdict
1848
+ let verdict = "safe";
1849
+ const hasUnsupported = unsupportedEdit.filter((f) => !outsidePaths.includes(f)).length > 0;
1850
+ if (outsidePaths.length > 0 || unsupportedEdit.some((f) => anyPathMatches(f, fsNoise) || anyPathMatches(f, fsDoNotEdit))) {
1851
+ verdict = "risky";
1852
+ }
1853
+ else if (riskMode === "strict" && hasUnsupported) {
1854
+ verdict = "risky";
1855
+ }
1856
+ else if (hasUnsupported) {
1857
+ verdict = "caution";
1858
+ }
1859
+ else if (riskMode === "strict" && combinedMissingSlots.length > 0) {
1860
+ verdict = "caution";
1861
+ }
1862
+ if (fsEditCandidates.length === 0 && groups.length < 3) {
1863
+ verdict = "insufficient_evidence";
1864
+ issues.push("FlowSeeker retrieval produced too few candidates for confident validation");
1865
+ }
1866
+ // Evidence summary
1867
+ const evidenceSummary = [
1868
+ `FlowSeeker found ${fsEditCandidates.length} edit candidate(s), ${fsReadContext.length} read context file(s), ${fsTests.length} test/verify file(s).`,
1869
+ `Matched ${matchedEdit.length}/${proposedEditFiles.length} proposed edit file(s).`,
1870
+ fsMissingSlots.length > 0 ? `Missing slots: ${fsMissingSlots.join(", ")}.` : "All required slots covered.",
1871
+ ].join(" ");
1872
+ const output = {
1873
+ verdict,
1874
+ issues,
1875
+ suggestedEditFiles: [...new Set([...matchedEdit, ...fsEditCandidates.slice(0, 5)])].filter((f) => !outsidePaths.includes(f)),
1876
+ suggestedReadFiles: fsReadContext.slice(0, 5),
1877
+ suggestedTestFiles: fsTests.slice(0, 5),
1878
+ missingSlots: combinedMissingSlots,
1879
+ doNotEditYet: [...fsDoNotEdit, ...fsNoise.slice(0, 5)].filter((f) => !outsidePaths.includes(f)),
1880
+ rejectedPaths: outsidePaths,
1881
+ evidenceSummary,
1882
+ riskMode,
1883
+ confidence: verdict === "safe" ? "high" : verdict === "caution" ? "medium" : "low",
1884
+ matchedProposedEditFiles: matchedEdit,
1885
+ unsupportedProposedEditFiles: unsupportedEdit.filter((f) => !outsidePaths.includes(f)),
1886
+ proposedEditFilesOnlySeenAsReadContext: readContextOnly,
1887
+ topFlowSeekerEditCandidates: fsEditCandidates.slice(0, 5),
1888
+ };
1889
+ return textResult(JSON.stringify(output, null, 2));
1890
+ }
266
1891
  //# sourceMappingURL=mcpTools.js.map