knodin 0.10.8 → 0.12.0
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-model.js +1 -0
- package/dist/src/compact-structural.js +8 -3
- package/dist/src/credential-patterns.js +14 -1
- package/dist/src/engine/index.js +371 -74
- package/dist/src/engine/parse-pool.js +21 -4
- package/dist/src/engine/seal.js +122 -3
- package/dist/src/engine/sealed-open.js +125 -8
- package/dist/src/engine/text-matches.js +121 -79
- package/dist/src/graph-query-health.js +9 -3
- package/dist/src/init.js +109 -3
- package/dist/src/lifecycle-health.js +46 -3
- package/dist/src/mcp-worker-supervisor.js +10 -2
- package/dist/src/repair-progress.js +12 -0
- package/dist/src/structural-fast-path.js +64 -1
- package/dist/src/tools/knodin-tools.js +32 -9
- package/docs/releases/0.11.0.md +227 -0
- package/docs/releases/0.12.0.md +281 -0
- package/package.json +5 -2
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# knodin 0.12.0
|
|
2
|
+
|
|
3
|
+
Ten open defects worked as one batch, plus three more found while investigating
|
|
4
|
+
them. Nearly all are the same failure in different clothing: a result that is
|
|
5
|
+
*complete* and a result that is *empty* arriving in the same shape, so the reader
|
|
6
|
+
cannot tell which one they got. One is worse than that, and it goes first.
|
|
7
|
+
|
|
8
|
+
No schema change and no re-index. An earlier draft of this release carried both,
|
|
9
|
+
for an embedding change that measured well on the test embedder and turned out to
|
|
10
|
+
do nothing on the real model. That story is told below, because the way it was
|
|
11
|
+
nearly shipped is more instructive than the fix — and because the same mistake,
|
|
12
|
+
caught a second time, is what led to the retrieval benchmark this release adds.
|
|
13
|
+
|
|
14
|
+
## `review` answered a different question and said nothing about it
|
|
15
|
+
|
|
16
|
+
The MCP gateway accepted `auditRange`, `auditBase` and `auditHead` on `review`,
|
|
17
|
+
discarded them, and reviewed `HEAD~1..HEAD` instead. The response reported
|
|
18
|
+
`base: HEAD~1` truthfully — but carried `staleness: fresh`, `availability:
|
|
19
|
+
healthy` and a full freshness envelope, so unless the caller happened to inspect
|
|
20
|
+
`base`, it read as authoritative for the range they had asked about.
|
|
21
|
+
|
|
22
|
+
Measured against a real branch, the difference was one changed file versus
|
|
23
|
+
twenty. That is not a near-miss; it is a review of the wrong thing, presented
|
|
24
|
+
with the same confidence as a review of the right thing. It was found while
|
|
25
|
+
running a product validation, where the conclusion would otherwise have been
|
|
26
|
+
"review does not catch this defect" — a false negative manufactured entirely out
|
|
27
|
+
of discarded input, and reported as a finding.
|
|
28
|
+
|
|
29
|
+
Those three parameters belong to `prs`, and only to `prs`; the schema had always
|
|
30
|
+
said so and nothing enforced it. They are now refused on every other operation,
|
|
31
|
+
and the refusal on `review` names the parameter that would have worked
|
|
32
|
+
(`diffScope: "compare"` with `from`/`toRevision`). An unused-parameter error is
|
|
33
|
+
always better than a confident answer to an unasked question.
|
|
34
|
+
|
|
35
|
+
## `count: 0` could not tell a typo from dead code
|
|
36
|
+
|
|
37
|
+
On a healthy, fresh graph, `knodin query callers_of someMisspelling` returned
|
|
38
|
+
`count: 0`, `staleness: fresh`, exit 0 — byte-identical to the answer for a real
|
|
39
|
+
symbol that genuinely has no callers.
|
|
40
|
+
|
|
41
|
+
The uninitialized-graph case was fixed earlier and now fails loudly, which made
|
|
42
|
+
this one worse rather than better: before that fix an empty result at least felt
|
|
43
|
+
untrustworthy, and afterwards it arrived with a certificate. The freshness stamp
|
|
44
|
+
certifies the **graph**; it says nothing about whether the **target** resolved.
|
|
45
|
+
Those are separate axes and are now reported separately. A query for a symbol
|
|
46
|
+
that exists in no open repository answers `targetResolution: "not-found"`,
|
|
47
|
+
renders as `target-not-found` rather than `no-match`, and exits non-zero, since
|
|
48
|
+
a misspelling is operator error rather than a negative result. A real symbol
|
|
49
|
+
with no callers still answers `resolved` and exits 0 — that is the honest
|
|
50
|
+
negative, and it is now distinguishable from the typo.
|
|
51
|
+
|
|
52
|
+
This matters most for the symbol it is easiest to lose: a seam is *by
|
|
53
|
+
definition* a well-documented symbol with no callers yet, so `count: 0` was
|
|
54
|
+
destroying exactly the signal that would have said "use this one". One session
|
|
55
|
+
nearly hand-rolled a duplicate of a blessed seam for that reason.
|
|
56
|
+
|
|
57
|
+
## `callers_of` counted callers and read as call sites
|
|
58
|
+
|
|
59
|
+
`callers_of` answers "which symbols call X", and its output — `count: N` above
|
|
60
|
+
one line per row — reads as "N call sites, here they are". When a function calls
|
|
61
|
+
the target twice, only the first line appeared and the count was lower than the
|
|
62
|
+
number of call sites. Ground truth of four calls reported as three.
|
|
63
|
+
|
|
64
|
+
Anyone sizing an edit from that count under-sizes it. Worse, the reporting
|
|
65
|
+
session initially scored a correct answer as a bug, because cross-checking with
|
|
66
|
+
grep found a call the tool had "missed" — it had not missed it, it had merged
|
|
67
|
+
it. Each row now carries `callSiteLines` with every line for that caller, and
|
|
68
|
+
the result carries `callSiteCount` alongside `count`, which is documented as
|
|
69
|
+
counting rows. Renaming the field alone would not have been enough: it would
|
|
70
|
+
have left the reader doing arithmetic they had no data for.
|
|
71
|
+
|
|
72
|
+
## A team repo with dead hooks had no safe way out
|
|
73
|
+
|
|
74
|
+
Every exit mutated something. Plain `repair` fixed graph rows and left the
|
|
75
|
+
background indexer dead, so the schema drifted again. `init` rewrote tracked
|
|
76
|
+
`AGENTS.md`, `GEMINI.md` and `.mcp.json` — churn every teammate diffs, on a
|
|
77
|
+
branch that has nothing to do with it. `configure --scope personal` stripped the
|
|
78
|
+
shared block for everyone else. The one clean option, `init --scope personal`,
|
|
79
|
+
is precisely the one refused while tracked team integration is active.
|
|
80
|
+
|
|
81
|
+
`knodin repair --lifecycle` rewrites the managed Git hooks and the background
|
|
82
|
+
refresh script and touches nothing else — no indexing, no graph rows, and it
|
|
83
|
+
never enters the agent-configuration path at any scope. Fault and remedy now
|
|
84
|
+
match in scope: the fault is in the hooks' contents, so the hooks' contents are
|
|
85
|
+
what changes. Every remediation message that used to say "run `knodin init`" for
|
|
86
|
+
a lifecycle fault now points here instead.
|
|
87
|
+
|
|
88
|
+
This does not settle the separate question of whether a per-developer scope
|
|
89
|
+
preference should mutate team-shared tracked files. It removes the trap.
|
|
90
|
+
|
|
91
|
+
## Hooks that predate the interpreter fallback are now named before they break
|
|
92
|
+
|
|
93
|
+
Scripts generated before the `KNODIN_NODE` fallback pin an absolute Homebrew
|
|
94
|
+
Cellar interpreter with nothing to fall back to. Homebrew deletes that directory
|
|
95
|
+
on upgrade, so every background refresh afterwards dies with exit 127 — in the
|
|
96
|
+
background, where the only trace is a log file — and the graph drifts until
|
|
97
|
+
someone happens to run `status`. One `brew upgrade` did this to three
|
|
98
|
+
repositories on one machine in a day.
|
|
99
|
+
|
|
100
|
+
The existing check could not see these at all: it keys off a `KNODIN_NODE=`
|
|
101
|
+
line, and these scripts have none, so the most dangerous shape read as healthy.
|
|
102
|
+
Health now reports a script that predates the fallback **while its interpreter
|
|
103
|
+
still exists**, because the whole point is that it is a time bomb rather than an
|
|
104
|
+
already-detonated one. A script it cannot recognise is still left alone; an
|
|
105
|
+
unparseable hook is not evidence of a pinned interpreter, and guessing would
|
|
106
|
+
invent a failure rather than report one.
|
|
107
|
+
|
|
108
|
+
## Search ranks a depended-upon implementation above its own fresh duplicate
|
|
109
|
+
|
|
110
|
+
Semantic similarity alone ranked a minutes-old duplicate above the
|
|
111
|
+
implementation it duplicated. The duplicate is short and made almost entirely of
|
|
112
|
+
the query's own words; the canonical version is diluted by an actual body. The
|
|
113
|
+
searcher read their own new leaf as evidence that nothing existed.
|
|
114
|
+
|
|
115
|
+
Degree is now weighted into rank fusion, capped at about three rank positions.
|
|
116
|
+
It is deliberately a small weight, because the opposite failure is equally real
|
|
117
|
+
and lands on the *same symbols*: a seam is a documented symbol with no callers,
|
|
118
|
+
and a large centrality weight would bury seams to fix duplicates — trading one
|
|
119
|
+
silent miss for another. A connected implementation climbs past neighbours it
|
|
120
|
+
was scoring near; it does not win from anywhere. Symbols with no inbound
|
|
121
|
+
references are excluded from the centrality ranking rather than ranked last, so
|
|
122
|
+
an uncalled symbol is never penalised, it only forgoes a boost it has no
|
|
123
|
+
evidence for.
|
|
124
|
+
|
|
125
|
+
An accompanying change — bounding the source body inside the embedding budget —
|
|
126
|
+
was written, measured, justified in a comment, given a schema bump, and then
|
|
127
|
+
**reverted before release**. On the deterministic bag-of-words embedder the suite
|
|
128
|
+
runs, it raised the diluted implementation's similarity from 0.163 to 0.212. On
|
|
129
|
+
the real MiniLM model it does nothing: across two body shapes, three query
|
|
130
|
+
phrasings and both budgets, the duplicate wins all twelve times, and the cap
|
|
131
|
+
never moves the winner. Its apparent benefit came from the test fixture, which
|
|
132
|
+
emitted forty copies of one line — truncating identical lines removes a
|
|
133
|
+
redundancy bag-of-words punishes and a transformer largely ignores.
|
|
134
|
+
|
|
135
|
+
The general lesson is worth more than the change would have been: a short
|
|
136
|
+
document made almost entirely of query tokens beats a long one under cosine
|
|
137
|
+
similarity, and no reweighting of a single vector's inputs changes that. Graph
|
|
138
|
+
degree is the right remedy here precisely because it is not a similarity signal.
|
|
139
|
+
|
|
140
|
+
## Prose queries got no lexical signal at all, and now do
|
|
141
|
+
|
|
142
|
+
A query phrased as prose did not surface the symbol whose docstring restates it;
|
|
143
|
+
symbols sharing a word or two in their *identifier* won instead. The ticket
|
|
144
|
+
framed this as docstring prose being under-weighted against code in the
|
|
145
|
+
embedding. That premise turned out to be false — for the reported symbol the
|
|
146
|
+
embedded text is already about 95% docstring, and it still lost.
|
|
147
|
+
|
|
148
|
+
The actual cause was in rank fusion. The lexical channel selected the AND-matched
|
|
149
|
+
set unless every semantic score was bit-identical, which never happens under a
|
|
150
|
+
real model, so the OR set was computed on every query and discarded on every
|
|
151
|
+
query. And for a query phrased as prose the AND clause is empty by construction:
|
|
152
|
+
it demands one symbol containing every surviving query word, function words
|
|
153
|
+
included. Nothing matches, so the lexical contribution was zero for every
|
|
154
|
+
candidate and "hybrid search" was pure vector search. The docstring's BM25
|
|
155
|
+
evidence existed the whole time, in the set being thrown away.
|
|
156
|
+
|
|
157
|
+
Both operators are now fused as separate channels, the partial one at 0.75 weight
|
|
158
|
+
and truncated to its top 30. Because the AND set is a subset of the OR set by
|
|
159
|
+
construction, an exact match collects both terms and a partial match collects
|
|
160
|
+
one — so the precision bonus falls out of that containment rather than needing
|
|
161
|
+
machinery of its own. The weight buys a property worth stating: lexical evidence
|
|
162
|
+
can rescue a symbol the embedder under-ranked, and cannot resurrect one the
|
|
163
|
+
embedder judged irrelevant.
|
|
164
|
+
|
|
165
|
+
A partial match must also clear a quorum: it has to echo at least two of the
|
|
166
|
+
query's words. A single incidental token is not evidence, and this index cannot
|
|
167
|
+
tell the difference unaided — FTS5 here has no stemming and does not split
|
|
168
|
+
identifiers, so for "session authentication and token validations" the class
|
|
169
|
+
that does the work matches *nothing* (its docstring says "sessions" and
|
|
170
|
+
"validation") while a handler containing "session" matches once. Without the
|
|
171
|
+
quorum that handler outranked the authenticator, and no weight fixed it: every
|
|
172
|
+
value from 0.75 down to 0.15 flipped the same way, because the problem was which
|
|
173
|
+
symbol earned credit rather than how much.
|
|
174
|
+
|
|
175
|
+
Measured on the new benchmark below: **MRR 0.5178 → 0.754, recall@10 0.80 →
|
|
176
|
+
0.925**, with 19 of 40 queries improving, 4 regressing and 17 unchanged — a
|
|
177
|
+
two-sided exact sign test at p = 0.0026. Dev and test halves moved together,
|
|
178
|
+
which is what you want when nothing was tuned against the labels. The largest
|
|
179
|
+
gains were symbols that had been effectively unreachable: rank 24 → 1, 28 → 3,
|
|
180
|
+
14 → 2.
|
|
181
|
+
|
|
182
|
+
## Doc comments were being lost, and misattributed
|
|
183
|
+
|
|
184
|
+
Two defects in how a doc comment is matched to the thing it documents. Both fed
|
|
185
|
+
the embedding input and the FTS `summary` column, so both corrupted retrieval
|
|
186
|
+
directly — and they point in opposite directions, which is why neither showed up
|
|
187
|
+
as an obvious symptom.
|
|
188
|
+
|
|
189
|
+
**Multi-line `//` comments kept only their last line.** tree-sitter gives each
|
|
190
|
+
`//` line its own node and the extractor read a single previous sibling, so the
|
|
191
|
+
loop meant to rejoin lines could never fire for this style. The first line of a
|
|
192
|
+
doc comment is usually the sentence saying what the thing is for — exactly what a
|
|
193
|
+
capability question matches on — so this silently removed the most useful text
|
|
194
|
+
and left a grammatical fragment that still looked like a well-formed summary.
|
|
195
|
+
Block comments are one node and were never affected, which is why the damage
|
|
196
|
+
stayed invisible in codebases that prefer them.
|
|
197
|
+
|
|
198
|
+
**And a symbol with no doc comment adopted its neighbour's.** The sibling walk
|
|
199
|
+
stepped over as many as five non-comment nodes hunting for a comment, so prose
|
|
200
|
+
written about one symbol was embedded against another. Measured on this
|
|
201
|
+
repository before the fix: **857 of 1,329 documented symbols under `src/` — 64% —
|
|
202
|
+
shared their summary text with another symbol**, and spot checks showed plain
|
|
203
|
+
misattribution rather than genuine repetition, including an interface carrying a
|
|
204
|
+
neighbouring function's docstring. After the fix, 718 symbols are documented and
|
|
205
|
+
**none** share text.
|
|
206
|
+
|
|
207
|
+
That second one is the worse of the pair. Absent prose merely loses a hit;
|
|
208
|
+
misattributed prose manufactures a confident wrong one.
|
|
209
|
+
|
|
210
|
+
A related gap is recorded but not fixed: a decorated declaration still never
|
|
211
|
+
receives its doc comment, because the decorator is a sibling of the declaration
|
|
212
|
+
but a child of the exporting statement. It was verified to predate both fixes and
|
|
213
|
+
is pinned by a characterization test.
|
|
214
|
+
|
|
215
|
+
## A benchmark for retrieval quality, and why it could not be a test
|
|
216
|
+
|
|
217
|
+
`npm run bench:c103` measures whether a capability question phrased as prose
|
|
218
|
+
finds the symbol implementing it, through the real ranker and the real embedding
|
|
219
|
+
model, over knodin's own source at a pinned commit. Forty hand-written queries,
|
|
220
|
+
15 dev / 25 test, MRR primary.
|
|
221
|
+
|
|
222
|
+
It exists as an opt-in benchmark rather than a spec because the fast suite
|
|
223
|
+
installs a deterministic bag-of-words embedder, and that embedder produces the
|
|
224
|
+
**opposite** answer on exactly this case: an identifier match beats a docstring
|
|
225
|
+
match on it, while the docstring wins by 2.75x on the real model. Two ranking
|
|
226
|
+
assertions written for this release were deleted for pinning that inversion. A
|
|
227
|
+
test that asserts the reverse of production is worse than no test — it fails
|
|
228
|
+
changes that help users and passes changes that hurt them.
|
|
229
|
+
|
|
230
|
+
So the fast suite now asserts only ranking properties no choice of embedder can
|
|
231
|
+
change, which in practice means rank arithmetic over graph structure, and
|
|
232
|
+
retrieval quality is measured where it can be measured honestly. The existing
|
|
233
|
+
`c86` evaluation could not host this: it ranks raw text by exact cosine, never
|
|
234
|
+
touching fusion or the ranking weights, and is saturated at recall@5 = 1.0.
|
|
235
|
+
|
|
236
|
+
## What is not fixed
|
|
237
|
+
|
|
238
|
+
**Mean-pooling dilution inside long docstrings.** Only one sentence of a
|
|
239
|
+
multi-sentence docstring may be on topic, and averaged over the whole comment it
|
|
240
|
+
becomes a minority of the vector while a short, entirely-on-topic competitor does
|
|
241
|
+
not. Splitting docstrings into sentence chunks and max-pooling was prototyped
|
|
242
|
+
against the real model and moves the reported symbol from rank 4 of 5 to rank 2 —
|
|
243
|
+
but it needs roughly 1.5x the vectors, a composite primary key, and a re-embed
|
|
244
|
+
migration.
|
|
245
|
+
|
|
246
|
+
It is deliberately **not** in this release. The lexical fix above addressed the
|
|
247
|
+
same failure for no migration at all, and the benchmark now says prose retrieval
|
|
248
|
+
sits at MRR 0.754 with recall@10 of 0.95. Chunking should be re-measured against
|
|
249
|
+
that new baseline before anyone pays for it — which is the whole reason the
|
|
250
|
+
benchmark was built first.
|
|
251
|
+
|
|
252
|
+
Worth stating plainly: the benchmark corpus is knodin's own source, and the
|
|
253
|
+
defect was reported against a different codebase. The improvement is real and
|
|
254
|
+
significant here; it has not been demonstrated there.
|
|
255
|
+
|
|
256
|
+
## Smaller things
|
|
257
|
+
|
|
258
|
+
`repair` now seeds from an indexed sibling worktree when there is **no** database
|
|
259
|
+
at all, reflink-copying a baseline and reconciling only the differing paths
|
|
260
|
+
instead of rebuilding the corpus from scratch. A database that exists but is
|
|
261
|
+
damaged is deliberately excluded — that is the case `repair` exists for, and
|
|
262
|
+
seeding it would replace a verifiable local rebuild with another checkout's
|
|
263
|
+
state.
|
|
264
|
+
|
|
265
|
+
`readGitHeadFast` is deleted. Its caller invoked it only when `.git` was absent,
|
|
266
|
+
which was its own first bail-out, so the two conditions were exactly
|
|
267
|
+
anti-correlated and the function could only ever return `null` — making the
|
|
268
|
+
`&& headUnchanged` term it fed inert. Coverage confirmed zero hits across the
|
|
269
|
+
whole suite. Git history settled which side was the bug: the fast HEAD read
|
|
270
|
+
landed first, and the later `lease = 0` for git-backed repositories superseded
|
|
271
|
+
it.
|
|
272
|
+
|
|
273
|
+
Two timing-sensitive specs no longer produce false pre-push failures. Both had
|
|
274
|
+
been widened only for instrumented coverage runs, on the theory that
|
|
275
|
+
instrumentation caused the contention. It does not — the failures reproduce on
|
|
276
|
+
ordinary uninstrumented runs under host load — and the uninstrumented run is
|
|
277
|
+
exactly the one developers gate on. The budgets are now unconditional, matching
|
|
278
|
+
the correction already recorded in `SUBPROCESS_BUDGET_MS`. A timeout that fires
|
|
279
|
+
on a loaded machine reports a failure that did not happen, and a gate whose
|
|
280
|
+
failures have to be re-run in isolation before they can be believed is not
|
|
281
|
+
doing its job.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knodin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"knodin": {
|
|
5
|
-
"compatibility": "
|
|
5
|
+
"compatibility": "breaking"
|
|
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",
|
|
@@ -63,6 +63,8 @@
|
|
|
63
63
|
"docs/releases/0.10.6.md",
|
|
64
64
|
"docs/releases/0.10.7.md",
|
|
65
65
|
"docs/releases/0.10.8.md",
|
|
66
|
+
"docs/releases/0.11.0.md",
|
|
67
|
+
"docs/releases/0.12.0.md",
|
|
66
68
|
"docs/releases/0.3.0.md",
|
|
67
69
|
"docs/releases/0.4.0.md",
|
|
68
70
|
"docs/releases/0.4.1.md",
|
|
@@ -121,6 +123,7 @@
|
|
|
121
123
|
"check:hygiene": "node scripts/check-repository-hygiene.mjs",
|
|
122
124
|
"check:roadmap": "tsx scripts/check-roadmap.ts",
|
|
123
125
|
"verify:c79": "tsx scripts/verify-c79.ts",
|
|
126
|
+
"bench:c103": "tsx benchmarks/evaluations/c103-prose-retrieval/runner.ts",
|
|
124
127
|
"bench:c80": "tsx benchmarks/evaluations/c80-structural-routing/runner.ts",
|
|
125
128
|
"verify:c80": "tsx scripts/verify-c80.ts",
|
|
126
129
|
"bench:c81": "tsx benchmarks/evaluations/c81-powered-benchmark/runner.ts",
|