knodin 0.10.2 → 0.10.4
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 +41 -4
- 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 +317 -22
- package/dist/src/engine/text-matches.js +309 -0
- package/dist/src/init-progress.js +36 -5
- package/dist/src/init.js +8 -1
- package/dist/src/lifecycle-health.js +36 -0
- package/dist/src/node-runtime.js +33 -0
- package/dist/src/release-compatibility.js +95 -0
- package/dist/src/release-preflight.js +18 -0
- package/dist/src/tools/knodin-tools.js +36 -7
- package/dist/src/update-policy.js +6 -1
- package/dist/src/wait-for-fresh.js +43 -1
- package/docs/releases/0.10.3.md +93 -0
- package/docs/releases/0.10.4.md +161 -0
- package/package.json +4 -2
package/dist/bin/cli.js
CHANGED
|
@@ -77,8 +77,17 @@ function integrationAgents(repo) {
|
|
|
77
77
|
const repositoryDetected = inspectRepositoryIntegrationStatus(repo)?.agents ?? [];
|
|
78
78
|
return [...new Set([...detectSupportedAgents(), ...previous, ...repositoryDetected])];
|
|
79
79
|
}
|
|
80
|
-
function formatInitHuman(result) {
|
|
81
|
-
|
|
80
|
+
export function formatInitHuman(result) {
|
|
81
|
+
// `configured` is what THIS run wrote, not what exists on the machine. The
|
|
82
|
+
// old wording called an empty list "no supported coding agents detected",
|
|
83
|
+
// asserting a detection result init never computed — and it read as a flat
|
|
84
|
+
// contradiction of the `status` line seconds later, which lists the agents
|
|
85
|
+
// that genuinely are configured (EASFDC-8499).
|
|
86
|
+
//
|
|
87
|
+
// Deliberately not calling the detector here: a formatter that reads the
|
|
88
|
+
// machine cannot be tested deterministically, and the honest fix is to stop
|
|
89
|
+
// claiming more than this value supports rather than to go find more.
|
|
90
|
+
let agents = `${result.paths.scope} — no agents configured by this run`;
|
|
82
91
|
if (result.paths.scope === "cli-only")
|
|
83
92
|
agents = "CLI-only — AI agents are not configured to discover knodin";
|
|
84
93
|
else if (result.paths.agentIntegration.configured.length > 0)
|
|
@@ -138,16 +147,29 @@ function formatRepairHuman(result) {
|
|
|
138
147
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
139
148
|
return `Repair finished with ${outstanding.toLocaleString()} remaining issue(s).${detail} Run \`knodin status --deep\` for details.\n`;
|
|
140
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* The semantic gap, stated rather than left to be discovered.
|
|
152
|
+
*
|
|
153
|
+
* An index that deferred embeddings leaves `search` returning a short list, and
|
|
154
|
+
* a short list is indistinguishable from a thorough search that found little.
|
|
155
|
+
* Saying nothing here is what turns a deliberate deferral into a silent one.
|
|
156
|
+
*/
|
|
157
|
+
function formatSemanticGap(readiness) {
|
|
158
|
+
if (readiness === undefined || readiness === "ready")
|
|
159
|
+
return "";
|
|
160
|
+
return ` Semantic coverage is ${readiness}: \`search\` will under-return until \`knodin index\` completes embeddings.`;
|
|
161
|
+
}
|
|
141
162
|
function formatIndexHuman(result) {
|
|
163
|
+
const semantic = formatSemanticGap(result.semanticReadiness);
|
|
142
164
|
if (result.indexed.length === 0 && result.unchanged.length > 0) {
|
|
143
165
|
const noun = result.unchanged.length === 1 ? "file" : "files";
|
|
144
|
-
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified
|
|
166
|
+
return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.${semantic}\n`;
|
|
145
167
|
}
|
|
146
168
|
const unchanged = result.unchanged.length > 0
|
|
147
169
|
? `; ${result.unchanged.length.toLocaleString()} already current`
|
|
148
170
|
: "";
|
|
149
171
|
const noun = result.indexed.length === 1 ? "file" : "files";
|
|
150
|
-
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified
|
|
172
|
+
return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.${semantic}\n`;
|
|
151
173
|
}
|
|
152
174
|
function formatIndexVerificationError(result) {
|
|
153
175
|
const firstIssue = result.verification.missing.files[0] ?? result.verification.missing.records[0];
|
|
@@ -2168,6 +2190,8 @@ async function main() {
|
|
|
2168
2190
|
process.exit(1);
|
|
2169
2191
|
}
|
|
2170
2192
|
const clean = rest.includes("--clean") || rest.includes("--force");
|
|
2193
|
+
const skipEmbeddings = rest.includes("--skip-embeddings");
|
|
2194
|
+
const under = selectorValue("--under");
|
|
2171
2195
|
const scipPath = selectorValue("--scip");
|
|
2172
2196
|
const sarifPath = selectorValue("--sarif");
|
|
2173
2197
|
// A bound the operator cannot move is just a failure, so every import
|
|
@@ -2198,6 +2222,8 @@ async function main() {
|
|
|
2198
2222
|
let indexResult;
|
|
2199
2223
|
try {
|
|
2200
2224
|
indexResult = await engine.index(plan.repo, plan.files, clean, {
|
|
2225
|
+
skipEmbeddings,
|
|
2226
|
+
...(under !== undefined ? { under } : {}),
|
|
2201
2227
|
scip: scipPath
|
|
2202
2228
|
? {
|
|
2203
2229
|
path: scipPath,
|
|
@@ -2904,6 +2930,17 @@ async function main() {
|
|
|
2904
2930
|
process.exitCode = finalExitCode;
|
|
2905
2931
|
return;
|
|
2906
2932
|
}
|
|
2933
|
+
// `search` has no human formatter — results fall through to the generic JSON
|
|
2934
|
+
// emitter — so a semantically incomplete graph produced a short list with
|
|
2935
|
+
// nothing saying why. The list looks like a thorough answer, which is the
|
|
2936
|
+
// whole failure. Printed to stderr so it annotates without corrupting output
|
|
2937
|
+
// anyone is piping.
|
|
2938
|
+
if (cmd === "search" && !jsonOutput) {
|
|
2939
|
+
const readiness = result.semanticReadiness;
|
|
2940
|
+
const gap = formatSemanticGap(readiness);
|
|
2941
|
+
if (gap)
|
|
2942
|
+
process.stderr.write(`${gap.trim()}\n`);
|
|
2943
|
+
}
|
|
2907
2944
|
if (cmd === "status" && !jsonOutput) {
|
|
2908
2945
|
process.stdout.write(formatStatusHuman(boundedResult));
|
|
2909
2946
|
process.exitCode = finalExitCode;
|
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);
|