knodin 0.8.4 → 0.8.6

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 (34) hide show
  1. package/dist/bin/cli.js +272 -24
  2. package/dist/src/cli-model.js +26 -0
  3. package/dist/src/engine/candidate-database.js +167 -0
  4. package/dist/src/engine/embeddings.js +8 -5
  5. package/dist/src/engine/index.js +1244 -305
  6. package/dist/src/engine/prune.js +8 -0
  7. package/dist/src/file-metrics.js +153 -0
  8. package/dist/src/repair-lease.js +8 -5
  9. package/dist/src/response-budget.js +2 -1
  10. package/dist/src/shared-index/artifact.js +65 -0
  11. package/dist/src/shared-index/cache.js +267 -0
  12. package/dist/src/shared-index/compatibility.js +70 -0
  13. package/dist/src/shared-index/config.js +224 -0
  14. package/dist/src/shared-index/contract.js +117 -0
  15. package/dist/src/shared-index/manifest.js +71 -0
  16. package/dist/src/shared-index/opportunistic.js +37 -0
  17. package/dist/src/shared-index/overlay.js +98 -0
  18. package/dist/src/shared-index/provenance.js +149 -0
  19. package/dist/src/shared-index/publisher.js +198 -0
  20. package/dist/src/shared-index/restore.js +209 -0
  21. package/dist/src/shared-index/s3-client.js +124 -0
  22. package/dist/src/shared-index/schemas.js +86 -0
  23. package/dist/src/shared-index/selection.js +111 -0
  24. package/dist/src/tools/knodin-tools.js +70 -3
  25. package/docs/CLI.md +39 -0
  26. package/docs/MCP.md +12 -0
  27. package/docs/SHARED-INDEX-CONTRACT.md +132 -0
  28. package/docs/releases/0.8.5.md +25 -0
  29. package/docs/releases/0.8.6.md +10 -0
  30. package/package.json +12 -3
  31. package/schemas/shared-index-branch-pointer-v1.schema.json +66 -0
  32. package/schemas/shared-index-config-v1.schema.json +97 -0
  33. package/schemas/shared-index-manifest-v1.schema.json +104 -0
  34. package/schemas/shared-index-provenance-v1.schema.json +100 -0
package/dist/bin/cli.js CHANGED
@@ -365,7 +365,7 @@ async function runRemoteCommand(args) {
365
365
  * empty result this whole feature exists to avoid.
366
366
  */
367
367
  async function indexMirror(source, deferSemantic) {
368
- const engine = createEngine();
368
+ const engine = createEngine({ watcher: "disabled" });
369
369
  try {
370
370
  await engine.index(source, undefined, true, { skipEmbeddings: true });
371
371
  const structural = await engine.status(source, { audit: "deep" });
@@ -420,11 +420,18 @@ function formatStatusHuman(result) {
420
420
  const freshnessLine = `Freshness: ${result.freshness.state}; indexed ${head(result.freshness.indexedHead)}, ` +
421
421
  `current ${head(result.freshness.currentHead)} (${result.freshness.commitRelation}${distance}); ` +
422
422
  `${result.freshness.workingTree.pendingPaths ?? "unknown"} pending path(s).\n` +
423
- `Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n`;
424
- if (result.status === "healthy")
425
- return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).\n${freshnessLine}${lifecycleLine}${integrationLine}`;
423
+ `Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n` +
424
+ `Freshness mechanism: ${result.freshnessMechanism.strategy}; watcher ${result.freshnessMechanism.watcher}${result.freshnessMechanism.diagnostic ? ` (${result.freshnessMechanism.diagnostic})` : ""}.\n`;
425
+ if (result.status === "healthy") {
426
+ const auditNote = result.verification.mode === "persisted-audit"
427
+ ? ` Cached deep-audit evidence from ${result.verification.auditVerifiedAt ?? "an earlier run"}; freshness probed ${result.verification.verifiedAt ?? "now"}. Run \`knodin status --deep\` for an exact current audit.`
428
+ : "";
429
+ return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).${auditNote}\n${freshnessLine}${lifecycleLine}${integrationLine}`;
430
+ }
426
431
  if (result.status === "stale")
427
- return `Graph content is intact but evidence is stale (${coverage}).\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
432
+ return `Graph content is intact but evidence is stale (${coverage}).${result.verification.mode === "persisted-audit"
433
+ ? ` Cached deep-audit evidence is from ${result.verification.auditVerifiedAt ?? "an earlier run"}; freshness was probed ${result.verification.verifiedAt ?? "now"}. Run \`knodin status --deep\` for an exact current audit.`
434
+ : ""}\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
428
435
  const outstanding = result.missing.files.length + result.missing.records.length;
429
436
  const firstIssue = result.missing.files[0] ?? result.missing.records[0];
430
437
  const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
@@ -634,7 +641,7 @@ async function main() {
634
641
  }, 50);
635
642
  interval.unref();
636
643
  process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
637
- const engine = createEngine();
644
+ const engine = createEngine({ watcher: "disabled" });
638
645
  try {
639
646
  const systemConfig = loadSystemConfiguration(repository);
640
647
  const summary = await initializeRepositories([repository], {
@@ -689,7 +696,11 @@ async function main() {
689
696
  const runtimeCommand = resolveCliRuntimeCommand(process);
690
697
  // Internal machine-to-machine commands stay JSON regardless of TTY state.
691
698
  // when an older installed hook predates the explicit --json argument.
692
- const jsonOutput = invocation.options.json === true || cmd === "hook-refresh" || cmd === "agent-event";
699
+ const jsonOutput = invocation.options.json === true ||
700
+ invocation.options.format === "json" ||
701
+ cmd === "hook-refresh" ||
702
+ cmd === "agent-event" ||
703
+ (cmd === "shared" && rawRest.includes("--jsonl"));
693
704
  // `--json` is a shared output flag. Repair owns its richer --json/--jsonl
694
705
  // parser; all other commands receive their original arguments minus it.
695
706
  const rest = cmd === "repair" ? rawRest : rawRest.filter((argument) => argument !== "--json");
@@ -756,7 +767,7 @@ async function main() {
756
767
  if (!plan.json) {
757
768
  process.stderr.write("warning: `knodin fleet init` is deprecated; use `knodin repos init --linked-worktrees=skip|include` (alias retained for two minor releases)\n");
758
769
  }
759
- const engine = createEngine();
770
+ const engine = createEngine({ watcher: "disabled" });
760
771
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
761
772
  const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
762
773
  const summary = await initializeRepositories(plan.roots, {
@@ -793,7 +804,7 @@ async function main() {
793
804
  depth: plan.depth,
794
805
  linkedWorktrees: plan.linkedWorktrees,
795
806
  });
796
- const engine = createEngine();
807
+ const engine = createEngine({ watcher: "disabled" });
797
808
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
798
809
  const repositories = [];
799
810
  const discoveryRecords = plan.signals
@@ -860,7 +871,7 @@ async function main() {
860
871
  process.exitCode = summary.exitCode;
861
872
  return;
862
873
  }
863
- const engine = createEngine();
874
+ const engine = createEngine({ watcher: "disabled" });
864
875
  const summary = await initializeRepositories(plan.roots, {
865
876
  command: runtimeCommand,
866
877
  depth: plan.depth,
@@ -898,7 +909,7 @@ async function main() {
898
909
  return;
899
910
  }
900
911
  if (plan.command === "search") {
901
- const engine = createEngine();
912
+ const engine = createEngine({ watcher: "disabled" });
902
913
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
903
914
  const output = await searchRepositories(plan.roots, plan.query ?? "", {
904
915
  depth: plan.depth,
@@ -927,7 +938,7 @@ async function main() {
927
938
  depth: plan.depth,
928
939
  linkedWorktrees: plan.linkedWorktrees,
929
940
  });
930
- const engine = createEngine();
941
+ const engine = createEngine({ watcher: "disabled" });
931
942
  const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
932
943
  const repositories = [];
933
944
  for (const repository of discovery.repositories) {
@@ -1167,7 +1178,7 @@ async function main() {
1167
1178
  let compressionResult;
1168
1179
  let diagnosisUnavailable = false;
1169
1180
  if (action === "diagnose") {
1170
- const diagnosisEngine = createEngine();
1181
+ const diagnosisEngine = createEngine({ watcher: "disabled" });
1171
1182
  try {
1172
1183
  const health = await inspectGraphQueryHealth(repo, (target) => diagnosisEngine.status(target, { audit: "cached" }));
1173
1184
  if (health.available) {
@@ -1259,7 +1270,13 @@ async function main() {
1259
1270
  process.stdout.write(formatGenericHuman("compress delete", compressionResult));
1260
1271
  return;
1261
1272
  }
1262
- const engine = createEngine();
1273
+ const engine = createEngine({ watcher: "disabled" });
1274
+ const opportunisticSharedRestore = async () => {
1275
+ if (!fs.existsSync(path.join(repo, ".knodin", "shared-index.yaml")))
1276
+ return null;
1277
+ const { attemptOpportunisticSharedRestore } = await import("../src/shared-index/opportunistic.js");
1278
+ return attemptOpportunisticSharedRestore(repo, engine);
1279
+ };
1263
1280
  let result;
1264
1281
  let repairOutput;
1265
1282
  let repairWasPlan = false;
@@ -1281,6 +1298,213 @@ async function main() {
1281
1298
  return decorateGraphQueryResult(value, verified.state, verified.graph.freshness);
1282
1299
  };
1283
1300
  switch (cmd) {
1301
+ case "shared": {
1302
+ const [cacheModule, compatibilityModule, configModule, manifestModule, provenanceModule, publisherModule, restoreModule, s3Module, selectionModule,] = await Promise.all([
1303
+ import("../src/shared-index/cache.js"),
1304
+ import("../src/shared-index/compatibility.js"),
1305
+ import("../src/shared-index/config.js"),
1306
+ import("../src/shared-index/manifest.js"),
1307
+ import("../src/shared-index/provenance.js"),
1308
+ import("../src/shared-index/publisher.js"),
1309
+ import("../src/shared-index/restore.js"),
1310
+ import("../src/shared-index/s3-client.js"),
1311
+ import("../src/shared-index/selection.js"),
1312
+ ]);
1313
+ const { SharedIndexCache } = cacheModule;
1314
+ const { currentSharedIndexCompatibility } = compatibilityModule;
1315
+ const { loadSharedIndexConfig, publicKeyFingerprint, writeSharedIndexConfig } = configModule;
1316
+ const { branchPointerKey } = manifestModule;
1317
+ const { readSharedIndexProvenance, readSharedRestoreAttempt } = provenanceModule;
1318
+ const { createSharedPublisherBundle, finalizeSharedPublisherBundle, guardPublisherPointer, prepareSharedPublisherDatabase, } = publisherModule;
1319
+ const { restoreSharedIndex } = restoreModule;
1320
+ const { createSharedObjectStore, SharedIndexError } = s3Module;
1321
+ const { configuredBranchCandidates } = selectionModule;
1322
+ const action = invocation.commandPath[1];
1323
+ if (action === "configure") {
1324
+ const existing = loadSharedIndexConfig(repo);
1325
+ if (invocation.options.disable === true) {
1326
+ if (existing.state !== "configured")
1327
+ throw new Error("knodin shared configure --disable requires a valid existing configuration");
1328
+ writeSharedIndexConfig(repo, { ...existing.config, enabled: false });
1329
+ result = { state: "configured", config: { ...existing.config, enabled: false } };
1330
+ break;
1331
+ }
1332
+ const required = (name) => {
1333
+ const value = invocation.options[name];
1334
+ if (typeof value !== "string" || !value)
1335
+ throw new Error(`knodin shared configure requires --${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`);
1336
+ return value;
1337
+ };
1338
+ const publicKeyPath = path.resolve(process.cwd(), required("publicKey"));
1339
+ const publicKeyPem = fs.readFileSync(publicKeyPath, "utf8");
1340
+ const branches = Array.isArray(invocation.options.branch)
1341
+ ? invocation.options.branch.filter((value) => typeof value === "string")
1342
+ : [];
1343
+ if (branches.length === 0)
1344
+ throw new Error("knodin shared configure requires --branch");
1345
+ const config = {
1346
+ schemaVersion: 1,
1347
+ enabled: true,
1348
+ registrationId: required("registrationId"),
1349
+ aws: { region: required("region"), bucket: required("bucket") },
1350
+ repository: {
1351
+ host: required("host"),
1352
+ owner: required("owner"),
1353
+ name: required("name"),
1354
+ },
1355
+ branches,
1356
+ signing: {
1357
+ algorithm: "RSASSA_PSS_SHA_256",
1358
+ keyId: required("keyId"),
1359
+ publicKeySha256: publicKeyFingerprint(publicKeyPem),
1360
+ publicKeyPem,
1361
+ },
1362
+ };
1363
+ const target = writeSharedIndexConfig(repo, config);
1364
+ result = { state: "configured", path: target, config: loadSharedIndexConfig(repo) };
1365
+ break;
1366
+ }
1367
+ if (action === "publisher-metadata") {
1368
+ const configuration = loadSharedIndexConfig(repo);
1369
+ if (configuration.state !== "configured")
1370
+ throw new Error("knodin shared publisher-metadata requires valid reviewed configuration");
1371
+ result = prepareSharedPublisherDatabase(repo);
1372
+ break;
1373
+ }
1374
+ if (action === "publisher-bundle") {
1375
+ const configuration = loadSharedIndexConfig(repo);
1376
+ if (configuration.state !== "configured")
1377
+ throw new Error("knodin shared publisher-bundle requires valid reviewed configuration");
1378
+ const branch = invocation.positionals[0];
1379
+ const output = invocation.options.output;
1380
+ if (!branch || typeof output !== "string")
1381
+ throw new Error("knodin shared publisher-bundle requires <branch> --output <path>");
1382
+ result = await createSharedPublisherBundle({
1383
+ repo,
1384
+ config: configuration.config,
1385
+ branch,
1386
+ outputDirectory: path.resolve(process.cwd(), output),
1387
+ });
1388
+ break;
1389
+ }
1390
+ if (action === "publisher-finalize") {
1391
+ const configuration = loadSharedIndexConfig(repo);
1392
+ if (configuration.state !== "configured")
1393
+ throw new Error("knodin shared publisher-finalize requires valid reviewed configuration");
1394
+ const bundlePath = invocation.positionals[0];
1395
+ if (!bundlePath)
1396
+ throw new Error("knodin shared publisher-finalize requires <bundle>");
1397
+ const bundle = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), bundlePath), "utf8"));
1398
+ const pointer = finalizeSharedPublisherBundle({ config: configuration.config, bundle });
1399
+ result = { bundle, pointer: JSON.parse(pointer.toString("utf8")) };
1400
+ break;
1401
+ }
1402
+ if (action === "publisher-guard") {
1403
+ const configuration = loadSharedIndexConfig(repo);
1404
+ if (configuration.state !== "configured")
1405
+ throw new Error("knodin shared publisher-guard requires valid reviewed configuration");
1406
+ const [pointerPath, branch, commit] = invocation.positionals;
1407
+ if (!pointerPath || !branch || !commit)
1408
+ throw new Error("knodin shared publisher-guard requires <pointer> <branch> <commit>");
1409
+ guardPublisherPointer({
1410
+ repo,
1411
+ config: configuration.config,
1412
+ branch,
1413
+ newCommit: commit,
1414
+ existingPointerBytes: fs.readFileSync(path.resolve(process.cwd(), pointerPath)),
1415
+ });
1416
+ result = { allowed: true, branch, commit };
1417
+ break;
1418
+ }
1419
+ if (action === "status") {
1420
+ const configuration = loadSharedIndexConfig(repo);
1421
+ const graph = await engine.status(repo, { audit: "cached" });
1422
+ let credentialProbe = "not-requested";
1423
+ if (invocation.options.probe === true) {
1424
+ if (configuration.state !== "configured" || !configuration.config.enabled) {
1425
+ credentialProbe = { state: "unavailable", category: "configuration" };
1426
+ }
1427
+ else {
1428
+ const store = createSharedObjectStore(configuration.config);
1429
+ try {
1430
+ const current = spawnSync(gitExecutable(), ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).stdout.trim();
1431
+ const branch = configuredBranchCandidates(configuration.config, current || undefined)[0];
1432
+ if (!store || !branch)
1433
+ throw new SharedIndexError("configuration", "shared-index probe has no configured local branch candidate");
1434
+ await store.head(branchPointerKey(configuration.config, branch));
1435
+ credentialProbe = { state: "available" };
1436
+ }
1437
+ catch (error) {
1438
+ credentialProbe = {
1439
+ state: "unavailable",
1440
+ category: error instanceof SharedIndexError ? error.category : "unavailable",
1441
+ };
1442
+ }
1443
+ }
1444
+ }
1445
+ result = {
1446
+ configuration,
1447
+ provenance: readSharedIndexProvenance(repo),
1448
+ lastRestoreAttempt: readSharedRestoreAttempt(repo),
1449
+ credentialProbe,
1450
+ freshnessMechanism: graph.freshnessMechanism,
1451
+ };
1452
+ break;
1453
+ }
1454
+ if (action === "pull") {
1455
+ const configuration = loadSharedIndexConfig(repo);
1456
+ if (configuration.state !== "configured")
1457
+ throw new Error("knodin shared pull requires valid .knodin/shared-index.yaml");
1458
+ const store = createSharedObjectStore(configuration.config);
1459
+ if (!store)
1460
+ throw new Error("knodin shared pull: shared-index access is disabled");
1461
+ const progressMode = invocation.options.jsonl === true
1462
+ ? "jsonl"
1463
+ : typeof invocation.options.progress === "string"
1464
+ ? invocation.options.progress
1465
+ : process.stderr.isTTY
1466
+ ? "plain"
1467
+ : "none";
1468
+ if (!["plain", "jsonl", "none"].includes(progressMode))
1469
+ throw new Error("knodin shared pull: --progress must be plain, jsonl, or none");
1470
+ let lastHumanPhase = "";
1471
+ let lastHumanWrite = 0;
1472
+ result = await restoreSharedIndex({
1473
+ repo,
1474
+ config: configuration.config,
1475
+ store,
1476
+ cache: new SharedIndexCache(),
1477
+ engine,
1478
+ clientCompatibility: currentSharedIndexCompatibility(),
1479
+ onProgress: (event) => {
1480
+ if (progressMode === "jsonl")
1481
+ process.stdout.write(`${JSON.stringify({ type: "progress", ...event })}\n`);
1482
+ else if (progressMode === "plain") {
1483
+ const now = Date.now();
1484
+ if (event.phase !== lastHumanPhase ||
1485
+ event.phase === "completed" ||
1486
+ event.phase === "failed" ||
1487
+ now - lastHumanWrite >= 125) {
1488
+ lastHumanPhase = event.phase;
1489
+ lastHumanWrite = now;
1490
+ const count = event.completed !== undefined && event.total !== undefined
1491
+ ? ` ${event.completed.toLocaleString()}/${event.total.toLocaleString()}`
1492
+ : "";
1493
+ const rate = event.ratePerSecond
1494
+ ? ` · ${Math.round(event.ratePerSecond).toLocaleString()}/s`
1495
+ : "";
1496
+ const eta = event.etaSeconds ? ` · ETA ${Math.ceil(event.etaSeconds)}s` : "";
1497
+ process.stderr.write(`[shared:${event.phase}]${count}${rate}${eta} ${event.message}\n`);
1498
+ }
1499
+ }
1500
+ },
1501
+ });
1502
+ if (!result.restored)
1503
+ repairExitCode = 1;
1504
+ break;
1505
+ }
1506
+ throw new Error("knodin shared: unknown action");
1507
+ }
1284
1508
  case "agent-event": {
1285
1509
  agentEventOutput = true;
1286
1510
  const event = invocation.positionals[0];
@@ -1472,8 +1696,11 @@ async function main() {
1472
1696
  else {
1473
1697
  throw new Error("knodin hook-refresh: invalid lifecycle event");
1474
1698
  }
1475
- const indexed = await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
1476
- result = { indexed };
1699
+ const shared = await opportunisticSharedRestore();
1700
+ const indexed = shared?.restored
1701
+ ? []
1702
+ : await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
1703
+ result = { indexed, ...(shared ? { sharedRestore: shared } : {}) };
1477
1704
  break;
1478
1705
  }
1479
1706
  case "init": {
@@ -1486,6 +1713,11 @@ async function main() {
1486
1713
  let paths;
1487
1714
  try {
1488
1715
  try {
1716
+ if (!fs.existsSync(resolveDbPath(repo))) {
1717
+ const shared = await opportunisticSharedRestore();
1718
+ if (shared?.restored)
1719
+ process.stderr.write("[init:shared] Restored and verified shared snapshot\n");
1720
+ }
1489
1721
  paths = await initializeRepository(repo, {
1490
1722
  command: runtimeCommand,
1491
1723
  index: (target, options) => engine.index(target, undefined, false, options),
@@ -1638,7 +1870,7 @@ async function main() {
1638
1870
  case "status": {
1639
1871
  const snapshot = async () => ({
1640
1872
  ...attachLifecycleHealth(repo, await engine.status(repo, {
1641
- audit: rest.includes("--deep") ? "deep" : "cached",
1873
+ audit: rest.includes("--deep") ? "deep" : "adaptive",
1642
1874
  })),
1643
1875
  integration: inspectRepositoryIntegrationStatus(repo),
1644
1876
  update: trustedUpdateStatus({
@@ -1917,7 +2149,7 @@ async function main() {
1917
2149
  ? ""
1918
2150
  : (rest[1] ?? "");
1919
2151
  if (!pattern) {
1920
- 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|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");
2152
+ 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");
1921
2153
  process.exit(1);
1922
2154
  }
1923
2155
  const directionValue = selectorValue("--direction");
@@ -1926,6 +2158,16 @@ async function main() {
1926
2158
  process.exit(1);
1927
2159
  }
1928
2160
  const facetsValue = selectorValue("--facets")?.split(",").filter(Boolean);
2161
+ const testScopeValue = selectorValue("--test-scope");
2162
+ const queryFormat = selectorValue("--format");
2163
+ if (queryFormat && (pattern !== "file_metrics" || queryFormat !== "json")) {
2164
+ process.stderr.write("knodin query: --format is supported only as file_metrics --format json\n");
2165
+ process.exit(1);
2166
+ }
2167
+ if (testScopeValue && !["all", "test", "production"].includes(testScopeValue)) {
2168
+ process.stderr.write("knodin query: --test-scope must be all, test, or production\n");
2169
+ process.exit(1);
2170
+ }
1929
2171
  if (facetsValue?.some((facet) => !["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"].includes(facet))) {
1930
2172
  process.stderr.write("knodin query: --facets contains an unknown architecture facet\n");
1931
2173
  process.exit(1);
@@ -1970,7 +2212,7 @@ async function main() {
1970
2212
  process.stderr.write("knodin query: --limit must be a positive integer\n");
1971
2213
  process.exit(1);
1972
2214
  }
1973
- const queryHealth = pattern.startsWith("lsp_")
2215
+ const queryHealth = pattern.startsWith("lsp_") || pattern === "file_metrics"
1974
2216
  ? null
1975
2217
  : await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1976
2218
  if (queryHealth && !queryHealth.available) {
@@ -1996,6 +2238,10 @@ async function main() {
1996
2238
  }
1997
2239
  : undefined, {
1998
2240
  minLines: selectorValue("--min-lines") ? Number(selectorValue("--min-lines")) : undefined,
2241
+ base: pattern === "file_metrics" ? selectorValue("--base") : undefined,
2242
+ testScope: pattern === "file_metrics"
2243
+ ? testScopeValue
2244
+ : undefined,
1999
2245
  minComplexity: selectorValue("--min-complexity")
2000
2246
  ? Number(selectorValue("--min-complexity"))
2001
2247
  : undefined,
@@ -2020,6 +2266,8 @@ async function main() {
2020
2266
  : selectorValue("--relations")?.split(",").filter(Boolean),
2021
2267
  detailLevel,
2022
2268
  });
2269
+ if (pattern === "file_metrics")
2270
+ result = result.fileMetrics;
2023
2271
  if (pattern === "impact" || pattern === "dead_code") {
2024
2272
  const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
2025
2273
  result = incorporateSystemQueryEvidence(config, repo, pattern, target, result);
@@ -2220,11 +2468,10 @@ async function main() {
2220
2468
  }
2221
2469
  if (statusWasWatched)
2222
2470
  return;
2223
- const boundedResult = applyResponseBudget(result, cmd, responseBudget, {
2224
- bytes: 65_536,
2225
- tokens: 16_384,
2226
- items: 100,
2227
- });
2471
+ const fileMetricsOutput = cmd === "query" && rest[0] === "file_metrics";
2472
+ const boundedResult = applyResponseBudget(result, cmd, responseBudget, fileMetricsOutput
2473
+ ? { bytes: 16_777_216, tokens: 4_194_304, items: 100_000 }
2474
+ : { bytes: 65_536, tokens: 16_384, items: 100 });
2228
2475
  const finalExitCode = Math.max(Number(process.exitCode ?? 0), repairExitCode);
2229
2476
  if (cmd === "init" && !jsonOutput) {
2230
2477
  process.stdout.write(formatInitHuman(boundedResult));
@@ -2336,6 +2583,7 @@ catch (err) {
2336
2583
  "system",
2337
2584
  "repos",
2338
2585
  "remote",
2586
+ "shared",
2339
2587
  "update",
2340
2588
  ]);
2341
2589
  const diagnostic = recordDiagnosticFailure(candidate, {
@@ -64,6 +64,28 @@ function addRemoteCommands(program, capture) {
64
64
  leaf(remote, "remove <identity>", "delete a mirror's clone, graph, and registry entry", capture);
65
65
  leaf(remote, "refresh <identity>", "fetch the mirror again and re-index it", capture);
66
66
  }
67
+ function addSharedIndexCommands(program, capture) {
68
+ const shared = program.command("shared").description("manage verified shared-index snapshots");
69
+ leaf(shared, "status", "inspect shared-index configuration and provenance without network", capture).option("--probe", "explicitly probe S3 authentication and pointer access");
70
+ leaf(shared, "pull", "discover, verify, reconcile, audit, and promote a shared snapshot", capture)
71
+ .option("--jsonl", "stream lossless phase events as JSONL")
72
+ .option("--progress <mode>", "plain, jsonl, or none");
73
+ leaf(shared, "configure", "write reviewed repository trust configuration", capture)
74
+ .option("--disable", "disable automatic shared-index access without deleting graph or cache")
75
+ .option("--registration-id <id>", "publisher registration identifier")
76
+ .option("--region <region>", "AWS region")
77
+ .option("--bucket <bucket>", "S3 bucket")
78
+ .option("--host <host>", "canonical Git host")
79
+ .option("--owner <owner>", "repository owner")
80
+ .option("--name <name>", "repository name")
81
+ .addOption(option("--branch <pattern>", "exact branch or terminal slash-star pattern", "collect"))
82
+ .option("--key-id <id>", "reviewed signing key identifier")
83
+ .option("--public-key <path>", "PEM public-key file (credentials are never accepted)");
84
+ leaf(shared, "publisher-metadata", "stamp and emit the exact client-derived publisher compatibility metadata", capture);
85
+ leaf(shared, "publisher-bundle <branch>", "stream a graph and canonical manifest bundle", capture).requiredOption("--output <path>", "new or empty private output directory");
86
+ leaf(shared, "publisher-finalize <bundle>", "verify a detached signature and emit the canonical branch pointer", capture);
87
+ leaf(shared, "publisher-guard <pointer> <branch> <commit>", "refuse a backward or diverged branch-pointer update", capture);
88
+ }
67
89
  function addRepositoryCommands(program, capture) {
68
90
  const repos = program.command("repos").description("manage a portfolio of repositories");
69
91
  for (const action of ["discover", "init", "status", "doctor"]) {
@@ -142,6 +164,9 @@ function addGraphCommands(program, capture) {
142
164
  .addOption(option("--min-complexity <count>", "minimum complexity", "integer"))
143
165
  .option("--kinds <values>", "comma-separated symbol kinds")
144
166
  .option("--path <prefix>", "repo-relative path prefix")
167
+ .option("--base <ref>", "compare file_metrics with the merge-base of this Git ref")
168
+ .option("--test-scope <scope>", "file_metrics scope: all, test, or production")
169
+ .option("--format <format>", "file_metrics machine output format; currently json")
145
170
  .option("--variable <name>", "flow-analysis variable")
146
171
  .option("--facets <values>", "comma-separated architecture facets")
147
172
  .addOption(option("--top <count>", "maximum ranked results", "integer"))
@@ -262,6 +287,7 @@ function createCliProgram(capture = () => { }) {
262
287
  leaf(program, "refresh-artifacts [event]", "refresh external graph artifacts", capture);
263
288
  addRepositoryCommands(program, capture);
264
289
  addRemoteCommands(program, capture);
290
+ addSharedIndexCommands(program, capture);
265
291
  addGraphCommands(program, capture);
266
292
  addArtifactCommands(program, capture);
267
293
  const system = program.command("system").description("inspect declared multi-repository systems");
@@ -0,0 +1,167 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { resolveDbPath, resolveStateDir } from "./state-paths.js";
5
+ const PROMOTION_SCHEMA_VERSION = 1;
6
+ const CANDIDATES_DIRECTORY = "candidates";
7
+ const PROMOTION_MARKER = "promotion.json";
8
+ function inside(parent, child) {
9
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
10
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
11
+ }
12
+ function assertPromotionRecord(repo, value) {
13
+ if (!value || typeof value !== "object")
14
+ throw new Error("invalid promotion recovery record");
15
+ const record = value;
16
+ const stateDir = resolveStateDir(repo);
17
+ const activePath = resolveDbPath(repo);
18
+ if (record.schemaVersion !== PROMOTION_SCHEMA_VERSION ||
19
+ !record.candidatePath ||
20
+ !record.backupPath ||
21
+ record.activePath !== activePath ||
22
+ !inside(stateDir, record.candidatePath) ||
23
+ !inside(stateDir, record.backupPath) ||
24
+ !record.phase ||
25
+ !["prepared", "backed-up", "promoted"].includes(record.phase)) {
26
+ throw new Error("invalid promotion recovery record");
27
+ }
28
+ return record;
29
+ }
30
+ function fsyncFile(target) {
31
+ const descriptor = fs.openSync(target, "r");
32
+ try {
33
+ fs.fsyncSync(descriptor);
34
+ }
35
+ finally {
36
+ fs.closeSync(descriptor);
37
+ }
38
+ }
39
+ function fsyncDirectory(target) {
40
+ try {
41
+ const descriptor = fs.openSync(target, "r");
42
+ try {
43
+ fs.fsyncSync(descriptor);
44
+ }
45
+ finally {
46
+ fs.closeSync(descriptor);
47
+ }
48
+ }
49
+ catch (error) {
50
+ // Some platforms do not support directory fsync. File fsync and same-filesystem
51
+ // rename remain the strongest available ordering guarantee there.
52
+ if (process.platform !== "win32")
53
+ throw error;
54
+ }
55
+ }
56
+ function atomicRecord(target, record) {
57
+ const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
58
+ fs.writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600 });
59
+ fsyncFile(temporary);
60
+ fs.renameSync(temporary, target);
61
+ fsyncDirectory(path.dirname(target));
62
+ }
63
+ export function candidateRoot(repo) {
64
+ return path.join(resolveStateDir(repo), CANDIDATES_DIRECTORY);
65
+ }
66
+ export function promotionMarkerPath(repo) {
67
+ return path.join(resolveStateDir(repo), PROMOTION_MARKER);
68
+ }
69
+ export function allocateCandidate(repo) {
70
+ const resolvedRepo = path.resolve(repo);
71
+ const root = candidateRoot(resolvedRepo);
72
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
73
+ const directory = fs.mkdtempSync(path.join(root, "candidate-"));
74
+ fs.chmodSync(directory, 0o700);
75
+ return {
76
+ schemaVersion: 1,
77
+ id: path.basename(directory),
78
+ repo: resolvedRepo,
79
+ databasePath: path.join(directory, "db.sqlite"),
80
+ createdAt: new Date().toISOString(),
81
+ };
82
+ }
83
+ export function assertCandidate(repo, candidate) {
84
+ const resolvedRepo = path.resolve(repo);
85
+ if (candidate.schemaVersion !== 1 ||
86
+ candidate.repo !== resolvedRepo ||
87
+ !inside(candidateRoot(resolvedRepo), candidate.databasePath) ||
88
+ path.basename(candidate.databasePath) !== "db.sqlite") {
89
+ throw new Error("candidate does not belong to this repository");
90
+ }
91
+ }
92
+ /** Restore the previous active graph after a process stopped between promotion renames. */
93
+ export function recoverInterruptedPromotion(repo) {
94
+ const marker = promotionMarkerPath(repo);
95
+ if (!fs.existsSync(marker))
96
+ return "none";
97
+ const record = assertPromotionRecord(repo, JSON.parse(fs.readFileSync(marker, "utf8")));
98
+ const activeExists = fs.existsSync(record.activePath);
99
+ const backupExists = fs.existsSync(record.backupPath);
100
+ let outcome;
101
+ if (!activeExists && backupExists) {
102
+ fs.renameSync(record.backupPath, record.activePath);
103
+ fsyncDirectory(path.dirname(record.activePath));
104
+ outcome = "recovered";
105
+ }
106
+ else if (activeExists) {
107
+ outcome = record.phase === "promoted" ? "completed" : "recovered";
108
+ }
109
+ else {
110
+ throw new Error("interrupted promotion has neither active nor backup database");
111
+ }
112
+ fs.rmSync(marker, { force: true });
113
+ fsyncDirectory(path.dirname(marker));
114
+ return outcome;
115
+ }
116
+ /** Same-filesystem, marker-backed promotion. Caller must close and audit both databases first. */
117
+ export function promoteCandidateFile(repo, candidate) {
118
+ assertCandidate(repo, candidate);
119
+ const activePath = resolveDbPath(repo);
120
+ const stateDir = resolveStateDir(repo);
121
+ if (!fs.existsSync(candidate.databasePath))
122
+ throw new Error("candidate database is missing");
123
+ for (const suffix of ["-wal", "-shm"])
124
+ if (fs.existsSync(`${candidate.databasePath}${suffix}`))
125
+ throw new Error("candidate database has uncheckpointed sidecar files");
126
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
127
+ fsyncFile(candidate.databasePath);
128
+ const backupPath = path.join(stateDir, `db.sqlite.backup.${Date.now()}.${crypto.randomUUID().slice(0, 8)}`);
129
+ const marker = promotionMarkerPath(repo);
130
+ let record = {
131
+ schemaVersion: 1,
132
+ phase: "prepared",
133
+ candidatePath: candidate.databasePath,
134
+ activePath,
135
+ backupPath,
136
+ };
137
+ atomicRecord(marker, record);
138
+ try {
139
+ if (fs.existsSync(activePath))
140
+ fs.renameSync(activePath, backupPath);
141
+ record = { ...record, phase: "backed-up" };
142
+ atomicRecord(marker, record);
143
+ fs.renameSync(candidate.databasePath, activePath);
144
+ record = { ...record, phase: "promoted" };
145
+ atomicRecord(marker, record);
146
+ fsyncFile(activePath);
147
+ fsyncDirectory(stateDir);
148
+ fs.rmSync(marker, { force: true });
149
+ fsyncDirectory(stateDir);
150
+ return fs.existsSync(backupPath) ? backupPath : null;
151
+ }
152
+ catch (error) {
153
+ if (fs.existsSync(activePath) && fs.existsSync(backupPath)) {
154
+ fs.mkdirSync(path.dirname(candidate.databasePath), { recursive: true, mode: 0o700 });
155
+ fs.renameSync(activePath, candidate.databasePath);
156
+ }
157
+ if (!fs.existsSync(activePath) && fs.existsSync(backupPath))
158
+ fs.renameSync(backupPath, activePath);
159
+ fs.rmSync(marker, { force: true });
160
+ fsyncDirectory(stateDir);
161
+ throw error;
162
+ }
163
+ }
164
+ export function discardCandidateFiles(repo, candidate) {
165
+ assertCandidate(repo, candidate);
166
+ fs.rmSync(path.dirname(candidate.databasePath), { recursive: true, force: true });
167
+ }