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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SHARED_INDEX_LIMITS } from "./contract.js";
|
|
3
|
+
import { SharedIndexError } from "./s3-client.js";
|
|
4
|
+
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
5
|
+
const gitObject = z.string().regex(/^[a-f0-9]{40,64}$/);
|
|
6
|
+
const timestamp = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/);
|
|
7
|
+
const repository = z
|
|
8
|
+
.object({
|
|
9
|
+
host: z
|
|
10
|
+
.string()
|
|
11
|
+
.max(253)
|
|
12
|
+
.regex(/^(?!.*\.\.)(?!.*(?:^|\.)-)(?!.*-(?:\.|$))[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/),
|
|
13
|
+
owner: z
|
|
14
|
+
.string()
|
|
15
|
+
.max(255)
|
|
16
|
+
.regex(/^[a-z0-9](?:[a-z0-9-]{0,253}[a-z0-9])?$/),
|
|
17
|
+
name: z
|
|
18
|
+
.string()
|
|
19
|
+
.max(255)
|
|
20
|
+
.regex(/^[a-z0-9](?:[a-z0-9._-]{0,253}[a-z0-9])?$/),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
export const branchPointerSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
schemaVersion: z.literal(1),
|
|
26
|
+
registrationId: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,127}$/),
|
|
27
|
+
repository,
|
|
28
|
+
branch: z.string().min(1).max(1024),
|
|
29
|
+
commit: gitObject,
|
|
30
|
+
inputFingerprint: sha256,
|
|
31
|
+
manifestKey: z.string().min(1).max(2048),
|
|
32
|
+
manifestSha256: sha256,
|
|
33
|
+
updatedAt: timestamp,
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
export const compatibilitySchema = z
|
|
37
|
+
.object({
|
|
38
|
+
knodinVersion: z.string().regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/),
|
|
39
|
+
databaseSchemaVersion: z.number().int().positive().max(2_147_483_647),
|
|
40
|
+
modelId: z.string().min(1).max(512),
|
|
41
|
+
modelDigest: sha256,
|
|
42
|
+
parserBundleDigest: sha256,
|
|
43
|
+
indexConfigDigest: sha256,
|
|
44
|
+
prunePolicyDigest: sha256,
|
|
45
|
+
})
|
|
46
|
+
.strict();
|
|
47
|
+
export const manifestSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
schemaVersion: z.literal(1),
|
|
50
|
+
registrationId: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,127}$/),
|
|
51
|
+
repository,
|
|
52
|
+
commit: gitObject,
|
|
53
|
+
generatedAt: timestamp,
|
|
54
|
+
compatibility: compatibilitySchema,
|
|
55
|
+
graph: z
|
|
56
|
+
.object({
|
|
57
|
+
key: z.string().min(1).max(2048),
|
|
58
|
+
compressedSha256: sha256,
|
|
59
|
+
compressedBytes: z.number().int().positive().max(SHARED_INDEX_LIMITS.compressedBytes),
|
|
60
|
+
uncompressedSha256: sha256,
|
|
61
|
+
uncompressedBytes: z.number().int().positive().max(SHARED_INDEX_LIMITS.uncompressedBytes),
|
|
62
|
+
})
|
|
63
|
+
.strict(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
function parse(label, bytes, limit, schema) {
|
|
67
|
+
if (bytes.byteLength > limit)
|
|
68
|
+
throw new SharedIndexError("integrity", `shared-index ${label} exceeds its byte limit`);
|
|
69
|
+
let value;
|
|
70
|
+
try {
|
|
71
|
+
value = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new SharedIndexError("integrity", `shared-index ${label} is not valid JSON`);
|
|
75
|
+
}
|
|
76
|
+
const result = schema.safeParse(value);
|
|
77
|
+
if (!result.success)
|
|
78
|
+
throw new SharedIndexError("integrity", `shared-index ${label} does not match schema v1`);
|
|
79
|
+
return result.data;
|
|
80
|
+
}
|
|
81
|
+
export function parseBranchPointer(bytes) {
|
|
82
|
+
return parse("branch pointer", bytes, SHARED_INDEX_LIMITS.pointerBytes, branchPointerSchema);
|
|
83
|
+
}
|
|
84
|
+
export function parseManifest(bytes) {
|
|
85
|
+
return parse("manifest", bytes, SHARED_INDEX_LIMITS.manifestBytes, manifestSchema);
|
|
86
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import child_process from "node:child_process";
|
|
2
|
+
import { SHARED_INDEX_LIMITS, sharedBranchMatches, } from "./contract.js";
|
|
3
|
+
import { branchPointerKey, signatureKey, validatePointerBinding, verifyManifest, } from "./manifest.js";
|
|
4
|
+
import { SharedIndexError } from "./s3-client.js";
|
|
5
|
+
import { parseBranchPointer, parseManifest, } from "./schemas.js";
|
|
6
|
+
function git(repo, args) {
|
|
7
|
+
const result = child_process.spawnSync("git", args, {
|
|
8
|
+
cwd: repo,
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
11
|
+
});
|
|
12
|
+
return { status: result.status, stdout: result.stdout.trim() };
|
|
13
|
+
}
|
|
14
|
+
function localHead(repo) {
|
|
15
|
+
const result = git(repo, ["rev-parse", "--verify", "HEAD"]);
|
|
16
|
+
if (result.status !== 0 || !/^[a-f0-9]{40,64}$/.test(result.stdout))
|
|
17
|
+
throw new SharedIndexError("unavailable", "shared-index could not resolve local HEAD");
|
|
18
|
+
return result.stdout;
|
|
19
|
+
}
|
|
20
|
+
function currentBranch(repo) {
|
|
21
|
+
const result = git(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
22
|
+
return result.status === 0 && result.stdout ? result.stdout.normalize("NFC") : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function configuredBranchCandidates(config, branch) {
|
|
25
|
+
const candidates = new Set();
|
|
26
|
+
for (const pattern of config.branches) {
|
|
27
|
+
if (pattern.endsWith("/*")) {
|
|
28
|
+
if (branch && sharedBranchMatches(pattern, branch))
|
|
29
|
+
candidates.add(branch);
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
candidates.add(pattern.normalize("NFC"));
|
|
33
|
+
}
|
|
34
|
+
return [...candidates];
|
|
35
|
+
}
|
|
36
|
+
async function readBounded(stream, maxBytes) {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let received = 0;
|
|
39
|
+
for await (const chunk of stream) {
|
|
40
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
41
|
+
received += bytes.byteLength;
|
|
42
|
+
if (received > maxBytes)
|
|
43
|
+
throw new SharedIndexError("integrity", "shared-index object exceeded its byte limit");
|
|
44
|
+
chunks.push(bytes);
|
|
45
|
+
}
|
|
46
|
+
return Buffer.concat(chunks, received);
|
|
47
|
+
}
|
|
48
|
+
async function objectBytes(store, key, maxBytes, signal) {
|
|
49
|
+
const metadata = await store.head(key, { signal });
|
|
50
|
+
if (metadata.bytes > maxBytes)
|
|
51
|
+
throw new SharedIndexError("integrity", "shared-index object exceeds its byte limit");
|
|
52
|
+
const stream = await store.get(key, { maxBytes, signal });
|
|
53
|
+
const bytes = await readBounded(stream, maxBytes);
|
|
54
|
+
if (bytes.byteLength !== metadata.bytes)
|
|
55
|
+
throw new SharedIndexError("integrity", "shared-index object length changed during download");
|
|
56
|
+
return bytes;
|
|
57
|
+
}
|
|
58
|
+
function commitRelation(repo, head, commit) {
|
|
59
|
+
if (commit === head)
|
|
60
|
+
return { relation: "exact", distance: 0 };
|
|
61
|
+
const ancestor = git(repo, ["merge-base", "--is-ancestor", commit, head]);
|
|
62
|
+
if (ancestor.status !== 0)
|
|
63
|
+
return null;
|
|
64
|
+
const count = git(repo, ["rev-list", "--count", `${commit}..${head}`]);
|
|
65
|
+
const distance = Number(count.stdout);
|
|
66
|
+
if (count.status !== 0 || !Number.isSafeInteger(distance) || distance < 1)
|
|
67
|
+
return null;
|
|
68
|
+
return { relation: "ancestor", distance };
|
|
69
|
+
}
|
|
70
|
+
function rejection(branch, error) {
|
|
71
|
+
if (error instanceof SharedIndexError)
|
|
72
|
+
return { branch, category: error.category, reason: error.message.slice(0, 512) };
|
|
73
|
+
return {
|
|
74
|
+
branch,
|
|
75
|
+
category: "unavailable",
|
|
76
|
+
reason: "shared-index snapshot discovery failed",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export async function selectSharedSnapshot(options) {
|
|
80
|
+
const head = localHead(options.repo);
|
|
81
|
+
const branches = configuredBranchCandidates(options.config, currentBranch(options.repo));
|
|
82
|
+
const settled = await Promise.all(branches.map(async (branch) => {
|
|
83
|
+
try {
|
|
84
|
+
const pointerBytes = await objectBytes(options.store, branchPointerKey(options.config, branch), SHARED_INDEX_LIMITS.pointerBytes, options.signal);
|
|
85
|
+
const pointer = parseBranchPointer(pointerBytes);
|
|
86
|
+
if (pointer.branch.normalize("NFC") !== branch)
|
|
87
|
+
throw new SharedIndexError("integrity", "shared-index pointer branch does not match its discovery key");
|
|
88
|
+
validatePointerBinding(options.config, pointer);
|
|
89
|
+
const relation = commitRelation(options.repo, head, pointer.commit);
|
|
90
|
+
if (!relation)
|
|
91
|
+
throw new SharedIndexError("compatibility", "shared-index snapshot commit is not a proven local ancestor");
|
|
92
|
+
const [manifestBytes, signature] = await Promise.all([
|
|
93
|
+
objectBytes(options.store, pointer.manifestKey, SHARED_INDEX_LIMITS.manifestBytes, options.signal),
|
|
94
|
+
objectBytes(options.store, signatureKey(pointer.manifestKey), SHARED_INDEX_LIMITS.signatureBytes, options.signal),
|
|
95
|
+
]);
|
|
96
|
+
const manifest = parseManifest(manifestBytes);
|
|
97
|
+
verifyManifest(options.config, pointer, manifestBytes, manifest, signature, options.clientCompatibility);
|
|
98
|
+
return { branch, pointer, manifest, manifestBytes, signature, ...relation };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return rejection(branch, error);
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
104
|
+
const verified = settled.filter((item) => "manifest" in item);
|
|
105
|
+
verified.sort((left, right) => left.distance - right.distance || left.branch.localeCompare(right.branch));
|
|
106
|
+
return {
|
|
107
|
+
...(verified[0] ? { selected: verified[0] } : {}),
|
|
108
|
+
verified,
|
|
109
|
+
rejected: settled.filter((item) => !("manifest" in item)),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `server.tool()` / Zod-to-handler inference — that path OOMs `tsc`). One flat
|
|
7
7
|
* gateway keeps the client LLM's context small.
|
|
8
8
|
*/
|
|
9
|
+
import fs from "node:fs";
|
|
9
10
|
import nodePath from "node:path";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { resolveCliRuntimeCommand } from "../cli-args.js";
|
|
@@ -17,6 +18,7 @@ import { getDocSection, listDocTopics } from "../docs-sections.js";
|
|
|
17
18
|
import { diagnoseInstallation } from "../doctor.js";
|
|
18
19
|
import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../engine/index.js";
|
|
19
20
|
import { measurePerfPhaseSync } from "../engine/perf.js";
|
|
21
|
+
import { resolveDbPath } from "../engine/state-paths.js";
|
|
20
22
|
import { runExecutionProfile } from "../execution-profile.js";
|
|
21
23
|
import { diagnoseFailure, } from "../failure-diagnosis.js";
|
|
22
24
|
import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
|
|
@@ -31,6 +33,8 @@ import { createRepairPlan } from "../repair-progress.js";
|
|
|
31
33
|
import { runRepositoryInitializationProcess } from "../repository-init-process.js";
|
|
32
34
|
import { discoverRepositories, initializeRepositories, inventoryRepository, searchRepositories, } from "../repository-management.js";
|
|
33
35
|
import { applyResponseBudget } from "../response-budget.js";
|
|
36
|
+
import { loadSharedIndexConfig } from "../shared-index/config.js";
|
|
37
|
+
import { EMPTY_OVERLAY, readSharedIndexProvenance, readSharedRestoreAttempt, } from "../shared-index/provenance.js";
|
|
34
38
|
import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../system-config.js";
|
|
35
39
|
import { trustedUpdateStatus } from "../update-policy.js";
|
|
36
40
|
import { waitForFresh } from "../wait-for-fresh.js";
|
|
@@ -113,6 +117,7 @@ const QUERY_PATTERNS = [
|
|
|
113
117
|
"structural_implementations_of",
|
|
114
118
|
"tests_for",
|
|
115
119
|
"file_summary",
|
|
120
|
+
"file_metrics",
|
|
116
121
|
"batch_outline",
|
|
117
122
|
"project_overview",
|
|
118
123
|
"shortest_path",
|
|
@@ -150,6 +155,7 @@ const GRAPH_INDEPENDENT_QUERY_PATTERNS = new Set([
|
|
|
150
155
|
"lsp_definitions",
|
|
151
156
|
"lsp_declarations",
|
|
152
157
|
"lsp_implementations",
|
|
158
|
+
"file_metrics",
|
|
153
159
|
]);
|
|
154
160
|
function buildDocumentedKnodinTools() {
|
|
155
161
|
return [
|
|
@@ -302,7 +308,7 @@ function buildDocumentedKnodinTools() {
|
|
|
302
308
|
},
|
|
303
309
|
base: {
|
|
304
310
|
type: "string",
|
|
305
|
-
description: "review/context: compatibility base ref (default HEAD~1). For review this maps to scope=all versus the base; compare uses it as the default older ref. context uses it only for risk.",
|
|
311
|
+
description: "review/context: compatibility base ref (default HEAD~1). query file_metrics: local ref whose merge-base is measured. For review this maps to scope=all versus the base; compare uses it as the default older ref. context uses it only for risk.",
|
|
306
312
|
},
|
|
307
313
|
diffScope: {
|
|
308
314
|
type: "string",
|
|
@@ -379,7 +385,7 @@ function buildDocumentedKnodinTools() {
|
|
|
379
385
|
testScope: {
|
|
380
386
|
type: "string",
|
|
381
387
|
enum: ["all", "test", "production"],
|
|
382
|
-
description: "search: include all, test-only, or production-only
|
|
388
|
+
description: "search/file_metrics: include all, test-only, or production-only files.",
|
|
383
389
|
},
|
|
384
390
|
offset: {
|
|
385
391
|
type: "number",
|
|
@@ -1305,6 +1311,45 @@ async function dispatchKnodinTool(args) {
|
|
|
1305
1311
|
tokens: tokenBudget,
|
|
1306
1312
|
items: itemBudget,
|
|
1307
1313
|
};
|
|
1314
|
+
let sharedEnvelope;
|
|
1315
|
+
const sharedIndexEnvelope = () => {
|
|
1316
|
+
if (sharedEnvelope)
|
|
1317
|
+
return sharedEnvelope;
|
|
1318
|
+
const configuration = loadSharedIndexConfig(repo);
|
|
1319
|
+
const active = readSharedIndexProvenance(repo);
|
|
1320
|
+
const lastRestoreAttempt = readSharedRestoreAttempt(repo);
|
|
1321
|
+
if (active) {
|
|
1322
|
+
sharedEnvelope = {
|
|
1323
|
+
...active,
|
|
1324
|
+
enabled: configuration.state === "configured" ? configuration.config.enabled : false,
|
|
1325
|
+
...(lastRestoreAttempt ? { lastRestoreAttempt } : {}),
|
|
1326
|
+
};
|
|
1327
|
+
return sharedEnvelope;
|
|
1328
|
+
}
|
|
1329
|
+
const state = configuration.state === "not-configured"
|
|
1330
|
+
? "not-configured"
|
|
1331
|
+
: configuration.state === "invalid"
|
|
1332
|
+
? "configuration-invalid"
|
|
1333
|
+
: !configuration.config.enabled
|
|
1334
|
+
? "disabled"
|
|
1335
|
+
: lastRestoreAttempt?.outcome === "failed"
|
|
1336
|
+
? "restore-failed"
|
|
1337
|
+
: "local-only";
|
|
1338
|
+
sharedEnvelope = {
|
|
1339
|
+
schemaVersion: 1,
|
|
1340
|
+
enabled: configuration.state === "configured" && configuration.config.enabled,
|
|
1341
|
+
state,
|
|
1342
|
+
source: "local-index",
|
|
1343
|
+
...(configuration.state === "configured"
|
|
1344
|
+
? { cache: "not-used", overlay: EMPTY_OVERLAY }
|
|
1345
|
+
: {}),
|
|
1346
|
+
...(lastRestoreAttempt ? { lastRestoreAttempt } : {}),
|
|
1347
|
+
...(lastRestoreAttempt?.failureCategory
|
|
1348
|
+
? { failureCategory: lastRestoreAttempt.failureCategory }
|
|
1349
|
+
: {}),
|
|
1350
|
+
};
|
|
1351
|
+
return sharedEnvelope;
|
|
1352
|
+
};
|
|
1308
1353
|
const observe = (output, op, minimal = false) => {
|
|
1309
1354
|
const root = output;
|
|
1310
1355
|
const budgetMeta = root.responseBudget;
|
|
@@ -1333,11 +1378,31 @@ async function dispatchKnodinTool(args) {
|
|
|
1333
1378
|
return root;
|
|
1334
1379
|
};
|
|
1335
1380
|
const bounded = (result, op = String(operation), minimal = false) => {
|
|
1381
|
+
const rootOperation = op.split(":")[0];
|
|
1382
|
+
const includeSharedIndex = [
|
|
1383
|
+
"status",
|
|
1384
|
+
"context",
|
|
1385
|
+
"query",
|
|
1386
|
+
"explain",
|
|
1387
|
+
"review",
|
|
1388
|
+
"map",
|
|
1389
|
+
"search",
|
|
1390
|
+
].includes(rootOperation);
|
|
1391
|
+
const enriched = includeSharedIndex && result && typeof result === "object" && !Array.isArray(result)
|
|
1392
|
+
? { ...result, sharedIndex: sharedIndexEnvelope() }
|
|
1393
|
+
: result;
|
|
1336
1394
|
const key = `${op}${minimal ? ":minimal" : ""}`;
|
|
1337
|
-
const output = applyResponseBudget(
|
|
1395
|
+
const output = applyResponseBudget(enriched, op, budget, RESPONSE_DEFAULTS[key] ?? RESPONSE_DEFAULTS.default);
|
|
1338
1396
|
return observe(output, op, minimal);
|
|
1339
1397
|
};
|
|
1340
1398
|
const graphRead = async (op, run, minimal = false) => {
|
|
1399
|
+
const sharedConfiguration = loadSharedIndexConfig(repo);
|
|
1400
|
+
if (!fs.existsSync(resolveDbPath(repo)) &&
|
|
1401
|
+
sharedConfiguration.state === "configured" &&
|
|
1402
|
+
sharedConfiguration.config.enabled) {
|
|
1403
|
+
const { attemptOpportunisticSharedRestore } = await import("../shared-index/opportunistic.js");
|
|
1404
|
+
await attemptOpportunisticSharedRestore(repo, engine);
|
|
1405
|
+
}
|
|
1341
1406
|
const health = await inspectGatewayGraphHealth(repo);
|
|
1342
1407
|
if (!health.available)
|
|
1343
1408
|
return bounded(health, op, minimal);
|
|
@@ -1597,6 +1662,8 @@ async function dispatchKnodinTool(args) {
|
|
|
1597
1662
|
includeDataFlow,
|
|
1598
1663
|
}
|
|
1599
1664
|
: undefined, {
|
|
1665
|
+
base: pattern === "file_metrics" ? base : undefined,
|
|
1666
|
+
testScope: pattern === "file_metrics" ? testScope : undefined,
|
|
1600
1667
|
minLines,
|
|
1601
1668
|
minComplexity,
|
|
1602
1669
|
kinds,
|
package/docs/CLI.md
CHANGED
|
@@ -6,6 +6,27 @@ options, numeric parsing, unknown-option rejection, and root or command help.
|
|
|
6
6
|
`bin/cli.ts` dispatches the validated invocation to the existing product
|
|
7
7
|
handlers so JSON output contracts and established commands remain compatible.
|
|
8
8
|
|
|
9
|
+
## Shared indexes
|
|
10
|
+
|
|
11
|
+
`knodin shared status` reports reviewed configuration, active graph provenance,
|
|
12
|
+
the last bounded restore outcome, cache state, and freshness strategy without
|
|
13
|
+
network access. Add `--probe` only for an explicit S3 credential/object check.
|
|
14
|
+
|
|
15
|
+
`knodin shared pull` discovers signed branch pointers, selects the nearest
|
|
16
|
+
locally proven ancestor, downloads or reuses an immutable digest-addressed
|
|
17
|
+
artifact, reconciles the current checkout in a candidate database, deep-audits,
|
|
18
|
+
and atomically promotes it. `--json` returns one result; `--jsonl` also streams
|
|
19
|
+
lossless phase records. A failed pull leaves the previous graph usable.
|
|
20
|
+
|
|
21
|
+
`knodin shared configure` writes the reviewed `.knodin/shared-index.yaml` trust
|
|
22
|
+
configuration from explicit repository, S3, branch, and public-key options. It
|
|
23
|
+
never reads or writes AWS credentials. `--disable` prevents automatic S3 access
|
|
24
|
+
without deleting local graph or cache state.
|
|
25
|
+
|
|
26
|
+
Index Hub publishers run `knodin index --clean` followed by
|
|
27
|
+
`knodin shared publisher-metadata --json`; the latter stamps and emits the exact
|
|
28
|
+
compatibility fingerprint used by clients.
|
|
29
|
+
|
|
9
30
|
Run `knodin --help` for the root command inventory or append `--help` to any
|
|
10
31
|
declared command path, for example:
|
|
11
32
|
|
|
@@ -59,6 +80,24 @@ dynamic names, reflection, computed aliases, and unsupported languages are
|
|
|
59
80
|
reported rather than guessed. This is a static heuristic, not runtime reachability
|
|
60
81
|
or an exploitability verdict.
|
|
61
82
|
|
|
83
|
+
`knodin query file_metrics [--base <ref>] [--test-scope all|test|production]
|
|
84
|
+
--format json` emits the versioned `schemas/file-metrics-v1.schema.json`
|
|
85
|
+
contract. Each source file carries lexical line, import, route, and effect
|
|
86
|
+
counts. With `--base`, knodin resolves the local merge-base and reads both trees
|
|
87
|
+
directly from Git object storage; it does not check out or build a second graph.
|
|
88
|
+
Added and removed files have null deltas, preventing consumers from mistaking
|
|
89
|
+
either for an architectural improvement or regression. Output contains paths
|
|
90
|
+
and integer counts only and supplies measurements, not thresholds or a verdict.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
knodin query file_metrics --base origin/main --test-scope production --format json > metrics.json
|
|
94
|
+
jq -e '[.files[] | select(.status == "changed") | .delta | to_entries[] | select(.value > 0)] | length == 0' metrics.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
An invalid ref or unresolvable merge-base fails non-zero. Binary and invalid
|
|
98
|
+
UTF-8 files are skipped rather than reported as zero. The query is graph
|
|
99
|
+
independent, so damaged graph state cannot become a misleading empty result.
|
|
100
|
+
|
|
62
101
|
For support evidence, `knodin diagnostics enable|status|preview|archive|inspect|clear|disable`
|
|
63
102
|
manages an explicit local failure journal and redacted gzip JSON bundles.
|
|
64
103
|
Collection does not upload data. See `docs/DIAGNOSTICS.md` for retention,
|
package/docs/MCP.md
CHANGED
|
@@ -4,11 +4,23 @@ knodin exposes exactly one MCP tool named `knodin`. Capabilities such as
|
|
|
4
4
|
context, explain, review, search, docs, doctor, repositories, and systems are
|
|
5
5
|
operations of that gateway, not separate top-level tools.
|
|
6
6
|
|
|
7
|
+
Shared-index support also stays inside that one gateway. Status, context,
|
|
8
|
+
query, explain, review, map, and search responses include a bounded
|
|
9
|
+
`sharedIndex` provenance object. `source: "shared-snapshot"` identifies origin;
|
|
10
|
+
the independent freshness envelope says whether completed reconciliation and
|
|
11
|
+
deep audit still match the checkout. AWS credentials, raw S3 exceptions, and
|
|
12
|
+
source path lists are never returned.
|
|
13
|
+
|
|
7
14
|
Use `operation: "query", pattern: "resource_reachability"` for the same bounded,
|
|
8
15
|
cached, repo-wide TS/JS analysis as the CLI. It requires no symbol and retains
|
|
9
16
|
the gateway's freshness refusal and response budgets; returned paths remain
|
|
10
17
|
source-evidenced static heuristics with explicit coverage and omissions.
|
|
11
18
|
|
|
19
|
+
Use `operation: "query", pattern: "file_metrics"`, optional `base`, and
|
|
20
|
+
`testScope` for graph-independent, Git-backed file measurements. The versioned
|
|
21
|
+
`fileMetrics` envelope contains paths and integer counts only—no source snippets,
|
|
22
|
+
thresholds, or policy verdict.
|
|
23
|
+
|
|
12
24
|
Use `operation: "evidence"` for deterministic `locate`, `outline`, `evidence`,
|
|
13
25
|
and `expand` source delivery with a verified complete-file hash handshake and
|
|
14
26
|
recoverable hard-budget continuations. See `docs/PROGRESSIVE-EVIDENCE.md`.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Shared-index contract v1
|
|
2
|
+
|
|
3
|
+
This document freezes the wire and trust contract shared by the Knodin client
|
|
4
|
+
and an Index Hub publisher. The normative machine schemas are:
|
|
5
|
+
|
|
6
|
+
- `schemas/shared-index-config-v1.schema.json`
|
|
7
|
+
- `schemas/shared-index-branch-pointer-v1.schema.json`
|
|
8
|
+
- `schemas/shared-index-manifest-v1.schema.json`
|
|
9
|
+
- `schemas/shared-index-provenance-v1.schema.json`
|
|
10
|
+
|
|
11
|
+
The cross-implementation fixtures are in
|
|
12
|
+
`src/__tests__/fixtures/shared-index-contract-v1.json`. Producers and consumers
|
|
13
|
+
must validate the same fixtures before claiming v1 compatibility.
|
|
14
|
+
|
|
15
|
+
## Trust and identity
|
|
16
|
+
|
|
17
|
+
The reviewed `.knodin/shared-index.yaml` public key is the only trust root. A
|
|
18
|
+
pointer, manifest, signature, object metadata, or S3 response cannot replace
|
|
19
|
+
it. Configuration contains no credentials, role provisioning, endpoint
|
|
20
|
+
override, portal data, Terraform data, or GitHub App data. `enabled: false`
|
|
21
|
+
forbids automatic S3 access.
|
|
22
|
+
|
|
23
|
+
Repository identity is canonicalized by removing an optional `https://` or
|
|
24
|
+
`ssh://git@` transport prefix from a remote identity, removing one trailing
|
|
25
|
+
dot from the host, lower-casing host/owner/name with the `en-US` locale,
|
|
26
|
+
removing one `.git` suffix from the name, and NFC-normalizing every component.
|
|
27
|
+
All three canonical components are compared. The S3 namespace is:
|
|
28
|
+
|
|
29
|
+
`v1/<encoded-host>/<encoded-owner>/<encoded-repo>/`
|
|
30
|
+
|
|
31
|
+
Each component and branch is encoded independently as UTF-8 RFC 3986 percent
|
|
32
|
+
encoding with uppercase hex. `/`, spaces, Unicode, and `%` are therefore data,
|
|
33
|
+
not separators. Branch patterns are either an exact Git ref name or a single
|
|
34
|
+
terminal `/*` prefix pattern; matching is Unicode code-point exact after NFC
|
|
35
|
+
normalization. A branch pointer never overrides local commit or ancestry truth.
|
|
36
|
+
|
|
37
|
+
## Signed bytes
|
|
38
|
+
|
|
39
|
+
Manifest JSON uses RFC 8785 JSON Canonicalization Scheme bytes: UTF-8, sorted
|
|
40
|
+
object keys, no insignificant whitespace, and no trailing newline. The
|
|
41
|
+
signature covers those raw manifest bytes, not a pre-hashed digest supplied by
|
|
42
|
+
the caller. The algorithm is RSA-PSS with SHA-256, MGF1-SHA-256, and an exact
|
|
43
|
+
32-byte salt. Public keys are PEM-encoded X.509 SubjectPublicKeyInfo. The
|
|
44
|
+
`publicKeySha256` value is lower-case hex SHA-256 over its DER SPKI bytes.
|
|
45
|
+
|
|
46
|
+
The pointer is untrusted discovery metadata. Before signature verification the
|
|
47
|
+
client bounds and validates it, checks registration/repository identity,
|
|
48
|
+
confines `manifestKey` beneath the canonical immutable prefix, downloads the
|
|
49
|
+
manifest, and matches SHA-256 over the exact downloaded canonical bytes.
|
|
50
|
+
The detached signature key is the manifest key with terminal `manifest.json`
|
|
51
|
+
replaced by `manifest.sig`. Immutable snapshots use exactly
|
|
52
|
+
`snapshots/<commit>/<inputFingerprint>/`; the graph key is exactly
|
|
53
|
+
`graph.sqlite.zst` in that directory. Alternate filenames and nested paths are
|
|
54
|
+
rejected even when they remain inside the repository prefix.
|
|
55
|
+
|
|
56
|
+
## Compatibility
|
|
57
|
+
|
|
58
|
+
V1 requires exact equality for database schema version, model ID and digest,
|
|
59
|
+
parser bundle digest, index configuration digest, and prune-policy digest. The
|
|
60
|
+
client's Knodin major version must equal the manifest major version and the
|
|
61
|
+
client minor version must be greater than or equal to the manifest minor
|
|
62
|
+
version. Pre-release versions are compatible only when the complete Knodin
|
|
63
|
+
version is equal. Patch-version differences alone are compatible.
|
|
64
|
+
|
|
65
|
+
`knodin shared publisher-metadata --json` is the single publisher/client
|
|
66
|
+
implementation of these values. It also stamps `sharedIndexInputFingerprint`
|
|
67
|
+
into the checkpointed publisher database so restore can bind SQLite metadata
|
|
68
|
+
back to the signed manifest. In v1:
|
|
69
|
+
|
|
70
|
+
- `modelDigest` hashes a canonical descriptor containing model ID, the pinned
|
|
71
|
+
immutable model revision, 384 dimensions, and the installed Transformers.js
|
|
72
|
+
version.
|
|
73
|
+
- `parserBundleDigest` hashes parser policy version 1 and the installed
|
|
74
|
+
`web-tree-sitter`, `tree-sitter-wasms`, and `web-tree-sitter-sfapex`
|
|
75
|
+
versions.
|
|
76
|
+
- `indexConfigDigest` hashes index-policy version 1, database schema, and
|
|
77
|
+
embedding dimensions.
|
|
78
|
+
- `prunePolicyDigest` hashes prune-policy version 1, the sorted ignored
|
|
79
|
+
directory set, and the `.claude/worktrees` pattern.
|
|
80
|
+
|
|
81
|
+
Any represented behavior change must increment its policy descriptor version.
|
|
82
|
+
Publishers must not duplicate this logic in shell.
|
|
83
|
+
|
|
84
|
+
## Bounds
|
|
85
|
+
|
|
86
|
+
Limits include every byte received, not merely parsed content:
|
|
87
|
+
|
|
88
|
+
| Object | Maximum bytes |
|
|
89
|
+
| --- | ---: |
|
|
90
|
+
| Branch pointer | 64 KiB |
|
|
91
|
+
| Manifest | 256 KiB |
|
|
92
|
+
| Detached signature | 16 KiB |
|
|
93
|
+
| Compressed graph | 8 GiB |
|
|
94
|
+
| Decompressed graph | 32 GiB |
|
|
95
|
+
|
|
96
|
+
Compressed and decompressed sizes and SHA-256 digests must match the manifest
|
|
97
|
+
exactly. The limits are ceilings; a deployment may configure stricter local
|
|
98
|
+
bounds. Unknown schema versions and unknown fields fail closed. No failure
|
|
99
|
+
before audited candidate promotion may modify the active graph.
|
|
100
|
+
|
|
101
|
+
## Discovery, cache, and restore
|
|
102
|
+
|
|
103
|
+
Discovery never fetches Git history. Exact local HEAD is preferred; otherwise
|
|
104
|
+
only the nearest commit proven by the existing local object database to be an
|
|
105
|
+
ancestor is usable. In shallow clones an unprovable commit is rejected, while
|
|
106
|
+
an exact HEAD remains usable.
|
|
107
|
+
|
|
108
|
+
Verified compressed artifacts are cached privately under
|
|
109
|
+
`~/.knodin/shared-index-cache/v1/sha256/<compressedSha256>/graph.sqlite.zst`.
|
|
110
|
+
The cache uses per-digest cross-process locks and leases, verifies size and
|
|
111
|
+
digest before atomic installation, and never evicts a leased object. Every
|
|
112
|
+
worktree gets a distinct writable candidate database. Decompression enforces
|
|
113
|
+
the signed output size and digest, rejects trailing compressed data, validates
|
|
114
|
+
the SQLite header and internal commit/schema/fingerprint, reconciles final Git
|
|
115
|
+
and filesystem truth, deep-audits, and only then promotes.
|
|
116
|
+
|
|
117
|
+
## Operator surfaces
|
|
118
|
+
|
|
119
|
+
- `knodin shared status` is network-free. `--probe` is the explicit S3/auth
|
|
120
|
+
check.
|
|
121
|
+
- `knodin shared pull` performs an explicit restore; `--jsonl` retains every
|
|
122
|
+
phase event.
|
|
123
|
+
- `knodin shared configure` writes only `.knodin/shared-index.yaml` and never
|
|
124
|
+
accepts or stores AWS credentials. `--disable` preserves the graph and cache.
|
|
125
|
+
- `knodin shared publisher-metadata --json` emits and stamps the frozen
|
|
126
|
+
compatibility metadata after watcher-free clean indexing.
|
|
127
|
+
|
|
128
|
+
The single MCP gateway includes `sharedIndex` provenance in status, context,
|
|
129
|
+
query, explain, review, map, and search responses before response budgeting.
|
|
130
|
+
Snapshot provenance and freshness are separate claims. Failed attempts are
|
|
131
|
+
recorded separately and never replace the provenance or bytes of the active
|
|
132
|
+
graph.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# knodin 0.8.5
|
|
2
|
+
|
|
3
|
+
This release adds a versioned, fail-closed shared-index contract and safer
|
|
4
|
+
large-repository lifecycle operations while preserving local-only defaults and
|
|
5
|
+
the limitations recorded in the competitive roadmap.
|
|
6
|
+
|
|
7
|
+
- Adds strict repository configuration, discovery-pointer, immutable-manifest,
|
|
8
|
+
signature, compatibility, and provenance contracts shared by publishers and
|
|
9
|
+
clients.
|
|
10
|
+
- Adds native AWS default-credential-chain support, bounded S3 retrieval and
|
|
11
|
+
caching, verified decompression, snapshot selection, and recoverable restore.
|
|
12
|
+
- Adds watcher-free candidate database creation, deep audit, reconciliation,
|
|
13
|
+
atomic promotion, rollback, and interrupted-promotion recovery.
|
|
14
|
+
- Adds shared-index configuration, status, restore, and publisher operations to
|
|
15
|
+
the CLI and compact MCP gateway, plus a guarded publishing workflow.
|
|
16
|
+
- Adds `file_metrics` with arbitrary Git-ref comparison for source-derived
|
|
17
|
+
architectural ratchets without a checked-in numeric baseline.
|
|
18
|
+
- Hardens timing-sensitive structural-routing and behavioral-contract replay
|
|
19
|
+
under instrumented host load without weakening assertions or interactive
|
|
20
|
+
hang limits.
|
|
21
|
+
|
|
22
|
+
Shared indexes remain explicitly opt-in. Reviewed repository configuration is
|
|
23
|
+
the trust root, snapshots cannot replace it, and incompatible or ambiguous
|
|
24
|
+
evidence fails closed. This remains an ordinary 0.x release, not GA or a claim
|
|
25
|
+
of certified trusted distribution.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# knodin 0.8.6
|
|
2
|
+
|
|
3
|
+
This patch release makes bare `knodin status` adaptive and improves graceful repair cancellation.
|
|
4
|
+
|
|
5
|
+
- Repositories with at most 50,000 indexed files receive the existing exact deep audit.
|
|
6
|
+
- Larger repositories may reuse healthy persisted deep-audit evidence after a current Git freshness probe. The evidence is bound to the canonical repository, graph schema, knodin version, and SQLite/WAL fingerprint; `knodin status --deep` always bypasses it.
|
|
7
|
+
- Human and JSON status output distinguish when the full audit ran from when freshness was most recently probed.
|
|
8
|
+
- Deep status accepts an abort signal, embedding repair checks cancellation between batches, and cancelled candidate repair returns without starting another deep audit.
|
|
9
|
+
|
|
10
|
+
Persisted evidence does not claim that every source file was inspected during the latest status invocation. Use `knodin status --deep` whenever an exact current filesystem/schema/orphan audit is required.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knodin",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "knodin — source-evidenced local code intelligence with known bounds. Stable identity, fresh evidence, truthful budgets, and recoverable bounded views.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"docs/REPOSITORIES-AND-WORKTREES.md",
|
|
43
43
|
"docs/RELEASE-0.3-EVIDENCE.md",
|
|
44
44
|
"docs/SIGNED-UPDATES.md",
|
|
45
|
+
"docs/SHARED-INDEX-CONTRACT.md",
|
|
45
46
|
"docs/releases/0.3.0.md",
|
|
46
47
|
"docs/releases/0.4.0.md",
|
|
47
48
|
"docs/releases/0.4.1.md",
|
|
@@ -60,6 +61,8 @@
|
|
|
60
61
|
"docs/releases/0.8.2.md",
|
|
61
62
|
"docs/releases/0.8.3.md",
|
|
62
63
|
"docs/releases/0.8.4.md",
|
|
64
|
+
"docs/releases/0.8.5.md",
|
|
65
|
+
"docs/releases/0.8.6.md",
|
|
63
66
|
"docs/assets/knodin-favicon.svg",
|
|
64
67
|
"docs/SYSTEMS-AND-RELATIONSHIPS.md",
|
|
65
68
|
"docs/TELEMETRY.md",
|
|
@@ -68,6 +71,10 @@
|
|
|
68
71
|
"roadmap/competitive-roadmap.md",
|
|
69
72
|
"schemas/release-attestation-v1.schema.json",
|
|
70
73
|
"schemas/support-bundle-v2.schema.json",
|
|
74
|
+
"schemas/shared-index-config-v1.schema.json",
|
|
75
|
+
"schemas/shared-index-branch-pointer-v1.schema.json",
|
|
76
|
+
"schemas/shared-index-manifest-v1.schema.json",
|
|
77
|
+
"schemas/shared-index-provenance-v1.schema.json",
|
|
71
78
|
"*.wasm"
|
|
72
79
|
],
|
|
73
80
|
"engines": {
|
|
@@ -138,14 +145,16 @@
|
|
|
138
145
|
"lint:fix": "biome check --write ."
|
|
139
146
|
},
|
|
140
147
|
"dependencies": {
|
|
141
|
-
"@
|
|
148
|
+
"@aws-sdk/client-s3": "3.1106.0",
|
|
149
|
+
"@huggingface/transformers": "4.2.0",
|
|
142
150
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
151
|
+
"@smithy/node-http-handler": "4.9.13",
|
|
143
152
|
"chokidar": "^5.0.0",
|
|
144
153
|
"commander": "15.0.0",
|
|
145
154
|
"gpt-tokenizer": "3.4.0",
|
|
146
155
|
"ora": "9.4.1",
|
|
147
156
|
"re2-wasm": "1.0.2",
|
|
148
|
-
"tree-sitter-wasms": "
|
|
157
|
+
"tree-sitter-wasms": "0.1.13",
|
|
149
158
|
"web-tree-sitter": "0.20.8",
|
|
150
159
|
"web-tree-sitter-sfapex": "2.4.1",
|
|
151
160
|
"yaml": "2.9.0",
|