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.
Files changed (34) hide show
  1. package/dist/bin/cli.js +272 -24
  2. package/dist/src/cli-model.js +26 -0
  3. package/dist/src/engine/candidate-database.js +167 -0
  4. package/dist/src/engine/embeddings.js +8 -5
  5. package/dist/src/engine/index.js +1244 -305
  6. package/dist/src/engine/prune.js +8 -0
  7. package/dist/src/file-metrics.js +153 -0
  8. package/dist/src/repair-lease.js +8 -5
  9. package/dist/src/response-budget.js +2 -1
  10. package/dist/src/shared-index/artifact.js +65 -0
  11. package/dist/src/shared-index/cache.js +267 -0
  12. package/dist/src/shared-index/compatibility.js +70 -0
  13. package/dist/src/shared-index/config.js +224 -0
  14. package/dist/src/shared-index/contract.js +117 -0
  15. package/dist/src/shared-index/manifest.js +71 -0
  16. package/dist/src/shared-index/opportunistic.js +37 -0
  17. package/dist/src/shared-index/overlay.js +98 -0
  18. package/dist/src/shared-index/provenance.js +149 -0
  19. package/dist/src/shared-index/publisher.js +198 -0
  20. package/dist/src/shared-index/restore.js +209 -0
  21. package/dist/src/shared-index/s3-client.js +124 -0
  22. package/dist/src/shared-index/schemas.js +86 -0
  23. package/dist/src/shared-index/selection.js +111 -0
  24. package/dist/src/tools/knodin-tools.js +70 -3
  25. package/docs/CLI.md +39 -0
  26. package/docs/MCP.md +12 -0
  27. package/docs/SHARED-INDEX-CONTRACT.md +132 -0
  28. package/docs/releases/0.8.5.md +25 -0
  29. package/docs/releases/0.8.6.md +10 -0
  30. package/package.json +12 -3
  31. package/schemas/shared-index-branch-pointer-v1.schema.json +66 -0
  32. package/schemas/shared-index-config-v1.schema.json +97 -0
  33. package/schemas/shared-index-manifest-v1.schema.json +104 -0
  34. 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 symbols.",
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(result, op, budget, RESPONSE_DEFAULTS[key] ?? RESPONSE_DEFAULTS.default);
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.4",
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
- "@huggingface/transformers": "^4.2.0",
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": "^0.1.13",
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",