knodin 0.10.7 → 0.10.8
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
CHANGED
|
@@ -456,10 +456,17 @@ function formatStatusHuman(result) {
|
|
|
456
456
|
? result.missing.records[0]
|
|
457
457
|
: (result.missing.files[0] ?? result.missing.records[0]);
|
|
458
458
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
459
|
+
// A linked worktree with no database is the one case where `repair` is both
|
|
460
|
+
// the wrong first step and the expensive one: `init` seeds from an indexed
|
|
461
|
+
// sibling and reconciles only what differs, while `repair` builds from
|
|
462
|
+
// scratch. The engine has already worked out that this is that case, so
|
|
463
|
+
// defer to the step it wrote rather than recomputing the judgement here.
|
|
464
|
+
const worktreeStep = result.repairSteps?.find((step) => step.includes("per-worktree"));
|
|
465
|
+
const repairCommand = worktreeStep ??
|
|
466
|
+
(result.lifecycle?.status === "degraded" &&
|
|
467
|
+
result.missing.records.every((record) => result.lifecycle?.issues.includes(record))
|
|
468
|
+
? "Run `knodin init`."
|
|
469
|
+
: "Run `knodin repair`.");
|
|
463
470
|
return `Graph or lifecycle needs repair: ${outstanding} issue(s) found (${coverage}).${detail} ${repairCommand}\n${lifecycleLine}${integrationLine}`;
|
|
464
471
|
}
|
|
465
472
|
function humanLabel(key) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Git checkout-layout questions, kept out of the engine on purpose.
|
|
5
|
+
*
|
|
6
|
+
* `engine/index.ts` carries a pinned budget on direct `fs` reads
|
|
7
|
+
* (`sealed-reader-inventory.spec.ts`) because a sealed artifact answers with no
|
|
8
|
+
* working tree: a reader that touches the filesystem there gets ENOENT, which
|
|
9
|
+
* the nearest guard turns into `""` or `null`, and that reads downstream as
|
|
10
|
+
* "this symbol has no body" rather than "this was not covered".
|
|
11
|
+
*
|
|
12
|
+
* Git *metadata* is categorically outside that concern — a sealed artifact has
|
|
13
|
+
* no `.git` at all, and these functions are never on a source-content path — so
|
|
14
|
+
* routing them through the sealed resolver would add a branch that can never
|
|
15
|
+
* execute. Keeping them here says that in the layout rather than by spending
|
|
16
|
+
* budget the engine reserves for source reads, and it makes them directly
|
|
17
|
+
* unit-testable, which they are not as engine-private helpers.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* True only for a `git worktree add` checkout.
|
|
21
|
+
*
|
|
22
|
+
* A linked worktree's `.git` is a FILE pointing at an administrative directory
|
|
23
|
+
* under the main checkout, and that directory contains `commondir` naming the
|
|
24
|
+
* shared repository.
|
|
25
|
+
*
|
|
26
|
+
* The `.git` file alone is NOT sufficient and treating it as sufficient is a
|
|
27
|
+
* live bug: submodules and `--separate-git-dir` clones use one too, and neither
|
|
28
|
+
* has a main checkout whose graph could cover it, so per-worktree guidance would
|
|
29
|
+
* be actively wrong for them. `commondir` is the marker that actually
|
|
30
|
+
* distinguishes the case.
|
|
31
|
+
*/
|
|
32
|
+
export function isLinkedWorktree(repo) {
|
|
33
|
+
const marker = path.join(repo, ".git");
|
|
34
|
+
try {
|
|
35
|
+
// An ordinary checkout keeps a `.git` directory; `statSync` throws when
|
|
36
|
+
// there is no `.git` at all, which is an ordinary case — knodin indexes
|
|
37
|
+
// plain directories too.
|
|
38
|
+
if (!fs.statSync(marker).isFile())
|
|
39
|
+
return false;
|
|
40
|
+
const pointer = fs.readFileSync(marker, "utf8").trim();
|
|
41
|
+
const gitDir = /^gitdir:\s*(.+)$/m.exec(pointer)?.[1]?.trim();
|
|
42
|
+
if (!gitDir)
|
|
43
|
+
return false;
|
|
44
|
+
return fs.existsSync(path.join(path.resolve(repo, gitDir), "commondir"));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Every read is inside the guard on purpose. This is best-effort detection
|
|
48
|
+
// reached from the missing-database remediation path, and that path has to
|
|
49
|
+
// answer: an unreadable `.git`, or one that disappears between the stat and
|
|
50
|
+
// the read, must degrade to "not a worktree" rather than crash `status`.
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/dist/src/engine/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import { allocateCandidate, assertCandidate, discardCandidateFiles, listCandidat
|
|
|
32
32
|
import { computeSimilarity, generateEmbedding, generateEmbeddings, } from "./embeddings.js";
|
|
33
33
|
import { walkRepoFiles } from "./file-walker.js";
|
|
34
34
|
import { clearGitHistorySignalCache, collectGitHistorySignals, } from "./git-history.js";
|
|
35
|
+
import { isLinkedWorktree } from "./git-layout.js";
|
|
35
36
|
// Runtime import, but not a cycle: parse-pool imports only TYPES from here,
|
|
36
37
|
// which erase at compile time. parse-worker's runtime import of this module
|
|
37
38
|
// resolves inside the worker thread, never in this one.
|
|
@@ -13083,6 +13084,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13083
13084
|
verification: { mode: "deep-audit", verifiedAt },
|
|
13084
13085
|
freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
|
|
13085
13086
|
repairSteps: [
|
|
13087
|
+
...(isLinkedWorktree(resolved)
|
|
13088
|
+
? [
|
|
13089
|
+
"Run `knodin init` here: graphs are per-worktree and this one has none of its own, so the main checkout's graph does not cover it. `init` seeds from an indexed sibling worktree where one exists rather than rebuilding from scratch.",
|
|
13090
|
+
]
|
|
13091
|
+
: []),
|
|
13086
13092
|
"Run `knodin repair` to create the local index.",
|
|
13087
13093
|
"Run `knodin status` again to verify health.",
|
|
13088
13094
|
],
|
|
@@ -24,6 +24,36 @@ Results distinguish:
|
|
|
24
24
|
- stale linked-worktree metadata;
|
|
25
25
|
- an unrelated repository under the same discovery root.
|
|
26
26
|
|
|
27
|
+
### Graphs are per-worktree
|
|
28
|
+
|
|
29
|
+
A linked worktree carries its own `.knodin` with its own graph, `indexedHead`,
|
|
30
|
+
and freshness. A healthy main checkout does not cover it, so a worktree created
|
|
31
|
+
by `git worktree add` has **no graph until `knodin init` runs inside it**.
|
|
32
|
+
|
|
33
|
+
`git worktree add` creates no `.knodin` of its own — the directory is knodin's,
|
|
34
|
+
written by `init` and by the managed lifecycle hooks. Where those hooks are
|
|
35
|
+
already installed in the repository, a new worktree can therefore appear to have
|
|
36
|
+
a `.knodin` while holding no graph at all, which is the more confusing shape of
|
|
37
|
+
this: the directory exists, and it is empty of anything that can answer.
|
|
38
|
+
|
|
39
|
+
That does not mean a new worktree pays for a full index. `init` selects the
|
|
40
|
+
already-indexed sibling worktree closest in commit distance, reflink-copies its
|
|
41
|
+
baseline where the filesystem supports it, reconciles only the paths that differ
|
|
42
|
+
between that sibling's `indexedHead` and this HEAD, deep-audits the result, and
|
|
43
|
+
promotes it only if it is healthy — falling back to a full index when no
|
|
44
|
+
schema- and build-compatible sibling exists. `KNODIN_DISABLE_WORKTREE_SEED=1`
|
|
45
|
+
forces the full path.
|
|
46
|
+
|
|
47
|
+
Seeding is on the `init` path only, which is why an uninitialized worktree is
|
|
48
|
+
steered to `init` rather than `repair`: both produce a correct index, but
|
|
49
|
+
`repair` rebuilds from scratch.
|
|
50
|
+
|
|
51
|
+
Nothing silently answers from the wrong graph in that state: structural queries
|
|
52
|
+
refuse with `available: false`, `state: not-initialized`, and exit code 1 rather
|
|
53
|
+
than returning an empty result, and status leads its remediation with the
|
|
54
|
+
per-worktree model and `knodin init`. Read a refusal as "this checkout was never
|
|
55
|
+
indexed", never as "there is nothing here to find".
|
|
56
|
+
|
|
27
57
|
### Opt-in repository signals
|
|
28
58
|
|
|
29
59
|
`repos discover --json --signals` adds a deterministic `signals` object to
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# knodin 0.10.8
|
|
2
|
+
|
|
3
|
+
One change, prompted by a field report from an agent session that built a
|
|
4
|
+
duplicate implementation of a hook that already existed. Most of that report
|
|
5
|
+
described behaviour knodin already has; the part it got right is fixed here.
|
|
6
|
+
|
|
7
|
+
## A linked worktree with no graph now says so in its remediation
|
|
8
|
+
|
|
9
|
+
`git worktree add` gives you a checkout with no graph in it. Graphs are
|
|
10
|
+
**per-worktree**, so a perfectly healthy main checkout tells you nothing about
|
|
11
|
+
the worktree you are standing in — and that makes the missing graph *more*
|
|
12
|
+
surprising, not less.
|
|
13
|
+
|
|
14
|
+
Git creates no `.knodin`; that directory is knodin's, written by `init` and by
|
|
15
|
+
the managed lifecycle hooks. Where those hooks are already installed, a fresh
|
|
16
|
+
worktree can present a `.knodin` that holds no graph — which is the more
|
|
17
|
+
confusing shape of this, and the one the original report hit.
|
|
18
|
+
|
|
19
|
+
Until now, status in that state produced generic remediation:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
remediation:
|
|
23
|
+
- Run `knodin repair` to create the local index.
|
|
24
|
+
- Run `knodin status` again to verify health.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That guidance works — `repair` does build the index from cold, and then tells
|
|
28
|
+
you to run `init` for lifecycle routing. What it does not do is explain *why*
|
|
29
|
+
there was no index in a repository whose main checkout is fine. A reader who
|
|
30
|
+
does not already know the per-worktree model has no way to tell "this checkout
|
|
31
|
+
was never indexed" from "there is nothing here to find", and the second reading
|
|
32
|
+
is the one that quietly confirms whatever hypothesis sent them looking.
|
|
33
|
+
|
|
34
|
+
A linked worktree now leads with the model and the fix, on both the
|
|
35
|
+
machine-readable `remediation` and the human `status` line:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Run `knodin init` here: graphs are per-worktree and this one has none of its
|
|
39
|
+
own, so the main checkout's graph does not cover it. `init` seeds from an
|
|
40
|
+
indexed sibling worktree where one exists rather than rebuilding from scratch.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Ordinary checkouts are unchanged. Detection reads the `.git` file's `gitdir:`
|
|
44
|
+
pointer and requires a `commondir` beside it — no subprocess, no registry read,
|
|
45
|
+
and it works on a repository too damaged to answer anything else.
|
|
46
|
+
|
|
47
|
+
A `.git` **file** alone is not enough, and treating it as enough was wrong in
|
|
48
|
+
the first cut of this fix: submodules and `--separate-git-dir` clones use one
|
|
49
|
+
too, and neither has a main checkout whose graph could cover it, so they would
|
|
50
|
+
have been handed an explanation that does not apply to them. `commondir` is
|
|
51
|
+
what actually distinguishes a linked worktree.
|
|
52
|
+
|
|
53
|
+
The check lives in a new `src/engine/git-layout.ts` rather than in the engine,
|
|
54
|
+
for a reason worth stating. `engine/index.ts` carries a pinned budget on direct
|
|
55
|
+
`fs` reads, because a sealed artifact answers with no working tree and a reader
|
|
56
|
+
that touches the filesystem there gets ENOENT — which the nearest guard turns
|
|
57
|
+
into `""` or `null`, and which reads downstream as "this symbol has no body"
|
|
58
|
+
rather than "this was not covered". Git *metadata* is categorically outside
|
|
59
|
+
that concern: a sealed artifact has no `.git` at all, so routing these reads
|
|
60
|
+
through the sealed resolver would add a branch that can never execute. Saying
|
|
61
|
+
so in the layout is more honest than spending budget the engine reserves for
|
|
62
|
+
source reads — and it makes the check directly unit-testable, which it was not
|
|
63
|
+
as an engine-private helper.
|
|
64
|
+
|
|
65
|
+
Its spec covers every branch (100% of statements and branches): a plain
|
|
66
|
+
directory, an ordinary checkout, a real linked worktree, a real submodule, a
|
|
67
|
+
real `--separate-git-dir` clone, a malformed `.git` file naming no gitdir, and a
|
|
68
|
+
dangling pointer whose administrative directory was pruned.
|
|
69
|
+
|
|
70
|
+
### Why `init` and not `repair` here
|
|
71
|
+
|
|
72
|
+
`repair` does build the index from cold, and for an ordinary checkout it stays
|
|
73
|
+
the right advice. In a linked worktree it is the *expensive* answer: seeding
|
|
74
|
+
lives on the `init` path only (`indexOrSeed`), so `repair` rebuilds from scratch
|
|
75
|
+
while `init` reflink-copies an indexed sibling's baseline, reconciles only the
|
|
76
|
+
paths that differ between that sibling's `indexedHead` and this HEAD,
|
|
77
|
+
deep-audits the candidate, and promotes it only if it passes — falling back to a
|
|
78
|
+
full index when no compatible sibling exists.
|
|
79
|
+
|
|
80
|
+
The human `status` renderer had been computing its own one-line advice and
|
|
81
|
+
ignoring `repairSteps` entirely, so the first fix reached the JSON surface and
|
|
82
|
+
not the line a person actually reads. It now defers to the step the engine
|
|
83
|
+
already wrote. Measured on a 20-file fixture, `init` in a fresh worktree with an
|
|
84
|
+
indexed sibling completes in about three seconds.
|
|
85
|
+
|
|
86
|
+
Worth stating plainly, because the surprise runs the other way from the cost:
|
|
87
|
+
the per-worktree model does **not** mean a new worktree pays for a full index.
|
|
88
|
+
|
|
89
|
+
## What the same report claimed that measurement did not support
|
|
90
|
+
|
|
91
|
+
Recorded because acting on any of these would have been a regression, and
|
|
92
|
+
because each was reported in good faith from accumulated notes rather than from
|
|
93
|
+
a run against this release.
|
|
94
|
+
|
|
95
|
+
**"Structural queries return empty rather than erroring on an uninitialized
|
|
96
|
+
graph."** They do not, and have not for some time. Against a committed
|
|
97
|
+
repository with no index, `knodin query callers_of <symbol>` returns
|
|
98
|
+
`available: false`, `status: unavailable`, `state: not-initialized`, the full
|
|
99
|
+
status envelope, and **exit code 1**. The same holds for `impact`, `dead_code`,
|
|
100
|
+
`tests_for`, and `file_summary`, on both the CLI and the MCP gateway — both
|
|
101
|
+
route through the same `inspectGraphQueryHealth` gate before and after the
|
|
102
|
+
operation runs. There was no empty result set to fix.
|
|
103
|
+
|
|
104
|
+
This one matters most, because an empty structural result that agrees with your
|
|
105
|
+
hypothesis is the worst failure this tool could have. It is worth restating that
|
|
106
|
+
the gate is there, and that it fails loudly on both surfaces.
|
|
107
|
+
|
|
108
|
+
**"`knodin init --scope personal` exits 0 having done nothing."** It exits **1**.
|
|
109
|
+
The refusal is thrown, and every non-`agent-event` throw reaches the top-level
|
|
110
|
+
handler, which prints to stderr and exits 1. Reproduced against a repository with
|
|
111
|
+
tracked team integration active.
|
|
112
|
+
|
|
113
|
+
**"A backgrounded `knodin repair` produces no progress output."** Repair emits
|
|
114
|
+
ordered per-phase progress on stderr — audit, planning, file reconciliation,
|
|
115
|
+
symbol identities, embeddings, orphan cleanup, verification — and finishes with a
|
|
116
|
+
verified summary naming indexed file and symbol counts.
|
|
117
|
+
|
|
118
|
+
Both of these turned out to have the same cause, since confirmed by the
|
|
119
|
+
reporter: the commands were run through `| tail`. A pipeline's exit status is the
|
|
120
|
+
last command's, so `init`'s 1 was recorded as `tail`'s 0; and `tail` flushes
|
|
121
|
+
nothing until the stream closes, so a capture file read mid-run looked empty
|
|
122
|
+
while repair was in fact reporting every phase. Neither surface changed. Worth
|
|
123
|
+
naming because one habit produced two bug reports against the wrong component.
|
|
124
|
+
|
|
125
|
+
## Not changed here: semantic search ranking
|
|
126
|
+
|
|
127
|
+
A follow-up from the same session measured something real and unaddressed: on a
|
|
128
|
+
healthy graph, `"hands-free voice conversation mode hook with barge-in"` ranked a
|
|
129
|
+
constant list (`BARGE_IN_PHRASES`, 0.588) and the caller's own minutes-old
|
|
130
|
+
duplicate (0.552) above the canonical 513-line implementation, which did not
|
|
131
|
+
appear in the top hits at all. Mechanism-shaped phrasing
|
|
132
|
+
(`"voice pipeline auto restart mic after TTS"`) put the canonical hook at #2,
|
|
133
|
+
0.517.
|
|
134
|
+
|
|
135
|
+
So feature-shaped phrasing — the phrasing a developer uses when asking "does this
|
|
136
|
+
already exist?" — ranked worst for exactly that question, and surfaced the new
|
|
137
|
+
duplicate as apparent confirmation. Centrality data the fix would need
|
|
138
|
+
(in-degree, community membership) is already in the graph and already in the
|
|
139
|
+
output; it is not weighted into ranking.
|
|
140
|
+
|
|
141
|
+
That is a ranking change with its own evaluation burden and it is not bundled
|
|
142
|
+
into a remediation-string fix. It is recorded here so it is not rediscovered from
|
|
143
|
+
scratch.
|
|
144
|
+
|
|
145
|
+
## The pre-push test gate was not gating
|
|
146
|
+
|
|
147
|
+
Found because this release's own broken commit sailed through it.
|
|
148
|
+
|
|
149
|
+
`lefthook.yml`'s `quality-gates` block ran `bun run test:coverage` and then
|
|
150
|
+
`sh scripts/run-sonar-scan.sh` as two lines of one shell block with no
|
|
151
|
+
`set -e`. A shell block's exit status is its **last** command's, so a failing
|
|
152
|
+
suite followed by a passing Sonar scan reported success, and the push was
|
|
153
|
+
allowed. The suite printed `1 failed | 2058 passed` and
|
|
154
|
+
`script "test:coverage" exited with code 1`, and the push completed anyway.
|
|
155
|
+
|
|
156
|
+
`set -e` now leads the block. Coverage thresholds and Sonar were unaffected;
|
|
157
|
+
what was broken is that a red suite could not stop a push.
|
|
158
|
+
|
|
159
|
+
## Verification
|
|
160
|
+
|
|
161
|
+
`npm test`, `npm run lint`, and `npm run typecheck` pass. The new behaviour is
|
|
162
|
+
pinned by a test in `src/__tests__/unit/index-health.spec.ts` that builds a real
|
|
163
|
+
linked worktree with `git worktree add`, asserts the per-worktree line leads its
|
|
164
|
+
remediation, and asserts the ordinary checkout keeps repair-first guidance with
|
|
165
|
+
no per-worktree line.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knodin",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.8",
|
|
4
4
|
"knodin": {
|
|
5
5
|
"compatibility": "compatible"
|
|
6
6
|
},
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"docs/releases/0.10.5.md",
|
|
63
63
|
"docs/releases/0.10.6.md",
|
|
64
64
|
"docs/releases/0.10.7.md",
|
|
65
|
+
"docs/releases/0.10.8.md",
|
|
65
66
|
"docs/releases/0.3.0.md",
|
|
66
67
|
"docs/releases/0.4.0.md",
|
|
67
68
|
"docs/releases/0.4.1.md",
|