knodin 0.5.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/LICENSE +21 -0
- package/README.md +590 -0
- package/dist/bin/cli.js +1704 -0
- package/dist/src/agent-integration.js +250 -0
- package/dist/src/artifact-refresh.js +81 -0
- package/dist/src/cli-args.js +267 -0
- package/dist/src/cli-model.js +324 -0
- package/dist/src/compact-structural.js +96 -0
- package/dist/src/competitive-constraints.js +20 -0
- package/dist/src/competitive-manifest.js +330 -0
- package/dist/src/competitive-measurement.js +183 -0
- package/dist/src/competitive-runner.js +453 -0
- package/dist/src/competitive-sandbox.js +108 -0
- package/dist/src/context-export.js +422 -0
- package/dist/src/context.js +102 -0
- package/dist/src/docs-sections.js +141 -0
- package/dist/src/doctor.js +380 -0
- package/dist/src/engine/ann-hnsw.js +271 -0
- package/dist/src/engine/embeddings.js +193 -0
- package/dist/src/engine/file-walker.js +43 -0
- package/dist/src/engine/index.js +13030 -0
- package/dist/src/engine/perf.js +115 -0
- package/dist/src/engine/prune.js +112 -0
- package/dist/src/engine/source-policy.js +69 -0
- package/dist/src/engine/sqlite.js +71 -0
- package/dist/src/engine/symbol-delete.js +58 -0
- package/dist/src/failure-diagnosis.js +590 -0
- package/dist/src/fleet.js +7 -0
- package/dist/src/git-executable.js +31 -0
- package/dist/src/graph-query-health.js +115 -0
- package/dist/src/index-activity.js +125 -0
- package/dist/src/init-progress-worker.js +107 -0
- package/dist/src/init-progress.js +155 -0
- package/dist/src/init.js +985 -0
- package/dist/src/lifecycle-health.js +213 -0
- package/dist/src/lsp-readonly.js +217 -0
- package/dist/src/output-compression.js +629 -0
- package/dist/src/output-telemetry.js +359 -0
- package/dist/src/pr-triage.js +638 -0
- package/dist/src/relationship-adapters.js +370 -0
- package/dist/src/release-attestation.js +533 -0
- package/dist/src/repair-progress-worker.js +121 -0
- package/dist/src/repair-progress.js +262 -0
- package/dist/src/repository-init-process.js +173 -0
- package/dist/src/repository-management.js +1089 -0
- package/dist/src/response-budget.js +184 -0
- package/dist/src/server.js +53 -0
- package/dist/src/system-config.js +615 -0
- package/dist/src/terminal-help.js +83 -0
- package/dist/src/tools/knodin-tools.js +1438 -0
- package/dist/src/tools/reckon-tools.js +5 -0
- package/dist/src/update-policy.js +944 -0
- package/dist/src/update-trust.js +503 -0
- package/dist/src/version.js +13 -0
- package/dist/src/visualization.js +162 -0
- package/dist/src/wait-for-fresh.js +98 -0
- package/dist/src/worktree-lifecycle.js +231 -0
- package/docs/CLI.md +39 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
- package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
- package/docs/DOCTOR-AND-UPDATES.md +84 -0
- package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
- package/docs/INSTALLATION.md +208 -0
- package/docs/MCP.md +100 -0
- package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
- package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
- package/docs/SIGNED-UPDATES.md +146 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
- package/docs/TELEMETRY.md +42 -0
- package/docs/releases/0.3.0.md +46 -0
- package/docs/releases/0.4.0.md +68 -0
- package/docs/releases/0.4.1.md +28 -0
- package/docs/releases/0.4.2.md +27 -0
- package/docs/releases/0.4.3.md +23 -0
- package/docs/releases/0.5.0.md +29 -0
- package/package.json +110 -0
- package/schemas/release-attestation-v1.schema.json +210 -0
- package/tree-sitter-prisma.wasm +0 -0
- package/tree-sitter-sql.wasm +0 -0
- package/tree-sitter-xml.wasm +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { attachLifecycleHealth, inspectLifecycleHealth } from "./lifecycle-health.js";
|
|
5
|
+
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
6
|
+
const PROCESSOR_RETRY_MS = 500;
|
|
7
|
+
function installedProcessorIsRunning(repo) {
|
|
8
|
+
try {
|
|
9
|
+
const pid = Number(fs.readFileSync(path.join(repo, ".reckon", "hooks", "refresh.lock", "pid"), "utf-8"));
|
|
10
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
11
|
+
return false;
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function launchInstalledLifecycleProcessor(repo) {
|
|
20
|
+
const processor = path.join(repo, ".reckon", "hooks", "background-index.sh");
|
|
21
|
+
if (fs.statSync(processor).mode & 0o111) {
|
|
22
|
+
const child = childProcess.spawn(processor, [], {
|
|
23
|
+
cwd: repo,
|
|
24
|
+
stdio: "ignore",
|
|
25
|
+
detached: process.platform !== "win32",
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
});
|
|
28
|
+
child.once("error", () => undefined);
|
|
29
|
+
child.unref();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function requiresStructuralRepair(graph) {
|
|
33
|
+
if (Object.values(graph.orphaned).some((count) => count > 0))
|
|
34
|
+
return true;
|
|
35
|
+
const ordinary = [
|
|
36
|
+
"indexed file no longer exists: ",
|
|
37
|
+
"indexed file is no longer eligible: ",
|
|
38
|
+
"indexed snapshot differs from disk: ",
|
|
39
|
+
];
|
|
40
|
+
return graph.missing.records.some((record) => !ordinary.some((prefix) => record.startsWith(prefix)));
|
|
41
|
+
}
|
|
42
|
+
function launchQueuedProcessorIfNeeded(repo, elapsed, timeoutMs, lastAttemptAt, processQueuedEvents) {
|
|
43
|
+
if (elapsed >= timeoutMs ||
|
|
44
|
+
inspectLifecycleHealth(repo).queuedEvents === 0 ||
|
|
45
|
+
installedProcessorIsRunning(repo) ||
|
|
46
|
+
elapsed - lastAttemptAt < PROCESSOR_RETRY_MS)
|
|
47
|
+
return lastAttemptAt;
|
|
48
|
+
try {
|
|
49
|
+
processQueuedEvents(repo);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Lifecycle health retains the queued event and wait remains truthful.
|
|
53
|
+
}
|
|
54
|
+
return elapsed;
|
|
55
|
+
}
|
|
56
|
+
function completedStatus(graph) {
|
|
57
|
+
if (graph.status === "healthy" && graph.freshness.state === "fresh")
|
|
58
|
+
return "fresh";
|
|
59
|
+
if (graph.status === "repair-needed" && requiresStructuralRepair(graph))
|
|
60
|
+
return "repair-needed";
|
|
61
|
+
if (graph.freshness.state === "unknown")
|
|
62
|
+
return "unknown";
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
async function reconcileOrdinaryDrift(engine, repo, graph) {
|
|
66
|
+
const shouldReconcile = (graph.status === "stale" || graph.status === "repair-needed") &&
|
|
67
|
+
graph.freshness.state !== "queued";
|
|
68
|
+
if (!shouldReconcile)
|
|
69
|
+
return graph;
|
|
70
|
+
await engine.query("stats", "", repo);
|
|
71
|
+
return attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
|
|
72
|
+
}
|
|
73
|
+
function completedResult(status, startedAt, polls, graph) {
|
|
74
|
+
return { status, waitedMs: Date.now() - startedAt, polls, graph };
|
|
75
|
+
}
|
|
76
|
+
/** Wait for queued work and reconcile ordinary drift; structural repair stays explicit. */
|
|
77
|
+
export async function waitForFresh(engine, repo, timeoutMs = 30_000, processQueuedEvents = launchInstalledLifecycleProcessor) {
|
|
78
|
+
const startedAt = Date.now();
|
|
79
|
+
let polls = 0;
|
|
80
|
+
let lastProcessorAttemptAt = Number.NEGATIVE_INFINITY;
|
|
81
|
+
for (;;) {
|
|
82
|
+
polls++;
|
|
83
|
+
const beforeStatusElapsed = Date.now() - startedAt;
|
|
84
|
+
lastProcessorAttemptAt = launchQueuedProcessorIfNeeded(repo, beforeStatusElapsed, timeoutMs, lastProcessorAttemptAt, processQueuedEvents);
|
|
85
|
+
let graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "cached" }));
|
|
86
|
+
let status = completedStatus(graph);
|
|
87
|
+
if (status)
|
|
88
|
+
return completedResult(status, startedAt, polls, graph);
|
|
89
|
+
graph = await reconcileOrdinaryDrift(engine, repo, graph);
|
|
90
|
+
status = completedStatus(graph);
|
|
91
|
+
if (status)
|
|
92
|
+
return completedResult(status, startedAt, polls, graph);
|
|
93
|
+
const elapsed = Date.now() - startedAt;
|
|
94
|
+
if (elapsed >= timeoutMs)
|
|
95
|
+
return { status: "timeout", waitedMs: elapsed, polls, graph };
|
|
96
|
+
await delay(Math.min(100, timeoutMs - elapsed));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function git(repo, args) {
|
|
6
|
+
return childProcess.execFileSync("git", args, {
|
|
7
|
+
cwd: repo,
|
|
8
|
+
encoding: "utf-8",
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function canonicalPath(target) {
|
|
13
|
+
try {
|
|
14
|
+
return fs.realpathSync(target);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return path.resolve(target);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function commonDirectory(repo) {
|
|
21
|
+
const value = git(repo, ["rev-parse", "--path-format=absolute", "--git-common-dir"]).trim();
|
|
22
|
+
return path.resolve(repo, value);
|
|
23
|
+
}
|
|
24
|
+
function registryPath(repo) {
|
|
25
|
+
return path.join(commonDirectory(repo), "knodin", "worktrees.json");
|
|
26
|
+
}
|
|
27
|
+
function emptyRegistry() {
|
|
28
|
+
return { schemaVersion: 1, graphState: "per-worktree", entries: [] };
|
|
29
|
+
}
|
|
30
|
+
function readRegistry(repo) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(fs.readFileSync(registryPath(repo), "utf-8"));
|
|
33
|
+
return parsed.schemaVersion === 1 && Array.isArray(parsed.entries) ? parsed : emptyRegistry();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return emptyRegistry();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeRegistry(repo, registry) {
|
|
40
|
+
const target = registryPath(repo);
|
|
41
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
42
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
43
|
+
fs.writeFileSync(temporary, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
|
|
44
|
+
fs.renameSync(temporary, target);
|
|
45
|
+
}
|
|
46
|
+
function identityFor(repo, worktreePath, classification) {
|
|
47
|
+
let administrativePath = classification;
|
|
48
|
+
try {
|
|
49
|
+
administrativePath = path.resolve(worktreePath, git(worktreePath, ["rev-parse", "--path-format=absolute", "--git-dir"]).trim());
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// A removed worktree retains its registry identity.
|
|
53
|
+
}
|
|
54
|
+
return crypto
|
|
55
|
+
.createHash("sha256")
|
|
56
|
+
.update(`${commonDirectory(repo)}\0${administrativePath}`)
|
|
57
|
+
.digest("hex")
|
|
58
|
+
.slice(0, 24);
|
|
59
|
+
}
|
|
60
|
+
function listGitWorktrees(repo) {
|
|
61
|
+
const fields = git(repo, ["worktree", "list", "--porcelain", "-z"]).split("\0");
|
|
62
|
+
const records = [];
|
|
63
|
+
for (const field of fields) {
|
|
64
|
+
if (field.startsWith("worktree ")) {
|
|
65
|
+
records.push({
|
|
66
|
+
path: path.resolve(field.slice("worktree ".length)),
|
|
67
|
+
classification: records.length === 0 ? "main" : "linked",
|
|
68
|
+
head: null,
|
|
69
|
+
branch: null,
|
|
70
|
+
locked: false,
|
|
71
|
+
prunable: null,
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const current = records.at(-1);
|
|
76
|
+
if (!current)
|
|
77
|
+
continue;
|
|
78
|
+
if (field.startsWith("HEAD "))
|
|
79
|
+
current.head = field.slice(5);
|
|
80
|
+
else if (field.startsWith("branch "))
|
|
81
|
+
current.branch = field.slice(7).replace(/^refs\/heads\//, "");
|
|
82
|
+
else if (field === "detached")
|
|
83
|
+
current.branch = null;
|
|
84
|
+
else if (field.startsWith("locked"))
|
|
85
|
+
current.locked = true;
|
|
86
|
+
else if (field.startsWith("prunable "))
|
|
87
|
+
current.prunable = field.slice(9);
|
|
88
|
+
}
|
|
89
|
+
return records;
|
|
90
|
+
}
|
|
91
|
+
/** Register one initialized checkout in Git-common metadata without sharing its graph database. */
|
|
92
|
+
export function registerInitializedWorktree(repoPath) {
|
|
93
|
+
const repo = path.resolve(repoPath);
|
|
94
|
+
const current = listGitWorktrees(repo).find((worktree) => canonicalPath(worktree.path) === canonicalPath(repo));
|
|
95
|
+
if (!current)
|
|
96
|
+
throw new Error(`knodin worktrees: ${repo} is not a registered Git worktree`);
|
|
97
|
+
const registry = readRegistry(repo);
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const identity = identityFor(repo, current.path, current.classification);
|
|
100
|
+
const existing = registry.entries.find((entry) => entry.identity === identity);
|
|
101
|
+
if (existing) {
|
|
102
|
+
existing.path = current.path;
|
|
103
|
+
existing.lastSeenAt = now;
|
|
104
|
+
delete existing.removedAt;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
registry.entries.push({
|
|
108
|
+
identity,
|
|
109
|
+
path: current.path,
|
|
110
|
+
classification: current.classification,
|
|
111
|
+
registeredAt: now,
|
|
112
|
+
lastSeenAt: now,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
registry.entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
116
|
+
writeRegistry(repo, registry);
|
|
117
|
+
}
|
|
118
|
+
export async function inspectWorktrees(repoPath, status) {
|
|
119
|
+
const repo = path.resolve(repoPath);
|
|
120
|
+
const current = listGitWorktrees(repo);
|
|
121
|
+
const registry = readRegistry(repo);
|
|
122
|
+
const livePaths = new Set(current.map((worktree) => worktree.path));
|
|
123
|
+
const worktrees = [];
|
|
124
|
+
for (const worktree of current) {
|
|
125
|
+
const identity = identityFor(repo, worktree.path, worktree.classification);
|
|
126
|
+
const registered = registry.entries.some((entry) => entry.identity === identity);
|
|
127
|
+
const graphInitialized = fs.existsSync(path.join(worktree.path, ".reckon", "db.sqlite"));
|
|
128
|
+
let indexedHead = null;
|
|
129
|
+
if (graphInitialized) {
|
|
130
|
+
try {
|
|
131
|
+
indexedHead = (await status(worktree.path)).lastIndexedHead || null;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
indexedHead = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
worktrees.push({
|
|
138
|
+
...worktree,
|
|
139
|
+
identity,
|
|
140
|
+
registered,
|
|
141
|
+
graphInitialized,
|
|
142
|
+
indexedHead,
|
|
143
|
+
freshness: indexedHead === null || worktree.head === null
|
|
144
|
+
? "unknown"
|
|
145
|
+
: indexedHead === worktree.head
|
|
146
|
+
? "fresh"
|
|
147
|
+
: "stale",
|
|
148
|
+
removedAt: null,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
for (const entry of registry.entries) {
|
|
152
|
+
if (livePaths.has(entry.path))
|
|
153
|
+
continue;
|
|
154
|
+
worktrees.push({
|
|
155
|
+
identity: entry.identity,
|
|
156
|
+
path: entry.path,
|
|
157
|
+
classification: entry.classification,
|
|
158
|
+
head: null,
|
|
159
|
+
branch: null,
|
|
160
|
+
locked: false,
|
|
161
|
+
prunable: "registered worktree is absent from Git's worktree inventory",
|
|
162
|
+
registered: true,
|
|
163
|
+
graphInitialized: fs.existsSync(path.join(entry.path, ".reckon", "db.sqlite")),
|
|
164
|
+
indexedHead: null,
|
|
165
|
+
freshness: "removed",
|
|
166
|
+
removedAt: entry.removedAt ?? null,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
worktrees.sort((left, right) => left.path.localeCompare(right.path));
|
|
170
|
+
return { schemaVersion: 1, graphState: "per-worktree", worktrees };
|
|
171
|
+
}
|
|
172
|
+
/** Reconcile the durable registry with Git metadata; no checkout or graph is deleted. */
|
|
173
|
+
export function reconcileWorktrees(repoPath) {
|
|
174
|
+
const repo = path.resolve(repoPath);
|
|
175
|
+
const current = listGitWorktrees(repo);
|
|
176
|
+
const registry = readRegistry(repo);
|
|
177
|
+
const now = new Date().toISOString();
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const added = [];
|
|
180
|
+
const restored = [];
|
|
181
|
+
for (const worktree of current) {
|
|
182
|
+
const identity = identityFor(repo, worktree.path, worktree.classification);
|
|
183
|
+
seen.add(identity);
|
|
184
|
+
const existing = registry.entries.find((entry) => entry.identity === identity);
|
|
185
|
+
if (!existing) {
|
|
186
|
+
registry.entries.push({
|
|
187
|
+
identity,
|
|
188
|
+
path: worktree.path,
|
|
189
|
+
classification: worktree.classification,
|
|
190
|
+
registeredAt: now,
|
|
191
|
+
lastSeenAt: now,
|
|
192
|
+
});
|
|
193
|
+
added.push(worktree.path);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (existing.removedAt)
|
|
197
|
+
restored.push(worktree.path);
|
|
198
|
+
existing.path = worktree.path;
|
|
199
|
+
existing.lastSeenAt = now;
|
|
200
|
+
delete existing.removedAt;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const removed = [];
|
|
204
|
+
for (const entry of registry.entries) {
|
|
205
|
+
if (!seen.has(entry.identity) && !entry.removedAt) {
|
|
206
|
+
entry.removedAt = now;
|
|
207
|
+
removed.push(entry.path);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
registry.entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
211
|
+
writeRegistry(repo, registry);
|
|
212
|
+
return { schemaVersion: 1, added, restored, removed };
|
|
213
|
+
}
|
|
214
|
+
/** Remove exactly one linked worktree through Git, then retain removal evidence in the registry. */
|
|
215
|
+
export function removeManagedWorktree(repoPath, targetPath, dryRun) {
|
|
216
|
+
const repo = path.resolve(repoPath);
|
|
217
|
+
const target = path.resolve(targetPath);
|
|
218
|
+
const record = listGitWorktrees(repo).find((worktree) => canonicalPath(worktree.path) === canonicalPath(target));
|
|
219
|
+
if (!record)
|
|
220
|
+
throw new Error(`knodin worktrees remove: ${target} is not a registered Git worktree`);
|
|
221
|
+
if (record.classification === "main")
|
|
222
|
+
throw new Error("knodin worktrees remove: refusing to remove the main worktree");
|
|
223
|
+
if (dryRun)
|
|
224
|
+
return { schemaVersion: 1, target, dryRun: true, removed: false };
|
|
225
|
+
childProcess.execFileSync("git", ["worktree", "remove", target], {
|
|
226
|
+
cwd: repo,
|
|
227
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
228
|
+
});
|
|
229
|
+
reconcileWorktrees(repo);
|
|
230
|
+
return { schemaVersion: 1, target, dryRun: false, removed: true };
|
|
231
|
+
}
|
package/docs/CLI.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Declarative CLI contract
|
|
2
|
+
|
|
3
|
+
knodin's command grammar is defined once in `src/cli-model.ts` with Commander
|
|
4
|
+
15.0.0. The model owns root and nested commands, positionals, global and local
|
|
5
|
+
options, numeric parsing, unknown-option rejection, and root or command help.
|
|
6
|
+
`bin/cli.ts` dispatches the validated invocation to the existing product
|
|
7
|
+
handlers so JSON output contracts and established commands remain compatible.
|
|
8
|
+
|
|
9
|
+
Run `knodin --help` for the root command inventory or append `--help` to any
|
|
10
|
+
declared command path, for example:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
knodin repos search --help
|
|
14
|
+
knodin update --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Help uses the active terminal width. Non-TTY output uses a deterministic
|
|
18
|
+
100-column layout so snapshots, documentation, and automation do not depend on
|
|
19
|
+
the caller's environment. Narrow and wide formatting are covered by pure tests;
|
|
20
|
+
the Unix acceptance fixture also exercises a real pseudo-terminal when the
|
|
21
|
+
host provides one. Windows uses the same deterministic formatter and Commander
|
|
22
|
+
model without depending on a Unix `script` executable.
|
|
23
|
+
|
|
24
|
+
Commander is an exact production dependency rather than a floating range. Its
|
|
25
|
+
resolved integrity is pinned by both npm and Bun lockfiles and flows through
|
|
26
|
+
the normal SBOM, provenance, package-install, vulnerability, and signed-release
|
|
27
|
+
gates. It adds no transitive runtime dependencies. Updating it requires the
|
|
28
|
+
same lockfile review and full CLI compatibility suite as any parser change.
|
|
29
|
+
|
|
30
|
+
Command-specific semantic checks remain close to their product handlers (for
|
|
31
|
+
example repository existence, mutually exclusive configuration modes, and
|
|
32
|
+
graph-query target rules), but syntax cannot reach a handler unless the shared
|
|
33
|
+
declarative model accepts it first.
|
|
34
|
+
|
|
35
|
+
`knodin pack grep` evaluates user patterns with exact-pinned RE2 WebAssembly,
|
|
36
|
+
not Node's backtracking regular-expression engine. Backreferences and lookaround
|
|
37
|
+
are rejected because they cannot retain RE2's linear-time guarantee. Route
|
|
38
|
+
matching treats application path text literally and recognizes only complete
|
|
39
|
+
Express/NestJS-style `:name` or `{name}` path segments as parameters.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Bounded diagnostic-output compression
|
|
2
|
+
|
|
3
|
+
knodin can deterministically compress already-produced build and test output
|
|
4
|
+
without running the command that created it. This capability is available from
|
|
5
|
+
the CLI and the single MCP gateway in post-0.3 development.
|
|
6
|
+
|
|
7
|
+
knodin deliberately does **not** expose `knodin run`. Portable command
|
|
8
|
+
execution remains excluded until the containment gate below can be proven.
|
|
9
|
+
|
|
10
|
+
## Use it
|
|
11
|
+
|
|
12
|
+
Prefer a repository-relative file or stdin so the full log does not first cross
|
|
13
|
+
an agent's context boundary:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
build-command > build.log 2>&1
|
|
17
|
+
knodin compress build.log --exit-code 1 --lines 200
|
|
18
|
+
|
|
19
|
+
build-command 2>&1 | knodin compress - --exit-code 1 --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The same operation is available through the one-tool MCP gateway:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"operation": "compress",
|
|
27
|
+
"artifactPath": "build.log",
|
|
28
|
+
"exitCode": 1,
|
|
29
|
+
"lineBudget": 200,
|
|
30
|
+
"compressionByteBudget": 16384
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`artifactPath` is repository-bound and must be a regular, non-symlinked file.
|
|
35
|
+
Direct `text` and ordered `events` inputs support integrations and behavioral
|
|
36
|
+
parity, but they do not prove context savings if the raw value has already been
|
|
37
|
+
placed in the model's context.
|
|
38
|
+
|
|
39
|
+
The always-loaded MCP schema advertises the common path to stay within its
|
|
40
|
+
token ceiling. Advanced, still runtime-validated arguments are `events`,
|
|
41
|
+
`signal`, `strategy`, `adapter`, `contextLines`, `retain`, `redactSecrets`,
|
|
42
|
+
`maxInputBytes`, and `raw`. Use this canonical guide when composing them.
|
|
43
|
+
|
|
44
|
+
## Contract
|
|
45
|
+
|
|
46
|
+
- Strategies: `smart`, `head-tail`, and `errors-only`.
|
|
47
|
+
- Adapters: generic fallback plus Vitest, Jest, pytest, Go test, Maven, Gradle,
|
|
48
|
+
.NET, and Cargo detection.
|
|
49
|
+
- Preserved metadata: original exit code and terminating signal.
|
|
50
|
+
- Preserved signals: detected errors, causes, stack frames, failing tests,
|
|
51
|
+
summaries, warnings, and bounded surrounding context.
|
|
52
|
+
- Hard limits: rendered content never exceeds the requested line count or
|
|
53
|
+
UTF-8 content-byte budget.
|
|
54
|
+
- Truthfulness: the status is `complete`, `compressed`, or
|
|
55
|
+
`insufficient-budget`; detected signals that cannot fit are listed rather
|
|
56
|
+
than silently described as preserved.
|
|
57
|
+
- Accounting: every omitted range has inclusive source-line bounds and its
|
|
58
|
+
exact source bytes, including original delimiters.
|
|
59
|
+
- Recovery: raw ordered events are retained locally by default and can be read
|
|
60
|
+
in bounded ranges without rerunning the command.
|
|
61
|
+
|
|
62
|
+
The default compact MCP result uses documented tuples:
|
|
63
|
+
|
|
64
|
+
- `exit`: `[exitCode, signal]`;
|
|
65
|
+
- `budget`: `[usedLines, lineLimit, usedUtf8Bytes, byteLimit]`;
|
|
66
|
+
- `fidelity`: `[detectedSignals, preservedSignals, unpreservedSignalIds]`;
|
|
67
|
+
- each `omitted` entry:
|
|
68
|
+
`[inclusiveStartLine, inclusiveEndLine, exactSourceBytes]`.
|
|
69
|
+
|
|
70
|
+
`artifactId` is the SHA-256 identity used with
|
|
71
|
+
`compressionAction: "read"`, `"diagnose"`, or `"delete"`. Request
|
|
72
|
+
`detailLevel: "standard"` for the fully named audit envelope.
|
|
73
|
+
|
|
74
|
+
CLI drill-down and cleanup are explicit:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
knodin compress read <artifact-id> --start 80 --end 140
|
|
78
|
+
knodin compress read <artifact-id> --raw
|
|
79
|
+
knodin compress delete <artifact-id>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Human output prints an explicit fidelity warning when a budget cannot retain
|
|
83
|
+
every detected signal. `--json` emits the stable, fully named core result.
|
|
84
|
+
|
|
85
|
+
## Connect a failure to code
|
|
86
|
+
|
|
87
|
+
After compression, diagnose the retained artifact without rerunning the failed
|
|
88
|
+
command:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
knodin compress diagnose <artifact-id> --limit 10 --context 2
|
|
92
|
+
knodin compress diagnose <artifact-id> --max-output-bytes 16384 --json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The same graph read is available through the single MCP gateway:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"operation": "compress",
|
|
100
|
+
"compressionAction": "diagnose",
|
|
101
|
+
"artifactId": "<sha256>",
|
|
102
|
+
"limit": 10,
|
|
103
|
+
"contextLines": 2,
|
|
104
|
+
"compressionByteBudget": 16384,
|
|
105
|
+
"detailLevel": "standard"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
MCP callers may provide already-produced `text` instead of `artifactId`, but
|
|
110
|
+
must provide exactly one. Direct text is bounded and secret-redacted before
|
|
111
|
+
diagnosis. Retained artifacts are preferable because omitted regions remain
|
|
112
|
+
available locally and the raw log need not first enter the agent context.
|
|
113
|
+
|
|
114
|
+
Diagnosis recognizes source locations emitted by major JavaScript/TypeScript,
|
|
115
|
+
Python, Java, C#, Go, and Rust toolchains. Every candidate is resolved against
|
|
116
|
+
tracked files inside the current repository. Foreign CI checkout prefixes can
|
|
117
|
+
map by a unique tracked suffix; basename-only matches remain unresolved when
|
|
118
|
+
ambiguous. Traversal, symlinks outside the repository, untracked files, and
|
|
119
|
+
missing graph records are refused rather than guessed.
|
|
120
|
+
|
|
121
|
+
For each resolved location, the fully named result contains:
|
|
122
|
+
|
|
123
|
+
- the reported path, line, column, and original evidence line;
|
|
124
|
+
- the uniquely resolved tracked file and nearest package manifest;
|
|
125
|
+
- the smallest enclosing graph symbol, stable identity, signature, and source
|
|
126
|
+
span;
|
|
127
|
+
- related tests, upstream callers, and downstream dependencies;
|
|
128
|
+
- recent commits touching the file;
|
|
129
|
+
- a source snippet inside a hard aggregate UTF-8 byte budget; and
|
|
130
|
+
- the indexed commit, current commit, and freshness state.
|
|
131
|
+
|
|
132
|
+
The default compact MCP result preserves the same evidence with tuples:
|
|
133
|
+
|
|
134
|
+
- `package`: `[name, manifest, ecosystem]`;
|
|
135
|
+
- `owner`: `[identity, symbol, kind, startLine, endLine, signature]`;
|
|
136
|
+
- relation entries:
|
|
137
|
+
`[identity, symbol, file, kind, line, confidence]`;
|
|
138
|
+
- `recent`: `[shortCommit, authoredAt, subject]`; and
|
|
139
|
+
- `freshness`: `[state, indexedHead, currentHead]`.
|
|
140
|
+
|
|
141
|
+
`resolved` means every detected reference was mapped with complete input,
|
|
142
|
+
current graph evidence, and an untruncated context bundle. `partial` identifies
|
|
143
|
+
any omitted input, unresolved reference, stale graph, or exhausted context
|
|
144
|
+
budget. `unresolved` means no safe repository location was established.
|
|
145
|
+
|
|
146
|
+
The relationships are static diagnostic candidates, not proof of runtime
|
|
147
|
+
causality. Package ownership is evidenced by the nearest tracked
|
|
148
|
+
`package.json`, `pyproject.toml`, `pom.xml`, `Cargo.toml`, `go.mod`, or
|
|
149
|
+
`.csproj`; malformed or unnamed manifests remain explicit. Dynamic dispatch,
|
|
150
|
+
generated paths, source maps, and framework wiring can require additional
|
|
151
|
+
runtime evidence.
|
|
152
|
+
|
|
153
|
+
## Local retention and redaction
|
|
154
|
+
|
|
155
|
+
Artifacts are stored beneath `.reckon/output/` with a private directory mode
|
|
156
|
+
and `0600` files. The default returned content and bounded reads redact common
|
|
157
|
+
credential forms and remove ANSI/control bytes. `--raw` is an explicit local
|
|
158
|
+
request and can reveal the original secrets. Redaction is defense in depth,
|
|
159
|
+
not a guarantee that every possible secret format is recognized.
|
|
160
|
+
|
|
161
|
+
Input is bounded to 16 MiB by default and cannot be configured above 64 MiB.
|
|
162
|
+
Oversized input is rejected rather than partially retained. Output artifacts
|
|
163
|
+
are content-addressed, reads are byte-bounded, deletion requires an exact
|
|
164
|
+
artifact identity, and symlinked storage paths are refused.
|
|
165
|
+
|
|
166
|
+
## Command-execution security gate
|
|
167
|
+
|
|
168
|
+
The safe compressor processes data only; it does not invoke a shell or child
|
|
169
|
+
process. A future runner must address this threat model with executable,
|
|
170
|
+
platform-specific evidence:
|
|
171
|
+
|
|
172
|
+
| Threat | Required proof before shipping a runner |
|
|
173
|
+
|---|---|
|
|
174
|
+
| Shell injection | Argument-vector execution by default; no implicit shell |
|
|
175
|
+
| Repository prompt injection | Repository text cannot alter executable or policy authorization |
|
|
176
|
+
| Arbitrary filesystem access | Repository-root binding plus an enforceable filesystem sandbox |
|
|
177
|
+
| Network access and exfiltration | Deny-by-default network containment where claimed |
|
|
178
|
+
| Environment or credential leakage | Minimal allowlisted environment and no ambient credentials |
|
|
179
|
+
| Child-process escape | Complete descendant tracking and termination |
|
|
180
|
+
| Timeout | Process-tree termination, not only parent termination |
|
|
181
|
+
| CPU, memory, disk, output exhaustion | Enforced resource and output caps |
|
|
182
|
+
| Symlink or path traversal | Realpath containment and race-resistant file handling |
|
|
183
|
+
| Destructive commands | Allowlisted profiles or per-call approval with an explicit audit record |
|
|
184
|
+
| Cross-worktree effects | Authorized worktree binding and named affected roots |
|
|
185
|
+
| Telemetry leakage | No command text or raw output in default telemetry; redact before opt-in export |
|
|
186
|
+
| Platform differences | Separate verified guarantees and explicit unsupported cases |
|
|
187
|
+
|
|
188
|
+
Node's cross-platform child-process API alone cannot impose a hard memory limit
|
|
189
|
+
on an arbitrary executable, Windows job-object behavior differs from POSIX
|
|
190
|
+
process groups, and portable network/filesystem containment requires more than
|
|
191
|
+
an output buffer and timeout. Until a reviewed native or operating-system
|
|
192
|
+
containment adapter proves the table above, command execution is an intentional
|
|
193
|
+
safety non-goal. Existing trusted execution tools can write a log and pass that
|
|
194
|
+
artifact to knodin for bounded compression.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Dead code and impact
|
|
2
|
+
|
|
3
|
+
`dead_code` reports static candidates, not deletion proof. A zero resolved
|
|
4
|
+
reference count means the current indexed static graph found no supported
|
|
5
|
+
reference. It does not prove that reflection, runtime dependency injection,
|
|
6
|
+
framework configuration, metadata, deployed infrastructure, or external
|
|
7
|
+
consumers cannot invoke the code.
|
|
8
|
+
|
|
9
|
+
Impact follows source-evidenced resolved relationships. High-confidence
|
|
10
|
+
incoming cross-repository relationships can make a dependency visible to
|
|
11
|
+
impact and dead-code analysis. Heuristic or unresolved relationships lower
|
|
12
|
+
deletion confidence; they must not falsely mark code live or safe.
|
|
13
|
+
|
|
14
|
+
Salesforce Apex, Flow, metadata, LWC/Aura/Visualforce, scheduled/platform
|
|
15
|
+
entrypoints, and deployed-org dependencies require platform corroboration.
|
|
16
|
+
Configuration-driven frameworks and generated registries have similar limits.
|
|
17
|
+
|
|
18
|
+
Before deletion:
|
|
19
|
+
|
|
20
|
+
1. run `knodin query impact <symbol> --direction upstream`;
|
|
21
|
+
2. inspect `tests_for`, relevant contracts, and system relationships;
|
|
22
|
+
3. verify index health and freshness;
|
|
23
|
+
4. investigate unresolved or heuristic incoming evidence;
|
|
24
|
+
5. use platform/runtime evidence where static analysis is incomplete.
|
|
25
|
+
|
|
26
|
+
An unavailable, uninitialized, empty, unhealthy, or partially omitted
|
|
27
|
+
repository cannot produce an ordinary safe zero-result answer.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Doctor and update awareness
|
|
2
|
+
|
|
3
|
+
`knodin doctor` reports:
|
|
4
|
+
|
|
5
|
+
- package, CLI, source, and MCP server version agreement;
|
|
6
|
+
- current Node runtime and npm prefix;
|
|
7
|
+
- manager ownership with confidence and evidence;
|
|
8
|
+
- resolved executable, symlink/shim chain, and duplicate candidates;
|
|
9
|
+
- supported agent configuration candidates;
|
|
10
|
+
- a real MCP initialize/tools-list result;
|
|
11
|
+
- Git-hook routing and refresh health;
|
|
12
|
+
- graph health;
|
|
13
|
+
- configured registry/channel;
|
|
14
|
+
- cached latest **trusted** release state; and
|
|
15
|
+
- the manager-safe action boundary when a signed update is known.
|
|
16
|
+
|
|
17
|
+
A manager is reported as known only from manager-owned environment evidence or
|
|
18
|
+
an identified shim. A path substring is labeled a medium-confidence heuristic,
|
|
19
|
+
not proof.
|
|
20
|
+
|
|
21
|
+
The former npm `latest` lookup has been removed from production paths. Doctor,
|
|
22
|
+
CLI status, and MCP status now consume the same signed-only state as:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
knodin update status
|
|
26
|
+
knodin update check
|
|
27
|
+
knodin update explain
|
|
28
|
+
knodin update apply
|
|
29
|
+
knodin update rollback
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Until the production root ceremony embeds an independently reviewed root and
|
|
33
|
+
pin, these commands report `trust-unconfigured` and make no request. A user
|
|
34
|
+
configuration file containing both a root path and its digest is deliberately
|
|
35
|
+
inert: accepting both from one mutable file would not be independent pinning.
|
|
36
|
+
|
|
37
|
+
After activation, an interactive ordinary command may atomically claim a due
|
|
38
|
+
background check. The foreground command does not wait for the network.
|
|
39
|
+
Metadata requests have a 512 KiB per-role cap, a 100 ms–10 s configured
|
|
40
|
+
timeout, no redirects, and remain within one approved HTTPS origin and path.
|
|
41
|
+
Disable checks or all network access with:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
RECKON_UPDATE_CHECK=0 knodin doctor
|
|
45
|
+
RECKON_OFFLINE=1 knodin update check
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requests contain fixed metadata or signed target paths only. They never contain
|
|
49
|
+
a repository name, source path, symbol, command, username, or telemetry. A
|
|
50
|
+
failed check retains the last verified monotonic state but reports
|
|
51
|
+
`check-failed`; it never turns failure into `up-to-date`.
|
|
52
|
+
|
|
53
|
+
Apply is opt-in and fails closed. It verifies threshold-signed metadata,
|
|
54
|
+
release age/channel/version policy, artifact length and SHA-256, a signed
|
|
55
|
+
provenance digest, configured channel cross-checks, and both candidate and
|
|
56
|
+
rollback artifacts before any manager action. Artifacts are quarantined with
|
|
57
|
+
private permissions, fresh exclusive paths, and a symlink-rejecting quarantine
|
|
58
|
+
root. Every enterprise-only primary or cross-check origin must be explicitly
|
|
59
|
+
approved. Only a plain npm-owned install currently has an exact local-artifact
|
|
60
|
+
adapter; mise, Volta, nvm/fnm/asdf, Homebrew, and unknown ownership remain
|
|
61
|
+
`manager-action-required` until their rollback-safe adapters are certified. No
|
|
62
|
+
manager is ever asked to resolve a mutable version or tag, and a rollback is not
|
|
63
|
+
reported successful until the restored installation passes its health check.
|
|
64
|
+
|
|
65
|
+
Automatic activation also requires a certified sandbox smoke test. None is
|
|
66
|
+
enabled in the production CLI yet, so `apply-patch`/`apply-minor` policies stop
|
|
67
|
+
after verified quarantine instead of executing untrusted code. C64 supplies
|
|
68
|
+
the provenance/SBOM and five-channel attestation machinery; the production
|
|
69
|
+
release must populate it, and C65 must supply compromised-channel and recovery
|
|
70
|
+
evidence before this gate can open.
|
|
71
|
+
|
|
72
|
+
Run `knodin status --deep` for graph details and `knodin repair` for surgical
|
|
73
|
+
repair. Re-run `knodin init` after changing the executable owner or MCP path.
|
|
74
|
+
|
|
75
|
+
## Query availability
|
|
76
|
+
|
|
77
|
+
Every graph read reports an explicit availability outcome. `no-match` means a
|
|
78
|
+
healthy graph was queried and produced no match. It is distinct from
|
|
79
|
+
`not-initialized`, `empty-repository`, `empty-index`, `repair-needed`,
|
|
80
|
+
`indexing`, and `unknown`; those states fail closed and include remediation
|
|
81
|
+
instead of returning an ordinary zero. An otherwise complete graph with broken
|
|
82
|
+
or displaced lifecycle hooks remains queryable but reports
|
|
83
|
+
`lifecycle-degraded`, so callers can use current evidence without mistaking its
|
|
84
|
+
refresh path for healthy.
|