knodin 0.8.4 → 0.8.5
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/dist/bin/cli.js +262 -20
- package/dist/src/cli-model.js +26 -0
- package/dist/src/engine/candidate-database.js +167 -0
- package/dist/src/engine/embeddings.js +8 -5
- package/dist/src/engine/index.js +930 -262
- package/dist/src/engine/prune.js +8 -0
- package/dist/src/file-metrics.js +153 -0
- package/dist/src/repair-lease.js +8 -5
- package/dist/src/response-budget.js +2 -1
- package/dist/src/shared-index/artifact.js +65 -0
- package/dist/src/shared-index/cache.js +267 -0
- package/dist/src/shared-index/compatibility.js +70 -0
- package/dist/src/shared-index/config.js +224 -0
- package/dist/src/shared-index/contract.js +117 -0
- package/dist/src/shared-index/manifest.js +71 -0
- package/dist/src/shared-index/opportunistic.js +37 -0
- package/dist/src/shared-index/overlay.js +98 -0
- package/dist/src/shared-index/provenance.js +149 -0
- package/dist/src/shared-index/publisher.js +198 -0
- package/dist/src/shared-index/restore.js +209 -0
- package/dist/src/shared-index/s3-client.js +124 -0
- package/dist/src/shared-index/schemas.js +86 -0
- package/dist/src/shared-index/selection.js +111 -0
- package/dist/src/tools/knodin-tools.js +70 -3
- package/docs/CLI.md +39 -0
- package/docs/MCP.md +12 -0
- package/docs/SHARED-INDEX-CONTRACT.md +132 -0
- package/docs/releases/0.8.5.md +25 -0
- package/package.json +11 -3
- package/schemas/shared-index-branch-pointer-v1.schema.json +66 -0
- package/schemas/shared-index-config-v1.schema.json +97 -0
- package/schemas/shared-index-manifest-v1.schema.json +104 -0
- 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,7 +420,8 @@ 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
|
|
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`;
|
|
424
425
|
if (result.status === "healthy")
|
|
425
426
|
return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).\n${freshnessLine}${lifecycleLine}${integrationLine}`;
|
|
426
427
|
if (result.status === "stale")
|
|
@@ -634,7 +635,7 @@ async function main() {
|
|
|
634
635
|
}, 50);
|
|
635
636
|
interval.unref();
|
|
636
637
|
process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
|
|
637
|
-
const engine = createEngine();
|
|
638
|
+
const engine = createEngine({ watcher: "disabled" });
|
|
638
639
|
try {
|
|
639
640
|
const systemConfig = loadSystemConfiguration(repository);
|
|
640
641
|
const summary = await initializeRepositories([repository], {
|
|
@@ -689,7 +690,11 @@ async function main() {
|
|
|
689
690
|
const runtimeCommand = resolveCliRuntimeCommand(process);
|
|
690
691
|
// Internal machine-to-machine commands stay JSON regardless of TTY state.
|
|
691
692
|
// when an older installed hook predates the explicit --json argument.
|
|
692
|
-
const jsonOutput = invocation.options.json === true ||
|
|
693
|
+
const jsonOutput = invocation.options.json === true ||
|
|
694
|
+
invocation.options.format === "json" ||
|
|
695
|
+
cmd === "hook-refresh" ||
|
|
696
|
+
cmd === "agent-event" ||
|
|
697
|
+
(cmd === "shared" && rawRest.includes("--jsonl"));
|
|
693
698
|
// `--json` is a shared output flag. Repair owns its richer --json/--jsonl
|
|
694
699
|
// parser; all other commands receive their original arguments minus it.
|
|
695
700
|
const rest = cmd === "repair" ? rawRest : rawRest.filter((argument) => argument !== "--json");
|
|
@@ -756,7 +761,7 @@ async function main() {
|
|
|
756
761
|
if (!plan.json) {
|
|
757
762
|
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
763
|
}
|
|
759
|
-
const engine = createEngine();
|
|
764
|
+
const engine = createEngine({ watcher: "disabled" });
|
|
760
765
|
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
761
766
|
const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
|
|
762
767
|
const summary = await initializeRepositories(plan.roots, {
|
|
@@ -793,7 +798,7 @@ async function main() {
|
|
|
793
798
|
depth: plan.depth,
|
|
794
799
|
linkedWorktrees: plan.linkedWorktrees,
|
|
795
800
|
});
|
|
796
|
-
const engine = createEngine();
|
|
801
|
+
const engine = createEngine({ watcher: "disabled" });
|
|
797
802
|
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
798
803
|
const repositories = [];
|
|
799
804
|
const discoveryRecords = plan.signals
|
|
@@ -860,7 +865,7 @@ async function main() {
|
|
|
860
865
|
process.exitCode = summary.exitCode;
|
|
861
866
|
return;
|
|
862
867
|
}
|
|
863
|
-
const engine = createEngine();
|
|
868
|
+
const engine = createEngine({ watcher: "disabled" });
|
|
864
869
|
const summary = await initializeRepositories(plan.roots, {
|
|
865
870
|
command: runtimeCommand,
|
|
866
871
|
depth: plan.depth,
|
|
@@ -898,7 +903,7 @@ async function main() {
|
|
|
898
903
|
return;
|
|
899
904
|
}
|
|
900
905
|
if (plan.command === "search") {
|
|
901
|
-
const engine = createEngine();
|
|
906
|
+
const engine = createEngine({ watcher: "disabled" });
|
|
902
907
|
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
903
908
|
const output = await searchRepositories(plan.roots, plan.query ?? "", {
|
|
904
909
|
depth: plan.depth,
|
|
@@ -927,7 +932,7 @@ async function main() {
|
|
|
927
932
|
depth: plan.depth,
|
|
928
933
|
linkedWorktrees: plan.linkedWorktrees,
|
|
929
934
|
});
|
|
930
|
-
const engine = createEngine();
|
|
935
|
+
const engine = createEngine({ watcher: "disabled" });
|
|
931
936
|
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
932
937
|
const repositories = [];
|
|
933
938
|
for (const repository of discovery.repositories) {
|
|
@@ -1167,7 +1172,7 @@ async function main() {
|
|
|
1167
1172
|
let compressionResult;
|
|
1168
1173
|
let diagnosisUnavailable = false;
|
|
1169
1174
|
if (action === "diagnose") {
|
|
1170
|
-
const diagnosisEngine = createEngine();
|
|
1175
|
+
const diagnosisEngine = createEngine({ watcher: "disabled" });
|
|
1171
1176
|
try {
|
|
1172
1177
|
const health = await inspectGraphQueryHealth(repo, (target) => diagnosisEngine.status(target, { audit: "cached" }));
|
|
1173
1178
|
if (health.available) {
|
|
@@ -1259,7 +1264,13 @@ async function main() {
|
|
|
1259
1264
|
process.stdout.write(formatGenericHuman("compress delete", compressionResult));
|
|
1260
1265
|
return;
|
|
1261
1266
|
}
|
|
1262
|
-
const engine = createEngine();
|
|
1267
|
+
const engine = createEngine({ watcher: "disabled" });
|
|
1268
|
+
const opportunisticSharedRestore = async () => {
|
|
1269
|
+
if (!fs.existsSync(path.join(repo, ".knodin", "shared-index.yaml")))
|
|
1270
|
+
return null;
|
|
1271
|
+
const { attemptOpportunisticSharedRestore } = await import("../src/shared-index/opportunistic.js");
|
|
1272
|
+
return attemptOpportunisticSharedRestore(repo, engine);
|
|
1273
|
+
};
|
|
1263
1274
|
let result;
|
|
1264
1275
|
let repairOutput;
|
|
1265
1276
|
let repairWasPlan = false;
|
|
@@ -1281,6 +1292,213 @@ async function main() {
|
|
|
1281
1292
|
return decorateGraphQueryResult(value, verified.state, verified.graph.freshness);
|
|
1282
1293
|
};
|
|
1283
1294
|
switch (cmd) {
|
|
1295
|
+
case "shared": {
|
|
1296
|
+
const [cacheModule, compatibilityModule, configModule, manifestModule, provenanceModule, publisherModule, restoreModule, s3Module, selectionModule,] = await Promise.all([
|
|
1297
|
+
import("../src/shared-index/cache.js"),
|
|
1298
|
+
import("../src/shared-index/compatibility.js"),
|
|
1299
|
+
import("../src/shared-index/config.js"),
|
|
1300
|
+
import("../src/shared-index/manifest.js"),
|
|
1301
|
+
import("../src/shared-index/provenance.js"),
|
|
1302
|
+
import("../src/shared-index/publisher.js"),
|
|
1303
|
+
import("../src/shared-index/restore.js"),
|
|
1304
|
+
import("../src/shared-index/s3-client.js"),
|
|
1305
|
+
import("../src/shared-index/selection.js"),
|
|
1306
|
+
]);
|
|
1307
|
+
const { SharedIndexCache } = cacheModule;
|
|
1308
|
+
const { currentSharedIndexCompatibility } = compatibilityModule;
|
|
1309
|
+
const { loadSharedIndexConfig, publicKeyFingerprint, writeSharedIndexConfig } = configModule;
|
|
1310
|
+
const { branchPointerKey } = manifestModule;
|
|
1311
|
+
const { readSharedIndexProvenance, readSharedRestoreAttempt } = provenanceModule;
|
|
1312
|
+
const { createSharedPublisherBundle, finalizeSharedPublisherBundle, guardPublisherPointer, prepareSharedPublisherDatabase, } = publisherModule;
|
|
1313
|
+
const { restoreSharedIndex } = restoreModule;
|
|
1314
|
+
const { createSharedObjectStore, SharedIndexError } = s3Module;
|
|
1315
|
+
const { configuredBranchCandidates } = selectionModule;
|
|
1316
|
+
const action = invocation.commandPath[1];
|
|
1317
|
+
if (action === "configure") {
|
|
1318
|
+
const existing = loadSharedIndexConfig(repo);
|
|
1319
|
+
if (invocation.options.disable === true) {
|
|
1320
|
+
if (existing.state !== "configured")
|
|
1321
|
+
throw new Error("knodin shared configure --disable requires a valid existing configuration");
|
|
1322
|
+
writeSharedIndexConfig(repo, { ...existing.config, enabled: false });
|
|
1323
|
+
result = { state: "configured", config: { ...existing.config, enabled: false } };
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
const required = (name) => {
|
|
1327
|
+
const value = invocation.options[name];
|
|
1328
|
+
if (typeof value !== "string" || !value)
|
|
1329
|
+
throw new Error(`knodin shared configure requires --${name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`);
|
|
1330
|
+
return value;
|
|
1331
|
+
};
|
|
1332
|
+
const publicKeyPath = path.resolve(process.cwd(), required("publicKey"));
|
|
1333
|
+
const publicKeyPem = fs.readFileSync(publicKeyPath, "utf8");
|
|
1334
|
+
const branches = Array.isArray(invocation.options.branch)
|
|
1335
|
+
? invocation.options.branch.filter((value) => typeof value === "string")
|
|
1336
|
+
: [];
|
|
1337
|
+
if (branches.length === 0)
|
|
1338
|
+
throw new Error("knodin shared configure requires --branch");
|
|
1339
|
+
const config = {
|
|
1340
|
+
schemaVersion: 1,
|
|
1341
|
+
enabled: true,
|
|
1342
|
+
registrationId: required("registrationId"),
|
|
1343
|
+
aws: { region: required("region"), bucket: required("bucket") },
|
|
1344
|
+
repository: {
|
|
1345
|
+
host: required("host"),
|
|
1346
|
+
owner: required("owner"),
|
|
1347
|
+
name: required("name"),
|
|
1348
|
+
},
|
|
1349
|
+
branches,
|
|
1350
|
+
signing: {
|
|
1351
|
+
algorithm: "RSASSA_PSS_SHA_256",
|
|
1352
|
+
keyId: required("keyId"),
|
|
1353
|
+
publicKeySha256: publicKeyFingerprint(publicKeyPem),
|
|
1354
|
+
publicKeyPem,
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
const target = writeSharedIndexConfig(repo, config);
|
|
1358
|
+
result = { state: "configured", path: target, config: loadSharedIndexConfig(repo) };
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
if (action === "publisher-metadata") {
|
|
1362
|
+
const configuration = loadSharedIndexConfig(repo);
|
|
1363
|
+
if (configuration.state !== "configured")
|
|
1364
|
+
throw new Error("knodin shared publisher-metadata requires valid reviewed configuration");
|
|
1365
|
+
result = prepareSharedPublisherDatabase(repo);
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
if (action === "publisher-bundle") {
|
|
1369
|
+
const configuration = loadSharedIndexConfig(repo);
|
|
1370
|
+
if (configuration.state !== "configured")
|
|
1371
|
+
throw new Error("knodin shared publisher-bundle requires valid reviewed configuration");
|
|
1372
|
+
const branch = invocation.positionals[0];
|
|
1373
|
+
const output = invocation.options.output;
|
|
1374
|
+
if (!branch || typeof output !== "string")
|
|
1375
|
+
throw new Error("knodin shared publisher-bundle requires <branch> --output <path>");
|
|
1376
|
+
result = await createSharedPublisherBundle({
|
|
1377
|
+
repo,
|
|
1378
|
+
config: configuration.config,
|
|
1379
|
+
branch,
|
|
1380
|
+
outputDirectory: path.resolve(process.cwd(), output),
|
|
1381
|
+
});
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
if (action === "publisher-finalize") {
|
|
1385
|
+
const configuration = loadSharedIndexConfig(repo);
|
|
1386
|
+
if (configuration.state !== "configured")
|
|
1387
|
+
throw new Error("knodin shared publisher-finalize requires valid reviewed configuration");
|
|
1388
|
+
const bundlePath = invocation.positionals[0];
|
|
1389
|
+
if (!bundlePath)
|
|
1390
|
+
throw new Error("knodin shared publisher-finalize requires <bundle>");
|
|
1391
|
+
const bundle = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), bundlePath), "utf8"));
|
|
1392
|
+
const pointer = finalizeSharedPublisherBundle({ config: configuration.config, bundle });
|
|
1393
|
+
result = { bundle, pointer: JSON.parse(pointer.toString("utf8")) };
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
if (action === "publisher-guard") {
|
|
1397
|
+
const configuration = loadSharedIndexConfig(repo);
|
|
1398
|
+
if (configuration.state !== "configured")
|
|
1399
|
+
throw new Error("knodin shared publisher-guard requires valid reviewed configuration");
|
|
1400
|
+
const [pointerPath, branch, commit] = invocation.positionals;
|
|
1401
|
+
if (!pointerPath || !branch || !commit)
|
|
1402
|
+
throw new Error("knodin shared publisher-guard requires <pointer> <branch> <commit>");
|
|
1403
|
+
guardPublisherPointer({
|
|
1404
|
+
repo,
|
|
1405
|
+
config: configuration.config,
|
|
1406
|
+
branch,
|
|
1407
|
+
newCommit: commit,
|
|
1408
|
+
existingPointerBytes: fs.readFileSync(path.resolve(process.cwd(), pointerPath)),
|
|
1409
|
+
});
|
|
1410
|
+
result = { allowed: true, branch, commit };
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
if (action === "status") {
|
|
1414
|
+
const configuration = loadSharedIndexConfig(repo);
|
|
1415
|
+
const graph = await engine.status(repo, { audit: "cached" });
|
|
1416
|
+
let credentialProbe = "not-requested";
|
|
1417
|
+
if (invocation.options.probe === true) {
|
|
1418
|
+
if (configuration.state !== "configured" || !configuration.config.enabled) {
|
|
1419
|
+
credentialProbe = { state: "unavailable", category: "configuration" };
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
const store = createSharedObjectStore(configuration.config);
|
|
1423
|
+
try {
|
|
1424
|
+
const current = spawnSync(gitExecutable(), ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).stdout.trim();
|
|
1425
|
+
const branch = configuredBranchCandidates(configuration.config, current || undefined)[0];
|
|
1426
|
+
if (!store || !branch)
|
|
1427
|
+
throw new SharedIndexError("configuration", "shared-index probe has no configured local branch candidate");
|
|
1428
|
+
await store.head(branchPointerKey(configuration.config, branch));
|
|
1429
|
+
credentialProbe = { state: "available" };
|
|
1430
|
+
}
|
|
1431
|
+
catch (error) {
|
|
1432
|
+
credentialProbe = {
|
|
1433
|
+
state: "unavailable",
|
|
1434
|
+
category: error instanceof SharedIndexError ? error.category : "unavailable",
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
result = {
|
|
1440
|
+
configuration,
|
|
1441
|
+
provenance: readSharedIndexProvenance(repo),
|
|
1442
|
+
lastRestoreAttempt: readSharedRestoreAttempt(repo),
|
|
1443
|
+
credentialProbe,
|
|
1444
|
+
freshnessMechanism: graph.freshnessMechanism,
|
|
1445
|
+
};
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
if (action === "pull") {
|
|
1449
|
+
const configuration = loadSharedIndexConfig(repo);
|
|
1450
|
+
if (configuration.state !== "configured")
|
|
1451
|
+
throw new Error("knodin shared pull requires valid .knodin/shared-index.yaml");
|
|
1452
|
+
const store = createSharedObjectStore(configuration.config);
|
|
1453
|
+
if (!store)
|
|
1454
|
+
throw new Error("knodin shared pull: shared-index access is disabled");
|
|
1455
|
+
const progressMode = invocation.options.jsonl === true
|
|
1456
|
+
? "jsonl"
|
|
1457
|
+
: typeof invocation.options.progress === "string"
|
|
1458
|
+
? invocation.options.progress
|
|
1459
|
+
: process.stderr.isTTY
|
|
1460
|
+
? "plain"
|
|
1461
|
+
: "none";
|
|
1462
|
+
if (!["plain", "jsonl", "none"].includes(progressMode))
|
|
1463
|
+
throw new Error("knodin shared pull: --progress must be plain, jsonl, or none");
|
|
1464
|
+
let lastHumanPhase = "";
|
|
1465
|
+
let lastHumanWrite = 0;
|
|
1466
|
+
result = await restoreSharedIndex({
|
|
1467
|
+
repo,
|
|
1468
|
+
config: configuration.config,
|
|
1469
|
+
store,
|
|
1470
|
+
cache: new SharedIndexCache(),
|
|
1471
|
+
engine,
|
|
1472
|
+
clientCompatibility: currentSharedIndexCompatibility(),
|
|
1473
|
+
onProgress: (event) => {
|
|
1474
|
+
if (progressMode === "jsonl")
|
|
1475
|
+
process.stdout.write(`${JSON.stringify({ type: "progress", ...event })}\n`);
|
|
1476
|
+
else if (progressMode === "plain") {
|
|
1477
|
+
const now = Date.now();
|
|
1478
|
+
if (event.phase !== lastHumanPhase ||
|
|
1479
|
+
event.phase === "completed" ||
|
|
1480
|
+
event.phase === "failed" ||
|
|
1481
|
+
now - lastHumanWrite >= 125) {
|
|
1482
|
+
lastHumanPhase = event.phase;
|
|
1483
|
+
lastHumanWrite = now;
|
|
1484
|
+
const count = event.completed !== undefined && event.total !== undefined
|
|
1485
|
+
? ` ${event.completed.toLocaleString()}/${event.total.toLocaleString()}`
|
|
1486
|
+
: "";
|
|
1487
|
+
const rate = event.ratePerSecond
|
|
1488
|
+
? ` · ${Math.round(event.ratePerSecond).toLocaleString()}/s`
|
|
1489
|
+
: "";
|
|
1490
|
+
const eta = event.etaSeconds ? ` · ETA ${Math.ceil(event.etaSeconds)}s` : "";
|
|
1491
|
+
process.stderr.write(`[shared:${event.phase}]${count}${rate}${eta} ${event.message}\n`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
});
|
|
1496
|
+
if (!result.restored)
|
|
1497
|
+
repairExitCode = 1;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
throw new Error("knodin shared: unknown action");
|
|
1501
|
+
}
|
|
1284
1502
|
case "agent-event": {
|
|
1285
1503
|
agentEventOutput = true;
|
|
1286
1504
|
const event = invocation.positionals[0];
|
|
@@ -1472,8 +1690,11 @@ async function main() {
|
|
|
1472
1690
|
else {
|
|
1473
1691
|
throw new Error("knodin hook-refresh: invalid lifecycle event");
|
|
1474
1692
|
}
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1693
|
+
const shared = await opportunisticSharedRestore();
|
|
1694
|
+
const indexed = shared?.restored
|
|
1695
|
+
? []
|
|
1696
|
+
: await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
|
|
1697
|
+
result = { indexed, ...(shared ? { sharedRestore: shared } : {}) };
|
|
1477
1698
|
break;
|
|
1478
1699
|
}
|
|
1479
1700
|
case "init": {
|
|
@@ -1486,6 +1707,11 @@ async function main() {
|
|
|
1486
1707
|
let paths;
|
|
1487
1708
|
try {
|
|
1488
1709
|
try {
|
|
1710
|
+
if (!fs.existsSync(resolveDbPath(repo))) {
|
|
1711
|
+
const shared = await opportunisticSharedRestore();
|
|
1712
|
+
if (shared?.restored)
|
|
1713
|
+
process.stderr.write("[init:shared] Restored and verified shared snapshot\n");
|
|
1714
|
+
}
|
|
1489
1715
|
paths = await initializeRepository(repo, {
|
|
1490
1716
|
command: runtimeCommand,
|
|
1491
1717
|
index: (target, options) => engine.index(target, undefined, false, options),
|
|
@@ -1917,7 +2143,7 @@ async function main() {
|
|
|
1917
2143
|
? ""
|
|
1918
2144
|
: (rest[1] ?? "");
|
|
1919
2145
|
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");
|
|
2146
|
+
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
2147
|
process.exit(1);
|
|
1922
2148
|
}
|
|
1923
2149
|
const directionValue = selectorValue("--direction");
|
|
@@ -1926,6 +2152,16 @@ async function main() {
|
|
|
1926
2152
|
process.exit(1);
|
|
1927
2153
|
}
|
|
1928
2154
|
const facetsValue = selectorValue("--facets")?.split(",").filter(Boolean);
|
|
2155
|
+
const testScopeValue = selectorValue("--test-scope");
|
|
2156
|
+
const queryFormat = selectorValue("--format");
|
|
2157
|
+
if (queryFormat && (pattern !== "file_metrics" || queryFormat !== "json")) {
|
|
2158
|
+
process.stderr.write("knodin query: --format is supported only as file_metrics --format json\n");
|
|
2159
|
+
process.exit(1);
|
|
2160
|
+
}
|
|
2161
|
+
if (testScopeValue && !["all", "test", "production"].includes(testScopeValue)) {
|
|
2162
|
+
process.stderr.write("knodin query: --test-scope must be all, test, or production\n");
|
|
2163
|
+
process.exit(1);
|
|
2164
|
+
}
|
|
1929
2165
|
if (facetsValue?.some((facet) => !["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"].includes(facet))) {
|
|
1930
2166
|
process.stderr.write("knodin query: --facets contains an unknown architecture facet\n");
|
|
1931
2167
|
process.exit(1);
|
|
@@ -1970,7 +2206,7 @@ async function main() {
|
|
|
1970
2206
|
process.stderr.write("knodin query: --limit must be a positive integer\n");
|
|
1971
2207
|
process.exit(1);
|
|
1972
2208
|
}
|
|
1973
|
-
const queryHealth = pattern.startsWith("lsp_")
|
|
2209
|
+
const queryHealth = pattern.startsWith("lsp_") || pattern === "file_metrics"
|
|
1974
2210
|
? null
|
|
1975
2211
|
: await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
|
|
1976
2212
|
if (queryHealth && !queryHealth.available) {
|
|
@@ -1996,6 +2232,10 @@ async function main() {
|
|
|
1996
2232
|
}
|
|
1997
2233
|
: undefined, {
|
|
1998
2234
|
minLines: selectorValue("--min-lines") ? Number(selectorValue("--min-lines")) : undefined,
|
|
2235
|
+
base: pattern === "file_metrics" ? selectorValue("--base") : undefined,
|
|
2236
|
+
testScope: pattern === "file_metrics"
|
|
2237
|
+
? testScopeValue
|
|
2238
|
+
: undefined,
|
|
1999
2239
|
minComplexity: selectorValue("--min-complexity")
|
|
2000
2240
|
? Number(selectorValue("--min-complexity"))
|
|
2001
2241
|
: undefined,
|
|
@@ -2020,6 +2260,8 @@ async function main() {
|
|
|
2020
2260
|
: selectorValue("--relations")?.split(",").filter(Boolean),
|
|
2021
2261
|
detailLevel,
|
|
2022
2262
|
});
|
|
2263
|
+
if (pattern === "file_metrics")
|
|
2264
|
+
result = result.fileMetrics;
|
|
2023
2265
|
if (pattern === "impact" || pattern === "dead_code") {
|
|
2024
2266
|
const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
|
|
2025
2267
|
result = incorporateSystemQueryEvidence(config, repo, pattern, target, result);
|
|
@@ -2220,11 +2462,10 @@ async function main() {
|
|
|
2220
2462
|
}
|
|
2221
2463
|
if (statusWasWatched)
|
|
2222
2464
|
return;
|
|
2223
|
-
const
|
|
2224
|
-
|
|
2225
|
-
tokens:
|
|
2226
|
-
items: 100
|
|
2227
|
-
});
|
|
2465
|
+
const fileMetricsOutput = cmd === "query" && rest[0] === "file_metrics";
|
|
2466
|
+
const boundedResult = applyResponseBudget(result, cmd, responseBudget, fileMetricsOutput
|
|
2467
|
+
? { bytes: 16_777_216, tokens: 4_194_304, items: 100_000 }
|
|
2468
|
+
: { bytes: 65_536, tokens: 16_384, items: 100 });
|
|
2228
2469
|
const finalExitCode = Math.max(Number(process.exitCode ?? 0), repairExitCode);
|
|
2229
2470
|
if (cmd === "init" && !jsonOutput) {
|
|
2230
2471
|
process.stdout.write(formatInitHuman(boundedResult));
|
|
@@ -2336,6 +2577,7 @@ catch (err) {
|
|
|
2336
2577
|
"system",
|
|
2337
2578
|
"repos",
|
|
2338
2579
|
"remote",
|
|
2580
|
+
"shared",
|
|
2339
2581
|
"update",
|
|
2340
2582
|
]);
|
|
2341
2583
|
const diagnostic = recordDiagnosticFailure(candidate, {
|
package/dist/src/cli-model.js
CHANGED
|
@@ -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
|
+
}
|