knodin 0.12.1 → 0.13.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/README.md +16 -1
- package/dist/bin/cli.js +230 -31
- package/dist/src/agent-events.js +25 -7
- package/dist/src/authenticated-cursor.js +81 -0
- package/dist/src/backup-retention.js +20 -6
- package/dist/src/class-consumer-contract.js +18 -0
- package/dist/src/class-consumer-cursor.js +91 -0
- package/dist/src/class-consumer-delivery.js +22 -0
- package/dist/src/class-consumer-page.js +148 -0
- package/dist/src/cli-model.js +9 -3
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/doctor.js +59 -6
- package/dist/src/engine/apex-class-uses.js +430 -0
- package/dist/src/engine/apex-entry-points.js +98 -0
- package/dist/src/engine/apex-receiver.js +301 -0
- package/dist/src/engine/embedding-reuse.js +57 -0
- package/dist/src/engine/embeddings.js +22 -0
- package/dist/src/engine/index-coverage.js +215 -0
- package/dist/src/engine/index.js +2507 -254
- package/dist/src/engine/salesforce-components.js +460 -0
- package/dist/src/engine/seal.js +3 -0
- package/dist/src/engine/sqlite.js +44 -0
- package/dist/src/evidence-bundle.js +283 -0
- package/dist/src/evidence-graph.js +163 -0
- package/dist/src/failure-diagnosis.js +80 -5
- package/dist/src/file-dependency.js +35 -0
- package/dist/src/graph-query-health.js +47 -1
- package/dist/src/implementation-search.js +69 -0
- package/dist/src/index-coverage-read.js +33 -0
- package/dist/src/init.js +91 -13
- package/dist/src/investigation.js +195 -0
- package/dist/src/lifecycle-health.js +62 -2
- package/dist/src/mcp-reliability.js +4 -0
- package/dist/src/mcp-worker-supervisor.js +122 -6
- package/dist/src/node-runtime.js +96 -0
- package/dist/src/progressive-evidence.js +4 -4
- package/dist/src/response-budget.js +129 -3
- package/dist/src/server.js +18 -5
- package/dist/src/shared-index/publisher.js +41 -1
- package/dist/src/tools/knodin-tools.js +224 -41
- package/docs/CLI.md +92 -0
- package/docs/MCP.md +67 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
- package/docs/SALESFORCE-BINDINGS.md +121 -0
- package/docs/SALESFORCE-DEAD-CODE.md +45 -0
- package/docs/SCOPED-INDEXING.md +76 -0
- package/docs/apex-receiver-resolution.md +41 -0
- package/docs/releases/0.12.2.md +88 -0
- package/docs/releases/0.13.0.md +62 -0
- package/docs/structural-only-indexing.md +20 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ Start broad, inspect the likely change surface, then review the actual diff:
|
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
152
|
# Orient on an unfamiliar checkout
|
|
153
|
-
knodin context
|
|
153
|
+
knodin context "understand this checkout"
|
|
154
154
|
|
|
155
155
|
# Explain a symbol with source and relationships
|
|
156
156
|
knodin explain createServer
|
|
@@ -170,6 +170,21 @@ knodin map
|
|
|
170
170
|
knodin pack createServer --max-tokens 4000
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
For a task that needs an implementation and its dependencies:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
knodin context "find code that persists an order" --implementations --json
|
|
177
|
+
knodin context "rename persist" --symbol persist --file src/storage.ts --json
|
|
178
|
+
knodin evidence expand --files src/storage.ts --items 100 --json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Discovery returns candidates for you to select; investigation collects source,
|
|
182
|
+
callers, importers, tests, and impact for the selected identity. Evidence bundles
|
|
183
|
+
page exact source and issue reusable per-file receipts after complete delivery.
|
|
184
|
+
Ambiguity, incomplete coverage, and budget limits stay visible. See the
|
|
185
|
+
[CLI guide](docs/CLI.md#task-discovery-and-investigation) and
|
|
186
|
+
[evidence protocol](docs/PROGRESSIVE-EVIDENCE.md#graph-backed-file-bundles).
|
|
187
|
+
|
|
173
188
|
If freshness or lifecycle checks fail:
|
|
174
189
|
|
|
175
190
|
```bash
|
package/dist/bin/cli.js
CHANGED
|
@@ -20,6 +20,10 @@ import { inspectClaudeAgentHooks, installClaudeAgentHooks, uninstallClaudeAgentH
|
|
|
20
20
|
import { detectSupportedAgents, parseInitScope, } from "../src/agent-integration.js";
|
|
21
21
|
import { refreshExternalGraphArtifacts, writeArtifactRefreshRecord, } from "../src/artifact-refresh.js";
|
|
22
22
|
import { formatBackupHuman, installBackupRetention, listBackups, maybeRunOpportunisticRetention, pruneBackups, removeBackupRetention, retentionDoctor, retentionStatus, runInstalledRetention, } from "../src/backup-retention.js";
|
|
23
|
+
import { ClassConsumerQueryError, } from "../src/class-consumer-contract.js";
|
|
24
|
+
import { readClassConsumerCursor } from "../src/class-consumer-cursor.js";
|
|
25
|
+
import { validateClassConsumerDelivery } from "../src/class-consumer-delivery.js";
|
|
26
|
+
import { finalizeClassConsumerPage } from "../src/class-consumer-page.js";
|
|
23
27
|
import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, planIndex, resolveCliRuntimeCommand, resolveRepo, } from "../src/cli-args.js";
|
|
24
28
|
import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
|
|
25
29
|
import { buildKnodinContext } from "../src/context.js";
|
|
@@ -31,19 +35,24 @@ import { createEngine, describeThrown, KNODIN_SCHEMA_VERSION, REPO_WIDE_QUERY_PA
|
|
|
31
35
|
import { runSeal } from "../src/engine/seal-command.js";
|
|
32
36
|
import { runSealedQuery } from "../src/engine/sealed-query.js";
|
|
33
37
|
import { resolveDbPath } from "../src/engine/state-paths.js";
|
|
38
|
+
import { deliverEvidenceBundle } from "../src/evidence-bundle.js";
|
|
39
|
+
import { createEvidenceGraphAdapter } from "../src/evidence-graph.js";
|
|
34
40
|
import { diagnoseFailure, } from "../src/failure-diagnosis.js";
|
|
35
41
|
import { gitExecutable } from "../src/git-executable.js";
|
|
36
42
|
import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
|
|
43
|
+
import { findImplementationCandidates } from "../src/implementation-search.js";
|
|
37
44
|
import { createIndexActivityReporter } from "../src/index-activity.js";
|
|
45
|
+
import { indexCoverageCore, indexCoverageNotice, indexCoverageSnapshot, } from "../src/index-coverage-read.js";
|
|
38
46
|
import { InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, repairLifecycleRouting, } from "../src/init.js";
|
|
39
47
|
import { createInitProgressRenderer } from "../src/init-progress.js";
|
|
48
|
+
import { buildTaskInvestigation } from "../src/investigation.js";
|
|
40
49
|
import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
|
|
41
50
|
import { acknowledgeUpdateFailure, queryOwnerAvailability, readManagerUpdateState, readUpdateJournal, resolveManagerOwnership, setManualPin, unpinUpdate, updateAttention, writeManagerUpdateState, } from "../src/manager-update.js";
|
|
42
51
|
import { addMirror, listMirrors, refreshMirror, removeMirror } from "../src/mirror.js";
|
|
43
52
|
import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
|
|
44
53
|
import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
|
|
45
54
|
import { auditPullRequests } from "../src/pr-triage.js";
|
|
46
|
-
import { deliverProgressiveEvidence, } from "../src/progressive-evidence.js";
|
|
55
|
+
import { deliverProgressiveEvidence, evidenceHandleSecret, } from "../src/progressive-evidence.js";
|
|
47
56
|
import { acquireRepairLease } from "../src/repair-lease.js";
|
|
48
57
|
import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, resolveRepairProgressMode, serializeRepairJsonlRecord, } from "../src/repair-progress.js";
|
|
49
58
|
import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
|
|
@@ -161,15 +170,21 @@ function formatSemanticGap(readiness) {
|
|
|
161
170
|
}
|
|
162
171
|
function formatIndexHuman(result) {
|
|
163
172
|
const semantic = formatSemanticGap(result.semanticReadiness);
|
|
173
|
+
const scope = formatIndexCoverageHuman(result.indexCoverage);
|
|
164
174
|
if (result.indexed.length === 0 && result.unchanged.length > 0) {
|
|
165
175
|
const noun = result.unchanged.length === 1 ? "file" : "files";
|
|
166
|
-
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.${semantic}\n`;
|
|
176
|
+
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.${semantic}\n${scope}\n`;
|
|
167
177
|
}
|
|
168
178
|
const unchanged = result.unchanged.length > 0
|
|
169
179
|
? `; ${result.unchanged.length.toLocaleString()} already current`
|
|
170
180
|
: "";
|
|
171
181
|
const noun = result.indexed.length === 1 ? "file" : "files";
|
|
172
|
-
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.${semantic}\n`;
|
|
182
|
+
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.${semantic}\n${scope}\n`;
|
|
183
|
+
}
|
|
184
|
+
function formatIndexCoverageHuman(coverage) {
|
|
185
|
+
if (!coverage)
|
|
186
|
+
return "Local graph index coverage is unknown; a negative graph answer does not establish absence.";
|
|
187
|
+
return `Local graph index coverage: ${coverage.mode}; intent ${coverage.intentState}; repository inventory complete: ${coverage.repositoryComplete}. ${indexCoverageNotice(coverage) ?? "Inventory extent does not establish complete static relationships or runtime use."}`;
|
|
173
188
|
}
|
|
174
189
|
function formatIndexVerificationError(result) {
|
|
175
190
|
const firstIssue = result.verification.missing.files[0] ?? result.verification.missing.records[0];
|
|
@@ -195,8 +210,15 @@ function formatLifecycleLine(lifecycle, isMirror) {
|
|
|
195
210
|
return "";
|
|
196
211
|
if (isMirror)
|
|
197
212
|
return "Hooks: not applicable; a mirror is refreshed explicitly, not by Git events.\n";
|
|
198
|
-
if (lifecycle.status === "healthy")
|
|
199
|
-
|
|
213
|
+
if (lifecycle.status === "healthy") {
|
|
214
|
+
// A silent self-heal would leave the user unable to explain why the hook's
|
|
215
|
+
// interpreter changed; a degraded report for a fixed condition would cry
|
|
216
|
+
// wolf. Say what happened, once, on the healthy line.
|
|
217
|
+
const heal = lifecycle.interpreterSelfHeal;
|
|
218
|
+
return heal
|
|
219
|
+
? `Hooks: installed and executable; background indexer interpreter self-healed (${heal.from} -> ${heal.to}).\n`
|
|
220
|
+
: "Hooks: installed and executable.\n";
|
|
221
|
+
}
|
|
200
222
|
const issue = lifecycle.issues[0] ?? "refresh capability is not verified";
|
|
201
223
|
return `Lifecycle refresh: ${lifecycle.status}; ${issue}. Run \`knodin repair --lifecycle\`.\n`;
|
|
202
224
|
}
|
|
@@ -407,7 +429,7 @@ function formatStatusHuman(result) {
|
|
|
407
429
|
const indexedCounts = result.coverage.countsUnknown
|
|
408
430
|
? "indexed and symbol counts unknown; the graph could not be read"
|
|
409
431
|
: `${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols`;
|
|
410
|
-
const coverage = `${result.coverage.sourceFiles} source files, ${indexedCounts}${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}`;
|
|
432
|
+
const coverage = `${result.coverage.sourceFiles} source files, ${indexedCounts}${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}. ${formatIndexCoverageHuman(result.indexCoverage)}`;
|
|
411
433
|
if (result.status === "indexing" && result.activity) {
|
|
412
434
|
const count = result.activity.phaseTotal === undefined
|
|
413
435
|
? ""
|
|
@@ -449,24 +471,34 @@ function formatStatusHuman(result) {
|
|
|
449
471
|
// led with an arbitrary source filename, so an absent graph read as damage
|
|
450
472
|
// proportional to repository size. `missing.files` still carries the repair
|
|
451
473
|
// worklist — only the count and the headline shown to a human change here.
|
|
452
|
-
const
|
|
453
|
-
? result.missing.records
|
|
454
|
-
: result.missing.files
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
474
|
+
const issueItems = result.coverage.countsUnknown
|
|
475
|
+
? result.missing.records
|
|
476
|
+
: [...result.missing.files, ...result.missing.records];
|
|
477
|
+
const outstanding = issueItems.length;
|
|
478
|
+
// One issue keeps the established single-line shape; more are listed bounded
|
|
479
|
+
// rather than hidden behind "First issue" — a report that names one problem
|
|
480
|
+
// out of several sends the user to fix it and then surprises them with the
|
|
481
|
+
// rest, one status invocation at a time (KNODIN-38).
|
|
482
|
+
const shownIssues = issueItems.slice(0, 5);
|
|
483
|
+
const hiddenIssues = outstanding - shownIssues.length;
|
|
484
|
+
const detail = shownIssues.length === 0
|
|
485
|
+
? ""
|
|
486
|
+
: shownIssues.length === 1
|
|
487
|
+
? ` First issue: ${shownIssues[0]}.`
|
|
488
|
+
: ` Issues: ${shownIssues.join("; ")}${hiddenIssues > 0 ? `; +${hiddenIssues} more (see --json)` : ""}.`;
|
|
459
489
|
// A linked worktree with no database is the one case where `repair` is both
|
|
460
490
|
// the wrong first step and the expensive one: `init` seeds from an indexed
|
|
461
491
|
// sibling and reconciles only what differs, while `repair` builds from
|
|
462
492
|
// scratch. The engine has already worked out that this is that case, so
|
|
463
493
|
// defer to the step it wrote rather than recomputing the judgement here.
|
|
464
494
|
const worktreeStep = result.repairSteps?.find((step) => step.includes("per-worktree"));
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
495
|
+
// Lifecycle-only damage takes `repair --lifecycle`, not `init`: the graph is
|
|
496
|
+
// healthy, and the engine's own repairSteps already say so — prescribing
|
|
497
|
+
// `init` here contradicted the lifecycle line printed directly beneath it.
|
|
498
|
+
const lifecycleOnly = result.lifecycle?.status === "degraded" &&
|
|
499
|
+
(result.coverage.countsUnknown || result.missing.files.length === 0) &&
|
|
500
|
+
result.missing.records.every((record) => result.lifecycle?.issues.includes(record));
|
|
501
|
+
const repairCommand = worktreeStep ?? (lifecycleOnly ? "Run `knodin repair --lifecycle`." : "Run `knodin repair`.");
|
|
470
502
|
return `Graph or lifecycle needs repair: ${outstanding} issue(s) found (${coverage}).${detail} ${repairCommand}\n${lifecycleLine}${integrationLine}`;
|
|
471
503
|
}
|
|
472
504
|
function humanLabel(key) {
|
|
@@ -1520,6 +1552,8 @@ async function main() {
|
|
|
1520
1552
|
return attemptOpportunisticSharedRestore(repo, engine);
|
|
1521
1553
|
};
|
|
1522
1554
|
let result;
|
|
1555
|
+
let finalizedClassConsumer = false;
|
|
1556
|
+
let classConsumerErrorContract;
|
|
1523
1557
|
let repairOutput;
|
|
1524
1558
|
let repairWasPlan = false;
|
|
1525
1559
|
let repairWasLifecycle = false;
|
|
@@ -1538,7 +1572,7 @@ async function main() {
|
|
|
1538
1572
|
process.exitCode = 1;
|
|
1539
1573
|
return verified;
|
|
1540
1574
|
}
|
|
1541
|
-
return decorateGraphQueryResult(value, verified.state, verified.graph.freshness);
|
|
1575
|
+
return decorateGraphQueryResult(value, verified.state, verified.graph.freshness, verified.graph.indexCoverage, health.graph.indexCoverage);
|
|
1542
1576
|
};
|
|
1543
1577
|
switch (cmd) {
|
|
1544
1578
|
case "shared": {
|
|
@@ -1813,10 +1847,11 @@ async function main() {
|
|
|
1813
1847
|
}
|
|
1814
1848
|
const freshness = status.freshness?.state ?? "unknown";
|
|
1815
1849
|
const cacheKey = JSON.stringify({
|
|
1816
|
-
schemaVersion:
|
|
1850
|
+
schemaVersion: 2,
|
|
1817
1851
|
head: status.freshness?.currentHead,
|
|
1818
1852
|
fingerprint: status.freshness?.workingTree?.indexedFingerprint,
|
|
1819
1853
|
generation: status.indexGeneration,
|
|
1854
|
+
indexCoverage: indexCoverageCore(status.indexCoverage),
|
|
1820
1855
|
budgets: [600, 8192, 12],
|
|
1821
1856
|
});
|
|
1822
1857
|
const cacheEligible = canUseSessionContextCache(status.status, freshness);
|
|
@@ -1831,7 +1866,11 @@ async function main() {
|
|
|
1831
1866
|
cacheHit = false;
|
|
1832
1867
|
try {
|
|
1833
1868
|
const context = await buildKnodinContext(engine, "Orient this coding session", repo, undefined, []);
|
|
1834
|
-
|
|
1869
|
+
const verified = await engine.status(repo, { audit: "cached" });
|
|
1870
|
+
if (indexCoverageSnapshot(status.indexCoverage) !==
|
|
1871
|
+
indexCoverageSnapshot(verified.indexCoverage))
|
|
1872
|
+
throw new Error("Index coverage changed during session context generation");
|
|
1873
|
+
text = renderSessionContext(context, freshness, verified.indexCoverage);
|
|
1835
1874
|
writeSessionContextCache(repo, cacheKey, text);
|
|
1836
1875
|
}
|
|
1837
1876
|
catch {
|
|
@@ -1867,13 +1906,20 @@ async function main() {
|
|
|
1867
1906
|
if (client !== undefined &&
|
|
1868
1907
|
!["claude", "codex", "gemini", "copilot", "antigravity"].includes(client))
|
|
1869
1908
|
throw new Error("knodin doctor: --client must be claude, codex, gemini, copilot, or antigravity");
|
|
1870
|
-
const unsupported = rest.filter((argument, index) => argument !== "--client" && rest[index - 1] !== "--client");
|
|
1909
|
+
const unsupported = rest.filter((argument, index) => argument !== "--client" && argument !== "--deep" && rest[index - 1] !== "--client");
|
|
1871
1910
|
if (unsupported.length > 0)
|
|
1872
1911
|
throw new Error(`knodin doctor: unknown option ${unsupported[0]}`);
|
|
1873
1912
|
const diagnosis = await diagnoseInstallation(repo, {
|
|
1874
1913
|
currentVersion: KNODIN_VERSION,
|
|
1875
1914
|
runtimeCommand: [...runtimeCommand, "serve"],
|
|
1876
|
-
|
|
1915
|
+
// Adaptive, like `status`: the persisted-audit path runs the same
|
|
1916
|
+
// drift probe, so a stale graph still reports stale, while a forced
|
|
1917
|
+
// deep audit on a very large repository cost doctor two full stat
|
|
1918
|
+
// sweeps every run (KNODIN-39). `--deep` keeps the exact audit
|
|
1919
|
+
// available on demand.
|
|
1920
|
+
graph: await engine.status(repo, {
|
|
1921
|
+
audit: rest.includes("--deep") ? "deep" : "adaptive",
|
|
1922
|
+
}),
|
|
1877
1923
|
client,
|
|
1878
1924
|
});
|
|
1879
1925
|
const manager = diagnosis.manager.name;
|
|
@@ -1884,7 +1930,23 @@ async function main() {
|
|
|
1884
1930
|
: "unknown",
|
|
1885
1931
|
env: process.env,
|
|
1886
1932
|
});
|
|
1887
|
-
|
|
1933
|
+
// One MCP probe backs every client row; the human listing printed the
|
|
1934
|
+
// identical initialize/toolsList block five times, reading as five
|
|
1935
|
+
// independent server checks (KNODIN-40). JSON keeps the per-client
|
|
1936
|
+
// shape for compatibility.
|
|
1937
|
+
const agents = diagnosis.agents;
|
|
1938
|
+
result = jsonOutput
|
|
1939
|
+
? diagnosis
|
|
1940
|
+
: {
|
|
1941
|
+
...diagnosis,
|
|
1942
|
+
agents: {
|
|
1943
|
+
...agents,
|
|
1944
|
+
clients: agents.clients?.map((clientRow) => ({
|
|
1945
|
+
...clientRow,
|
|
1946
|
+
server: "shared MCP probe; see the mcp section",
|
|
1947
|
+
})),
|
|
1948
|
+
},
|
|
1949
|
+
};
|
|
1888
1950
|
break;
|
|
1889
1951
|
}
|
|
1890
1952
|
case "system": {
|
|
@@ -2647,6 +2709,24 @@ async function main() {
|
|
|
2647
2709
|
case "evidence": {
|
|
2648
2710
|
const level = rest[0];
|
|
2649
2711
|
const file = rest[1];
|
|
2712
|
+
const files = selectorValue("--files")?.split(",");
|
|
2713
|
+
if (files) {
|
|
2714
|
+
if (level !== "evidence" && level !== "expand")
|
|
2715
|
+
throw new Error("Evidence bundles require evidence or expand level");
|
|
2716
|
+
if (file && !file.startsWith("--"))
|
|
2717
|
+
throw new Error("Choose a file or --files, not both");
|
|
2718
|
+
result = await deliverEvidenceBundle({
|
|
2719
|
+
repo,
|
|
2720
|
+
files,
|
|
2721
|
+
level,
|
|
2722
|
+
continuation: selectorValue("--continuation"),
|
|
2723
|
+
alreadyPresent: selectorValue("--already-present")?.split(","),
|
|
2724
|
+
byteLimit: responseBudget.bytes,
|
|
2725
|
+
tokenLimit: responseBudget.tokens,
|
|
2726
|
+
itemLimit: responseBudget.items,
|
|
2727
|
+
}, createEvidenceGraphAdapter(engine));
|
|
2728
|
+
break;
|
|
2729
|
+
}
|
|
2650
2730
|
if (!file || !["locate", "outline", "evidence", "expand"].includes(level))
|
|
2651
2731
|
throw new Error("knodin evidence requires locate|outline|evidence|expand <file>");
|
|
2652
2732
|
result = deliverProgressiveEvidence({
|
|
@@ -2677,7 +2757,7 @@ async function main() {
|
|
|
2677
2757
|
? ""
|
|
2678
2758
|
: (rest[1] ?? "");
|
|
2679
2759
|
if (!pattern) {
|
|
2680
|
-
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");
|
|
2760
|
+
process.stderr.write("knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|file_metrics|batch_outline|project_overview|shortest_path|cross_substrate_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|resource_reachability|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|class_consumers|children_of|federated_repos|mcp_tools|api_contract_mismatches)\n");
|
|
2681
2761
|
process.exit(1);
|
|
2682
2762
|
}
|
|
2683
2763
|
const directionValue = selectorValue("--direction");
|
|
@@ -2748,6 +2828,82 @@ async function main() {
|
|
|
2748
2828
|
process.exitCode = 1;
|
|
2749
2829
|
break;
|
|
2750
2830
|
}
|
|
2831
|
+
if (pattern === "class_consumers") {
|
|
2832
|
+
if (queryLimitRaw && Number(queryLimitRaw) > 1000)
|
|
2833
|
+
throw new Error("class_consumers --limit must not exceed 1000 sites");
|
|
2834
|
+
if (["--offset", "--kinds", "--relations", "--direction", "--depth", "--exclude-tests"].some((flag) => rest.includes(flag)))
|
|
2835
|
+
throw new Error("class_consumers supports --test-scope/--path and --continuation, not offset/kinds/relations/direction/depth/exclude-tests");
|
|
2836
|
+
const secret = evidenceHandleSecret(repo).secret;
|
|
2837
|
+
const token = selectorValue("--continuation");
|
|
2838
|
+
const cursor = token ? readClassConsumerCursor(token, secret) : undefined;
|
|
2839
|
+
const request = {
|
|
2840
|
+
symbol: target,
|
|
2841
|
+
selector,
|
|
2842
|
+
testScope: (testScopeValue ?? "all"),
|
|
2843
|
+
path: selectorValue("--path"),
|
|
2844
|
+
limit: queryLimitRaw ? Number(queryLimitRaw) : 100,
|
|
2845
|
+
...(cursor
|
|
2846
|
+
? {
|
|
2847
|
+
after: cursor.after,
|
|
2848
|
+
expectedQueryDigest: cursor.queryDigest,
|
|
2849
|
+
expectedSnapshotDigest: cursor.snapshotDigest,
|
|
2850
|
+
}
|
|
2851
|
+
: {}),
|
|
2852
|
+
};
|
|
2853
|
+
try {
|
|
2854
|
+
const page = await engine.queryClassConsumers(repo, request);
|
|
2855
|
+
// Defer maintenance, not the audit, until a later eligible status or close.
|
|
2856
|
+
const verified = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached", checkpoint: "defer" })));
|
|
2857
|
+
if (!verified.available) {
|
|
2858
|
+
result = verified;
|
|
2859
|
+
process.exitCode = 1;
|
|
2860
|
+
break;
|
|
2861
|
+
}
|
|
2862
|
+
await validateClassConsumerDelivery(page, request, (next) => engine.queryClassConsumers(repo, next));
|
|
2863
|
+
result = finalizeClassConsumerPage(page, {
|
|
2864
|
+
secret,
|
|
2865
|
+
budget: responseBudget,
|
|
2866
|
+
decorate: (candidate) => decorateGraphQueryResult(candidate, verified.state, undefined, verified.graph.indexCoverage, queryHealth?.available ? queryHealth.graph.indexCoverage : undefined),
|
|
2867
|
+
});
|
|
2868
|
+
finalizedClassConsumer = true;
|
|
2869
|
+
}
|
|
2870
|
+
catch (error) {
|
|
2871
|
+
if (error instanceof ClassConsumerQueryError &&
|
|
2872
|
+
[
|
|
2873
|
+
"CLASS_CONSUMER_TARGET_NOT_FOUND",
|
|
2874
|
+
"CLASS_CONSUMER_TARGET_AMBIGUOUS",
|
|
2875
|
+
"CLASS_CONSUMER_TARGET_CAPABILITY_MISSING",
|
|
2876
|
+
].includes(error.code)) {
|
|
2877
|
+
result = {
|
|
2878
|
+
available: false,
|
|
2879
|
+
status: "unavailable",
|
|
2880
|
+
pattern,
|
|
2881
|
+
targetResolution: error.code === "CLASS_CONSUMER_TARGET_CAPABILITY_MISSING"
|
|
2882
|
+
? "capability-missing"
|
|
2883
|
+
: error.code === "CLASS_CONSUMER_TARGET_NOT_FOUND"
|
|
2884
|
+
? "not-found"
|
|
2885
|
+
: "ambiguous",
|
|
2886
|
+
code: error.code,
|
|
2887
|
+
error: error.message,
|
|
2888
|
+
candidates: error.candidates,
|
|
2889
|
+
...(queryHealth?.available
|
|
2890
|
+
? {
|
|
2891
|
+
indexCoverage: queryHealth.graph.indexCoverage,
|
|
2892
|
+
freshness: queryHealth.graph.freshness,
|
|
2893
|
+
}
|
|
2894
|
+
: {}),
|
|
2895
|
+
};
|
|
2896
|
+
classConsumerErrorContract = {
|
|
2897
|
+
code: error.code,
|
|
2898
|
+
targetResolution: result.targetResolution,
|
|
2899
|
+
};
|
|
2900
|
+
process.exitCode = 1;
|
|
2901
|
+
}
|
|
2902
|
+
else
|
|
2903
|
+
throw error;
|
|
2904
|
+
}
|
|
2905
|
+
break;
|
|
2906
|
+
}
|
|
2751
2907
|
result = await engine.query(pattern, target, repo, to, queryLimitRaw ? Number(queryLimitRaw) : undefined, depth, detailLevel, selector, pattern === "impact"
|
|
2752
2908
|
? {
|
|
2753
2909
|
mode: selectorValue("--impact-mode") === "file" ? "file" : "symbol",
|
|
@@ -2807,12 +2963,12 @@ async function main() {
|
|
|
2807
2963
|
process.exitCode = 1;
|
|
2808
2964
|
break;
|
|
2809
2965
|
}
|
|
2810
|
-
result = decorateGraphQueryResult(result, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness);
|
|
2966
|
+
result = decorateGraphQueryResult(result, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness, verifiedQueryHealth.graph.indexCoverage, queryHealth.graph.indexCoverage);
|
|
2811
2967
|
}
|
|
2812
2968
|
// A target that does not exist is operator error, not a negative
|
|
2813
2969
|
// answer, so it exits non-zero like an unavailable graph rather than
|
|
2814
2970
|
// like real dead code (KNODIN-28). `resolved` with zero rows keeps
|
|
2815
|
-
// exit 0:
|
|
2971
|
+
// exit 0: no callers were found within the reported index coverage.
|
|
2816
2972
|
if (result?.targetResolution === "not-found")
|
|
2817
2973
|
process.exitCode = 1;
|
|
2818
2974
|
break;
|
|
@@ -2994,7 +3150,26 @@ async function main() {
|
|
|
2994
3150
|
process.stderr.write('knodin context requires a "<task>" description\n');
|
|
2995
3151
|
process.exit(1);
|
|
2996
3152
|
}
|
|
2997
|
-
|
|
3153
|
+
const symbol = selectorValue("--symbol");
|
|
3154
|
+
if (rest.includes("--implementations")) {
|
|
3155
|
+
if (symbol)
|
|
3156
|
+
throw new Error("Choose --implementations or --symbol, not both");
|
|
3157
|
+
result = await graphRead(() => findImplementationCandidates(engine, repo, {
|
|
3158
|
+
task,
|
|
3159
|
+
limit: selectorValue("--limit") ? Number(selectorValue("--limit")) : undefined,
|
|
3160
|
+
offset: selectorValue("--offset") ? Number(selectorValue("--offset")) : undefined,
|
|
3161
|
+
}));
|
|
3162
|
+
break;
|
|
3163
|
+
}
|
|
3164
|
+
result = await graphRead(() => symbol
|
|
3165
|
+
? buildTaskInvestigation(engine, repo, {
|
|
3166
|
+
task,
|
|
3167
|
+
symbol,
|
|
3168
|
+
...selector,
|
|
3169
|
+
limit: selectorValue("--limit") ? Number(selectorValue("--limit")) : undefined,
|
|
3170
|
+
depth: selectorValue("--depth") ? Number(selectorValue("--depth")) : undefined,
|
|
3171
|
+
})
|
|
3172
|
+
: buildKnodinContext(engine, task, repo, rest[1]));
|
|
2998
3173
|
break;
|
|
2999
3174
|
}
|
|
3000
3175
|
default:
|
|
@@ -3002,6 +3177,8 @@ async function main() {
|
|
|
3002
3177
|
process.exit(1);
|
|
3003
3178
|
}
|
|
3004
3179
|
await engine.close();
|
|
3180
|
+
if (result?.available === false)
|
|
3181
|
+
process.exitCode = 1;
|
|
3005
3182
|
if (agentEventOutput) {
|
|
3006
3183
|
if (result !== null && result !== undefined)
|
|
3007
3184
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
@@ -3010,9 +3187,31 @@ async function main() {
|
|
|
3010
3187
|
if (statusWasWatched)
|
|
3011
3188
|
return;
|
|
3012
3189
|
const fileMetricsOutput = cmd === "query" && rest[0] === "file_metrics";
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3190
|
+
// The bundle protocol budgets its signed manifest, source and continuation together.
|
|
3191
|
+
// Generic post-truncation would invalidate that recoverable envelope.
|
|
3192
|
+
const evidenceBundle = cmd === "evidence" && selectorValue("--files") !== undefined;
|
|
3193
|
+
const boundedResult = evidenceBundle || finalizedClassConsumer
|
|
3194
|
+
? result
|
|
3195
|
+
: applyResponseBudget(result, cmd === "query" && rest[0] === "flow_analysis" ? "query:flow_analysis" : cmd, responseBudget, fileMetricsOutput
|
|
3196
|
+
? { bytes: 16_777_216, tokens: 4_194_304, items: 100_000 }
|
|
3197
|
+
: { bytes: 65_536, tokens: 16_384, items: 100 });
|
|
3198
|
+
if (classConsumerErrorContract) {
|
|
3199
|
+
const delivered = boundedResult;
|
|
3200
|
+
if (delivered.code !== classConsumerErrorContract.code ||
|
|
3201
|
+
delivered.targetResolution !== classConsumerErrorContract.targetResolution ||
|
|
3202
|
+
delivered.available !== false ||
|
|
3203
|
+
delivered.status !== "unavailable")
|
|
3204
|
+
throw new Error("Response budget cannot preserve the class-consumer error contract; increase the budget.");
|
|
3205
|
+
}
|
|
3206
|
+
if (cmd === "query" && rest[0] === "inheritors_of") {
|
|
3207
|
+
const qualification = result
|
|
3208
|
+
?.inheritanceQuery;
|
|
3209
|
+
const delivered = boundedResult
|
|
3210
|
+
?.inheritanceQuery;
|
|
3211
|
+
if (qualification &&
|
|
3212
|
+
["version", "mode", "partial", "truncated", "apex"].some((key) => delivered?.[key] !== qualification[key]))
|
|
3213
|
+
throw new Error("Response budget cannot preserve the inheritance qualification; increase the budget.");
|
|
3214
|
+
}
|
|
3016
3215
|
const finalExitCode = Math.max(Number(process.exitCode ?? 0), repairExitCode);
|
|
3017
3216
|
if (cmd === "init" && !jsonOutput) {
|
|
3018
3217
|
process.stdout.write(formatInitHuman(boundedResult));
|
package/dist/src/agent-events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { indexCoverageCore, indexCoverageNotice } from "./index-coverage-read.js";
|
|
3
4
|
import { countOutputTokens } from "./output-telemetry.js";
|
|
4
5
|
import { appendSessionEvent, } from "./session-telemetry.js";
|
|
5
6
|
export const SESSION_CONTEXT_BUDGET_LINE = "Budget: at most 600 tokens, 8 KiB, 12 items.";
|
|
@@ -81,23 +82,40 @@ export function writeSessionContextCache(repo, key, text) {
|
|
|
81
82
|
export function canUseSessionContextCache(status, freshness) {
|
|
82
83
|
return status === "healthy" && freshness === "fresh";
|
|
83
84
|
}
|
|
84
|
-
export function renderSessionContext(context, freshness) {
|
|
85
|
+
export function renderSessionContext(context, freshness, coverage) {
|
|
85
86
|
const list = (values, render) => (values ?? []).slice(0, 3).map(render).join(", ") || "none reported";
|
|
86
|
-
|
|
87
|
+
const header = [
|
|
87
88
|
"## knodin session context",
|
|
88
89
|
`Graph evidence: ${freshness}. Treat stale or unavailable evidence as incomplete.`,
|
|
89
90
|
SESSION_CONTEXT_BUDGET_LINE,
|
|
90
|
-
`
|
|
91
|
+
`Index coverage: ${JSON.stringify(indexCoverageCore(coverage) ?? { mode: "unknown", repositoryComplete: false, negativeScope: "unknown" })}`,
|
|
92
|
+
indexCoverageNotice(coverage) ??
|
|
93
|
+
(coverage
|
|
94
|
+
? "Inventory extent does not prove complete relationships or runtime use."
|
|
95
|
+
: "Index coverage is unknown; negatives do not establish repository-wide absence."),
|
|
96
|
+
];
|
|
97
|
+
const details = [
|
|
98
|
+
`Indexed scope: ${context.stats?.files ?? "?"} files, ${context.stats?.symbols ?? "?"} symbols.`,
|
|
91
99
|
`Subsystems: ${list(context.communities, (value) => `${value.name} (${value.size})`)}`,
|
|
92
100
|
`Hubs: ${list(context.hubs, (value) => `${value.symbol} (${value.degree})`)}`,
|
|
93
101
|
`Flows: ${list(context.flows, (value) => value.symbol ?? "unnamed")}`,
|
|
94
102
|
`Suggested next operation: ${context.suggestedOperation ?? "context"} (heuristic only).`,
|
|
95
103
|
"Use exact source evidence and preserve ambiguity, freshness, omissions, and response budgets.",
|
|
96
|
-
]
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
];
|
|
105
|
+
let truncated = false;
|
|
106
|
+
for (;;) {
|
|
107
|
+
const text = [
|
|
108
|
+
...header,
|
|
109
|
+
...details,
|
|
110
|
+
...(truncated ? ["Context truncated to the configured session budget."] : []),
|
|
111
|
+
].join("\n");
|
|
112
|
+
if (countOutputTokens(text) <= 600 && Buffer.byteLength(text) <= 8192)
|
|
113
|
+
return text;
|
|
114
|
+
if (details.length === 0)
|
|
115
|
+
throw new RangeError("Session context budget cannot preserve index coverage");
|
|
116
|
+
details.pop();
|
|
117
|
+
truncated = true;
|
|
99
118
|
}
|
|
100
|
-
return text;
|
|
101
119
|
}
|
|
102
120
|
export function recordClaudeLifecycleEvent(repo, event, payload) {
|
|
103
121
|
const sessionId = payload.session_id;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export const MAX_CURSOR_PAYLOAD_BYTES = 4096;
|
|
3
|
+
export const MAX_AUTHENTICATED_CURSOR_LENGTH = 6144;
|
|
4
|
+
const DOMAIN = "knodin:authenticated-cursor:v1\0";
|
|
5
|
+
function checkParameters(secret, prefix) {
|
|
6
|
+
if (!Buffer.isBuffer(secret) || secret.length !== 32)
|
|
7
|
+
throw new TypeError("cursor signing key must be 32 bytes");
|
|
8
|
+
if (!/^[a-z][a-z0-9]{1,15}$/.test(prefix))
|
|
9
|
+
throw new TypeError("invalid cursor prefix");
|
|
10
|
+
}
|
|
11
|
+
function signature(secret, prefix, body) {
|
|
12
|
+
return createHmac("sha256", secret)
|
|
13
|
+
.update(DOMAIN)
|
|
14
|
+
.update(prefix)
|
|
15
|
+
.update("\0")
|
|
16
|
+
.update(body)
|
|
17
|
+
.digest();
|
|
18
|
+
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const prototype = Object.getPrototypeOf(value);
|
|
23
|
+
return prototype === Object.prototype || prototype === null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Authentication only, not encryption, schema validation or a freshness lease.
|
|
27
|
+
* Callers supply the existing per-repository evidence secret and a distinct
|
|
28
|
+
* protocol prefix; they must validate payload shape and current query/snapshot.
|
|
29
|
+
* Sign only the boundary of rows actually delivered after response budgeting.
|
|
30
|
+
*/
|
|
31
|
+
export function signAuthenticatedCursor(value, secret, prefix) {
|
|
32
|
+
checkParameters(secret, prefix);
|
|
33
|
+
let serialized;
|
|
34
|
+
try {
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
throw new Error("cursor payload is not a record");
|
|
37
|
+
serialized = JSON.stringify(value);
|
|
38
|
+
if (typeof serialized !== "string" || !isRecord(JSON.parse(serialized)))
|
|
39
|
+
throw new Error("serialized cursor payload is not a record");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new TypeError("cursor payload must be a serializable JSON object");
|
|
43
|
+
}
|
|
44
|
+
if (Buffer.byteLength(serialized) > MAX_CURSOR_PAYLOAD_BYTES)
|
|
45
|
+
throw new RangeError("cursor payload exceeds its byte limit");
|
|
46
|
+
const body = Buffer.from(serialized).toString("base64url");
|
|
47
|
+
return `${prefix}.${body}.${signature(secret, prefix, body).toString("base64url")}`;
|
|
48
|
+
}
|
|
49
|
+
export function verifyAuthenticatedCursor(value, secret, prefix) {
|
|
50
|
+
checkParameters(secret, prefix);
|
|
51
|
+
const invalid = () => new Error("invalid authenticated cursor");
|
|
52
|
+
if (typeof value !== "string" || value.length > MAX_AUTHENTICATED_CURSOR_LENGTH)
|
|
53
|
+
throw invalid();
|
|
54
|
+
const parts = value.split(".");
|
|
55
|
+
if (parts.length !== 3 || parts[0] !== prefix)
|
|
56
|
+
throw invalid();
|
|
57
|
+
const [, body, encodedSignature] = parts;
|
|
58
|
+
if (!/^[A-Za-z0-9_-]+$/.test(body) || !/^[A-Za-z0-9_-]+$/.test(encodedSignature))
|
|
59
|
+
throw invalid();
|
|
60
|
+
const actual = Buffer.from(encodedSignature, "base64url");
|
|
61
|
+
const expected = signature(secret, prefix, body);
|
|
62
|
+
if (actual.length !== expected.length ||
|
|
63
|
+
actual.toString("base64url") !== encodedSignature ||
|
|
64
|
+
!timingSafeEqual(actual, expected))
|
|
65
|
+
throw invalid();
|
|
66
|
+
const bytes = Buffer.from(body, "base64url");
|
|
67
|
+
if (bytes.length > MAX_CURSOR_PAYLOAD_BYTES || bytes.toString("base64url") !== body)
|
|
68
|
+
throw invalid();
|
|
69
|
+
const text = bytes.toString("utf8");
|
|
70
|
+
if (!Buffer.from(text).equals(bytes))
|
|
71
|
+
throw invalid();
|
|
72
|
+
try {
|
|
73
|
+
const payload = JSON.parse(text);
|
|
74
|
+
if (isRecord(payload))
|
|
75
|
+
return payload;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Never echo untrusted cursor contents or decoder error details.
|
|
79
|
+
}
|
|
80
|
+
throw invalid();
|
|
81
|
+
}
|
|
@@ -355,12 +355,19 @@ function parseRetentionReceipt(value) {
|
|
|
355
355
|
}
|
|
356
356
|
function readInstalledRetention(options) {
|
|
357
357
|
const locations = homes(options);
|
|
358
|
+
const receiptPath = path.join(locations.stateRoot, "receipt.json");
|
|
358
359
|
const policy = parseRetentionPolicy(readJson(locations.policy));
|
|
359
|
-
const receipt = parseRetentionReceipt(readJson(
|
|
360
|
+
const receipt = parseRetentionReceipt(readJson(receiptPath));
|
|
361
|
+
// A machine where NEITHER file exists never had retention installed; that is
|
|
362
|
+
// a clean state, not a broken one, and reporting "missing-or-invalid" for it
|
|
363
|
+
// taught every fresh doctor run to cry attention-required (KNODIN-40). Any
|
|
364
|
+
// on-disk trace — even an unparseable file — is installation evidence and
|
|
365
|
+
// keeps the strict issue reporting.
|
|
366
|
+
const installEvidence = fs.existsSync(locations.policy) || fs.existsSync(receiptPath);
|
|
360
367
|
const issues = [];
|
|
361
|
-
if (!policy)
|
|
368
|
+
if (installEvidence && !policy)
|
|
362
369
|
issues.push("policy-missing-or-invalid");
|
|
363
|
-
if (!receipt)
|
|
370
|
+
if (installEvidence && !receipt)
|
|
364
371
|
issues.push("receipt-missing-or-invalid");
|
|
365
372
|
if (policy && receipt && digest(`${JSON.stringify(policy, null, 2)}\n`) !== receipt.policyDigest)
|
|
366
373
|
issues.push("policy-receipt-mismatch");
|
|
@@ -373,7 +380,7 @@ function readInstalledRetention(options) {
|
|
|
373
380
|
issues.push(`scheduler-missing: ${artifact.path}`);
|
|
374
381
|
}
|
|
375
382
|
}
|
|
376
|
-
return { policy, receipt, issues };
|
|
383
|
+
return { policy, receipt, issues, installEvidence };
|
|
377
384
|
}
|
|
378
385
|
function commandRunner(command, args) {
|
|
379
386
|
const result = childProcess.spawnSync(command, args, { encoding: "utf8" });
|
|
@@ -571,11 +578,18 @@ export function installBackupRetention(roots, options = {}) {
|
|
|
571
578
|
}
|
|
572
579
|
export function retentionStatus(options = {}) {
|
|
573
580
|
const locations = homes(options);
|
|
574
|
-
const { policy, receipt, issues } = readInstalledRetention(options);
|
|
581
|
+
const { policy, receipt, issues, installEvidence } = readInstalledRetention(options);
|
|
575
582
|
const lastRun = readJson(path.join(locations.stateRoot, "last-run.json"));
|
|
576
583
|
return {
|
|
577
584
|
schemaVersion: 1,
|
|
578
|
-
|
|
585
|
+
// Keyed on on-disk evidence rather than parse success: a never-installed
|
|
586
|
+
// machine is "not-installed" with no issues, while an installed-but-broken
|
|
587
|
+
// state (either file present, parseable or not) keeps attention-required.
|
|
588
|
+
status: !installEvidence && issues.length === 0
|
|
589
|
+
? "not-installed"
|
|
590
|
+
: issues.length === 0
|
|
591
|
+
? "healthy"
|
|
592
|
+
: "attention-required",
|
|
579
593
|
installed: Boolean(policy && receipt),
|
|
580
594
|
policy,
|
|
581
595
|
receipt,
|