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.
- package/README.md +16 -1
- package/dist/bin/cli.js +245 -18
- package/dist/src/agent-events.js +25 -7
- package/dist/src/authenticated-cursor.js +81 -0
- package/dist/src/backup-retention.js +345 -12
- package/dist/src/class-consumer-contract.js +18 -0
- package/dist/src/class-consumer-cursor.js +91 -0
- package/dist/src/class-consumer-delivery.js +22 -0
- package/dist/src/class-consumer-page.js +148 -0
- package/dist/src/cli-model.js +21 -3
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/engine/apex-class-uses.js +430 -0
- package/dist/src/engine/apex-entry-points.js +98 -0
- package/dist/src/engine/apex-receiver.js +301 -0
- package/dist/src/engine/candidate-database.js +624 -66
- package/dist/src/engine/embedding-reuse.js +57 -0
- package/dist/src/engine/embeddings.js +22 -0
- package/dist/src/engine/index-coverage.js +215 -0
- package/dist/src/engine/index.js +3854 -448
- package/dist/src/engine/salesforce-components.js +460 -0
- package/dist/src/engine/seal.js +3 -0
- package/dist/src/engine/sqlite.js +44 -0
- package/dist/src/engine/state-paths.js +192 -31
- package/dist/src/evidence-bundle.js +283 -0
- package/dist/src/evidence-graph.js +163 -0
- package/dist/src/failure-diagnosis.js +80 -5
- package/dist/src/file-dependency.js +35 -0
- package/dist/src/graph-query-health.js +47 -1
- package/dist/src/implementation-search.js +69 -0
- package/dist/src/index-coverage-read.js +33 -0
- package/dist/src/investigation.js +195 -0
- package/dist/src/manager-update.js +9 -10
- package/dist/src/mcp-reliability.js +4 -0
- package/dist/src/mcp-worker-supervisor.js +122 -6
- package/dist/src/mirror.js +2 -0
- package/dist/src/progressive-evidence.js +4 -4
- package/dist/src/response-budget.js +129 -3
- package/dist/src/server.js +18 -5
- package/dist/src/shared-index/publisher.js +41 -1
- package/dist/src/shared-index/restore.js +16 -6
- package/dist/src/storage-budget.js +146 -0
- package/dist/src/storage-inventory.js +263 -0
- package/dist/src/storage-management.js +204 -0
- package/dist/src/storage-policy-contract.js +121 -0
- package/dist/src/tools/knodin-tools.js +224 -41
- package/dist/src/worktree-seed.js +7 -1
- package/docs/BACKUP-RETENTION.md +46 -2
- package/docs/CLI.md +98 -2
- package/docs/MCP.md +75 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
- package/docs/SALESFORCE-BINDINGS.md +121 -0
- package/docs/SALESFORCE-DEAD-CODE.md +45 -0
- package/docs/SCOPED-INDEXING.md +76 -0
- package/docs/apex-receiver-resolution.md +41 -0
- package/docs/releases/0.13.0.md +62 -0
- package/docs/releases/0.13.1.md +216 -0
- package/docs/structural-only-indexing.md +20 -0
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ Start broad, inspect the likely change surface, then review the actual diff:
|
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
152
|
# Orient on an unfamiliar checkout
|
|
153
|
-
knodin context
|
|
153
|
+
knodin context "understand this checkout"
|
|
154
154
|
|
|
155
155
|
# Explain a symbol with source and relationships
|
|
156
156
|
knodin explain createServer
|
|
@@ -170,6 +170,21 @@ knodin map
|
|
|
170
170
|
knodin pack createServer --max-tokens 4000
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
For a task that needs an implementation and its dependencies:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
knodin context "find code that persists an order" --implementations --json
|
|
177
|
+
knodin context "rename persist" --symbol persist --file src/storage.ts --json
|
|
178
|
+
knodin evidence expand --files src/storage.ts --items 100 --json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Discovery returns candidates for you to select; investigation collects source,
|
|
182
|
+
callers, importers, tests, and impact for the selected identity. Evidence bundles
|
|
183
|
+
page exact source and issue reusable per-file receipts after complete delivery.
|
|
184
|
+
Ambiguity, incomplete coverage, and budget limits stay visible. See the
|
|
185
|
+
[CLI guide](docs/CLI.md#task-discovery-and-investigation) and
|
|
186
|
+
[evidence protocol](docs/PROGRESSIVE-EVIDENCE.md#graph-backed-file-bundles).
|
|
187
|
+
|
|
173
188
|
If freshness or lifecycle checks fail:
|
|
174
189
|
|
|
175
190
|
```bash
|
package/dist/bin/cli.js
CHANGED
|
@@ -20,6 +20,10 @@ import { inspectClaudeAgentHooks, installClaudeAgentHooks, uninstallClaudeAgentH
|
|
|
20
20
|
import { detectSupportedAgents, parseInitScope, } from "../src/agent-integration.js";
|
|
21
21
|
import { refreshExternalGraphArtifacts, writeArtifactRefreshRecord, } from "../src/artifact-refresh.js";
|
|
22
22
|
import { formatBackupHuman, installBackupRetention, listBackups, maybeRunOpportunisticRetention, pruneBackups, removeBackupRetention, retentionDoctor, retentionStatus, runInstalledRetention, } from "../src/backup-retention.js";
|
|
23
|
+
import { ClassConsumerQueryError, } from "../src/class-consumer-contract.js";
|
|
24
|
+
import { readClassConsumerCursor } from "../src/class-consumer-cursor.js";
|
|
25
|
+
import { validateClassConsumerDelivery } from "../src/class-consumer-delivery.js";
|
|
26
|
+
import { finalizeClassConsumerPage } from "../src/class-consumer-page.js";
|
|
23
27
|
import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, planIndex, resolveCliRuntimeCommand, resolveRepo, } from "../src/cli-args.js";
|
|
24
28
|
import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
|
|
25
29
|
import { buildKnodinContext } from "../src/context.js";
|
|
@@ -27,23 +31,29 @@ import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/co
|
|
|
27
31
|
import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, persistDiagnosticsPreview, recordDiagnosticFailure, } from "../src/diagnostics.js";
|
|
28
32
|
import { getDocSection, listDocTopics } from "../src/docs-sections.js";
|
|
29
33
|
import { diagnoseInstallation } from "../src/doctor.js";
|
|
34
|
+
import { sweepAbandonedCandidates } from "../src/engine/candidate-database.js";
|
|
30
35
|
import { createEngine, describeThrown, KNODIN_SCHEMA_VERSION, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
|
|
31
36
|
import { runSeal } from "../src/engine/seal-command.js";
|
|
32
37
|
import { runSealedQuery } from "../src/engine/sealed-query.js";
|
|
33
38
|
import { resolveDbPath } from "../src/engine/state-paths.js";
|
|
39
|
+
import { deliverEvidenceBundle } from "../src/evidence-bundle.js";
|
|
40
|
+
import { createEvidenceGraphAdapter } from "../src/evidence-graph.js";
|
|
34
41
|
import { diagnoseFailure, } from "../src/failure-diagnosis.js";
|
|
35
42
|
import { gitExecutable } from "../src/git-executable.js";
|
|
36
43
|
import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
|
|
44
|
+
import { findImplementationCandidates } from "../src/implementation-search.js";
|
|
37
45
|
import { createIndexActivityReporter } from "../src/index-activity.js";
|
|
46
|
+
import { indexCoverageCore, indexCoverageNotice, indexCoverageSnapshot, } from "../src/index-coverage-read.js";
|
|
38
47
|
import { InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, repairLifecycleRouting, } from "../src/init.js";
|
|
39
48
|
import { createInitProgressRenderer } from "../src/init-progress.js";
|
|
49
|
+
import { buildTaskInvestigation } from "../src/investigation.js";
|
|
40
50
|
import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
|
|
41
51
|
import { acknowledgeUpdateFailure, queryOwnerAvailability, readManagerUpdateState, readUpdateJournal, resolveManagerOwnership, setManualPin, unpinUpdate, updateAttention, writeManagerUpdateState, } from "../src/manager-update.js";
|
|
42
52
|
import { addMirror, listMirrors, refreshMirror, removeMirror } from "../src/mirror.js";
|
|
43
53
|
import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
|
|
44
54
|
import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
|
|
45
55
|
import { auditPullRequests } from "../src/pr-triage.js";
|
|
46
|
-
import { deliverProgressiveEvidence, } from "../src/progressive-evidence.js";
|
|
56
|
+
import { deliverProgressiveEvidence, evidenceHandleSecret, } from "../src/progressive-evidence.js";
|
|
47
57
|
import { acquireRepairLease } from "../src/repair-lease.js";
|
|
48
58
|
import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, resolveRepairProgressMode, serializeRepairJsonlRecord, } from "../src/repair-progress.js";
|
|
49
59
|
import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
|
|
@@ -51,6 +61,8 @@ import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, i
|
|
|
51
61
|
import { applyResponseBudget } from "../src/response-budget.js";
|
|
52
62
|
import { appendSessionEvent, clearSessionTelemetry, disableSessionTelemetry, enableSessionTelemetry, readSessionEvents, sessionTelemetryStatus, } from "../src/session-telemetry.js";
|
|
53
63
|
import { inspectKnodinSkills, installKnodinSkills, KNODIN_SKILLS, removeKnodinSkills, } from "../src/skill-management.js";
|
|
64
|
+
import { inventoryStorage } from "../src/storage-inventory.js";
|
|
65
|
+
import { configureStoragePolicy, postOperationStorageMaintenance, resolveStoragePolicy, } from "../src/storage-management.js";
|
|
54
66
|
import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
|
|
55
67
|
import { applySystemPlan, planSystemImport, planSystemLink, planSystemRelate, planSystemUnlink, planSystemUnrelate, } from "../src/system-management.js";
|
|
56
68
|
import { coordinationStatus } from "../src/update-coordination.js";
|
|
@@ -161,15 +173,21 @@ function formatSemanticGap(readiness) {
|
|
|
161
173
|
}
|
|
162
174
|
function formatIndexHuman(result) {
|
|
163
175
|
const semantic = formatSemanticGap(result.semanticReadiness);
|
|
176
|
+
const scope = formatIndexCoverageHuman(result.indexCoverage);
|
|
164
177
|
if (result.indexed.length === 0 && result.unchanged.length > 0) {
|
|
165
178
|
const noun = result.unchanged.length === 1 ? "file" : "files";
|
|
166
|
-
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.${semantic}\n`;
|
|
179
|
+
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.${semantic}\n${scope}\n`;
|
|
167
180
|
}
|
|
168
181
|
const unchanged = result.unchanged.length > 0
|
|
169
182
|
? `; ${result.unchanged.length.toLocaleString()} already current`
|
|
170
183
|
: "";
|
|
171
184
|
const noun = result.indexed.length === 1 ? "file" : "files";
|
|
172
|
-
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.${semantic}\n`;
|
|
185
|
+
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.${semantic}\n${scope}\n`;
|
|
186
|
+
}
|
|
187
|
+
function formatIndexCoverageHuman(coverage) {
|
|
188
|
+
if (!coverage)
|
|
189
|
+
return "Local graph index coverage is unknown; a negative graph answer does not establish absence.";
|
|
190
|
+
return `Local graph index coverage: ${coverage.mode}; intent ${coverage.intentState}; repository inventory complete: ${coverage.repositoryComplete}. ${indexCoverageNotice(coverage) ?? "Inventory extent does not establish complete static relationships or runtime use."}`;
|
|
173
191
|
}
|
|
174
192
|
function formatIndexVerificationError(result) {
|
|
175
193
|
const firstIssue = result.verification.missing.files[0] ?? result.verification.missing.records[0];
|
|
@@ -414,7 +432,7 @@ function formatStatusHuman(result) {
|
|
|
414
432
|
const indexedCounts = result.coverage.countsUnknown
|
|
415
433
|
? "indexed and symbol counts unknown; the graph could not be read"
|
|
416
434
|
: `${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols`;
|
|
417
|
-
const coverage = `${result.coverage.sourceFiles} source files, ${indexedCounts}${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}`;
|
|
435
|
+
const coverage = `${result.coverage.sourceFiles} source files, ${indexedCounts}${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}. ${formatIndexCoverageHuman(result.indexCoverage)}`;
|
|
418
436
|
if (result.status === "indexing" && result.activity) {
|
|
419
437
|
const count = result.activity.phaseTotal === undefined
|
|
420
438
|
? ""
|
|
@@ -1274,6 +1292,8 @@ async function main() {
|
|
|
1274
1292
|
const depth = invocation.options.depth;
|
|
1275
1293
|
const retentionDays = invocation.options.retentionDays;
|
|
1276
1294
|
const keepNewest = invocation.options.keepNewest;
|
|
1295
|
+
const maxCount = invocation.options.maxCount;
|
|
1296
|
+
const maxAllocatedBytes = invocation.options.maxBytes;
|
|
1277
1297
|
let output;
|
|
1278
1298
|
if (action === "list")
|
|
1279
1299
|
output = listBackups(defaultRoots(), { depth });
|
|
@@ -1282,16 +1302,70 @@ async function main() {
|
|
|
1282
1302
|
depth,
|
|
1283
1303
|
retentionDays,
|
|
1284
1304
|
keepNewest,
|
|
1305
|
+
maxCount,
|
|
1306
|
+
maxAllocatedBytes,
|
|
1285
1307
|
apply: invocation.options.apply === true,
|
|
1286
1308
|
});
|
|
1287
1309
|
}
|
|
1310
|
+
else if (action === "policy") {
|
|
1311
|
+
output = defaultRoots().map((repository) => {
|
|
1312
|
+
if (retentionAction === "status")
|
|
1313
|
+
return {
|
|
1314
|
+
repository,
|
|
1315
|
+
policy: resolveStoragePolicy(repository),
|
|
1316
|
+
inventory: inventoryStorage(repository),
|
|
1317
|
+
};
|
|
1318
|
+
if (retentionAction !== "preview" && retentionAction !== "apply")
|
|
1319
|
+
throw new Error("knodin backups policy requires preview, apply, or status");
|
|
1320
|
+
const changes = {
|
|
1321
|
+
apply: retentionAction === "apply",
|
|
1322
|
+
};
|
|
1323
|
+
if (maxCount !== undefined)
|
|
1324
|
+
changes.maxCount = maxCount;
|
|
1325
|
+
if (maxAllocatedBytes !== undefined)
|
|
1326
|
+
changes.maxAllocatedBytes = maxAllocatedBytes;
|
|
1327
|
+
if (invocation.options.reserveBytes !== undefined)
|
|
1328
|
+
changes.minFreeBytes = invocation.options.reserveBytes;
|
|
1329
|
+
const mode = invocation.options.mode;
|
|
1330
|
+
if (mode !== undefined) {
|
|
1331
|
+
changes.enabled = mode !== "disabled";
|
|
1332
|
+
changes.dryRun = mode === "preview";
|
|
1333
|
+
}
|
|
1334
|
+
const configured = configureStoragePolicy(repository, changes);
|
|
1335
|
+
const maintenance = retentionAction === "apply" ? postOperationStorageMaintenance(repository) : null;
|
|
1336
|
+
const candidateWarnings = [];
|
|
1337
|
+
const candidatesRemoved = maintenance?.policy?.enabled && !maintenance.policy.dryRun
|
|
1338
|
+
? sweepAbandonedCandidates(repository, {
|
|
1339
|
+
onDiagnostic: (warning) => {
|
|
1340
|
+
if (candidateWarnings.length < 8)
|
|
1341
|
+
candidateWarnings.push(warning.reason.slice(0, 512));
|
|
1342
|
+
},
|
|
1343
|
+
})
|
|
1344
|
+
: 0;
|
|
1345
|
+
return {
|
|
1346
|
+
repository,
|
|
1347
|
+
...configured,
|
|
1348
|
+
inventory: inventoryStorage(repository),
|
|
1349
|
+
cleanup: retentionAction === "apply"
|
|
1350
|
+
? { ...maintenance, candidatesRemoved, candidateWarnings }
|
|
1351
|
+
: pruneBackups([repository], {
|
|
1352
|
+
...configured.policy,
|
|
1353
|
+
depth: 0,
|
|
1354
|
+
includeRegistry: false,
|
|
1355
|
+
apply: false,
|
|
1356
|
+
}),
|
|
1357
|
+
};
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1288
1360
|
else if (action === "retention" && retentionAction === "install") {
|
|
1289
1361
|
output = installBackupRetention(roots, {
|
|
1290
1362
|
depth,
|
|
1291
1363
|
retentionDays,
|
|
1292
1364
|
keepNewest,
|
|
1365
|
+
maxCount,
|
|
1366
|
+
maxAllocatedBytes,
|
|
1293
1367
|
schedule: invocation.options.schedule,
|
|
1294
|
-
dryRun: invocation.options.dryRun
|
|
1368
|
+
dryRun: invocation.options.dryRun,
|
|
1295
1369
|
launcher: runtimeCommand,
|
|
1296
1370
|
});
|
|
1297
1371
|
}
|
|
@@ -1374,10 +1448,19 @@ async function main() {
|
|
|
1374
1448
|
const repo = resolved.repo;
|
|
1375
1449
|
if (cmd !== "doctor") {
|
|
1376
1450
|
try {
|
|
1377
|
-
maybeRunOpportunisticRetention(repo);
|
|
1451
|
+
const maintenance = maybeRunOpportunisticRetention(repo);
|
|
1452
|
+
const issues = "issues" in maintenance
|
|
1453
|
+
? maintenance.issues
|
|
1454
|
+
: "result" in maintenance && maintenance.result
|
|
1455
|
+
? maintenance.result.skipped.map((entry) => entry.reason)
|
|
1456
|
+
: [];
|
|
1457
|
+
if (issues?.length)
|
|
1458
|
+
process.stderr.write(`knodin storage: ${issues.slice(0, 8).join("; ").slice(0, 4096)}\n`);
|
|
1378
1459
|
}
|
|
1379
|
-
catch {
|
|
1380
|
-
//
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
// Cleanup remains nonfatal, but failures are not hidden or reported as
|
|
1462
|
+
// reclaimed storage. Keep stdout/JSON framing untouched.
|
|
1463
|
+
process.stderr.write(`knodin storage: ${String(error).slice(0, 512)}\n`);
|
|
1381
1464
|
}
|
|
1382
1465
|
}
|
|
1383
1466
|
if (cmd === "agent-event" && !fs.existsSync(resolveDbPath(repo)))
|
|
@@ -1537,6 +1620,8 @@ async function main() {
|
|
|
1537
1620
|
return attemptOpportunisticSharedRestore(repo, engine);
|
|
1538
1621
|
};
|
|
1539
1622
|
let result;
|
|
1623
|
+
let finalizedClassConsumer = false;
|
|
1624
|
+
let classConsumerErrorContract;
|
|
1540
1625
|
let repairOutput;
|
|
1541
1626
|
let repairWasPlan = false;
|
|
1542
1627
|
let repairWasLifecycle = false;
|
|
@@ -1555,7 +1640,7 @@ async function main() {
|
|
|
1555
1640
|
process.exitCode = 1;
|
|
1556
1641
|
return verified;
|
|
1557
1642
|
}
|
|
1558
|
-
return decorateGraphQueryResult(value, verified.state, verified.graph.freshness);
|
|
1643
|
+
return decorateGraphQueryResult(value, verified.state, verified.graph.freshness, verified.graph.indexCoverage, health.graph.indexCoverage);
|
|
1559
1644
|
};
|
|
1560
1645
|
switch (cmd) {
|
|
1561
1646
|
case "shared": {
|
|
@@ -1830,10 +1915,11 @@ async function main() {
|
|
|
1830
1915
|
}
|
|
1831
1916
|
const freshness = status.freshness?.state ?? "unknown";
|
|
1832
1917
|
const cacheKey = JSON.stringify({
|
|
1833
|
-
schemaVersion:
|
|
1918
|
+
schemaVersion: 2,
|
|
1834
1919
|
head: status.freshness?.currentHead,
|
|
1835
1920
|
fingerprint: status.freshness?.workingTree?.indexedFingerprint,
|
|
1836
1921
|
generation: status.indexGeneration,
|
|
1922
|
+
indexCoverage: indexCoverageCore(status.indexCoverage),
|
|
1837
1923
|
budgets: [600, 8192, 12],
|
|
1838
1924
|
});
|
|
1839
1925
|
const cacheEligible = canUseSessionContextCache(status.status, freshness);
|
|
@@ -1848,7 +1934,11 @@ async function main() {
|
|
|
1848
1934
|
cacheHit = false;
|
|
1849
1935
|
try {
|
|
1850
1936
|
const context = await buildKnodinContext(engine, "Orient this coding session", repo, undefined, []);
|
|
1851
|
-
|
|
1937
|
+
const verified = await engine.status(repo, { audit: "cached" });
|
|
1938
|
+
if (indexCoverageSnapshot(status.indexCoverage) !==
|
|
1939
|
+
indexCoverageSnapshot(verified.indexCoverage))
|
|
1940
|
+
throw new Error("Index coverage changed during session context generation");
|
|
1941
|
+
text = renderSessionContext(context, freshness, verified.indexCoverage);
|
|
1852
1942
|
writeSessionContextCache(repo, cacheKey, text);
|
|
1853
1943
|
}
|
|
1854
1944
|
catch {
|
|
@@ -2687,6 +2777,24 @@ async function main() {
|
|
|
2687
2777
|
case "evidence": {
|
|
2688
2778
|
const level = rest[0];
|
|
2689
2779
|
const file = rest[1];
|
|
2780
|
+
const files = selectorValue("--files")?.split(",");
|
|
2781
|
+
if (files) {
|
|
2782
|
+
if (level !== "evidence" && level !== "expand")
|
|
2783
|
+
throw new Error("Evidence bundles require evidence or expand level");
|
|
2784
|
+
if (file && !file.startsWith("--"))
|
|
2785
|
+
throw new Error("Choose a file or --files, not both");
|
|
2786
|
+
result = await deliverEvidenceBundle({
|
|
2787
|
+
repo,
|
|
2788
|
+
files,
|
|
2789
|
+
level,
|
|
2790
|
+
continuation: selectorValue("--continuation"),
|
|
2791
|
+
alreadyPresent: selectorValue("--already-present")?.split(","),
|
|
2792
|
+
byteLimit: responseBudget.bytes,
|
|
2793
|
+
tokenLimit: responseBudget.tokens,
|
|
2794
|
+
itemLimit: responseBudget.items,
|
|
2795
|
+
}, createEvidenceGraphAdapter(engine));
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2690
2798
|
if (!file || !["locate", "outline", "evidence", "expand"].includes(level))
|
|
2691
2799
|
throw new Error("knodin evidence requires locate|outline|evidence|expand <file>");
|
|
2692
2800
|
result = deliverProgressiveEvidence({
|
|
@@ -2717,7 +2825,7 @@ async function main() {
|
|
|
2717
2825
|
? ""
|
|
2718
2826
|
: (rest[1] ?? "");
|
|
2719
2827
|
if (!pattern) {
|
|
2720
|
-
process.stderr.write("knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|file_metrics|batch_outline|project_overview|shortest_path|cross_substrate_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|resource_reachability|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|children_of|federated_repos|mcp_tools|api_contract_mismatches)\n");
|
|
2828
|
+
process.stderr.write("knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|file_metrics|batch_outline|project_overview|shortest_path|cross_substrate_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|resource_reachability|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|class_consumers|children_of|federated_repos|mcp_tools|api_contract_mismatches)\n");
|
|
2721
2829
|
process.exit(1);
|
|
2722
2830
|
}
|
|
2723
2831
|
const directionValue = selectorValue("--direction");
|
|
@@ -2788,6 +2896,82 @@ async function main() {
|
|
|
2788
2896
|
process.exitCode = 1;
|
|
2789
2897
|
break;
|
|
2790
2898
|
}
|
|
2899
|
+
if (pattern === "class_consumers") {
|
|
2900
|
+
if (queryLimitRaw && Number(queryLimitRaw) > 1000)
|
|
2901
|
+
throw new Error("class_consumers --limit must not exceed 1000 sites");
|
|
2902
|
+
if (["--offset", "--kinds", "--relations", "--direction", "--depth", "--exclude-tests"].some((flag) => rest.includes(flag)))
|
|
2903
|
+
throw new Error("class_consumers supports --test-scope/--path and --continuation, not offset/kinds/relations/direction/depth/exclude-tests");
|
|
2904
|
+
const secret = evidenceHandleSecret(repo).secret;
|
|
2905
|
+
const token = selectorValue("--continuation");
|
|
2906
|
+
const cursor = token ? readClassConsumerCursor(token, secret) : undefined;
|
|
2907
|
+
const request = {
|
|
2908
|
+
symbol: target,
|
|
2909
|
+
selector,
|
|
2910
|
+
testScope: (testScopeValue ?? "all"),
|
|
2911
|
+
path: selectorValue("--path"),
|
|
2912
|
+
limit: queryLimitRaw ? Number(queryLimitRaw) : 100,
|
|
2913
|
+
...(cursor
|
|
2914
|
+
? {
|
|
2915
|
+
after: cursor.after,
|
|
2916
|
+
expectedQueryDigest: cursor.queryDigest,
|
|
2917
|
+
expectedSnapshotDigest: cursor.snapshotDigest,
|
|
2918
|
+
}
|
|
2919
|
+
: {}),
|
|
2920
|
+
};
|
|
2921
|
+
try {
|
|
2922
|
+
const page = await engine.queryClassConsumers(repo, request);
|
|
2923
|
+
// Defer maintenance, not the audit, until a later eligible status or close.
|
|
2924
|
+
const verified = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached", checkpoint: "defer" })));
|
|
2925
|
+
if (!verified.available) {
|
|
2926
|
+
result = verified;
|
|
2927
|
+
process.exitCode = 1;
|
|
2928
|
+
break;
|
|
2929
|
+
}
|
|
2930
|
+
await validateClassConsumerDelivery(page, request, (next) => engine.queryClassConsumers(repo, next));
|
|
2931
|
+
result = finalizeClassConsumerPage(page, {
|
|
2932
|
+
secret,
|
|
2933
|
+
budget: responseBudget,
|
|
2934
|
+
decorate: (candidate) => decorateGraphQueryResult(candidate, verified.state, undefined, verified.graph.indexCoverage, queryHealth?.available ? queryHealth.graph.indexCoverage : undefined),
|
|
2935
|
+
});
|
|
2936
|
+
finalizedClassConsumer = true;
|
|
2937
|
+
}
|
|
2938
|
+
catch (error) {
|
|
2939
|
+
if (error instanceof ClassConsumerQueryError &&
|
|
2940
|
+
[
|
|
2941
|
+
"CLASS_CONSUMER_TARGET_NOT_FOUND",
|
|
2942
|
+
"CLASS_CONSUMER_TARGET_AMBIGUOUS",
|
|
2943
|
+
"CLASS_CONSUMER_TARGET_CAPABILITY_MISSING",
|
|
2944
|
+
].includes(error.code)) {
|
|
2945
|
+
result = {
|
|
2946
|
+
available: false,
|
|
2947
|
+
status: "unavailable",
|
|
2948
|
+
pattern,
|
|
2949
|
+
targetResolution: error.code === "CLASS_CONSUMER_TARGET_CAPABILITY_MISSING"
|
|
2950
|
+
? "capability-missing"
|
|
2951
|
+
: error.code === "CLASS_CONSUMER_TARGET_NOT_FOUND"
|
|
2952
|
+
? "not-found"
|
|
2953
|
+
: "ambiguous",
|
|
2954
|
+
code: error.code,
|
|
2955
|
+
error: error.message,
|
|
2956
|
+
candidates: error.candidates,
|
|
2957
|
+
...(queryHealth?.available
|
|
2958
|
+
? {
|
|
2959
|
+
indexCoverage: queryHealth.graph.indexCoverage,
|
|
2960
|
+
freshness: queryHealth.graph.freshness,
|
|
2961
|
+
}
|
|
2962
|
+
: {}),
|
|
2963
|
+
};
|
|
2964
|
+
classConsumerErrorContract = {
|
|
2965
|
+
code: error.code,
|
|
2966
|
+
targetResolution: result.targetResolution,
|
|
2967
|
+
};
|
|
2968
|
+
process.exitCode = 1;
|
|
2969
|
+
}
|
|
2970
|
+
else
|
|
2971
|
+
throw error;
|
|
2972
|
+
}
|
|
2973
|
+
break;
|
|
2974
|
+
}
|
|
2791
2975
|
result = await engine.query(pattern, target, repo, to, queryLimitRaw ? Number(queryLimitRaw) : undefined, depth, detailLevel, selector, pattern === "impact"
|
|
2792
2976
|
? {
|
|
2793
2977
|
mode: selectorValue("--impact-mode") === "file" ? "file" : "symbol",
|
|
@@ -2847,12 +3031,12 @@ async function main() {
|
|
|
2847
3031
|
process.exitCode = 1;
|
|
2848
3032
|
break;
|
|
2849
3033
|
}
|
|
2850
|
-
result = decorateGraphQueryResult(result, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness);
|
|
3034
|
+
result = decorateGraphQueryResult(result, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness, verifiedQueryHealth.graph.indexCoverage, queryHealth.graph.indexCoverage);
|
|
2851
3035
|
}
|
|
2852
3036
|
// A target that does not exist is operator error, not a negative
|
|
2853
3037
|
// answer, so it exits non-zero like an unavailable graph rather than
|
|
2854
3038
|
// like real dead code (KNODIN-28). `resolved` with zero rows keeps
|
|
2855
|
-
// exit 0:
|
|
3039
|
+
// exit 0: no callers were found within the reported index coverage.
|
|
2856
3040
|
if (result?.targetResolution === "not-found")
|
|
2857
3041
|
process.exitCode = 1;
|
|
2858
3042
|
break;
|
|
@@ -3034,7 +3218,26 @@ async function main() {
|
|
|
3034
3218
|
process.stderr.write('knodin context requires a "<task>" description\n');
|
|
3035
3219
|
process.exit(1);
|
|
3036
3220
|
}
|
|
3037
|
-
|
|
3221
|
+
const symbol = selectorValue("--symbol");
|
|
3222
|
+
if (rest.includes("--implementations")) {
|
|
3223
|
+
if (symbol)
|
|
3224
|
+
throw new Error("Choose --implementations or --symbol, not both");
|
|
3225
|
+
result = await graphRead(() => findImplementationCandidates(engine, repo, {
|
|
3226
|
+
task,
|
|
3227
|
+
limit: selectorValue("--limit") ? Number(selectorValue("--limit")) : undefined,
|
|
3228
|
+
offset: selectorValue("--offset") ? Number(selectorValue("--offset")) : undefined,
|
|
3229
|
+
}));
|
|
3230
|
+
break;
|
|
3231
|
+
}
|
|
3232
|
+
result = await graphRead(() => symbol
|
|
3233
|
+
? buildTaskInvestigation(engine, repo, {
|
|
3234
|
+
task,
|
|
3235
|
+
symbol,
|
|
3236
|
+
...selector,
|
|
3237
|
+
limit: selectorValue("--limit") ? Number(selectorValue("--limit")) : undefined,
|
|
3238
|
+
depth: selectorValue("--depth") ? Number(selectorValue("--depth")) : undefined,
|
|
3239
|
+
})
|
|
3240
|
+
: buildKnodinContext(engine, task, repo, rest[1]));
|
|
3038
3241
|
break;
|
|
3039
3242
|
}
|
|
3040
3243
|
default:
|
|
@@ -3042,6 +3245,8 @@ async function main() {
|
|
|
3042
3245
|
process.exit(1);
|
|
3043
3246
|
}
|
|
3044
3247
|
await engine.close();
|
|
3248
|
+
if (result?.available === false)
|
|
3249
|
+
process.exitCode = 1;
|
|
3045
3250
|
if (agentEventOutput) {
|
|
3046
3251
|
if (result !== null && result !== undefined)
|
|
3047
3252
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
@@ -3050,9 +3255,31 @@ async function main() {
|
|
|
3050
3255
|
if (statusWasWatched)
|
|
3051
3256
|
return;
|
|
3052
3257
|
const fileMetricsOutput = cmd === "query" && rest[0] === "file_metrics";
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3258
|
+
// The bundle protocol budgets its signed manifest, source and continuation together.
|
|
3259
|
+
// Generic post-truncation would invalidate that recoverable envelope.
|
|
3260
|
+
const evidenceBundle = cmd === "evidence" && selectorValue("--files") !== undefined;
|
|
3261
|
+
const boundedResult = evidenceBundle || finalizedClassConsumer
|
|
3262
|
+
? result
|
|
3263
|
+
: applyResponseBudget(result, cmd === "query" && rest[0] === "flow_analysis" ? "query:flow_analysis" : cmd, responseBudget, fileMetricsOutput
|
|
3264
|
+
? { bytes: 16_777_216, tokens: 4_194_304, items: 100_000 }
|
|
3265
|
+
: { bytes: 65_536, tokens: 16_384, items: 100 });
|
|
3266
|
+
if (classConsumerErrorContract) {
|
|
3267
|
+
const delivered = boundedResult;
|
|
3268
|
+
if (delivered.code !== classConsumerErrorContract.code ||
|
|
3269
|
+
delivered.targetResolution !== classConsumerErrorContract.targetResolution ||
|
|
3270
|
+
delivered.available !== false ||
|
|
3271
|
+
delivered.status !== "unavailable")
|
|
3272
|
+
throw new Error("Response budget cannot preserve the class-consumer error contract; increase the budget.");
|
|
3273
|
+
}
|
|
3274
|
+
if (cmd === "query" && rest[0] === "inheritors_of") {
|
|
3275
|
+
const qualification = result
|
|
3276
|
+
?.inheritanceQuery;
|
|
3277
|
+
const delivered = boundedResult
|
|
3278
|
+
?.inheritanceQuery;
|
|
3279
|
+
if (qualification &&
|
|
3280
|
+
["version", "mode", "partial", "truncated", "apex"].some((key) => delivered?.[key] !== qualification[key]))
|
|
3281
|
+
throw new Error("Response budget cannot preserve the inheritance qualification; increase the budget.");
|
|
3282
|
+
}
|
|
3056
3283
|
const finalExitCode = Math.max(Number(process.exitCode ?? 0), repairExitCode);
|
|
3057
3284
|
if (cmd === "init" && !jsonOutput) {
|
|
3058
3285
|
process.stdout.write(formatInitHuman(boundedResult));
|
package/dist/src/agent-events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { indexCoverageCore, indexCoverageNotice } from "./index-coverage-read.js";
|
|
3
4
|
import { countOutputTokens } from "./output-telemetry.js";
|
|
4
5
|
import { appendSessionEvent, } from "./session-telemetry.js";
|
|
5
6
|
export const SESSION_CONTEXT_BUDGET_LINE = "Budget: at most 600 tokens, 8 KiB, 12 items.";
|
|
@@ -81,23 +82,40 @@ export function writeSessionContextCache(repo, key, text) {
|
|
|
81
82
|
export function canUseSessionContextCache(status, freshness) {
|
|
82
83
|
return status === "healthy" && freshness === "fresh";
|
|
83
84
|
}
|
|
84
|
-
export function renderSessionContext(context, freshness) {
|
|
85
|
+
export function renderSessionContext(context, freshness, coverage) {
|
|
85
86
|
const list = (values, render) => (values ?? []).slice(0, 3).map(render).join(", ") || "none reported";
|
|
86
|
-
|
|
87
|
+
const header = [
|
|
87
88
|
"## knodin session context",
|
|
88
89
|
`Graph evidence: ${freshness}. Treat stale or unavailable evidence as incomplete.`,
|
|
89
90
|
SESSION_CONTEXT_BUDGET_LINE,
|
|
90
|
-
`
|
|
91
|
+
`Index coverage: ${JSON.stringify(indexCoverageCore(coverage) ?? { mode: "unknown", repositoryComplete: false, negativeScope: "unknown" })}`,
|
|
92
|
+
indexCoverageNotice(coverage) ??
|
|
93
|
+
(coverage
|
|
94
|
+
? "Inventory extent does not prove complete relationships or runtime use."
|
|
95
|
+
: "Index coverage is unknown; negatives do not establish repository-wide absence."),
|
|
96
|
+
];
|
|
97
|
+
const details = [
|
|
98
|
+
`Indexed scope: ${context.stats?.files ?? "?"} files, ${context.stats?.symbols ?? "?"} symbols.`,
|
|
91
99
|
`Subsystems: ${list(context.communities, (value) => `${value.name} (${value.size})`)}`,
|
|
92
100
|
`Hubs: ${list(context.hubs, (value) => `${value.symbol} (${value.degree})`)}`,
|
|
93
101
|
`Flows: ${list(context.flows, (value) => value.symbol ?? "unnamed")}`,
|
|
94
102
|
`Suggested next operation: ${context.suggestedOperation ?? "context"} (heuristic only).`,
|
|
95
103
|
"Use exact source evidence and preserve ambiguity, freshness, omissions, and response budgets.",
|
|
96
|
-
]
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
];
|
|
105
|
+
let truncated = false;
|
|
106
|
+
for (;;) {
|
|
107
|
+
const text = [
|
|
108
|
+
...header,
|
|
109
|
+
...details,
|
|
110
|
+
...(truncated ? ["Context truncated to the configured session budget."] : []),
|
|
111
|
+
].join("\n");
|
|
112
|
+
if (countOutputTokens(text) <= 600 && Buffer.byteLength(text) <= 8192)
|
|
113
|
+
return text;
|
|
114
|
+
if (details.length === 0)
|
|
115
|
+
throw new RangeError("Session context budget cannot preserve index coverage");
|
|
116
|
+
details.pop();
|
|
117
|
+
truncated = true;
|
|
99
118
|
}
|
|
100
|
-
return text;
|
|
101
119
|
}
|
|
102
120
|
export function recordClaudeLifecycleEvent(repo, event, payload) {
|
|
103
121
|
const sessionId = payload.session_id;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export const MAX_CURSOR_PAYLOAD_BYTES = 4096;
|
|
3
|
+
export const MAX_AUTHENTICATED_CURSOR_LENGTH = 6144;
|
|
4
|
+
const DOMAIN = "knodin:authenticated-cursor:v1\0";
|
|
5
|
+
function checkParameters(secret, prefix) {
|
|
6
|
+
if (!Buffer.isBuffer(secret) || secret.length !== 32)
|
|
7
|
+
throw new TypeError("cursor signing key must be 32 bytes");
|
|
8
|
+
if (!/^[a-z][a-z0-9]{1,15}$/.test(prefix))
|
|
9
|
+
throw new TypeError("invalid cursor prefix");
|
|
10
|
+
}
|
|
11
|
+
function signature(secret, prefix, body) {
|
|
12
|
+
return createHmac("sha256", secret)
|
|
13
|
+
.update(DOMAIN)
|
|
14
|
+
.update(prefix)
|
|
15
|
+
.update("\0")
|
|
16
|
+
.update(body)
|
|
17
|
+
.digest();
|
|
18
|
+
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const prototype = Object.getPrototypeOf(value);
|
|
23
|
+
return prototype === Object.prototype || prototype === null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Authentication only, not encryption, schema validation or a freshness lease.
|
|
27
|
+
* Callers supply the existing per-repository evidence secret and a distinct
|
|
28
|
+
* protocol prefix; they must validate payload shape and current query/snapshot.
|
|
29
|
+
* Sign only the boundary of rows actually delivered after response budgeting.
|
|
30
|
+
*/
|
|
31
|
+
export function signAuthenticatedCursor(value, secret, prefix) {
|
|
32
|
+
checkParameters(secret, prefix);
|
|
33
|
+
let serialized;
|
|
34
|
+
try {
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
throw new Error("cursor payload is not a record");
|
|
37
|
+
serialized = JSON.stringify(value);
|
|
38
|
+
if (typeof serialized !== "string" || !isRecord(JSON.parse(serialized)))
|
|
39
|
+
throw new Error("serialized cursor payload is not a record");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new TypeError("cursor payload must be a serializable JSON object");
|
|
43
|
+
}
|
|
44
|
+
if (Buffer.byteLength(serialized) > MAX_CURSOR_PAYLOAD_BYTES)
|
|
45
|
+
throw new RangeError("cursor payload exceeds its byte limit");
|
|
46
|
+
const body = Buffer.from(serialized).toString("base64url");
|
|
47
|
+
return `${prefix}.${body}.${signature(secret, prefix, body).toString("base64url")}`;
|
|
48
|
+
}
|
|
49
|
+
export function verifyAuthenticatedCursor(value, secret, prefix) {
|
|
50
|
+
checkParameters(secret, prefix);
|
|
51
|
+
const invalid = () => new Error("invalid authenticated cursor");
|
|
52
|
+
if (typeof value !== "string" || value.length > MAX_AUTHENTICATED_CURSOR_LENGTH)
|
|
53
|
+
throw invalid();
|
|
54
|
+
const parts = value.split(".");
|
|
55
|
+
if (parts.length !== 3 || parts[0] !== prefix)
|
|
56
|
+
throw invalid();
|
|
57
|
+
const [, body, encodedSignature] = parts;
|
|
58
|
+
if (!/^[A-Za-z0-9_-]+$/.test(body) || !/^[A-Za-z0-9_-]+$/.test(encodedSignature))
|
|
59
|
+
throw invalid();
|
|
60
|
+
const actual = Buffer.from(encodedSignature, "base64url");
|
|
61
|
+
const expected = signature(secret, prefix, body);
|
|
62
|
+
if (actual.length !== expected.length ||
|
|
63
|
+
actual.toString("base64url") !== encodedSignature ||
|
|
64
|
+
!timingSafeEqual(actual, expected))
|
|
65
|
+
throw invalid();
|
|
66
|
+
const bytes = Buffer.from(body, "base64url");
|
|
67
|
+
if (bytes.length > MAX_CURSOR_PAYLOAD_BYTES || bytes.toString("base64url") !== body)
|
|
68
|
+
throw invalid();
|
|
69
|
+
const text = bytes.toString("utf8");
|
|
70
|
+
if (!Buffer.from(text).equals(bytes))
|
|
71
|
+
throw invalid();
|
|
72
|
+
try {
|
|
73
|
+
const payload = JSON.parse(text);
|
|
74
|
+
if (isRecord(payload))
|
|
75
|
+
return payload;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Never echo untrusted cursor contents or decoder error details.
|
|
79
|
+
}
|
|
80
|
+
throw invalid();
|
|
81
|
+
}
|