knodin 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +16 -1
  2. package/dist/bin/cli.js +173 -14
  3. package/dist/src/agent-events.js +25 -7
  4. package/dist/src/authenticated-cursor.js +81 -0
  5. package/dist/src/class-consumer-contract.js +18 -0
  6. package/dist/src/class-consumer-cursor.js +91 -0
  7. package/dist/src/class-consumer-delivery.js +22 -0
  8. package/dist/src/class-consumer-page.js +148 -0
  9. package/dist/src/cli-model.js +9 -3
  10. package/dist/src/docs-sections.js +1 -0
  11. package/dist/src/engine/apex-class-uses.js +430 -0
  12. package/dist/src/engine/apex-entry-points.js +98 -0
  13. package/dist/src/engine/apex-receiver.js +301 -0
  14. package/dist/src/engine/embedding-reuse.js +57 -0
  15. package/dist/src/engine/embeddings.js +22 -0
  16. package/dist/src/engine/index-coverage.js +215 -0
  17. package/dist/src/engine/index.js +2495 -252
  18. package/dist/src/engine/salesforce-components.js +460 -0
  19. package/dist/src/engine/seal.js +3 -0
  20. package/dist/src/engine/sqlite.js +44 -0
  21. package/dist/src/evidence-bundle.js +283 -0
  22. package/dist/src/evidence-graph.js +163 -0
  23. package/dist/src/failure-diagnosis.js +80 -5
  24. package/dist/src/file-dependency.js +35 -0
  25. package/dist/src/graph-query-health.js +47 -1
  26. package/dist/src/implementation-search.js +69 -0
  27. package/dist/src/index-coverage-read.js +33 -0
  28. package/dist/src/investigation.js +195 -0
  29. package/dist/src/mcp-reliability.js +4 -0
  30. package/dist/src/mcp-worker-supervisor.js +122 -6
  31. package/dist/src/progressive-evidence.js +4 -4
  32. package/dist/src/response-budget.js +129 -3
  33. package/dist/src/server.js +18 -5
  34. package/dist/src/shared-index/publisher.js +41 -1
  35. package/dist/src/tools/knodin-tools.js +224 -41
  36. package/docs/CLI.md +92 -0
  37. package/docs/MCP.md +67 -0
  38. package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
  39. package/docs/SALESFORCE-BINDINGS.md +121 -0
  40. package/docs/SALESFORCE-DEAD-CODE.md +45 -0
  41. package/docs/SCOPED-INDEXING.md +76 -0
  42. package/docs/apex-receiver-resolution.md +41 -0
  43. package/docs/releases/0.13.0.md +62 -0
  44. package/docs/structural-only-indexing.md +20 -0
  45. package/package.json +11 -5
@@ -9,6 +9,10 @@
9
9
  import fs from "node:fs";
10
10
  import nodePath from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
+ import { ClassConsumerQueryError, } from "../class-consumer-contract.js";
13
+ import { readClassConsumerCursor } from "../class-consumer-cursor.js";
14
+ import { validateClassConsumerDelivery } from "../class-consumer-delivery.js";
15
+ import { finalizeClassConsumerPage } from "../class-consumer-page.js";
12
16
  import { resolveCliRuntimeCommand } from "../cli-args.js";
13
17
  import { compactExplainResult, compactQueryResult, compactSearchResult, expandCompactIdentity, } from "../compact-structural.js";
14
18
  import { buildKnodinContext } from "../context.js";
@@ -20,17 +24,21 @@ import { createEngine, KNODIN_SCHEMA_VERSION, REPO_WIDE_QUERY_PATTERNS, } from "
20
24
  import { measurePerfPhaseSync } from "../engine/perf.js";
21
25
  import { runSealedQuery } from "../engine/sealed-query.js";
22
26
  import { resolveDbPath } from "../engine/state-paths.js";
27
+ import { deliverEvidenceBundle } from "../evidence-bundle.js";
28
+ import { createEvidenceGraphAdapter } from "../evidence-graph.js";
23
29
  import { runExecutionProfile } from "../execution-profile.js";
24
30
  import { diagnoseFailure, } from "../failure-diagnosis.js";
25
31
  import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
32
+ import { findImplementationCandidates } from "../implementation-search.js";
26
33
  import { inspectRepositoryIntegrationStatus } from "../init.js";
34
+ import { buildTaskInvestigation } from "../investigation.js";
27
35
  import { attachLifecycleHealth, attachRepairLifecycle } from "../lifecycle-health.js";
28
36
  import { readManagerUpdateState, readUpdateJournal, updateAttention } from "../manager-update.js";
29
37
  import { listMirrors } from "../mirror.js";
30
38
  import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../output-compression.js";
31
39
  import { appendTelemetryRecord, clearTelemetry, countOutputTokens, exportTelemetry, measureOutput, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../output-telemetry.js";
32
40
  import { auditPullRequests, ghUnavailableReason, triagePrDetail } from "../pr-triage.js";
33
- import { deliverProgressiveEvidence, } from "../progressive-evidence.js";
41
+ import { deliverProgressiveEvidence, evidenceHandleSecret, } from "../progressive-evidence.js";
34
42
  import { createRepairPlan } from "../repair-progress.js";
35
43
  import { runRepositoryInitializationProcess } from "../repository-init-process.js";
36
44
  import { discoverRepositories, initializeRepositories, inventoryRepository, searchRepositories, } from "../repository-management.js";
@@ -95,9 +103,8 @@ function inspectGatewayGraphHealth(repo) {
95
103
  * The probe is cheap — `engine.status(..., { audit: "cached" })` — so the latch
96
104
  * was buying very little in exchange for that failure mode.
97
105
  */
98
- async function compactGraphUnavailable(repo) {
99
- const health = await inspectGatewayGraphHealth(repo);
100
- return health.available ? null : health;
106
+ async function compactGraphHealth(repo) {
107
+ return inspectGatewayGraphHealth(repo);
101
108
  }
102
109
  /** Runs `fn`, converting any thrown error into a structured `{ error }` result
103
110
  * instead of letting it escape into the stdio transport. Extracted from the
@@ -155,6 +162,7 @@ const QUERY_PATTERNS = [
155
162
  "handlers_of",
156
163
  "endpoints_for",
157
164
  "consumers_of",
165
+ "class_consumers",
158
166
  "children_of",
159
167
  "community",
160
168
  "federated_repos",
@@ -236,9 +244,15 @@ function buildDocumentedKnodinTools() {
236
244
  enum: ["locate", "outline", "evidence", "expand"],
237
245
  description: "evidence only: deterministic progressive-delivery level.",
238
246
  },
247
+ evidenceFiles: {
248
+ type: "array",
249
+ items: { type: "string" },
250
+ maxItems: 32,
251
+ description: "evidence only: multiple repo-relative files for a verified evidence/expand bundle; exclusive with file. alreadyPresent accepts returned bundle file handles.",
252
+ },
239
253
  continuation: {
240
254
  type: "string",
241
- description: "evidence only: integrity-checked continuation returned by the prior level/page.",
255
+ description: "evidence/class_consumers: authenticated continuation from the previous page; repeat the same target and filters.",
242
256
  },
243
257
  baselineHash: {
244
258
  type: "string",
@@ -271,7 +285,7 @@ function buildDocumentedKnodinTools() {
271
285
  description: "feature_path — deterministic downstream DFS over resolved source references. It is bounded by depth and item limits, source-evidenced per hop, cycle-guarded, and never claims runtime execution; dynamic dispatch and unresolved calls are omitted. " +
272
286
  "lsp_diagnostics accepts a repo-relative TypeScript/JavaScript file; lsp_definitions, lsp_declarations, and lsp_implementations accept file:line:column. These use an optional local TypeScript language-service adapter, never start a daemon or write files, and return an explicit unavailable result when no local adapter supports the file type. " +
273
287
  "api_contract_mismatches — bounded, source-evidenced static mismatch findings for literal Express/Fastify routes and literal fetch/Axios clients. Matching requires method and normalized path, and requires exact origin equality when a client uses an absolute origin; dynamic routes remain explicitly unresolved. mcp_tools — list indexed TypeScript MCP SDK tool registrations, or pass symbol to look up one exact tool name; returns description/schema/handler/file and exact versus heuristic confidence. import_cycles — repo-wide canonical directed file-import cycles; no symbol required, type-only imports included, non-import lineage/ORM edges excluded, and limit/truncated bounds output. " +
274
- "query: the graph pattern to run (callers_of, callees_of, imports_of, importers_of, inheritors_of, structural_implementations_of, tests_for, file_summary, shortest_path, cross_substrate_path, impact, dead_code, large_functions, large_files, rename_preview, flows, flow_of, flow_analysis, resource_reachability, stats, traverse, knowledge_gaps, community, federated_repos). cross_substrate_path proves only one static Salesforce Flow action to one uniquely resolved Apex @InvocableMethod and returns exact endpoint identities/evidence, freshness, budgets, and explicit unsupported crossings; it does not resolve Terraform, dbt, or arbitrary substrate paths. resource_reachability is a repo-wide, bounded, cached, on-demand TS/JS static heuristic for literal process.env/fs.readFileSync sources reaching console.log/fetch/db.query sinks; it reports evidence, coverage, omissions, freshness, truncation, and continuation and is never runtime reachability or exploitability proof. structural_implementations_of returns TypeScript structurally typed object implementations separately from nominal extends/implements results. federated_repos — discover and list all registered/configured repository paths that this engine federates queries across. community — fetch a single community's full detail (name, size, cohesion, files, and symbols) by name or substring filter; empty `symbol` = all communities. large_files — whole-file line counts (min 200), sorted descending — the file-level counterpart to large_functions, for spotting god files rather than god functions; same optional path-substring filter, empty `symbol` = all files. stats — repo-level index size + health in one call (per-repo symbol/reference/dependency/embedding/file counts, per-kind and per-language breakdowns, orphaned-embedding count, schema version, last-indexed HEAD, and cached community/hub/bridge/flow totals); federated, empty `symbol`. traverse — typed BFS from `symbol` within `depth` hops (default 3, 1–6), selectable upstream/downstream/both and explicit edge kinds; every discovered edge includes direction, kind, provenance, confidence, files, and source line, with optional Tree-sitter-grounded argument expressions honestly marked heuristic. Sets `truncated` when the `limit` cap binds; single-repo. knowledge_gaps — repo-health weaknesses in one call: thin communities (<3 symbols), single-file communities, isolated (zero-degree) symbols, and untested hub/bridge hotspots; sourced from the cached map(), federated, empty `symbol`, one row carrying a `knowledgeGaps` block. surprising_connections — resolved edges scored by a composite surprise formula (cross-community, cross-language, peripheral-to-hub, cross-test-boundary, unusual edge kind) to surface unexpected coupling; sorted highest-first, top 15 by default, federated, empty `symbol`, each row carrying a `surprise` block. suggested_questions — prioritized, human-readable review prompts synthesized from knowledge_gaps + surprising_connections (untested hotspots first, then surprising edges, then thin/single-file communities); federated, empty `symbol`, each row a `question` string citing the real symbol/file, top 10 by default. architecture_overview — a federated architecture view with scoped community coupling and independently selectable packages, layers, boundaries, hotspots, entry points, and language facets; `path` is segment-safe and applies consistently. Spring/event patterns (Java): triggers_of — the schedule edge between a @Scheduled method and its synthetic scheduler (pass the method to get its scheduler, or the scheduler to list scheduled methods). publishers_of — methods that publish a given event type (ApplicationEventPublisher.publishEvent). listeners_of — @EventListener methods that listen for a given event type. handlers_of — methods that handle a given HTTP endpoint path (Spring @*Mapping + the existing JS/Python endpoints). endpoints_for — the inverse: endpoint path(s) a given method handles. consumers_of — classes/methods consuming a given @Value config property. children_of — symbols contained in a file path (same as file_summary) or, for a class name, its member symbols by line-range containment. All seven require `symbol` (the target); single-repo, not federated.",
288
+ "query: the graph pattern to run (callers_of, callees_of, imports_of, importers_of, inheritors_of, structural_implementations_of, tests_for, file_summary, shortest_path, cross_substrate_path, impact, dead_code, large_functions, large_files, rename_preview, flows, flow_of, flow_analysis, resource_reachability, stats, traverse, knowledge_gaps, community, federated_repos). cross_substrate_path proves only one static Salesforce Flow action to one uniquely resolved Apex @InvocableMethod and returns exact endpoint identities/evidence, freshness, budgets, and explicit unsupported crossings; it does not resolve Terraform, dbt, or arbitrary substrate paths. resource_reachability is a repo-wide, bounded, cached, on-demand TS/JS static heuristic for literal process.env/fs.readFileSync sources reaching console.log/fetch/db.query sinks; it reports evidence, coverage, omissions, freshness, truncation, and continuation and is never runtime reachability or exploitability proof. structural_implementations_of returns TypeScript structurally typed object implementations separately from nominal extends/implements results. federated_repos — discover and list all registered/configured repository paths that this engine federates queries across. community — fetch a single community's full detail (name, size, cohesion, files, and symbols) by name or substring filter; empty `symbol` = all communities. large_files — whole-file line counts (min 200), sorted descending — the file-level counterpart to large_functions, for spotting god files rather than god functions; same optional path-substring filter, empty `symbol` = all files. stats — repo-level index size + health in one call (per-repo symbol/reference/dependency/embedding/file counts, per-kind and per-language breakdowns, orphaned-embedding count, schema version, last-indexed HEAD, and cached community/hub/bridge/flow totals); federated, empty `symbol`. traverse — typed BFS from `symbol` within `depth` hops (default 3, 1–6), selectable upstream/downstream/both and explicit edge kinds; every discovered edge includes direction, kind, provenance, confidence, files, and source line, with optional Tree-sitter-grounded argument expressions honestly marked heuristic. Sets `truncated` when the `limit` cap binds; single-repo. knowledge_gaps — repo-health weaknesses in one call: thin communities (<3 symbols), single-file communities, isolated (zero-degree) symbols, and untested hub/bridge hotspots; sourced from the cached map(), federated, empty `symbol`, one row carrying a `knowledgeGaps` block. surprising_connections — resolved edges scored by a composite surprise formula (cross-community, cross-language, peripheral-to-hub, cross-test-boundary, unusual edge kind) to surface unexpected coupling; sorted highest-first, top 15 by default, federated, empty `symbol`, each row carrying a `surprise` block. suggested_questions — prioritized, human-readable review prompts synthesized from knowledge_gaps + surprising_connections (untested hotspots first, then surprising edges, then thin/single-file communities); federated, empty `symbol`, each row a `question` string citing the real symbol/file, top 10 by default. architecture_overview — a federated architecture view with scoped community coupling and independently selectable packages, layers, boundaries, hotspots, entry points, and language facets; `path` is segment-safe and applies consistently. class_consumers returns direct typed Apex class-use sites separately from method callers and Java config consumers; count is observed sites, consumerFileCount distinct files, testScope defaults all, limit caps sites (1-1000), path is a canonical prefix, and continuation resumes the same target/filters only while the graph snapshot remains unchanged. Supported syntax only, not runtime or complete class-use coverage. Spring/event patterns (Java): triggers_of — the schedule edge between a @Scheduled method and its synthetic scheduler (pass the method to get its scheduler, or the scheduler to list scheduled methods). publishers_of — methods that publish a given event type (ApplicationEventPublisher.publishEvent). listeners_of — @EventListener methods that listen for a given event type. handlers_of — methods that handle a given HTTP endpoint path (Spring @*Mapping + the existing JS/Python endpoints). endpoints_for — the inverse: endpoint path(s) a given method handles. consumers_of — classes/methods consuming a given @Value config property. children_of — symbols contained in a file path (same as file_summary) or, for a class name, its member symbols by line-range containment. All seven require `symbol` (the target); single-repo, not federated.",
275
289
  },
276
290
  depth: {
277
291
  type: "number",
@@ -405,7 +419,7 @@ function buildDocumentedKnodinTools() {
405
419
  testScope: {
406
420
  type: "string",
407
421
  enum: ["all", "test", "production"],
408
- description: "search/file_metrics: include all, test-only, or production-only files.",
422
+ description: "search/file_metrics/class_consumers: all (default), test-only, or production-only files.",
409
423
  },
410
424
  caseSensitive: {
411
425
  type: "boolean",
@@ -488,6 +502,11 @@ function buildDocumentedKnodinTools() {
488
502
  type: "string",
489
503
  description: "context only (required): free-text description of what you're doing (e.g. 'review PR #42', 'debug login timeout', 'how does auth work'). Drives a keyword heuristic that suggests which operation to run next — a hint only, never a constraint.",
490
504
  },
505
+ contextMode: {
506
+ type: "string",
507
+ enum: ["orientation", "implementation"],
508
+ description: "context only: implementation ranks candidate functions/methods/classes without selecting a target. Omit for orientation; supply symbol for a target investigation.",
509
+ },
491
510
  changedFiles: {
492
511
  type: "array",
493
512
  items: { type: "string" },
@@ -734,14 +753,14 @@ export function getDocumentedKnodinToolsForSchemaProof() {
734
753
  const COMPACT_PARAMETER_DESCRIPTIONS = {
735
754
  operation: "Capability.",
736
755
  symbol: "Target.",
737
- pattern: "Query pattern.",
738
- impactMode: "Symbol or file.",
756
+ pattern: "Pattern.",
757
+ impactMode: "Symbol/file.",
739
758
  apply: "Apply edits.",
740
759
  // Shortened from "Review diff scope." / "Documentation section." on main: the
741
760
  // `remote` operation added to the enum costs tokens, and this surface has a
742
761
  // hard ceiling enforced by tool-schema-budget.spec.ts.
743
762
  diffScope: "Diff scope.",
744
- section: "Docs section.",
763
+ section: "Docs.",
745
764
  persistTelemetry: "Persist telemetry.",
746
765
  };
747
766
  /**
@@ -996,6 +1015,11 @@ async function handlePullRequestsOperation(options) {
996
1015
  }));
997
1016
  }
998
1017
  function validateKnodinArgs(args) {
1018
+ if (args.contextMode !== undefined &&
1019
+ !["orientation", "implementation"].includes(args.contextMode))
1020
+ throw new Error("Invalid contextMode");
1021
+ if (args.contextMode === "implementation" && args.symbol)
1022
+ throw new Error("Choose implementation discovery or a target symbol, not both");
999
1023
  const requireStringArray = (name, value) => {
1000
1024
  if (value !== undefined &&
1001
1025
  (!Array.isArray(value) || value.some((item) => typeof item !== "string")))
@@ -1010,6 +1034,7 @@ function validateKnodinArgs(args) {
1010
1034
  "exclude",
1011
1035
  "roots",
1012
1036
  "alreadyPresent",
1037
+ "evidenceFiles",
1013
1038
  "chatFiles",
1014
1039
  ])
1015
1040
  requireStringArray(name, args[name]);
@@ -1148,6 +1173,10 @@ function compactFailureRelation(relation) {
1148
1173
  function compactFailureDiagnosis(result) {
1149
1174
  return {
1150
1175
  status: result.status,
1176
+ available: result.available,
1177
+ state: result.state,
1178
+ indexCoverage: result.indexCoverage,
1179
+ coverageNotice: result.coverageNotice,
1151
1180
  input: [result.input.kind, result.input.artifactId, result.input.bytes, result.input.complete],
1152
1181
  // `reference` is [reported path, line, column]. `owner` is
1153
1182
  // [stable identity, symbol, kind, start, end, signature].
@@ -1335,31 +1364,61 @@ function handleDocsOperation(section, bounded) {
1335
1364
  }
1336
1365
  async function tryCompactStructural(args, repo, selector, bounded) {
1337
1366
  const { operation, detailLevel, includeSource, symbol, file, byteBudget } = args;
1367
+ const compactCandidate = detailLevel === "compact" &&
1368
+ ((operation === "explain" && includeSource === true && !!symbol) ||
1369
+ (operation === "search" && !!args.query) ||
1370
+ (operation === "query" &&
1371
+ ["file_summary", "batch_outline", "project_overview"].includes(args.pattern ?? "")));
1372
+ if (!compactCandidate)
1373
+ return { handled: false, value: NOT_COMPACT_STRUCTURAL };
1374
+ const health = await compactGraphHealth(repo);
1375
+ // Compact strings cannot carry the mandatory scope contract. Scoped, pending
1376
+ // and unknown coverage use the normal decorated, budgeted response instead.
1377
+ if (health.available) {
1378
+ const coverage = health.graph.indexCoverage;
1379
+ if (coverage?.mode !== "full" ||
1380
+ coverage.intentState !== "complete" ||
1381
+ coverage.repositoryComplete !== true ||
1382
+ coverage.negativeScope !== "repository")
1383
+ return { handled: false, value: NOT_COMPACT_STRUCTURAL, qualifiedFallback: true };
1384
+ }
1338
1385
  const unavailable = async (op) => {
1339
- const health = await compactGraphUnavailable(repo);
1340
- return health ? bounded(health, op, true) : null;
1386
+ return health.available ? null : bounded(health, op, true);
1387
+ };
1388
+ const verifiedCompact = async (value) => {
1389
+ if (!health.available)
1390
+ return { handled: true, value };
1391
+ const after = await compactGraphHealth(repo);
1392
+ if (!after.available)
1393
+ return { handled: true, value: bounded(after, operation, true) };
1394
+ const beforeCoverage = health.graph.indexCoverage;
1395
+ const afterCoverage = after.graph.indexCoverage;
1396
+ if (!afterCoverage ||
1397
+ afterCoverage.mode !== "full" ||
1398
+ afterCoverage.intentState !== "complete" ||
1399
+ !afterCoverage.repositoryComplete ||
1400
+ afterCoverage.negativeScope !== "repository" ||
1401
+ afterCoverage.digest !== beforeCoverage?.digest ||
1402
+ afterCoverage.revision !== beforeCoverage?.revision ||
1403
+ afterCoverage.origin !== beforeCoverage?.origin)
1404
+ return { handled: false, value: NOT_COMPACT_STRUCTURAL, qualifiedFallback: true };
1405
+ return { handled: true, value };
1341
1406
  };
1342
1407
  if (operation === "explain" && detailLevel === "compact" && includeSource === true && symbol) {
1343
1408
  const blocked = await unavailable("explain");
1344
- return {
1345
- handled: true,
1346
- value: blocked ?? (await runCompactExplain(symbol, repo, selector, file, byteBudget)),
1347
- };
1409
+ return verifiedCompact(blocked ?? (await runCompactExplain(symbol, repo, selector, file, byteBudget)));
1348
1410
  }
1349
1411
  if (operation === "search" && detailLevel === "compact" && args.query) {
1350
1412
  const blocked = await unavailable("search");
1351
- return {
1352
- handled: true,
1353
- value: blocked ??
1354
- (await runCompactSearch(args.query, repo, Math.min(args.limit ?? args.itemBudget ?? 5, args.itemBudget ?? 100), {
1355
- languages: args.languages,
1356
- extensions: args.extensions,
1357
- kinds: args.kinds,
1358
- path: args.path,
1359
- testScope: args.testScope,
1360
- offset: args.offset,
1361
- }, byteBudget)),
1362
- };
1413
+ return verifiedCompact(blocked ??
1414
+ (await runCompactSearch(args.query, repo, Math.min(args.limit ?? args.itemBudget ?? 5, args.itemBudget ?? 100), {
1415
+ languages: args.languages,
1416
+ extensions: args.extensions,
1417
+ kinds: args.kinds,
1418
+ path: args.path,
1419
+ testScope: args.testScope,
1420
+ offset: args.offset,
1421
+ }, byteBudget)));
1363
1422
  }
1364
1423
  const compactPattern = operation === "query" &&
1365
1424
  detailLevel === "compact" &&
@@ -1371,11 +1430,8 @@ async function tryCompactStructural(args, repo, selector, bounded) {
1371
1430
  if (!compactPattern)
1372
1431
  return { handled: false, value: NOT_COMPACT_STRUCTURAL };
1373
1432
  const blocked = await unavailable(`query:${compactPattern}`);
1374
- return {
1375
- handled: true,
1376
- value: blocked ??
1377
- (await runCompactQuery(compactPattern, symbol ?? "", repo, Math.min(args.limit ?? args.itemBudget ?? 100, args.itemBudget ?? 1000), args.depth, selector, byteBudget)),
1378
- };
1433
+ return verifiedCompact(blocked ??
1434
+ (await runCompactQuery(compactPattern, symbol ?? "", repo, Math.min(args.limit ?? args.itemBudget ?? 100, args.itemBudget ?? 1000), args.depth, selector, byteBudget)));
1379
1435
  }
1380
1436
  const mcpRequestSignals = new WeakMap();
1381
1437
  export function attachMcpRequestSignal(args, signal) {
@@ -1423,7 +1479,7 @@ async function handleDiagnosticsOperation(repo, args, bounded) {
1423
1479
  })
1424
1480
  : persistDiagnosticsPreview(repo, options), `diagnostics:${args.telemetryAction === "export" ? "archive" : "preview"}`);
1425
1481
  }
1426
- async function dispatchKnodinTool(args) {
1482
+ async function dispatchKnodinTool(args, qualifiedCompactFallback = false) {
1427
1483
  const startedAt = performance.now();
1428
1484
  const parsedArgs = (args ?? {});
1429
1485
  const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, queries, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositories, scope, distributed, repositoryIds, components, evidence, relationshipType, requireComplete, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, planDigest, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, caseSensitive, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
@@ -1483,6 +1539,11 @@ async function dispatchKnodinTool(args) {
1483
1539
  const observe = (output, op, minimal = false) => {
1484
1540
  const root = output;
1485
1541
  const budgetMeta = root.responseBudget;
1542
+ const evidenceBudget = op === "evidence" &&
1543
+ (root.protocol === "knodin-evidence-bundle-v1" ||
1544
+ root.protocol === "knodin-progressive-evidence-v1")
1545
+ ? root.budget
1546
+ : undefined;
1486
1547
  if (op === "review")
1487
1548
  measurePerfPhaseSync("review_serialization", () => JSON.stringify(output));
1488
1549
  const telemetry = measureOutput({
@@ -1492,6 +1553,7 @@ async function dispatchKnodinTool(args) {
1492
1553
  startedAt,
1493
1554
  detailMode: minimal ? "minimal" : (detailLevel ?? "standard"),
1494
1555
  truncated: budgetMeta?.truncated === true ||
1556
+ evidenceBudget?.truncated === true ||
1495
1557
  root.telemetry?.truncated === true,
1496
1558
  schemaTokens: gatewaySchemaTokens(),
1497
1559
  });
@@ -1543,12 +1605,14 @@ async function dispatchKnodinTool(args) {
1543
1605
  const verified = await inspectGatewayGraphHealth(repo);
1544
1606
  if (!verified.available)
1545
1607
  return bounded(verified, op, minimal);
1546
- return bounded(decorateGraphQueryResult(result, verified.state, verified.graph.freshness), op, minimal);
1608
+ return bounded(decorateGraphQueryResult(result, verified.state, verified.graph.freshness, verified.graph.indexCoverage, health.graph.indexCoverage), op, minimal);
1547
1609
  };
1548
1610
  validateKnodinArgs(parsedArgs);
1549
1611
  const compactStructural = await tryCompactStructural(parsedArgs, repo, selector, bounded);
1550
1612
  if (compactStructural.handled)
1551
1613
  return compactStructural.value;
1614
+ if (compactStructural.qualifiedFallback)
1615
+ return dispatchKnodinTool({ ...parsedArgs, detailLevel: "minimal" }, true);
1552
1616
  if (detailLevel === "compact")
1553
1617
  throw new Error("knodin: compact detail is unsupported here");
1554
1618
  switch (operation) {
@@ -1687,6 +1751,22 @@ async function dispatchKnodinTool(args) {
1687
1751
  case "execute":
1688
1752
  return handleExecutionOperation(repo, parsedArgs.profile, engine, bounded);
1689
1753
  case "evidence":
1754
+ if (parsedArgs.evidenceFiles) {
1755
+ if (parsedArgs.file)
1756
+ throw new Error("Choose file or evidenceFiles, not both");
1757
+ if (parsedArgs.evidenceLevel !== "evidence" && parsedArgs.evidenceLevel !== "expand")
1758
+ throw new Error("Evidence bundles require evidence or expand level");
1759
+ return observe(await deliverEvidenceBundle({
1760
+ repo,
1761
+ files: parsedArgs.evidenceFiles,
1762
+ level: parsedArgs.evidenceLevel,
1763
+ continuation: parsedArgs.continuation,
1764
+ alreadyPresent: parsedArgs.alreadyPresent,
1765
+ byteLimit: parsedArgs.byteBudget,
1766
+ tokenLimit: parsedArgs.tokenBudget,
1767
+ itemLimit: parsedArgs.itemBudget,
1768
+ }, createEvidenceGraphAdapter(engine)), "evidence");
1769
+ }
1690
1770
  return handleProgressiveEvidenceOperation(repo, parsedArgs, observe);
1691
1771
  case "pack": {
1692
1772
  if (packAction !== undefined && !["export", "read", "grep"].includes(packAction))
@@ -1756,7 +1836,11 @@ async function dispatchKnodinTool(args) {
1756
1836
  case "context": {
1757
1837
  if (!task)
1758
1838
  throw new Error("knodin context requires `task`");
1759
- return graphRead("context", () => buildKnodinContext(engine, task, repo, base, changedFiles));
1839
+ if (parsedArgs.contextMode === "implementation")
1840
+ return graphRead("context", () => findImplementationCandidates(engine, repo, { task, limit, offset }));
1841
+ return graphRead("context", () => symbol
1842
+ ? buildTaskInvestigation(engine, repo, { task, symbol, ...selector, limit, depth })
1843
+ : buildKnodinContext(engine, task, repo, base, changedFiles));
1760
1844
  }
1761
1845
  case "search":
1762
1846
  if (!query)
@@ -1769,7 +1853,8 @@ async function dispatchKnodinTool(args) {
1769
1853
  kinds,
1770
1854
  path,
1771
1855
  testScope,
1772
- includeSource,
1856
+ includeSource: qualifiedCompactFallback ? false : includeSource,
1857
+ structural: qualifiedCompactFallback ? true : undefined,
1773
1858
  offset,
1774
1859
  }));
1775
1860
  case "textSearch": {
@@ -1824,7 +1909,7 @@ async function dispatchKnodinTool(args) {
1824
1909
  return bounded({
1825
1910
  results: outputs.map((item) => ({
1826
1911
  ...item,
1827
- output: decorateGraphQueryResult(item.output, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness),
1912
+ output: decorateGraphQueryResult(item.output, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness, verifiedQueryHealth.graph.indexCoverage, queryHealth.graph.indexCoverage),
1828
1913
  })),
1829
1914
  }, "query_batch");
1830
1915
  }
@@ -1862,12 +1947,100 @@ async function dispatchKnodinTool(args) {
1862
1947
  : await inspectGatewayGraphHealth(repo);
1863
1948
  if (queryHealth && !queryHealth.available)
1864
1949
  return bounded(queryHealth, `query:${pattern}`);
1950
+ if (pattern === "class_consumers") {
1951
+ if (limit !== undefined && limit > 1000)
1952
+ throw new Error("class_consumers limit must not exceed 1000 sites");
1953
+ if (offset !== undefined ||
1954
+ kinds !== undefined ||
1955
+ relationKinds !== undefined ||
1956
+ direction !== undefined ||
1957
+ depth !== undefined ||
1958
+ includeTests !== undefined)
1959
+ throw new Error("class_consumers supports testScope/path filters and signed continuation, not offset/kinds/relations/direction/depth");
1960
+ const secret = evidenceHandleSecret(repo).secret;
1961
+ const cursor = parsedArgs.continuation
1962
+ ? readClassConsumerCursor(parsedArgs.continuation, secret)
1963
+ : undefined;
1964
+ const request = {
1965
+ symbol: target ?? "",
1966
+ selector,
1967
+ testScope: testScope ?? "all",
1968
+ path,
1969
+ limit: limit ?? 100,
1970
+ ...(cursor
1971
+ ? {
1972
+ after: cursor.after,
1973
+ expectedQueryDigest: cursor.queryDigest,
1974
+ expectedSnapshotDigest: cursor.snapshotDigest,
1975
+ }
1976
+ : {}),
1977
+ };
1978
+ try {
1979
+ const page = await engine.queryClassConsumers(repo, request);
1980
+ // Defer maintenance, not the audit, until a later eligible status or close.
1981
+ const verified = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached", checkpoint: "defer" })));
1982
+ if (!verified.available)
1983
+ return bounded(verified, "query:class_consumers");
1984
+ // Revalidate facts revisions too: a health probe alone cannot detect every same-scope edit.
1985
+ await validateClassConsumerDelivery(page, request, (next) => engine.queryClassConsumers(repo, next));
1986
+ const sharedIndex = sharedIndexEnvelope();
1987
+ return observe(finalizeClassConsumerPage(page, {
1988
+ secret,
1989
+ budget,
1990
+ decorate: (candidate) => ({
1991
+ ...decorateGraphQueryResult(candidate, verified.state, undefined, verified.graph.indexCoverage, queryHealth?.available ? queryHealth.graph.indexCoverage : undefined),
1992
+ sharedIndex,
1993
+ }),
1994
+ }), "query:class_consumers");
1995
+ }
1996
+ catch (error) {
1997
+ if (error instanceof ClassConsumerQueryError &&
1998
+ [
1999
+ "CLASS_CONSUMER_TARGET_NOT_FOUND",
2000
+ "CLASS_CONSUMER_TARGET_AMBIGUOUS",
2001
+ "CLASS_CONSUMER_TARGET_CAPABILITY_MISSING",
2002
+ ].includes(error.code)) {
2003
+ const targetResolution = error.code === "CLASS_CONSUMER_TARGET_CAPABILITY_MISSING"
2004
+ ? "capability-missing"
2005
+ : error.code === "CLASS_CONSUMER_TARGET_NOT_FOUND"
2006
+ ? "not-found"
2007
+ : "ambiguous";
2008
+ const delivered = bounded({
2009
+ available: false,
2010
+ status: "unavailable",
2011
+ pattern,
2012
+ targetResolution,
2013
+ code: error.code,
2014
+ error: error.message,
2015
+ candidates: error.candidates,
2016
+ ...(queryHealth?.available
2017
+ ? {
2018
+ indexCoverage: queryHealth.graph.indexCoverage,
2019
+ freshness: queryHealth.graph.freshness,
2020
+ }
2021
+ : {}),
2022
+ }, "query:class_consumers");
2023
+ if (delivered.code !== error.code ||
2024
+ delivered.targetResolution !== targetResolution ||
2025
+ delivered.available !== false ||
2026
+ delivered.status !== "unavailable")
2027
+ throw new Error("Response budget cannot preserve the class-consumer error contract; increase the budget.");
2028
+ return delivered;
2029
+ }
2030
+ throw error;
2031
+ }
2032
+ }
1865
2033
  if (pattern === "rename_preview") {
1866
2034
  // rename_preview routes through the rename engine: dry-run (preview +
1867
2035
  // unified diff) by default, writing to disk only when `apply` is true.
1868
2036
  return engine
1869
2037
  .rename(symbol ?? "", to ?? "", repo, apply === true, true, selector)
1870
- .then((result) => bounded(decorateGraphQueryResult(result, queryHealth?.available ? queryHealth.state : "healthy", queryHealth?.available ? queryHealth.graph.freshness : undefined), "query:rename_preview"));
2038
+ .then(async (result) => {
2039
+ const verified = await inspectGatewayGraphHealth(repo);
2040
+ if (!verified.available)
2041
+ return bounded(verified, "query:rename_preview");
2042
+ return bounded(decorateGraphQueryResult(result, verified.state, verified.graph.freshness, verified.graph.indexCoverage, queryHealth?.available ? queryHealth.graph.indexCoverage : undefined), "query:rename_preview");
2043
+ });
1871
2044
  }
1872
2045
  const queryResult = await engine.query(pattern, target ?? "", repo, to, Math.min(limit ?? itemBudget ?? 100, itemBudget ?? 1000), depth, detailLevel === "source" ? undefined : detailLevel, selector, pattern === "impact"
1873
2046
  ? {
@@ -1908,9 +2081,19 @@ async function dispatchKnodinTool(args) {
1908
2081
  // A verified probe supersedes the unverified one for both fields, so
1909
2082
  // state and freshness always come from the same observation.
1910
2083
  const authoritative = verifiedQueryHealth?.available ? verifiedQueryHealth : queryHealth;
1911
- decorated = decorateGraphQueryResult(result, authoritative.state, authoritative.graph.freshness);
2084
+ decorated = decorateGraphQueryResult(result, authoritative.state, authoritative.graph.freshness, authoritative.graph.indexCoverage, queryHealth.graph.indexCoverage);
2085
+ }
2086
+ const delivered = bounded(decorated, `query:${pattern}`);
2087
+ if (pattern === "inheritors_of") {
2088
+ const qualification = decorated
2089
+ .inheritanceQuery;
2090
+ const retained = delivered
2091
+ .inheritanceQuery;
2092
+ if (qualification &&
2093
+ ["version", "mode", "partial", "truncated", "apex"].some((key) => retained?.[key] !== qualification[key]))
2094
+ throw new Error("Response budget cannot preserve the inheritance qualification; increase the budget.");
1912
2095
  }
1913
- return bounded(decorated, `query:${pattern}`);
2096
+ return delivered;
1914
2097
  }
1915
2098
  default:
1916
2099
  throw new Error(`unknown knodin operation: ${String(operation)}`);
package/docs/CLI.md CHANGED
@@ -66,6 +66,35 @@ example repository existence, mutually exclusive configuration modes, and
66
66
  graph-query target rules), but syntax cannot reach a handler unless the shared
67
67
  declarative model accepts it first.
68
68
 
69
+ ## Task discovery and investigation
70
+
71
+ `context` supports explicit implementation discovery and selected-symbol
72
+ investigation in addition to ordinary repository orientation:
73
+
74
+ ```bash
75
+ knodin context "find code that persists an order" --implementations --limit 10 --json
76
+ knodin context "rename persist" --symbol persist --file src/storage.ts --json
77
+ knodin context "rename persist" --symbol persist --identity '<returned-identity>' --limit 20 --depth 3 --json
78
+ ```
79
+
80
+ `--implementations` ranks functions, methods, constructors, and classes and
81
+ returns identity/file-specific followups. It chooses no target and intentionally
82
+ excludes types, interfaces, and constants; use ordinary `search` to find those.
83
+ Use `--offset` and `--limit` to page through candidates. Incomplete semantic
84
+ coverage remains visible even when an exact-name candidate can be returned.
85
+
86
+ `--symbol` composes definition/source, rename sites, callers, upstream impact,
87
+ tests, and file importers. A duplicated name requires `--file` or `--identity`;
88
+ an unknown target is reported separately from a symbol with no callers. Bounded
89
+ or unavailable sections include followups and unresolved work. This is static
90
+ evidence to inspect, not a complete edit plan or proof of test coverage.
91
+ `--implementations` and `--symbol` are mutually exclusive.
92
+
93
+ The single MCP gateway uses `operation: "context"`, `task`, and
94
+ `contextMode: "implementation"` for discovery. For investigation, supply
95
+ `symbol` with optional `file` or `identity` and omit implementation mode. Both
96
+ surfaces return compatible identity/file-specific followups.
97
+
69
98
  Optional Claude lifecycle integration is explicit and user-global:
70
99
 
71
100
  ```bash
@@ -128,6 +157,13 @@ exact local source and stable continuations. A returned evidence handle plus
128
157
  current-file match; all other baseline states return the required source. See
129
158
  `docs/PROGRESSIVE-EVIDENCE.md` for the protocol and limitations.
130
159
 
160
+ Use `knodin evidence evidence --files src/widget.ts,src/caller.ts --items 20
161
+ --json` for a multi-file bundle, or `expand` to include indexed direct file
162
+ dependencies. A signed `--continuation` recovers remaining source; completed
163
+ per-file receipts can later be passed through `--already-present` to reuse
164
+ unchanged content. Manifest entries have a null receipt until complete delivery,
165
+ and the manifest itself counts toward the byte/token budget.
166
+
131
167
  `knodin review` returns separately itemized graph impact, test gaps, structural
132
168
  centrality, churn, co-change, and coupling evidence. Git history has explicit
133
169
  commit/file/time bounds and truthful unavailable or truncation states; see
@@ -157,3 +193,59 @@ not Node's backtracking regular-expression engine. Backreferences and lookaround
157
193
  are rejected because they cannot retain RE2's linear-time guarantee. Route
158
194
  matching treats application path text literally and recognizes only complete
159
195
  Express/NestJS-style `:name` or `{name}` path segments as parameters.
196
+
197
+ ### Local flow-analysis budgets
198
+
199
+ `knodin query flow_analysis <symbol> --bytes 1536 --tokens 384 --json`
200
+ requires an effective budget of at least 1536 serialized bytes (384 estimated
201
+ tokens). The effective ceiling is the stricter of `--bytes` and `--tokens × 4`;
202
+ a smaller ceiling is rejected, never automatically enlarged. Required evidence
203
+ and qualifications may still require more than this minimum.
204
+
205
+ Flow budget metadata and drill-down continuation guidance use operation
206
+ `query:flow_analysis`, consistently with MCP. This guidance is not a resumable
207
+ cursor: repeat the query with a narrower selector or an explicitly larger budget.
208
+ Other commands retain their existing operation names and budget policies.
209
+
210
+ ### Typed Apex class consumers
211
+
212
+ `knodin query inheritors_of Base --json` aggregates same-name nominal inheritance
213
+ across languages; it does not select one global target. `inheritanceQuery` preserves
214
+ the query mode, partial/truncation flags, and Apex resolution status. Apex rows
215
+ require independently verified typed endpoints; an ambiguous or missing Apex
216
+ capability can leave qualified non-Apex results. Apex `--file` or `--identity`
217
+ selectors establish a selected typed target; non-Apex inheritance retains its
218
+ legacy name matching rather than promising exact identity resolution. Budgets
219
+ that cannot preserve this qualification fail closed.
220
+ The tuple describes engine analysis; `responseBudget.truncated` separately reports
221
+ wire-size trimming. Read both before interpreting a bounded result as complete.
222
+
223
+ `knodin query class_consumers GlobalWrappers --test-scope all --limit 100 --json`
224
+ returns direct typed source sites for an Apex class or interface, separately from method `callers_of` and Java
225
+ configuration `consumers_of`. `count` is total observed matching sites,
226
+ `consumerFileCount` is distinct files, and `returnedCount` is this page's sites.
227
+ `files` contains aggregates only for files represented on this page.
228
+ `targetKind` identifies the resolved declaration as `class` or `interface` and is
229
+ preserved on every page. Interface consumers cover supported type-position roles,
230
+ not constructors, static-field reads, `implements` relationships, or inferred
231
+ runtime dispatch. Query implementation relationships separately.
232
+
233
+ Use `--test-scope test|production|all` (default `all`), a canonical repository-relative
234
+ `--path` prefix, and stable `--identity`/`--file` target selectors when needed.
235
+ `--limit` accepts 1–1000 sites; `--bytes`, `--tokens`, and `--items` bound the complete
236
+ response. Repeat the same target and filters with `--continuation '<returned token>'`;
237
+ page budgets may change. Tokens authenticate the last delivered site and the graph,
238
+ coverage, and typed-facts snapshot. Forged, mismatched, or stale tokens fail closed.
239
+ A minimum complete site plus qualifications must fit; source fields are never shortened.
240
+
241
+ Sites retain exact spelling, role, source-owner identity when known, and exclusive
242
+ end positions: one-based lines/UTF-16 columns and zero-based UTF-8 byte offsets.
243
+ The target's own file is excluded. Nine supported syntax roles do not cover every
244
+ Apex use, metadata relationship, or runtime path. Read `classConsumerAnalysis` and
245
+ `indexCoverage` before interpreting a negative; missing/invalid descriptors and
246
+ unresolved uses remain qualified, not proof that a class is unused.
247
+ An indexed retained interface declaration without a safely matching interface
248
+ symbol returns `CLASS_CONSUMER_TARGET_CAPABILITY_MISSING`, `targetResolution: capability-missing`,
249
+ and a nonzero exit—not a successful zero or a fabricated identity. Follow the
250
+ reported repair guidance within the declared index scope. An unknown name remains
251
+ `not-found`; this qualification does not certify interfaces outside indexed coverage.