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.
- package/dist/bin/cli.js +272 -24
- 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 +1244 -305
- 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/docs/releases/0.8.6.md +10 -0
- package/package.json +12 -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,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
|
-
|
|
425
|
-
|
|
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})
|
|
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 ||
|
|
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
|
|
1476
|
-
|
|
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" : "
|
|
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
|
|
2224
|
-
|
|
2225
|
-
tokens:
|
|
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, {
|
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
|
+
}
|