knodin 0.13.0 → 0.14.0

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 CHANGED
@@ -31,6 +31,7 @@ import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/co
31
31
  import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, persistDiagnosticsPreview, recordDiagnosticFailure, } from "../src/diagnostics.js";
32
32
  import { getDocSection, listDocTopics } from "../src/docs-sections.js";
33
33
  import { diagnoseInstallation } from "../src/doctor.js";
34
+ import { sweepAbandonedCandidates } from "../src/engine/candidate-database.js";
34
35
  import { createEngine, describeThrown, KNODIN_SCHEMA_VERSION, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
35
36
  import { runSeal } from "../src/engine/seal-command.js";
36
37
  import { runSealedQuery } from "../src/engine/sealed-query.js";
@@ -60,6 +61,8 @@ import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, i
60
61
  import { applyResponseBudget } from "../src/response-budget.js";
61
62
  import { appendSessionEvent, clearSessionTelemetry, disableSessionTelemetry, enableSessionTelemetry, readSessionEvents, sessionTelemetryStatus, } from "../src/session-telemetry.js";
62
63
  import { inspectKnodinSkills, installKnodinSkills, KNODIN_SKILLS, removeKnodinSkills, } from "../src/skill-management.js";
64
+ import { inventoryStorage } from "../src/storage-inventory.js";
65
+ import { configureStoragePolicy, postOperationStorageMaintenance, resolveStoragePolicy, } from "../src/storage-management.js";
63
66
  import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
64
67
  import { applySystemPlan, planSystemImport, planSystemLink, planSystemRelate, planSystemUnlink, planSystemUnrelate, } from "../src/system-management.js";
65
68
  import { coordinationStatus } from "../src/update-coordination.js";
@@ -154,7 +157,17 @@ function formatRepairHuman(result) {
154
157
  result.after.missing.files.length + result.after.missing.records.length;
155
158
  const firstIssue = result.after.missing.files[0] ?? result.after.missing.records[0];
156
159
  const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
157
- return `Repair finished with ${outstanding.toLocaleString()} remaining issue(s).${detail} Run \`knodin status --deep\` for details.\n`;
160
+ // Say when nothing moved: on nova the same 111-issue graph went through
161
+ // status -> repair -> status unchanged, and "Repair finished with 141
162
+ // remaining issue(s)" read as progress. Then print the audit's own remedy
163
+ // rather than a generic pointer; for a legacy epoch-less graph the audit
164
+ // already knows repair cannot fix it and index --clean can (KNODIN-50).
165
+ const unchanged = result.repaired.length === 0 &&
166
+ result.before.missing.files.length === result.after.missing.files.length &&
167
+ result.before.missing.records.length === result.after.missing.records.length;
168
+ const progress = unchanged ? "Repair made no changes; " : "Repair finished with ";
169
+ const remedy = result.after.repairSteps[0] ?? "Run `knodin status --deep` for details.";
170
+ return `${progress}${outstanding.toLocaleString()} remaining issue(s).${detail} ${remedy}\n`;
158
171
  }
159
172
  /**
160
173
  * The semantic gap, stated rather than left to be discovered.
@@ -195,7 +208,8 @@ function formatIndexVerificationError(result) {
195
208
  const condition = result.verification.issueCount > 0
196
209
  ? `${result.verification.issueCount.toLocaleString()} graph issue(s) remain`
197
210
  : `graph verification reported status "${result.verification.status}"`;
198
- return `knodin index: requested work completed, but ${condition}.${detail} Run \`knodin repair\`.\n`;
211
+ const remedy = result.verification.repairSteps[0] ?? "Run `knodin repair`.";
212
+ return `knodin index: requested work completed, but ${condition}.${detail} ${remedy}\n`;
199
213
  }
200
214
  /**
201
215
  * The hooks/lifecycle line of `status`.
@@ -498,7 +512,14 @@ function formatStatusHuman(result) {
498
512
  const lifecycleOnly = result.lifecycle?.status === "degraded" &&
499
513
  (result.coverage.countsUnknown || result.missing.files.length === 0) &&
500
514
  result.missing.records.every((record) => result.lifecycle?.issues.includes(record));
501
- const repairCommand = worktreeStep ?? (lifecycleOnly ? "Run `knodin repair --lifecycle`." : "Run `knodin repair`.");
515
+ // Otherwise the engine's first step is authoritative too: it distinguishes
516
+ // "rebuild damaged rows" from "this graph predates the content-proof epoch
517
+ // and only index --clean can certify it" — a case where sending the user to
518
+ // repair costs two failing round trips (KNODIN-50).
519
+ const repairCommand = worktreeStep ??
520
+ (lifecycleOnly
521
+ ? "Run `knodin repair --lifecycle`."
522
+ : (result.repairSteps?.[0] ?? "Run `knodin repair`."));
502
523
  return `Graph or lifecycle needs repair: ${outstanding} issue(s) found (${coverage}).${detail} ${repairCommand}\n${lifecycleLine}${integrationLine}`;
503
524
  }
504
525
  function humanLabel(key) {
@@ -1289,6 +1310,8 @@ async function main() {
1289
1310
  const depth = invocation.options.depth;
1290
1311
  const retentionDays = invocation.options.retentionDays;
1291
1312
  const keepNewest = invocation.options.keepNewest;
1313
+ const maxCount = invocation.options.maxCount;
1314
+ const maxAllocatedBytes = invocation.options.maxBytes;
1292
1315
  let output;
1293
1316
  if (action === "list")
1294
1317
  output = listBackups(defaultRoots(), { depth });
@@ -1297,16 +1320,70 @@ async function main() {
1297
1320
  depth,
1298
1321
  retentionDays,
1299
1322
  keepNewest,
1323
+ maxCount,
1324
+ maxAllocatedBytes,
1300
1325
  apply: invocation.options.apply === true,
1301
1326
  });
1302
1327
  }
1328
+ else if (action === "policy") {
1329
+ output = defaultRoots().map((repository) => {
1330
+ if (retentionAction === "status")
1331
+ return {
1332
+ repository,
1333
+ policy: resolveStoragePolicy(repository),
1334
+ inventory: inventoryStorage(repository),
1335
+ };
1336
+ if (retentionAction !== "preview" && retentionAction !== "apply")
1337
+ throw new Error("knodin backups policy requires preview, apply, or status");
1338
+ const changes = {
1339
+ apply: retentionAction === "apply",
1340
+ };
1341
+ if (maxCount !== undefined)
1342
+ changes.maxCount = maxCount;
1343
+ if (maxAllocatedBytes !== undefined)
1344
+ changes.maxAllocatedBytes = maxAllocatedBytes;
1345
+ if (invocation.options.reserveBytes !== undefined)
1346
+ changes.minFreeBytes = invocation.options.reserveBytes;
1347
+ const mode = invocation.options.mode;
1348
+ if (mode !== undefined) {
1349
+ changes.enabled = mode !== "disabled";
1350
+ changes.dryRun = mode === "preview";
1351
+ }
1352
+ const configured = configureStoragePolicy(repository, changes);
1353
+ const maintenance = retentionAction === "apply" ? postOperationStorageMaintenance(repository) : null;
1354
+ const candidateWarnings = [];
1355
+ const candidatesRemoved = maintenance?.policy?.enabled && !maintenance.policy.dryRun
1356
+ ? sweepAbandonedCandidates(repository, {
1357
+ onDiagnostic: (warning) => {
1358
+ if (candidateWarnings.length < 8)
1359
+ candidateWarnings.push(warning.reason.slice(0, 512));
1360
+ },
1361
+ })
1362
+ : 0;
1363
+ return {
1364
+ repository,
1365
+ ...configured,
1366
+ inventory: inventoryStorage(repository),
1367
+ cleanup: retentionAction === "apply"
1368
+ ? { ...maintenance, candidatesRemoved, candidateWarnings }
1369
+ : pruneBackups([repository], {
1370
+ ...configured.policy,
1371
+ depth: 0,
1372
+ includeRegistry: false,
1373
+ apply: false,
1374
+ }),
1375
+ };
1376
+ });
1377
+ }
1303
1378
  else if (action === "retention" && retentionAction === "install") {
1304
1379
  output = installBackupRetention(roots, {
1305
1380
  depth,
1306
1381
  retentionDays,
1307
1382
  keepNewest,
1383
+ maxCount,
1384
+ maxAllocatedBytes,
1308
1385
  schedule: invocation.options.schedule,
1309
- dryRun: invocation.options.dryRun === true,
1386
+ dryRun: invocation.options.dryRun,
1310
1387
  launcher: runtimeCommand,
1311
1388
  });
1312
1389
  }
@@ -1389,10 +1466,19 @@ async function main() {
1389
1466
  const repo = resolved.repo;
1390
1467
  if (cmd !== "doctor") {
1391
1468
  try {
1392
- maybeRunOpportunisticRetention(repo);
1469
+ const maintenance = maybeRunOpportunisticRetention(repo);
1470
+ const issues = "issues" in maintenance
1471
+ ? maintenance.issues
1472
+ : "result" in maintenance && maintenance.result
1473
+ ? maintenance.result.skipped.map((entry) => entry.reason)
1474
+ : [];
1475
+ if (issues?.length)
1476
+ process.stderr.write(`knodin storage: ${issues.slice(0, 8).join("; ").slice(0, 4096)}\n`);
1393
1477
  }
1394
- catch {
1395
- // Best-effort cleanup must never make an ordinary command fail.
1478
+ catch (error) {
1479
+ // Cleanup remains nonfatal, but failures are not hidden or reported as
1480
+ // reclaimed storage. Keep stdout/JSON framing untouched.
1481
+ process.stderr.write(`knodin storage: ${String(error).slice(0, 512)}\n`);
1396
1482
  }
1397
1483
  }
1398
1484
  if (cmd === "agent-event" && !fs.existsSync(resolveDbPath(repo)))
@@ -2168,10 +2254,34 @@ async function main() {
2168
2254
  throw new Error("knodin hook-refresh: invalid lifecycle event");
2169
2255
  }
2170
2256
  const shared = await opportunisticSharedRestore();
2257
+ // Keep the audit that the scoped index already ran. Before this the
2258
+ // hook reported only the paths it wrote and exited 0, so a graph the
2259
+ // audit refused to certify — nova's 0.13.0 graph after the 0.13.1
2260
+ // upgrade, 33 commits behind with lifecycle "healthy" — looked like a
2261
+ // success on every commit (KNODIN-49).
2262
+ let verification;
2171
2263
  const indexed = shared?.restored
2172
2264
  ? []
2173
- : await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
2174
- result = { indexed, ...(shared ? { sharedRestore: shared } : {}) };
2265
+ : await refreshFromGitEvent(repo, event, async (target, files) => {
2266
+ const indexResult = await engine.index(target, files);
2267
+ verification = indexResult.verification;
2268
+ return indexResult;
2269
+ });
2270
+ if (verification?.status === "repair-needed") {
2271
+ const firstIssue = verification.missing.records[0] ?? verification.missing.files[0] ?? "unknown issue";
2272
+ const remedy = verification.repairSteps[0] ?? "Run `knodin repair`.";
2273
+ throw new Error(`knodin hook-refresh: indexed ${indexed.length} path(s) but the graph audit refused to certify the result (${verification.issueCount.toLocaleString()} issue(s); first: ${firstIssue}). ${remedy}`);
2274
+ }
2275
+ // `at` and `event` make each indexer.log record self-describing; the
2276
+ // background script supplies the queued event's sequence through
2277
+ // KNODIN_HOOK_EVENT_SEQUENCE so the line can be tied to its Git event.
2278
+ result = {
2279
+ at: new Date().toISOString(),
2280
+ event: { ...event, sequence: process.env.KNODIN_HOOK_EVENT_SEQUENCE ?? null },
2281
+ indexed,
2282
+ ...(verification ? { verification } : {}),
2283
+ ...(shared ? { sharedRestore: shared } : {}),
2284
+ };
2175
2285
  }
2176
2286
  finally {
2177
2287
  lifecycleLease.release();
@@ -2525,6 +2635,7 @@ async function main() {
2525
2635
  ? {
2526
2636
  mode: "source",
2527
2637
  identity: explained.identity,
2638
+ platformEntry: explained.platformEntry,
2528
2639
  symbol: explained.symbol,
2529
2640
  source: explained.source,
2530
2641
  staleness: explained.staleness,