knodin 0.10.1 → 0.10.3
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 +52 -8
- package/dist/src/cli-args.js +4 -0
- package/dist/src/cli-model.js +2 -0
- package/dist/src/engine/candidate-database.js +53 -0
- package/dist/src/engine/index.js +566 -97
- package/dist/src/engine/parse-worker.js +19 -6
- package/dist/src/init-progress.js +36 -5
- package/dist/src/init.js +75 -5
- package/dist/src/release-compatibility.js +95 -0
- package/dist/src/release-preflight.js +18 -0
- package/dist/src/update-policy.js +6 -1
- package/docs/releases/0.10.2.md +76 -0
- package/docs/releases/0.10.3.md +93 -0
- package/package.json +4 -2
package/dist/bin/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/co
|
|
|
27
27
|
import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, persistDiagnosticsPreview, recordDiagnosticFailure, } from "../src/diagnostics.js";
|
|
28
28
|
import { getDocSection, listDocTopics } from "../src/docs-sections.js";
|
|
29
29
|
import { diagnoseInstallation } from "../src/doctor.js";
|
|
30
|
-
import { createEngine, KNODIN_SCHEMA_VERSION, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
|
|
30
|
+
import { createEngine, describeThrown, KNODIN_SCHEMA_VERSION, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
|
|
31
31
|
import { runSeal } from "../src/engine/seal-command.js";
|
|
32
32
|
import { runSealedQuery } from "../src/engine/sealed-query.js";
|
|
33
33
|
import { resolveDbPath } from "../src/engine/state-paths.js";
|
|
@@ -138,16 +138,29 @@ function formatRepairHuman(result) {
|
|
|
138
138
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
139
139
|
return `Repair finished with ${outstanding.toLocaleString()} remaining issue(s).${detail} Run \`knodin status --deep\` for details.\n`;
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The semantic gap, stated rather than left to be discovered.
|
|
143
|
+
*
|
|
144
|
+
* An index that deferred embeddings leaves `search` returning a short list, and
|
|
145
|
+
* a short list is indistinguishable from a thorough search that found little.
|
|
146
|
+
* Saying nothing here is what turns a deliberate deferral into a silent one.
|
|
147
|
+
*/
|
|
148
|
+
function formatSemanticGap(readiness) {
|
|
149
|
+
if (readiness === undefined || readiness === "ready")
|
|
150
|
+
return "";
|
|
151
|
+
return ` Semantic coverage is ${readiness}: \`search\` will under-return until \`knodin index\` completes embeddings.`;
|
|
152
|
+
}
|
|
141
153
|
function formatIndexHuman(result) {
|
|
154
|
+
const semantic = formatSemanticGap(result.semanticReadiness);
|
|
142
155
|
if (result.indexed.length === 0 && result.unchanged.length > 0) {
|
|
143
156
|
const noun = result.unchanged.length === 1 ? "file" : "files";
|
|
144
|
-
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified
|
|
157
|
+
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.${semantic}\n`;
|
|
145
158
|
}
|
|
146
159
|
const unchanged = result.unchanged.length > 0
|
|
147
160
|
? `; ${result.unchanged.length.toLocaleString()} already current`
|
|
148
161
|
: "";
|
|
149
162
|
const noun = result.indexed.length === 1 ? "file" : "files";
|
|
150
|
-
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified
|
|
163
|
+
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.${semantic}\n`;
|
|
151
164
|
}
|
|
152
165
|
function formatIndexVerificationError(result) {
|
|
153
166
|
const firstIssue = result.verification.missing.files[0] ?? result.verification.missing.records[0];
|
|
@@ -199,10 +212,16 @@ function formatCoverageGaps(skipped) {
|
|
|
199
212
|
if (byExtension)
|
|
200
213
|
clauses.push(`not indexed: ${byExtension}`);
|
|
201
214
|
const unparsed = formatTally(skipped.unparsedByExtension);
|
|
215
|
+
// "not parsed", not "parsed but empty". Nothing in this tally was parsed:
|
|
216
|
+
// it collects files with no tree-sitter grammar, minified sources, and files
|
|
217
|
+
// whose indexing threw. Calling that "parsed but empty" asserts the parser
|
|
218
|
+
// looked and found nothing, which is the one thing it did not do — and it
|
|
219
|
+
// made a real outage unreadable, since a dead wasm module dumped thousands
|
|
220
|
+
// of never-parsed files into this bucket under a label saying they were fine.
|
|
202
221
|
if (unparsed)
|
|
203
|
-
clauses.push(`parsed
|
|
222
|
+
clauses.push(`not parsed: ${unparsed}`);
|
|
204
223
|
if (skipped.unparsedUnknown)
|
|
205
|
-
clauses.push("parsed
|
|
224
|
+
clauses.push("not-parsed tally unavailable — run a full index; this is a lower bound");
|
|
206
225
|
const count = skipped.unparsedUnknown ? `${skipped.total}+` : `${skipped.total}`;
|
|
207
226
|
return `. Not in graph: ${count} files (${clauses.join("; ")})`;
|
|
208
227
|
}
|
|
@@ -2124,17 +2143,27 @@ async function main() {
|
|
|
2124
2143
|
const agents = scope === "team" ? [] : integrationAgents(repo);
|
|
2125
2144
|
const paths = await initializeRepository(repo, {
|
|
2126
2145
|
command: runtimeCommand,
|
|
2127
|
-
|
|
2146
|
+
// Not `indexOrSeed`: seeding exists for a worktree with no database,
|
|
2147
|
+
// and this command has already refused to run without one. Indexing
|
|
2148
|
+
// the written files directly is the whole point of the narrow scope.
|
|
2149
|
+
index: (target, indexOptions, files) => engine.index(target, files, false, indexOptions),
|
|
2150
|
+
indexScope: "configuration",
|
|
2128
2151
|
scope,
|
|
2129
2152
|
agents,
|
|
2130
2153
|
allowTrackedTransition: true,
|
|
2131
2154
|
auditConfigurationChanges: true,
|
|
2132
2155
|
configureIntegration: true,
|
|
2133
2156
|
});
|
|
2157
|
+
const indexed = paths.scopedIndexPaths ?? [];
|
|
2134
2158
|
result = {
|
|
2135
2159
|
status: "success",
|
|
2136
2160
|
message: `knodin agent integration changed to ${scope}.`,
|
|
2137
|
-
|
|
2161
|
+
// Say what happened. Claiming "refreshed" after touching a few
|
|
2162
|
+
// configuration files misdescribes the graph's state, and did so most
|
|
2163
|
+
// misleadingly on a repository whose graph was in fact empty.
|
|
2164
|
+
graphInitialization: indexed.length === 0
|
|
2165
|
+
? "unchanged — no configuration files needed indexing"
|
|
2166
|
+
: `configuration files indexed (${indexed.length})`,
|
|
2138
2167
|
nextAction: "run `knodin status` and reload the configured client",
|
|
2139
2168
|
paths,
|
|
2140
2169
|
};
|
|
@@ -2152,6 +2181,8 @@ async function main() {
|
|
|
2152
2181
|
process.exit(1);
|
|
2153
2182
|
}
|
|
2154
2183
|
const clean = rest.includes("--clean") || rest.includes("--force");
|
|
2184
|
+
const skipEmbeddings = rest.includes("--skip-embeddings");
|
|
2185
|
+
const under = selectorValue("--under");
|
|
2155
2186
|
const scipPath = selectorValue("--scip");
|
|
2156
2187
|
const sarifPath = selectorValue("--sarif");
|
|
2157
2188
|
// A bound the operator cannot move is just a failure, so every import
|
|
@@ -2182,6 +2213,8 @@ async function main() {
|
|
|
2182
2213
|
let indexResult;
|
|
2183
2214
|
try {
|
|
2184
2215
|
indexResult = await engine.index(plan.repo, plan.files, clean, {
|
|
2216
|
+
skipEmbeddings,
|
|
2217
|
+
...(under !== undefined ? { under } : {}),
|
|
2185
2218
|
scip: scipPath
|
|
2186
2219
|
? {
|
|
2187
2220
|
path: scipPath,
|
|
@@ -2888,6 +2921,17 @@ async function main() {
|
|
|
2888
2921
|
process.exitCode = finalExitCode;
|
|
2889
2922
|
return;
|
|
2890
2923
|
}
|
|
2924
|
+
// `search` has no human formatter — results fall through to the generic JSON
|
|
2925
|
+
// emitter — so a semantically incomplete graph produced a short list with
|
|
2926
|
+
// nothing saying why. The list looks like a thorough answer, which is the
|
|
2927
|
+
// whole failure. Printed to stderr so it annotates without corrupting output
|
|
2928
|
+
// anyone is piping.
|
|
2929
|
+
if (cmd === "search" && !jsonOutput) {
|
|
2930
|
+
const readiness = result.semanticReadiness;
|
|
2931
|
+
const gap = formatSemanticGap(readiness);
|
|
2932
|
+
if (gap)
|
|
2933
|
+
process.stderr.write(`${gap.trim()}\n`);
|
|
2934
|
+
}
|
|
2891
2935
|
if (cmd === "status" && !jsonOutput) {
|
|
2892
2936
|
process.stdout.write(formatStatusHuman(boundedResult));
|
|
2893
2937
|
process.exitCode = finalExitCode;
|
|
@@ -2985,7 +3029,7 @@ catch (err) {
|
|
|
2985
3029
|
error: err,
|
|
2986
3030
|
});
|
|
2987
3031
|
const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
|
|
2988
|
-
console.error(`${
|
|
3032
|
+
console.error(`${describeThrown(err)}${correlation}`);
|
|
2989
3033
|
process.exit(1);
|
|
2990
3034
|
}
|
|
2991
3035
|
}
|
package/dist/src/cli-args.js
CHANGED
|
@@ -36,6 +36,10 @@ const GLOBAL_BOOLEAN_FLAGS = new Set(["--exclude-tests", "--data-flow", "--json"
|
|
|
36
36
|
* not to index the log as a source file.
|
|
37
37
|
*/
|
|
38
38
|
const COMMAND_VALUE_FLAGS = new Set([
|
|
39
|
+
// Takes a directory. Without this its argument reads as a positional, and
|
|
40
|
+
// `knodin index --under src` would be rejected as "a directory positional"
|
|
41
|
+
// by the very rule that exists to stop people scoping this way by accident.
|
|
42
|
+
"--under",
|
|
39
43
|
"--scip",
|
|
40
44
|
"--scip-max-bytes",
|
|
41
45
|
"--scip-max-files",
|
package/dist/src/cli-model.js
CHANGED
|
@@ -288,6 +288,8 @@ function createCliProgram(capture = () => { }) {
|
|
|
288
288
|
leaf(program, "index [files...]", "index a repository or selected files", capture)
|
|
289
289
|
.option("--clean", "rebuild selected index state")
|
|
290
290
|
.option("--force", "force clean indexing")
|
|
291
|
+
.option("--skip-embeddings", "build structure only and defer semantic embeddings; `search` under-returns until a later `knodin index` completes them")
|
|
292
|
+
.option("--under <dir>", "rebuild only the files under a repository-relative directory; coverage is then reported as a lower bound until a full index runs")
|
|
291
293
|
.option("--scip <file>", "opt in to a bounded local SCIP protobuf import")
|
|
292
294
|
.addOption(option("--scip-max-bytes <count>", "raise the SCIP input size ceiling", "integer"))
|
|
293
295
|
.addOption(option("--scip-max-files <count>", "raise the SCIP document ceiling", "integer"))
|
|
@@ -66,6 +66,59 @@ export function candidateRoot(repo) {
|
|
|
66
66
|
export function promotionMarkerPath(repo) {
|
|
67
67
|
return path.join(resolveStateDir(repo), PROMOTION_MARKER);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Candidates left on disk for this repository, newest first.
|
|
71
|
+
*
|
|
72
|
+
* A clean index discards its candidate on failure, but a killed process runs no
|
|
73
|
+
* discard — so survivors are exactly the runs that were interrupted rather than
|
|
74
|
+
* the ones that failed. Listing them is what lets the next run continue instead
|
|
75
|
+
* of repeating hours of work.
|
|
76
|
+
*
|
|
77
|
+
* Returns descriptors only. Whether any of them is safe to resume is decided by
|
|
78
|
+
* the caller from the database's own contents, not from the fact it exists.
|
|
79
|
+
*/
|
|
80
|
+
export function listCandidates(repo) {
|
|
81
|
+
const resolvedRepo = path.resolve(repo);
|
|
82
|
+
const root = candidateRoot(resolvedRepo);
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const found = [];
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.isDirectory() || !entry.name.startsWith("candidate-"))
|
|
93
|
+
continue;
|
|
94
|
+
const databasePath = path.join(root, entry.name, "db.sqlite");
|
|
95
|
+
let modifiedMs;
|
|
96
|
+
try {
|
|
97
|
+
modifiedMs = fs.statSync(databasePath).mtimeMs;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// A directory with no database is a half-created candidate, not a
|
|
101
|
+
// resumable one.
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
found.push({
|
|
105
|
+
candidate: {
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
id: entry.name,
|
|
108
|
+
repo: resolvedRepo,
|
|
109
|
+
databasePath,
|
|
110
|
+
createdAt: new Date(modifiedMs).toISOString(),
|
|
111
|
+
},
|
|
112
|
+
modifiedMs,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Sorted as its own statement rather than mid-chain: an in-place `sort`
|
|
116
|
+
// inside an expression mutates the array being read, which is harmless for
|
|
117
|
+
// this local but reads as a side effect at a glance. (`toSorted` would be
|
|
118
|
+
// the nicer form but needs a newer `lib` than this project targets.)
|
|
119
|
+
found.sort((a, b) => b.modifiedMs - a.modifiedMs);
|
|
120
|
+
return found.map((item) => item.candidate);
|
|
121
|
+
}
|
|
69
122
|
export function allocateCandidate(repo) {
|
|
70
123
|
const resolvedRepo = path.resolve(repo);
|
|
71
124
|
const root = candidateRoot(resolvedRepo);
|