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
|
@@ -183,6 +183,7 @@ function buildDocumentedKnodinTools() {
|
|
|
183
183
|
"review",
|
|
184
184
|
"map",
|
|
185
185
|
"search",
|
|
186
|
+
"textSearch",
|
|
186
187
|
"query",
|
|
187
188
|
"prs",
|
|
188
189
|
"context",
|
|
@@ -204,7 +205,7 @@ function buildDocumentedKnodinTools() {
|
|
|
204
205
|
"diagnostics",
|
|
205
206
|
"sealed",
|
|
206
207
|
],
|
|
207
|
-
description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. execute: run one immutable repository-defined profile only when independently enabled globally and locally; no executable or argv is accepted from the caller, unsupported containment fails closed, and output is compressed then diagnosed. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP. remote: list read-only mirrors of repositories that are reachable but not checked out locally, so you can run explain/query/map/search against one by passing its `path` as `repoPath`. Each mirror is a snapshot pinned at a commit; the remote is not watched. Acquiring, refreshing, and removing mirrors is CLI-only (`knodin remote add|refresh|remove`) because it clones another repository's source onto this machine and consumes disk nothing reclaims automatically — the user's decision to make, not an agent's. sealed: query a sealed artifact (`artifactPath`) with NO checkout — source is embedded in it. Returns attested identity, commit, ref, age, coverage, and a `degraded` list of what it cannot do; pass `symbol` to explain. Describes the attested commit and no later one, so staleness is `unknown`, never `fresh`. Creating one is CLI-only (`knodin seal`).",
|
|
208
|
+
description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. textSearch: use when what you are renaming is NOT a code symbol — a product name, a UI label, a literal — so `explain` does not apply. Read-only exact-text match repo-wide, including files yielding no symbols (markdown, config); each hit is classified string/comment/identifier/prose with `basis` stating whether that came from the file's parse tree or only its extension. Names files it could not read, and separates no-matches from could-not-search. `caseSensitive: false` also matches other casings, each labelled. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. execute: run one immutable repository-defined profile only when independently enabled globally and locally; no executable or argv is accepted from the caller, unsupported containment fails closed, and output is compressed then diagnosed. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP. remote: list read-only mirrors of repositories that are reachable but not checked out locally, so you can run explain/query/map/search against one by passing its `path` as `repoPath`. Each mirror is a snapshot pinned at a commit; the remote is not watched. Acquiring, refreshing, and removing mirrors is CLI-only (`knodin remote add|refresh|remove`) because it clones another repository's source onto this machine and consumes disk nothing reclaims automatically — the user's decision to make, not an agent's. sealed: query a sealed artifact (`artifactPath`) with NO checkout — source is embedded in it. Returns attested identity, commit, ref, age, coverage, and a `degraded` list of what it cannot do; pass `symbol` to explain. Describes the attested commit and no later one, so staleness is `unknown`, never `fresh`. Creating one is CLI-only (`knodin seal`).",
|
|
208
209
|
},
|
|
209
210
|
profile: {
|
|
210
211
|
type: "string",
|
|
@@ -221,7 +222,7 @@ function buildDocumentedKnodinTools() {
|
|
|
221
222
|
},
|
|
222
223
|
file: {
|
|
223
224
|
type: "string",
|
|
224
|
-
description: "Repo-relative definition file
|
|
225
|
+
description: "Repo-relative file. Disambiguates which definition is meant when a symbol resolves in several files; with query impact and impactMode=file it names the target file instead.",
|
|
225
226
|
},
|
|
226
227
|
evidenceLevel: {
|
|
227
228
|
type: "string",
|
|
@@ -272,7 +273,7 @@ function buildDocumentedKnodinTools() {
|
|
|
272
273
|
impactMode: {
|
|
273
274
|
type: "string",
|
|
274
275
|
enum: ["symbol", "file"],
|
|
275
|
-
description: "query impact only: stable symbol reach (default) or explicitly labeled changed-file blast radius.",
|
|
276
|
+
description: "query impact only: stable symbol reach (default) or explicitly labeled changed-file blast radius. With `file`, name the target in the `file` parameter, or pass comma-separated paths in `symbol` for a multi-file change set.",
|
|
276
277
|
},
|
|
277
278
|
direction: {
|
|
278
279
|
type: "string",
|
|
@@ -399,6 +400,10 @@ function buildDocumentedKnodinTools() {
|
|
|
399
400
|
enum: ["all", "test", "production"],
|
|
400
401
|
description: "search/file_metrics: include all, test-only, or production-only files.",
|
|
401
402
|
},
|
|
403
|
+
caseSensitive: {
|
|
404
|
+
type: "boolean",
|
|
405
|
+
description: "textSearch only; default true. False also matches other casings; each hit reports the case on disk and whether it matched exactly, so a case-sensitive token is not folded into the same decision as a label.",
|
|
406
|
+
},
|
|
402
407
|
offset: {
|
|
403
408
|
type: "number",
|
|
404
409
|
minimum: 0,
|
|
@@ -1398,7 +1403,7 @@ async function handleDiagnosticsOperation(repo, args, bounded) {
|
|
|
1398
1403
|
async function dispatchKnodinTool(args) {
|
|
1399
1404
|
const startedAt = performance.now();
|
|
1400
1405
|
const parsedArgs = (args ?? {});
|
|
1401
|
-
const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, queries, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositories, scope, distributed, repositoryIds, components, evidence, relationshipType, requireComplete, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, planDigest, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
|
|
1406
|
+
const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, queries, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositories, scope, distributed, repositoryIds, components, evidence, relationshipType, requireComplete, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, planDigest, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, caseSensitive, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
|
|
1402
1407
|
const repo = repoPath ?? process.cwd();
|
|
1403
1408
|
const selector = {
|
|
1404
1409
|
identity: expandCompactIdentity(identity),
|
|
@@ -1744,6 +1749,15 @@ async function dispatchKnodinTool(args) {
|
|
|
1744
1749
|
includeSource,
|
|
1745
1750
|
offset,
|
|
1746
1751
|
}));
|
|
1752
|
+
case "textSearch": {
|
|
1753
|
+
if (!query)
|
|
1754
|
+
throw new Error("knodin textSearch requires `query` (the exact text to find)");
|
|
1755
|
+
const found = await engine.searchText(query, repo, { caseSensitive });
|
|
1756
|
+
// Not routed through `graphRead`: this reads the working tree rather
|
|
1757
|
+
// than the graph, so it is answerable — and correct — on a repository
|
|
1758
|
+
// whose index is stale, absent, or still building.
|
|
1759
|
+
return bounded(found, "textSearch");
|
|
1760
|
+
}
|
|
1747
1761
|
case "prs": {
|
|
1748
1762
|
return bounded(await handlePullRequestsOperation({
|
|
1749
1763
|
repo,
|
|
@@ -1799,8 +1813,23 @@ async function dispatchKnodinTool(args) {
|
|
|
1799
1813
|
if (limit !== undefined && (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1))
|
|
1800
1814
|
throw new Error("knodin query: limit must be a positive integer");
|
|
1801
1815
|
const repoWide = REPO_WIDE_QUERY_PATTERNS.includes(pattern);
|
|
1802
|
-
|
|
1803
|
-
|
|
1816
|
+
// File-mode impact takes its target as comma-separated paths in
|
|
1817
|
+
// `symbol`. That is not the call anyone forms from this schema: a `file`
|
|
1818
|
+
// parameter is published right beside `impactMode`, and the obvious
|
|
1819
|
+
// reading — impactMode "file" plus file: <path> — was rejected with
|
|
1820
|
+
// "requires `symbol`", naming a field the caller had deliberately not
|
|
1821
|
+
// used. A user reported the capability as unavailable because of it.
|
|
1822
|
+
//
|
|
1823
|
+
// `file` is now accepted as the target here, and only here: in symbol
|
|
1824
|
+
// mode it keeps its existing meaning as a definition disambiguator.
|
|
1825
|
+
const fileModeImpact = pattern === "impact" && impactMode === "file";
|
|
1826
|
+
if (fileModeImpact && symbol && file && symbol !== file)
|
|
1827
|
+
throw new Error(`knodin query impact with impactMode=file received different targets in \`symbol\` (${symbol}) and \`file\` (${file}); pass one`);
|
|
1828
|
+
const target = fileModeImpact ? (symbol ?? file) : symbol;
|
|
1829
|
+
if (!target && !repoWide)
|
|
1830
|
+
throw new Error(fileModeImpact
|
|
1831
|
+
? "knodin query impact with impactMode=file requires a target path in `file`, or comma-separated paths in `symbol`"
|
|
1832
|
+
: `knodin query ${pattern} requires \`symbol\``);
|
|
1804
1833
|
if ((pattern === "shortest_path" || pattern === "cross_substrate_path") && !to)
|
|
1805
1834
|
throw new Error(`knodin query ${pattern} requires \`to\``);
|
|
1806
1835
|
if (pattern === "rename_preview" && !to)
|
|
@@ -1817,7 +1846,7 @@ async function dispatchKnodinTool(args) {
|
|
|
1817
1846
|
.rename(symbol ?? "", to ?? "", repo, apply === true, true, selector)
|
|
1818
1847
|
.then((result) => bounded(decorateGraphQueryResult(result, queryHealth?.available ? queryHealth.state : "healthy", queryHealth?.available ? queryHealth.graph.freshness : undefined), "query:rename_preview"));
|
|
1819
1848
|
}
|
|
1820
|
-
const queryResult = await engine.query(pattern,
|
|
1849
|
+
const queryResult = await engine.query(pattern, target ?? "", repo, to, Math.min(limit ?? itemBudget ?? 100, itemBudget ?? 1000), depth, detailLevel === "source" ? undefined : detailLevel, selector, pattern === "impact"
|
|
1821
1850
|
? {
|
|
1822
1851
|
mode: impactMode ?? "symbol",
|
|
1823
1852
|
direction,
|
|
@@ -234,7 +234,12 @@ function baseResult(options, status) {
|
|
|
234
234
|
networkUsed: false,
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Exported so release classification can reuse this parser rather than adding a
|
|
239
|
+
* third hand-rolled copy (`src/manager-update.ts:424` is the second, and a
|
|
240
|
+
* de-duplication candidate). Strict SemVer: leading zeros are rejected.
|
|
241
|
+
*/
|
|
242
|
+
export function parseVersion(version) {
|
|
238
243
|
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
239
244
|
if (!match)
|
|
240
245
|
return null;
|
|
@@ -62,6 +62,30 @@ function completedStatus(graph) {
|
|
|
62
62
|
return "unknown";
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Waiting here can only end at the timeout (EASFDC-8498).
|
|
67
|
+
*
|
|
68
|
+
* `queued` freshness means progress depends on the queued-event drainer, and
|
|
69
|
+
* `reconcileOrdinaryDrift` deliberately declines that state, so the in-process
|
|
70
|
+
* path cannot advance it either. If the drainer also cannot run, every
|
|
71
|
+
* remaining poll is spent relaunching a processor that will not start.
|
|
72
|
+
*
|
|
73
|
+
* This is the exact shape reported on 0.10.2: a hook exiting 127 on every
|
|
74
|
+
* invocation, freshness stuck at `queued`, and `status` recommending
|
|
75
|
+
* `wait --fresh` — which held the reason for its own futility on every poll and
|
|
76
|
+
* reported none of it.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately narrow. A degraded lifecycle with freshness that is NOT queued
|
|
79
|
+
* can still be reconciled in process, and failing fast there would refuse work
|
|
80
|
+
* the command can actually do.
|
|
81
|
+
*/
|
|
82
|
+
function blockedOnDeadDrainer(graph) {
|
|
83
|
+
return graph.freshness?.state === "queued" && !graph.lifecycle.refreshCapable;
|
|
84
|
+
}
|
|
85
|
+
function describeDeadDrainer(graph) {
|
|
86
|
+
const detail = graph.lifecycle.lastError ?? graph.lifecycle.issues[0] ?? "cause not recorded";
|
|
87
|
+
return `background refresh cannot run, so queued work will not drain: ${detail}`;
|
|
88
|
+
}
|
|
65
89
|
async function reconcileOrdinaryDrift(engine, repo, graph) {
|
|
66
90
|
const shouldReconcile = (graph.status === "stale" || graph.status === "repair-needed") &&
|
|
67
91
|
graph.freshness.state !== "queued";
|
|
@@ -90,9 +114,27 @@ export async function waitForFresh(engine, repo, timeoutMs = 30_000, processQueu
|
|
|
90
114
|
status = completedStatus(graph);
|
|
91
115
|
if (status)
|
|
92
116
|
return completedResult(status, startedAt, polls, graph);
|
|
117
|
+
// Checked after the completion checks, never before: a repository that is
|
|
118
|
+
// already fresh must still report `fresh`, even with a broken hook.
|
|
119
|
+
if (blockedOnDeadDrainer(graph))
|
|
120
|
+
return {
|
|
121
|
+
status: "lifecycle-degraded",
|
|
122
|
+
waitedMs: Date.now() - startedAt,
|
|
123
|
+
polls,
|
|
124
|
+
graph,
|
|
125
|
+
reason: describeDeadDrainer(graph),
|
|
126
|
+
};
|
|
93
127
|
const elapsed = Date.now() - startedAt;
|
|
94
128
|
if (elapsed >= timeoutMs)
|
|
95
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
status: "timeout",
|
|
131
|
+
waitedMs: elapsed,
|
|
132
|
+
polls,
|
|
133
|
+
graph,
|
|
134
|
+
// A timeout that names nothing is what sent the reporter looking in
|
|
135
|
+
// the wrong place. Say what was still outstanding.
|
|
136
|
+
reason: `still waiting on ${graph.lifecycle.queuedEvents} queued event(s)${graph.lifecycle.lastError ? `; last refresh error: ${graph.lifecycle.lastError}` : ""}`,
|
|
137
|
+
};
|
|
96
138
|
await delay(Math.min(100, timeoutMs - elapsed));
|
|
97
139
|
}
|
|
98
140
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# knodin 0.10.3
|
|
2
|
+
|
|
3
|
+
Four changes, all from one observation: after 0.10.2 fixed a parser leak that had
|
|
4
|
+
left 174,205 files silently unparsed on a large repository, the recovery advice
|
|
5
|
+
turned out to be unusable. A full reindex of that checkout has no knowable cost,
|
|
6
|
+
no way to ask for less work, and no way to survive an interruption.
|
|
7
|
+
|
|
8
|
+
## Indexing reports how much work is left, not just how many files
|
|
9
|
+
|
|
10
|
+
A full index of a 902,960-file Salesforce checkout appeared to hang for twenty
|
|
11
|
+
minutes at 71%. It had not hung — it had reached the profiles.
|
|
12
|
+
|
|
13
|
+
Progress was counted in files while the cost lives in bytes, and on that
|
|
14
|
+
repository the two disagree violently: **435 files over 1 MB hold 78% of all
|
|
15
|
+
source bytes.** So the counter raced through the cheap files and appeared to
|
|
16
|
+
stall exactly when the expensive ones began, which is the moment an operator is
|
|
17
|
+
most likely to conclude the process is wedged and kill it. The only way to tell
|
|
18
|
+
grinding from hung was to read the process's accumulated CPU time.
|
|
19
|
+
|
|
20
|
+
Both counters are now reported, and the estimate is derived from the byte rate.
|
|
21
|
+
Files answer "how far through the list"; bytes answer "how much work is left".
|
|
22
|
+
|
|
23
|
+
The estimate is labelled an estimate. During that one run, three separate
|
|
24
|
+
completion figures were derived from observed rates — 38 hours, 4 hours, and 15
|
|
25
|
+
minutes — and every one was wrong, because the rate ranged from 6.5 to 361 files
|
|
26
|
+
per second before collapsing to 0.065. Rates are now measured from the start of
|
|
27
|
+
the phase rather than process start, so the cold-start window no longer drags the
|
|
28
|
+
estimate; extrapolating from it is what produced the 38-hour figure.
|
|
29
|
+
|
|
30
|
+
## Rebuild one subtree with `--under`
|
|
31
|
+
|
|
32
|
+
`knodin index --under <dir>` rebuilds only that directory. Previously the
|
|
33
|
+
intuitive attempt was also the worst one: a lone directory argument means "index
|
|
34
|
+
this whole repository".
|
|
35
|
+
|
|
36
|
+
The file set is resolved inside the engine rather than by expanding paths
|
|
37
|
+
yourself, so a scoped rebuild indexes exactly the files a full index would. A
|
|
38
|
+
scope that matches nothing fails rather than reporting a successful rebuild of
|
|
39
|
+
nothing.
|
|
40
|
+
|
|
41
|
+
Coverage is reported as a lower bound afterwards, because a whole-repository
|
|
42
|
+
tally no longer describes a partially rebuilt graph. Trading a graph you were
|
|
43
|
+
told to distrust for one that is silently partial would be worse than doing
|
|
44
|
+
nothing.
|
|
45
|
+
|
|
46
|
+
## Defer embeddings with `--skip-embeddings`
|
|
47
|
+
|
|
48
|
+
The embedding phase dominates indexing cost, and the engine has always supported
|
|
49
|
+
deferring it — only mirrors could ask. `knodin index --skip-embeddings` now
|
|
50
|
+
builds structure alone, leaving `explain`, `query` and impact answerable while
|
|
51
|
+
semantic search waits.
|
|
52
|
+
|
|
53
|
+
The gap is stated rather than left to be discovered. An index that defers
|
|
54
|
+
embeddings says so, and `knodin search` now announces that it will under-return
|
|
55
|
+
until they are built. A short result list is otherwise indistinguishable from a
|
|
56
|
+
thorough search that found little.
|
|
57
|
+
|
|
58
|
+
Running `knodin index` again completes the deferred pass, and only the symbols
|
|
59
|
+
still lacking embeddings are processed.
|
|
60
|
+
|
|
61
|
+
## An interrupted `--clean` resumes
|
|
62
|
+
|
|
63
|
+
A clean index builds into a candidate database and promotes it atomically. That
|
|
64
|
+
is deliberate — it is why a failed rebuild cannot leave a half-built graph in
|
|
65
|
+
place of a working one, and it is unchanged.
|
|
66
|
+
|
|
67
|
+
But a killed process runs no discard, so the candidate survived on disk and the
|
|
68
|
+
next run ignored it and started over. On a repository where the rebuild takes
|
|
69
|
+
hours, an interruption cost all of it.
|
|
70
|
+
|
|
71
|
+
The next `knodin index --clean` now continues that build, skipping files already
|
|
72
|
+
recorded whose contents still match disk, and says how many it kept. Resumption
|
|
73
|
+
is refused when the tree has moved on since — continuing there would promote a
|
|
74
|
+
graph that never described any single state of the repository — and refused for
|
|
75
|
+
a candidate that was never marked as an interrupted build. Both cases fall back
|
|
76
|
+
to a full rebuild rather than guessing.
|
|
77
|
+
|
|
78
|
+
## Release compatibility is derived, not declared
|
|
79
|
+
|
|
80
|
+
`knodin.compatibility` had been the literal string `"breaking"` since it was
|
|
81
|
+
introduced, across four releases. Two of those genuinely were breaking and two
|
|
82
|
+
were bugfix patches, so it was accidentally correct half the time — which is
|
|
83
|
+
worse than being consistently wrong, because the correct entries make the field
|
|
84
|
+
look maintained.
|
|
85
|
+
|
|
86
|
+
It is now derived from the version change, with the pre-1.0 rule stated
|
|
87
|
+
explicitly: below 1.0 the minor is the breaking axis, so `0.10.x` to `0.11.0`
|
|
88
|
+
breaks and `0.10.2` to `0.10.3` does not. Nothing inside this repository reads
|
|
89
|
+
the field — the Homebrew tap reads it from the published package — so two guards
|
|
90
|
+
now check it, because otherwise nothing would.
|
|
91
|
+
|
|
92
|
+
Releases already published keep the label they shipped with. Published metadata
|
|
93
|
+
is immutable by design.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# knodin 0.10.4
|
|
2
|
+
|
|
3
|
+
Five changes from two pieces of field feedback. The first came from a
|
|
4
|
+
product-name rename in the Nova repository; the second from a checkout whose
|
|
5
|
+
background refresh had been failing silently since a Homebrew upgrade.
|
|
6
|
+
|
|
7
|
+
The theme is the same one 0.10.2 and 0.10.3 were about: a tool that reports
|
|
8
|
+
things it has not verified, and a failure that leaves no trace anywhere a person
|
|
9
|
+
looks.
|
|
10
|
+
|
|
11
|
+
## `textSearch`: exact text, with every match classified
|
|
12
|
+
|
|
13
|
+
The reported blocker: the refactor workflow assumes a rename targets a code
|
|
14
|
+
symbol. This one was a product-name literal spanning UI strings and documents,
|
|
15
|
+
where `explain` and symbol resolution do not apply. The reporter fell back to
|
|
16
|
+
`rg` and hand-applied patches for the whole task.
|
|
17
|
+
|
|
18
|
+
Searching is the easy half. The half worth building is classification, and their
|
|
19
|
+
case shows why: visible `NOVA` labels had to change, while `NOVA-CANARY-…` was a
|
|
20
|
+
deliberate case-sensitive security token that had to survive. A flat text search
|
|
21
|
+
returns both and leaves a human to separate them by eye across however many hits
|
|
22
|
+
a large repository produces — exactly the position knodin exists to remove
|
|
23
|
+
people from.
|
|
24
|
+
|
|
25
|
+
knodin can classify only because it already parses these files. A match in a
|
|
26
|
+
file with a grammar resolves to its smallest containing syntax node, and the
|
|
27
|
+
parser's own node type is the evidence. A file without a grammar supports a
|
|
28
|
+
claim about the file, not about the match.
|
|
29
|
+
|
|
30
|
+
That difference is reported rather than smoothed over. Every match carries the
|
|
31
|
+
`basis` its classification rests on — `parse-node`, `file-type`, or `none` — and
|
|
32
|
+
the grammar's raw node type travels alongside the coarse label so a bad mapping
|
|
33
|
+
is catchable. Classification will sometimes be wrong; it is offered as evidence
|
|
34
|
+
a human is reviewing, never as a decision already taken.
|
|
35
|
+
|
|
36
|
+
Three details that matter in practice:
|
|
37
|
+
|
|
38
|
+
- **Documents are searched.** Enumeration uses the indexer's prune rules but not
|
|
39
|
+
its source-extension filter, because markdown and config are the point.
|
|
40
|
+
- **Files that could not be read are named**, not omitted. A rename that
|
|
41
|
+
silently skipped files would leave a half-renamed repository looking finished.
|
|
42
|
+
- **"No matches" is distinguishable from "could not search."** An empty list
|
|
43
|
+
that reads as reassurance is the defect this whole operation exists to avoid.
|
|
44
|
+
|
|
45
|
+
`textSearch` reads the working tree rather than the graph, so it stays
|
|
46
|
+
answerable when the index is stale, absent, or still building — often exactly
|
|
47
|
+
when a rename is underway.
|
|
48
|
+
|
|
49
|
+
It previews only. Applying edits is deliberately not in this release.
|
|
50
|
+
|
|
51
|
+
## `impact` accepts the file you gave it
|
|
52
|
+
|
|
53
|
+
Reported verbatim against 0.10.2:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
operation: query, pattern: impact, impactMode: file,
|
|
57
|
+
file: documents/nova-architecture-deck.js
|
|
58
|
+
-> knodin query impact requires `symbol`
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
File-mode impact was never missing. It read its target from `symbol`, documented
|
|
62
|
+
in one clause buried inside that parameter's prose, while a `file` parameter sat
|
|
63
|
+
published right beside `impactMode` meaning something else entirely. The schema
|
|
64
|
+
offered both, and the combination anyone would form from them was the one that
|
|
65
|
+
failed.
|
|
66
|
+
|
|
67
|
+
That is worse than a missing feature: the reporter concluded the capability was
|
|
68
|
+
broken and moved on. A working feature was recorded as unavailable because of
|
|
69
|
+
parameter naming.
|
|
70
|
+
|
|
71
|
+
`file` is now accepted as the target under file mode, and keeps its original
|
|
72
|
+
meaning in symbol mode. Conflicting `symbol` and `file` values are refused with
|
|
73
|
+
both named rather than resolved by preference — silently picking one would
|
|
74
|
+
answer a question nobody asked. The error now states the supported call.
|
|
75
|
+
|
|
76
|
+
## Managed hooks survive a Node upgrade
|
|
77
|
+
|
|
78
|
+
A checkout's background refresh had exited 127 seventeen consecutive times:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
background-index.sh: /opt/homebrew/Cellar/node/26.5.0_1/bin/node:
|
|
82
|
+
No such file or directory
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`brew upgrade node` deletes the versioned Cellar directory. The hook had
|
|
86
|
+
recorded the interpreter's absolute path at generation time, and that path
|
|
87
|
+
contains the version, so it was guaranteed to stop existing.
|
|
88
|
+
|
|
89
|
+
The caller cannot avoid this: Node symlink-resolves `process.execPath`, so
|
|
90
|
+
invoking `/opt/homebrew/opt/node/bin/node` still reports the Cellar path. By the
|
|
91
|
+
time knodin can read its own interpreter, the durable path is gone.
|
|
92
|
+
|
|
93
|
+
The recorded path is now mapped back to `<prefix>/opt/<formula>/…`, the symlink
|
|
94
|
+
Homebrew repoints on upgrade, so it survives by construction rather than by
|
|
95
|
+
falling back at run time. The prefix is derived from the match, so Intel
|
|
96
|
+
`/usr/local` and Linuxbrew work through the same path. A non-Homebrew
|
|
97
|
+
interpreter is left untouched.
|
|
98
|
+
|
|
99
|
+
**Existing repositories need one `knodin init`** to pick up a corrected hook.
|
|
100
|
+
`repair` cannot do it: the fault lives in the hook's contents, not in graph
|
|
101
|
+
state — which is why a `repair` run in that checkout returned "healthy" while
|
|
102
|
+
the lifecycle stayed broken.
|
|
103
|
+
|
|
104
|
+
Lifecycle health now notices this class of failure. It previously checked that
|
|
105
|
+
the background script existed and was executable, neither of which says anything
|
|
106
|
+
about the runtime on its first line, so the repository reported a degraded
|
|
107
|
+
lifecycle while naming no cause. The reported issue now names the missing
|
|
108
|
+
interpreter and the command that rewrites the hook.
|
|
109
|
+
|
|
110
|
+
## `wait --fresh` stops instead of waiting for something that cannot happen
|
|
111
|
+
|
|
112
|
+
`wait --fresh` has always had a bounded timeout, and it was reached honestly.
|
|
113
|
+
But when the background refresh is failing, waiting cannot succeed, and the
|
|
114
|
+
command already knew that: it recomputes lifecycle health on every poll, so it
|
|
115
|
+
was holding the exit-127 error the entire time it spun, then reported the single
|
|
116
|
+
word `timeout`.
|
|
117
|
+
|
|
118
|
+
It now returns immediately when freshness is queued and lifecycle refresh cannot
|
|
119
|
+
run — the pairing that is genuinely futile — and names the recorded failure.
|
|
120
|
+
Timeouts carry a reason as well: what remained queued, and the last refresh
|
|
121
|
+
error.
|
|
122
|
+
|
|
123
|
+
The guard is narrow on purpose. A degraded lifecycle whose freshness is not
|
|
124
|
+
queued can still be reconciled in process, and a repository that is already
|
|
125
|
+
fresh still reports `fresh` even with a broken hook.
|
|
126
|
+
|
|
127
|
+
## `init` no longer denies agents that are configured
|
|
128
|
+
|
|
129
|
+
Six seconds apart, with nothing installed in between:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
$ knodin init
|
|
133
|
+
Agent integration: personal - no supported coding agents detected
|
|
134
|
+
$ knodin status
|
|
135
|
+
Agent integration: personal (claude, codex, gemini, antigravity)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Both commands were computing something correct — `init` reports what that run
|
|
139
|
+
configured, `status` reports what is configured — but `init` described its empty
|
|
140
|
+
set as a *detection* result it had never run, which reads as a denial of the
|
|
141
|
+
four agents the next command lists. It now says what the value supports: no
|
|
142
|
+
agents configured by this run.
|
|
143
|
+
|
|
144
|
+
## Known gaps
|
|
145
|
+
|
|
146
|
+
Two items from the same report are deliberately not fixed here rather than
|
|
147
|
+
guessed at:
|
|
148
|
+
|
|
149
|
+
- `status` and `repair` can disagree about indexed-file and symbol counts. In
|
|
150
|
+
the reported session `status` showed 0 indexed files and 95 issues while
|
|
151
|
+
`repair`, seconds later, reported 94 indexed files and 53 with symbols and
|
|
152
|
+
called itself "verified". Whether `repair` inspected or silently rebuilt
|
|
153
|
+
changes what the fix is, and the evidence does not settle it.
|
|
154
|
+
- `init --scope personal` leaves `.agents/`, `.codex/` and `.gemini/` untracked
|
|
155
|
+
and unexcluded, and migrates a tracked `.gitignore` entry without saying so.
|
|
156
|
+
|
|
157
|
+
## Compatibility
|
|
158
|
+
|
|
159
|
+
Compatible. Nothing is removed or renamed; `textSearch` is a new operation
|
|
160
|
+
alongside the existing ones, and `file` gains a meaning under `impactMode: file`
|
|
161
|
+
while keeping its previous one everywhere else.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knodin",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"knodin": {
|
|
5
|
-
"compatibility": "
|
|
5
|
+
"compatibility": "compatible"
|
|
6
6
|
},
|
|
7
7
|
"description": "knodin — source-evidenced local code intelligence with known bounds. Stable identity, fresh evidence, truthful budgets, and recoverable bounded views.",
|
|
8
8
|
"license": "MIT",
|
|
@@ -74,6 +74,8 @@
|
|
|
74
74
|
"docs/releases/0.10.0.md",
|
|
75
75
|
"docs/releases/0.10.1.md",
|
|
76
76
|
"docs/releases/0.10.2.md",
|
|
77
|
+
"docs/releases/0.10.3.md",
|
|
78
|
+
"docs/releases/0.10.4.md",
|
|
77
79
|
"docs/assets/knodin-favicon.svg",
|
|
78
80
|
"docs/SYSTEMS-AND-RELATIONSHIPS.md",
|
|
79
81
|
"docs/TELEMETRY.md",
|