knodin 0.12.2 → 0.13.1

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 (58) hide show
  1. package/README.md +16 -1
  2. package/dist/bin/cli.js +245 -18
  3. package/dist/src/agent-events.js +25 -7
  4. package/dist/src/authenticated-cursor.js +81 -0
  5. package/dist/src/backup-retention.js +345 -12
  6. package/dist/src/class-consumer-contract.js +18 -0
  7. package/dist/src/class-consumer-cursor.js +91 -0
  8. package/dist/src/class-consumer-delivery.js +22 -0
  9. package/dist/src/class-consumer-page.js +148 -0
  10. package/dist/src/cli-model.js +21 -3
  11. package/dist/src/docs-sections.js +1 -0
  12. package/dist/src/engine/apex-class-uses.js +430 -0
  13. package/dist/src/engine/apex-entry-points.js +98 -0
  14. package/dist/src/engine/apex-receiver.js +301 -0
  15. package/dist/src/engine/candidate-database.js +624 -66
  16. package/dist/src/engine/embedding-reuse.js +57 -0
  17. package/dist/src/engine/embeddings.js +22 -0
  18. package/dist/src/engine/index-coverage.js +215 -0
  19. package/dist/src/engine/index.js +3854 -448
  20. package/dist/src/engine/salesforce-components.js +460 -0
  21. package/dist/src/engine/seal.js +3 -0
  22. package/dist/src/engine/sqlite.js +44 -0
  23. package/dist/src/engine/state-paths.js +192 -31
  24. package/dist/src/evidence-bundle.js +283 -0
  25. package/dist/src/evidence-graph.js +163 -0
  26. package/dist/src/failure-diagnosis.js +80 -5
  27. package/dist/src/file-dependency.js +35 -0
  28. package/dist/src/graph-query-health.js +47 -1
  29. package/dist/src/implementation-search.js +69 -0
  30. package/dist/src/index-coverage-read.js +33 -0
  31. package/dist/src/investigation.js +195 -0
  32. package/dist/src/manager-update.js +9 -10
  33. package/dist/src/mcp-reliability.js +4 -0
  34. package/dist/src/mcp-worker-supervisor.js +122 -6
  35. package/dist/src/mirror.js +2 -0
  36. package/dist/src/progressive-evidence.js +4 -4
  37. package/dist/src/response-budget.js +129 -3
  38. package/dist/src/server.js +18 -5
  39. package/dist/src/shared-index/publisher.js +41 -1
  40. package/dist/src/shared-index/restore.js +16 -6
  41. package/dist/src/storage-budget.js +146 -0
  42. package/dist/src/storage-inventory.js +263 -0
  43. package/dist/src/storage-management.js +204 -0
  44. package/dist/src/storage-policy-contract.js +121 -0
  45. package/dist/src/tools/knodin-tools.js +224 -41
  46. package/dist/src/worktree-seed.js +7 -1
  47. package/docs/BACKUP-RETENTION.md +46 -2
  48. package/docs/CLI.md +98 -2
  49. package/docs/MCP.md +75 -0
  50. package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
  51. package/docs/SALESFORCE-BINDINGS.md +121 -0
  52. package/docs/SALESFORCE-DEAD-CODE.md +45 -0
  53. package/docs/SCOPED-INDEXING.md +76 -0
  54. package/docs/apex-receiver-resolution.md +41 -0
  55. package/docs/releases/0.13.0.md +62 -0
  56. package/docs/releases/0.13.1.md +216 -0
  57. package/docs/structural-only-indexing.md +20 -0
  58. package/package.json +12 -4
@@ -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)}`);
@@ -115,7 +115,13 @@ export async function seedWorktreeIndex(repoPath, engine, schemaVersion) {
115
115
  await engine.promoteCandidate(candidate);
116
116
  const promoted = candidate;
117
117
  candidate = undefined;
118
- const health = await engine.status(resolved, { audit: "deep", ignoreActiveOperation: true });
118
+ let health;
119
+ try {
120
+ health = await engine.status(resolved, { audit: "deep", ignoreActiveOperation: true });
121
+ }
122
+ finally {
123
+ engine.finalizePromotedStorage?.(resolved);
124
+ }
119
125
  return {
120
126
  seeded: true,
121
127
  reflinked: promoted.reflinked ?? false,
@@ -5,7 +5,8 @@ Knodin graph promotions leave completed snapshots named
5
5
  They are promotion snapshots, not a supported restore interface. Malformed and
6
6
  legacy names are inventory-only and are never pruning candidates.
7
7
 
8
- Nothing is deleted by default. Inspect one checkout or bounded portfolio roots:
8
+ One-shot pruning is preview-only unless `--apply` is supplied. Inspect one
9
+ checkout or bounded portfolio roots:
9
10
 
10
11
  ```bash
11
12
  knodin backups list
@@ -25,6 +26,48 @@ trees. It unions explicit roots with valid absolute checkout paths in
25
26
 
26
27
  ## Opt-in ongoing retention
27
28
 
29
+ ### Managed storage and legacy consent
30
+
31
+ New successfully indexed stores automatically receive `.knodin/storage-policy.json`
32
+ unless a healthy, applicable bounded global policy already manages that store.
33
+ In that case no local policy file is materialized; `backups policy status` reports
34
+ the effective policy. The local defaults provide:
35
+ one protected recoverable backup, a 1 GiB allocated-backup budget, and a 512 MiB
36
+ free-space reserve. Post-promotion maintenance runs after releasing the operation
37
+ lease. Count/byte quotas override the age window, but never recovery protection.
38
+ If protected data exceeds the quota, Knodin reports it and refuses unsafe further
39
+ candidate allocation; increase the budget or resolve the storage condition.
40
+
41
+ Existing unmanaged graphs require explicit consent:
42
+
43
+ ```bash
44
+ knodin backups policy status /path/to/repo --json
45
+ knodin backups policy preview /path/to/repo --json
46
+ knodin backups policy apply /path/to/repo --json
47
+ knodin backups policy apply /path/to/repo --max-count 2 --max-bytes 2147483648
48
+ knodin backups policy apply /path/to/repo --mode preview
49
+ knodin backups policy apply /path/to/repo --mode disabled
50
+ ```
51
+
52
+ Preview writes nothing and includes the pruning plan and bounded inventory.
53
+ Apply persists consent and runs policy-approved backup maintenance. Omitted
54
+ settings preserve an existing policy, including explicit dry-run. Use
55
+ `--mode enabled` to explicitly return to deleting eligible managed backups.
56
+ `--reserve-bytes` configures free space retained beyond a conservative scratch
57
+ estimate. Unknown capacity/allocated sizes refuse allocations instead of
58
+ guessing zero. The estimate is not a reservation against other writers.
59
+
60
+ Candidate maintenance removes only managed empty or audit-only directories with
61
+ a validated owner marker and confirmed exited owner. Owner-unknown legacy
62
+ directories, live owners, DB-bearing interrupted candidates, symlinks, leases
63
+ and promotion markers are protected. Permission/deletion failures and protected over-budget data are
64
+ visible, not claimed as reclaimed space. Allocated versus apparent totals differ;
65
+ APFS clones can share extents, so allocated-byte sums are not promised physical
66
+ recovery. Source mirrors and external development harness directories are
67
+ separate storage classes, not promotion backups.
68
+
69
+ ### Native daily scheduler
70
+
28
71
  Installation is the consent boundary:
29
72
 
30
73
  ```bash
@@ -34,7 +77,8 @@ knodin backups retention doctor
34
77
  knodin backups retention remove
35
78
  ```
36
79
 
37
- Options are `--retention-days` (3), `--keep-newest` (1), `--depth` (5),
80
+ Options are `--retention-days` (3), `--keep-newest` (1), `--max-count` (1),
81
+ `--max-bytes` (1073741824), `--depth` (5),
38
82
  `--schedule` (`04:17` local), and `--dry-run`. Installation writes a versioned
39
83
  policy to `$XDG_CONFIG_HOME/knodin/backup-retention.json` and owned receipt/run
40
84
  state beneath `$XDG_STATE_HOME/knodin/backup-retention/`. Ordinary Knodin use