simt-dev 0.7.1 → 0.8.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/CHANGELOG.md +36 -1
- package/README.md +169 -80
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
- package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +3 -1
- package/rules/mdc/intake-questions.mdc +8 -0
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +7 -5
- package/src/argv.mjs +19 -0
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +155 -113
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +12 -26
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +17 -535
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +15 -1037
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +116 -971
- package/src/bench/run.mjs +164 -881
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +11 -13
- package/src/ci/dependabot.mjs +25 -55
- package/src/commands/agent.mjs +1 -9
- package/src/commands/agenthook.mjs +66 -35
- package/src/commands/bench.mjs +2 -2
- package/src/commands/ci.mjs +11 -13
- package/src/commands/docs.mjs +88 -0
- package/src/commands/gate.mjs +10 -13
- package/src/commands/harness.mjs +3 -3
- package/src/commands/help.mjs +44 -3
- package/src/commands/hooks.mjs +20 -20
- package/src/commands/ignore.mjs +11 -7
- package/src/commands/impact.mjs +4 -8
- package/src/commands/index.mjs +49 -9
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +122 -104
- package/src/commands/lease.mjs +67 -18
- package/src/commands/map.mjs +10 -5
- package/src/commands/mcp.mjs +2 -17
- package/src/commands/repoindex.mjs +8 -8
- package/src/commands/rules.mjs +8 -4
- package/src/commands/shared.mjs +12 -16
- package/src/commands/skills.mjs +5 -6
- package/src/commands/skillscompile.mjs +3 -3
- package/src/commands/skillsinstall.mjs +5 -4
- package/src/commands/slice.mjs +199 -64
- package/src/commands/spec.mjs +78 -62
- package/src/commands/status.mjs +24 -12
- package/src/commands/sync.mjs +14 -14
- package/src/commands/tests.mjs +12 -12
- package/src/commands/tokens.mjs +19 -24
- package/src/commands/upgrade.mjs +8 -4
- package/src/commands/why.mjs +6 -13
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/decisions/index.mjs +3 -2
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +2 -1
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +5 -3
- package/src/hooks/dispatcher.mjs +26 -32
- package/src/hooks/gates.mjs +12 -2
- package/src/hooks/resolve.mjs +2 -7
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +2 -11
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +16 -11
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +124 -17
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +49 -9
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -1005
- package/src/{bench → measure}/corpus.mjs +1 -1
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/output/index.mjs +122 -31
- package/src/packs/manage.mjs +15 -9
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/manifest.mjs +10 -435
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +16 -547
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +13 -1
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +0 -7
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +8 -12
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +1 -1
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +3 -1
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/src/bench/tasks.mjs +0 -36
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The index as a handle (review 0003 Q76).
|
|
2
|
+
//
|
|
3
|
+
// `openIndex` returns a handle whose methods answer with pure data — rows and
|
|
4
|
+
// counts, no exit codes, no formatted lines — and with a structured error,
|
|
5
|
+
// `{ ok: false, error: { code, message } }`, when they cannot. The methods are
|
|
6
|
+
// thin over the same primitives the commands use, so a handle and a command
|
|
7
|
+
// cannot disagree about the same index.
|
|
8
|
+
//
|
|
9
|
+
// The graph loads once at open: files, symbols, edges. FTS bodies run in
|
|
10
|
+
// SQLite (`search` delegates to `searchFtsBm25`) and stored vectors are not
|
|
11
|
+
// loaded at all: a map without `--embed` does not touch them (Q76).
|
|
12
|
+
|
|
13
|
+
import { coverage } from './coverage.mjs';
|
|
14
|
+
import { governs } from './govern.mjs';
|
|
15
|
+
import { dependents } from './impact.mjs';
|
|
16
|
+
import { rankFiles } from './map.mjs';
|
|
17
|
+
import { isIndexStale, readEdges, readFiles, readSymbols, reindex, searchFtsBm25 } from './store.mjs';
|
|
18
|
+
|
|
19
|
+
const INDEX_CODES = {
|
|
20
|
+
BUILD: 'E_BUILD',
|
|
21
|
+
CLOSED: 'E_CLOSED',
|
|
22
|
+
NO_INDEX: 'E_NO_INDEX',
|
|
23
|
+
READ: 'E_READ',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function ok(data) {
|
|
27
|
+
return { ok: true, data };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fail(code, message) {
|
|
31
|
+
return { ok: false, error: { code, message } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The files, symbols and edges the index holds. The lazy projection of
|
|
36
|
+
* `loadGraph`: no FTS bodies, no vectors, because nothing on this handle
|
|
37
|
+
* scores them.
|
|
38
|
+
*/
|
|
39
|
+
async function loadGraphData(root) {
|
|
40
|
+
const [files, symbols, edges] = await Promise.all([
|
|
41
|
+
readFiles(root),
|
|
42
|
+
readSymbols(root),
|
|
43
|
+
readEdges(root),
|
|
44
|
+
]);
|
|
45
|
+
return { files, symbols, edges };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Open the repository's index.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} root
|
|
52
|
+
* @param {{build?: boolean, refresh?: boolean}} [options]
|
|
53
|
+
* `build` (default true) refreshes the index from the git index before the
|
|
54
|
+
* first read — incrementally, so a current repository pays for one
|
|
55
|
+
* `git ls-files -s`, not a walk of the tree. `refresh` re-reads the graph on
|
|
56
|
+
* every call instead of caching it at open.
|
|
57
|
+
* @returns {Promise<IndexHandle | {ok: false, error: {code: string, message: string}}>}
|
|
58
|
+
*/
|
|
59
|
+
export async function openIndex(root, { build = true, refresh = false } = {}) {
|
|
60
|
+
if (build) {
|
|
61
|
+
try {
|
|
62
|
+
await reindex(root);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return fail(INDEX_CODES.BUILD, `could not build the index (${error.message})`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let closed = false;
|
|
69
|
+
let cached = null;
|
|
70
|
+
|
|
71
|
+
async function graph() {
|
|
72
|
+
if (closed) throw new Error(INDEX_CODES.CLOSED);
|
|
73
|
+
if (cached === null || refresh) cached = await loadGraphData(root);
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function answered(fn) {
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.message === INDEX_CODES.CLOSED) return fail(INDEX_CODES.CLOSED, 'the index is closed');
|
|
82
|
+
return fail(INDEX_CODES.READ, error.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handle = {
|
|
87
|
+
ok: true,
|
|
88
|
+
|
|
89
|
+
/** Freshness and row counts of the open index. */
|
|
90
|
+
status: () =>
|
|
91
|
+
answered(async () => {
|
|
92
|
+
const g = await graph();
|
|
93
|
+
if (g.files.length === 0) return fail(INDEX_CODES.NO_INDEX, 'the index is empty');
|
|
94
|
+
const stale = await isIndexStale(root);
|
|
95
|
+
return ok({ stale, counts: { files: g.files.length, symbols: g.symbols.length, edges: g.edges.length } });
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* BM25 rows for a task string, best first.
|
|
100
|
+
* @param {string} task
|
|
101
|
+
* @param {{limit?: number}} [options]
|
|
102
|
+
* @returns {Promise<{ok: true, data: {path: string, kind: string, score: number}[]}|{ok: false, error: {code: string, message: string}}>}
|
|
103
|
+
*/
|
|
104
|
+
search: (task, { limit } = {}) =>
|
|
105
|
+
answered(async () => {
|
|
106
|
+
if (closed) throw new Error(INDEX_CODES.CLOSED);
|
|
107
|
+
const rows = await searchFtsBm25(root, task);
|
|
108
|
+
if (rows.error) return fail(INDEX_CODES.READ, rows.error);
|
|
109
|
+
return ok(limit === undefined ? rows : rows.slice(0, limit));
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Rank the repository's files for a task string (R22): graph proximity
|
|
114
|
+
* fused with the BM25 rows.
|
|
115
|
+
* @param {string} task
|
|
116
|
+
* @param {{limit?: number, embed?: boolean, allowRemote?: boolean}} [options]
|
|
117
|
+
* @returns {Promise<{ok: true, data: {path: string, rank: number, score: number}[]}|{ok: false, error: {code: string, message: string}}>}
|
|
118
|
+
*/
|
|
119
|
+
rank: (task, { limit, embed = false, allowRemote = false } = {}) =>
|
|
120
|
+
answered(async () => {
|
|
121
|
+
const g = await graph();
|
|
122
|
+
if (g.files.length === 0) return fail(INDEX_CODES.NO_INDEX, 'the index is empty');
|
|
123
|
+
const bm25 = String(task ?? '').trim().length > 0 ? await searchFtsBm25(root, task) : [];
|
|
124
|
+
if (bm25.error) return fail(INDEX_CODES.READ, bm25.error);
|
|
125
|
+
const ranked = rankFiles({ ...g, bm25, task });
|
|
126
|
+
return ok(limit === undefined ? ranked : ranked.slice(0, limit));
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The tests that exercise `paths` (Q77): the import closure when
|
|
131
|
+
* `transitive` (default true), the stored test edges otherwise.
|
|
132
|
+
* @param {Iterable<string>} paths
|
|
133
|
+
* @param {{transitive?: boolean, cochange?: boolean}} [options]
|
|
134
|
+
* @returns {Promise<{ok: true, data: string[]}|{ok: false, error: {code: string, message: string}}>}
|
|
135
|
+
*/
|
|
136
|
+
coverage: (paths, options) =>
|
|
137
|
+
answered(async () => {
|
|
138
|
+
const g = await graph();
|
|
139
|
+
return ok(coverage({ files: g.files, imports: g.edges }, paths, options));
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Who a change to `file` reaches (Q77): the reverse import closure and
|
|
144
|
+
* the tests that import it.
|
|
145
|
+
* @param {string} file
|
|
146
|
+
* @param {{limit?: number}} [options]
|
|
147
|
+
* @returns {Promise<{ok: true, data: {importers: string[], tests: string[], truncated: boolean}}|{ok: false, error: {code: string, message: string}}>}
|
|
148
|
+
*/
|
|
149
|
+
impact: (file, { limit } = {}) =>
|
|
150
|
+
answered(async () => {
|
|
151
|
+
const g = await graph();
|
|
152
|
+
return ok(dependents(file, g.edges, limit));
|
|
153
|
+
}),
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What governs `file` (Q77): the records in `corpus` whose Affects line
|
|
157
|
+
* names it, plus the rules, skills, gates and lessons whose path lists
|
|
158
|
+
* match.
|
|
159
|
+
* @param {string} file
|
|
160
|
+
* @param {{records?: {id: string, title: string, text: string}[], ctx?: {files: Set<string>, commands: Set<string>, roots: string[]}}} [corpus]
|
|
161
|
+
* @returns {Promise<{ok: true, data: {kind: string, id: string, detail?: string}[]}|{ok: false, error: {code: string, message: string}}>}
|
|
162
|
+
*/
|
|
163
|
+
governs: (file, corpus) => answered(() => ok(governs(file, corpus ?? {}))),
|
|
164
|
+
|
|
165
|
+
/** Release the handle. Calls after it answer `E_CLOSED`. */
|
|
166
|
+
close() {
|
|
167
|
+
closed = true;
|
|
168
|
+
cached = null;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return handle;
|
|
173
|
+
}
|
package/src/index/persist.mjs
CHANGED
|
@@ -9,19 +9,27 @@ import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
10
|
|
|
11
11
|
import { LOCAL_DIR } from '../state.mjs';
|
|
12
|
+
import { cmp } from '../util/sort.mjs';
|
|
12
13
|
|
|
13
14
|
export const SQLITE_FILE = join(LOCAL_DIR, 'index.sqlite');
|
|
14
15
|
export const JSONL_FILE = join(LOCAL_DIR, 'index.jsonl');
|
|
15
16
|
const SQLITE_FILE_LEGACY = '.simt/index.sqlite';
|
|
16
17
|
const JSONL_FILE_LEGACY = '.simt/index.jsonl';
|
|
17
|
-
export const SCHEMA_V = '
|
|
18
|
+
export const SCHEMA_V = '5';
|
|
18
19
|
|
|
19
20
|
// The FTS tables carry their tokenizer in their definition, so `if not exists`
|
|
20
21
|
// cannot migrate them: a v3 database would keep tokenizing new rows the v3 way
|
|
21
|
-
// (ASCII fold, kind and ref searchable)
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// (ASCII fold, kind and ref searchable), and a v4 `fts` table cannot grow the
|
|
23
|
+
// node_id column the v5 layout needs. The preamble creates them only when they
|
|
24
|
+
// are missing (a cold build); the 3 -> 4 and 4 -> 5 migrations below drop and
|
|
25
|
+
// recreate them, which is the versioned replacement for the per-write wipe (Q44).
|
|
26
|
+
export const FTS_SCHEMA = 'create virtual table if not exists fts using fts5(body, node_id UNINDEXED, kind UNINDEXED, tokenize = "unicode61 remove_diacritics 2")';
|
|
27
|
+
export const FTS_TRI_SCHEMA = 'create virtual table if not exists fts_tri using fts5(kind UNINDEXED, ref UNINDEXED, body, tokenize = "trigram")';
|
|
28
|
+
|
|
29
|
+
// The v4 `fts` shape. The 3 -> 4 step recreates the table with THIS shape so a
|
|
30
|
+
// v3 database emerges looking exactly like a genuine v4 one - the 4 -> 5 step
|
|
31
|
+
// then copies its `ref` column, which the v5 shape no longer has.
|
|
32
|
+
const FTS_V4_SCHEMA = 'create virtual table fts using fts5(kind UNINDEXED, ref UNINDEXED, body, tokenize = "unicode61 remove_diacritics 2")';
|
|
25
33
|
|
|
26
34
|
// A database written by a *newer* simt is never opened for write, and never
|
|
27
35
|
// read blind (review 0003 Q44): the mismatched canonical file stays with its
|
|
@@ -53,11 +61,58 @@ const MIGRATIONS = [
|
|
|
53
61
|
to: 4,
|
|
54
62
|
apply: (db) => {
|
|
55
63
|
db.exec('drop table if exists fts');
|
|
56
|
-
db.exec(
|
|
64
|
+
db.exec(FTS_V4_SCHEMA);
|
|
57
65
|
db.exec('drop table if exists fts_tri');
|
|
58
66
|
db.exec(FTS_TRI_SCHEMA);
|
|
59
67
|
},
|
|
60
68
|
},
|
|
69
|
+
// v4 is per-kind tables (files/symbols/edges/embeddings); v5 is the entity
|
|
70
|
+
// layout (sources/nodes/edges/vectors) with the read shapes restored as SQL
|
|
71
|
+
// in the store. `edges` and `fts` are built under `_v5` names and swapped in
|
|
72
|
+
// after the copy, because the v4 tables of those names still hold data.
|
|
73
|
+
// fts_tri keeps its (kind, ref, body) shape - the CJK trigram path is
|
|
74
|
+
// untouched (Q43).
|
|
75
|
+
{
|
|
76
|
+
from: 4,
|
|
77
|
+
to: 5,
|
|
78
|
+
apply: (db) => {
|
|
79
|
+
db.exec(TABLES);
|
|
80
|
+
db.exec(EDGES_DDL.replaceAll('edges(', 'edges_v5('));
|
|
81
|
+
db.exec(FTS_SCHEMA.replaceAll('exists fts ', 'exists fts_v5 '));
|
|
82
|
+
db.exec(`insert into sources(kind, path, revision, lang, bytes, status, props)
|
|
83
|
+
select 'blob', path, blob, lang, bytes, 'indexed', json_object('pkg', coalesce(pkg, '')) from files`);
|
|
84
|
+
db.exec(`insert into nodes(key, kind, source_id)
|
|
85
|
+
select 'file:' || path, 'file', id from sources where kind = 'blob'`);
|
|
86
|
+
db.exec(`insert into nodes(key, kind, source_id, name, line, props)
|
|
87
|
+
select 'sym:' || s.path || '#' || s.name || '@' || coalesce(s.line, 0), 'symbol', src.id,
|
|
88
|
+
s.name, s.line, json_object('kind', coalesce(s.kind, 'symbol'), 'sig', coalesce(s.sig, ''), 'exported', coalesce(s.exported, 0))
|
|
89
|
+
from symbols s join sources src on src.kind = 'blob' and src.path = s.path`);
|
|
90
|
+
db.exec(`insert into edges_v5(src_id, kind, dst_key, dst_id, weight, owner_source_id, trust)
|
|
91
|
+
select sn.id, e.kind, e.dst, dn.id, e.weight, src.id, 'observed'
|
|
92
|
+
from edges e
|
|
93
|
+
join sources src on src.kind = 'blob' and src.path = e.src
|
|
94
|
+
join nodes sn on sn.source_id = src.id and sn.kind = 'file'
|
|
95
|
+
left join sources dst on dst.kind = 'blob' and dst.path = e.dst
|
|
96
|
+
left join nodes dn on dn.source_id = dst.id and dn.kind = 'file'`);
|
|
97
|
+
db.exec(`insert into vectors(node_id, model, dims, vec)
|
|
98
|
+
select n.id, em.model, em.dims, em.vec
|
|
99
|
+
from embeddings em
|
|
100
|
+
join sources s on s.kind = 'blob' and s.path = em.path
|
|
101
|
+
join nodes n on n.source_id = s.id and n.kind = 'file'`);
|
|
102
|
+
db.exec(`insert into fts_v5(body, node_id, kind)
|
|
103
|
+
select f.body, n.id, f.kind
|
|
104
|
+
from fts f
|
|
105
|
+
join sources s on s.kind = 'blob' and s.path = f.ref
|
|
106
|
+
join nodes n on n.source_id = s.id and n.kind = 'file'`);
|
|
107
|
+
db.exec('drop table files');
|
|
108
|
+
db.exec('drop table symbols');
|
|
109
|
+
db.exec('drop table embeddings');
|
|
110
|
+
db.exec('drop table edges');
|
|
111
|
+
db.exec('drop table fts');
|
|
112
|
+
db.exec('alter table edges_v5 rename to edges');
|
|
113
|
+
db.exec('alter table fts_v5 rename to fts');
|
|
114
|
+
},
|
|
115
|
+
},
|
|
61
116
|
];
|
|
62
117
|
|
|
63
118
|
/**
|
|
@@ -67,21 +122,89 @@ const MIGRATIONS = [
|
|
|
67
122
|
* each is idempotent on an already-migrated table set.
|
|
68
123
|
*/
|
|
69
124
|
function migrateDb(db) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
125
|
+
let at = Number(db.prepare('pragma user_version').get()?.user_version ?? 0);
|
|
126
|
+
// `user_version 0` is ambiguous: a cold build has no legacy tables at all,
|
|
127
|
+
// and a pre-stamp database still holds the v3 shape. The 4 -> 5 step copies
|
|
128
|
+
// FROM the v4 tables (`files`, `symbols`, ...), so a cold build must not run
|
|
129
|
+
// it - the 3 -> 4 step would leave its `fts` in the v4 shape behind.
|
|
130
|
+
const legacy =
|
|
131
|
+
at !== 0 ||
|
|
132
|
+
db.prepare(`select count(*) as n from sqlite_master where type = 'table' and name in ('files', 'symbols', 'embeddings')`).get().n > 0;
|
|
133
|
+
if (legacy) {
|
|
134
|
+
for (const step of MIGRATIONS) {
|
|
135
|
+
if (at === 0 || (at >= step.from && at < step.to)) {
|
|
136
|
+
step.apply(db);
|
|
137
|
+
at = step.to; // the next step sees the version this one left behind
|
|
138
|
+
}
|
|
139
|
+
}
|
|
73
140
|
}
|
|
74
141
|
db.exec(`pragma user_version = ${Number(SCHEMA_V)}`);
|
|
75
142
|
}
|
|
76
143
|
|
|
77
|
-
const
|
|
78
|
-
create table if not exists
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
144
|
+
const TABLES = `
|
|
145
|
+
create table if not exists sources(
|
|
146
|
+
id integer primary key,
|
|
147
|
+
kind text not null default 'blob',
|
|
148
|
+
path text not null,
|
|
149
|
+
revision text,
|
|
150
|
+
lang text,
|
|
151
|
+
bytes integer,
|
|
152
|
+
status text not null default 'indexed',
|
|
153
|
+
props text,
|
|
154
|
+
unique(kind, path)
|
|
155
|
+
);
|
|
156
|
+
create table if not exists nodes(
|
|
157
|
+
id integer primary key,
|
|
158
|
+
key text not null unique,
|
|
159
|
+
kind text,
|
|
160
|
+
source_id integer,
|
|
161
|
+
name text,
|
|
162
|
+
line integer,
|
|
163
|
+
span_start integer,
|
|
164
|
+
span_end integer,
|
|
165
|
+
digest text,
|
|
166
|
+
trust text not null default 'observed',
|
|
167
|
+
props text
|
|
168
|
+
);
|
|
169
|
+
create table if not exists vectors(
|
|
170
|
+
node_id integer primary key,
|
|
171
|
+
model text,
|
|
172
|
+
dims integer,
|
|
173
|
+
vec blob
|
|
174
|
+
);
|
|
82
175
|
create table if not exists meta(key text primary key, value text);
|
|
83
176
|
`;
|
|
84
177
|
|
|
178
|
+
// The v5 edges table. Named `edges_v5` in the 4 -> 5 migration, because the
|
|
179
|
+
// v4 database still holds an `edges` table of a different shape until the
|
|
180
|
+
// copy below is done.
|
|
181
|
+
const EDGES_DDL = `create table if not exists edges(
|
|
182
|
+
src_id integer,
|
|
183
|
+
kind text,
|
|
184
|
+
dst_key text,
|
|
185
|
+
dst_id integer,
|
|
186
|
+
weight real,
|
|
187
|
+
owner_source_id integer,
|
|
188
|
+
trust text,
|
|
189
|
+
props text,
|
|
190
|
+
primary key(src_id, kind, dst_key)
|
|
191
|
+
);`;
|
|
192
|
+
|
|
193
|
+
// Q76: the v4 layout had no index on the edge endpoints or the node owners -
|
|
194
|
+
// the dependent, impact and coverage walks each paid a full scan per file.
|
|
195
|
+
// Executed after `migrateDb`, not in the preamble: on a v3 database the v3
|
|
196
|
+
// `edges` table survives `if not exists` until the 4 -> 5 step swaps in the
|
|
197
|
+
// v5 shape, and `idx_edges_dst` needs the `dst_id` column that only the v5
|
|
198
|
+
// table has.
|
|
199
|
+
const INDEXES = `
|
|
200
|
+
create index if not exists idx_edges_dst on edges(dst_id, kind);
|
|
201
|
+
create index if not exists idx_edges_owner on edges(owner_source_id);
|
|
202
|
+
create index if not exists idx_nodes_source on nodes(source_id);
|
|
203
|
+
create index if not exists idx_nodes_kind_name on nodes(kind, name);
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const SCHEMA = `${TABLES}\n${EDGES_DDL}\n`;
|
|
207
|
+
|
|
85
208
|
export function encodeVectorBlob(vec) {
|
|
86
209
|
if (!Array.isArray(vec) && !(vec instanceof Float32Array)) return Buffer.alloc(0);
|
|
87
210
|
const f32 = vec instanceof Float32Array ? vec : Float32Array.from(vec, (v) => Number(v) || 0);
|
|
@@ -186,55 +309,95 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts, embe
|
|
|
186
309
|
db.exec('pragma busy_timeout = 5000');
|
|
187
310
|
db.exec('pragma synchronous = normal');
|
|
188
311
|
db.exec(SCHEMA);
|
|
312
|
+
db.exec(FTS_SCHEMA);
|
|
313
|
+
db.exec(FTS_TRI_SCHEMA);
|
|
189
314
|
db.exec('begin immediate');
|
|
190
315
|
migrateDb(db);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
316
|
+
// The `edges` table is v5-shaped now on every path: the preamble created
|
|
317
|
+
// it on a cold build, the migration swapped it in on a legacy one.
|
|
318
|
+
db.exec(INDEXES);
|
|
319
|
+
// One source row per file, keyed (kind, path): the revision is the git blob
|
|
320
|
+
// sha, so an unchanged blob keeps its row, and the file's node, vectors,
|
|
321
|
+
// edges and fts rows all hang off it. `props` carries what the v5 columns
|
|
322
|
+
// do not name - the package directory, which the files read shape serves.
|
|
323
|
+
const upsertSource = db.prepare(`insert into sources(kind, path, revision, lang, bytes, status, props)
|
|
324
|
+
values('blob', ?, ?, ?, ?, 'indexed', ?)
|
|
325
|
+
on conflict(kind, path) do update set revision = excluded.revision, lang = excluded.lang,
|
|
326
|
+
bytes = excluded.bytes, props = excluded.props`);
|
|
327
|
+
const sourceIdOf = db.prepare(`select id from sources where kind = 'blob' and path = ?`);
|
|
328
|
+
// `prepare` compiles only the FIRST statement in a string (Q76): a
|
|
329
|
+
// multi-statement delete prepared as one ran only its head, and the
|
|
330
|
+
// surviving `file:` node collided with the next insert on `nodes.key`.
|
|
331
|
+
const dropDependents = [
|
|
332
|
+
db.prepare('delete from vectors where node_id in (select id from nodes where source_id = ?)'),
|
|
333
|
+
db.prepare('delete from fts where node_id in (select id from nodes where source_id = ?)'),
|
|
334
|
+
db.prepare('delete from edges where owner_source_id = ?'),
|
|
335
|
+
db.prepare('delete from nodes where source_id = ?'),
|
|
336
|
+
];
|
|
337
|
+
const addFileNode = db.prepare(`insert into nodes(key, kind, source_id, trust) values('file:' || ?, 'file', ?, 'observed')`);
|
|
338
|
+
const addSymbol = db.prepare(`insert into nodes(key, kind, source_id, name, line, props)
|
|
339
|
+
values('sym:' || ? || '#' || ? || '@' || ?, 'symbol', ?, ?, ?, ?)`);
|
|
340
|
+
const addEdge = db.prepare(`insert into edges(src_id, kind, dst_key, dst_id, weight, owner_source_id, trust)
|
|
341
|
+
values (?, ?, ?, ?, ?, ?, 'observed')`);
|
|
342
|
+
const addFts = db.prepare('insert into fts(body, node_id, kind) values (?, ?, ?)');
|
|
206
343
|
const addTri = db.prepare('insert into fts_tri(kind, ref, body) values (?, ?, ?)');
|
|
207
|
-
const
|
|
208
|
-
on conflict(
|
|
344
|
+
const upsertVector = db.prepare(`insert into vectors(node_id, model, dims, vec) values (?, ?, ?, ?)
|
|
345
|
+
on conflict(node_id) do update set model = excluded.model, dims = excluded.dims, vec = excluded.vec`);
|
|
346
|
+
const dropSource = [
|
|
347
|
+
db.prepare(`delete from vectors where node_id in (select id from nodes where source_id in (select id from sources where kind = 'blob' and path = ?))`),
|
|
348
|
+
db.prepare(`delete from fts where node_id in (select id from nodes where source_id in (select id from sources where kind = 'blob' and path = ?))`),
|
|
349
|
+
db.prepare(`delete from edges where owner_source_id in (select id from sources where kind = 'blob' and path = ?)`),
|
|
350
|
+
db.prepare('delete from edges where dst_key = ?'),
|
|
351
|
+
db.prepare(`delete from nodes where source_id in (select id from sources where kind = 'blob' and path = ?)`),
|
|
352
|
+
db.prepare(`delete from sources where kind = 'blob' and path = ?`),
|
|
353
|
+
];
|
|
209
354
|
for (const row of next) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
dropFts.run(row.path);
|
|
214
|
-
dropTri.run(row.path);
|
|
215
|
-
dropEmbed.run(row.path);
|
|
355
|
+
const sourceId = sourceIdOf.get(row.path)?.id;
|
|
356
|
+
if (sourceId !== undefined) for (const st of dropDependents) st.run(sourceId);
|
|
357
|
+
upsertSource.run(row.path, row.blob, row.lang, row.bytes, JSON.stringify({ pkg: row.pkg ?? '' }));
|
|
216
358
|
}
|
|
217
359
|
for (const path of removed) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
360
|
+
for (const st of dropSource) st.run(path);
|
|
361
|
+
// fts_tri is still path-keyed: the trigram table keeps its v4 shape (Q43).
|
|
362
|
+
db.prepare('delete from fts_tri where ref = ?').run(path);
|
|
363
|
+
}
|
|
364
|
+
for (const row of next) {
|
|
365
|
+
addFileNode.run(row.path, sourceIdOf.get(row.path).id);
|
|
366
|
+
}
|
|
367
|
+
// The ids a changed file's rows hang off were just rebuilt; an unchanged
|
|
368
|
+
// file's rows keep their old ids. One query maps every path to both.
|
|
369
|
+
const ids = new Map();
|
|
370
|
+
for (const r of db.prepare(`select s.path, s.id as source_id, n.id as node_id
|
|
371
|
+
from sources s join nodes n on n.source_id = s.id and n.kind = 'file'
|
|
372
|
+
where s.kind = 'blob'`).all()) {
|
|
373
|
+
ids.set(r.path, { sourceId: r.source_id, nodeId: r.node_id });
|
|
374
|
+
}
|
|
375
|
+
for (const row of symbols) {
|
|
376
|
+
const entry = ids.get(row.path);
|
|
377
|
+
if (!entry) continue;
|
|
378
|
+
addSymbol.run(row.path, row.name, row.line, entry.sourceId, row.name, row.line,
|
|
379
|
+
JSON.stringify({ kind: row.kind ?? 'symbol', sig: row.sig ?? '', exported: row.exported ? 1 : 0 }));
|
|
380
|
+
}
|
|
381
|
+
for (const row of edges) {
|
|
382
|
+
const src = ids.get(row.src);
|
|
383
|
+
if (!src) continue;
|
|
384
|
+
const dst = ids.get(row.dst);
|
|
385
|
+
addEdge.run(src.nodeId, row.kind, row.dst, dst ? dst.nodeId : null, row.weight, src.sourceId);
|
|
225
386
|
}
|
|
226
|
-
for (const row of symbols) addSymbol.run(row.path, row.name, row.kind, row.line, row.sig, row.exported);
|
|
227
|
-
for (const row of edges) addEdge.run(row.src, row.dst, row.kind, row.weight);
|
|
228
387
|
for (const row of fts) {
|
|
229
|
-
|
|
388
|
+
const entry = ids.get(row.ref);
|
|
389
|
+
if (!entry) continue;
|
|
390
|
+
addFts.run(row.body, entry.nodeId, row.kind);
|
|
230
391
|
// The trigram table carries folded raw text for CJK-bearing sections
|
|
231
392
|
// only; an ASCII-only section is already reachable through `fts`.
|
|
232
393
|
if (row.raw) addTri.run(row.kind, row.ref, row.raw);
|
|
233
394
|
}
|
|
234
395
|
for (const row of embeddings) {
|
|
396
|
+
const entry = ids.get(row.path);
|
|
397
|
+
if (!entry) continue;
|
|
235
398
|
const vecBlob = Buffer.isBuffer(row.vec) || row.vec instanceof Uint8Array ? row.vec : encodeVectorBlob(row.vec);
|
|
236
399
|
const dims = Number.isInteger(row.dims) ? row.dims : Math.floor(vecBlob.byteLength / 4);
|
|
237
|
-
|
|
400
|
+
upsertVector.run(entry.nodeId, row.model ?? '', dims, vecBlob);
|
|
238
401
|
}
|
|
239
402
|
const upsertMeta = db.prepare(
|
|
240
403
|
'insert into meta(key, value) values (?, ?) on conflict(key) do update set value = excluded.value',
|
|
@@ -529,13 +692,20 @@ export async function readTreeDigest(root, backend) {
|
|
|
529
692
|
}
|
|
530
693
|
|
|
531
694
|
export async function countSqlite(root) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
695
|
+
// One open, three counts: the v4 layout paid a full open/close per metric,
|
|
696
|
+
// and a read that only wants the counts was buying three connections (Q76).
|
|
697
|
+
const { rows } = await readSqliteRows(
|
|
698
|
+
root,
|
|
699
|
+
`select
|
|
700
|
+
(select count(*) from nodes where kind = 'symbol') as symbols,
|
|
701
|
+
(select count(*) from edges) as edges,
|
|
702
|
+
(select count(*) from vectors) as embeddings`,
|
|
703
|
+
);
|
|
704
|
+
const row = rows[0] ?? {};
|
|
535
705
|
return {
|
|
536
|
-
symbols: Number(symbols
|
|
537
|
-
edges: Number(edges
|
|
538
|
-
embeddings: Number(embeddings
|
|
706
|
+
symbols: Number(row.symbols ?? 0),
|
|
707
|
+
edges: Number(row.edges ?? 0),
|
|
708
|
+
embeddings: Number(row.embeddings ?? 0),
|
|
539
709
|
};
|
|
540
710
|
}
|
|
541
711
|
|
|
@@ -551,6 +721,3 @@ function byEdge(a, b) {
|
|
|
551
721
|
return cmp(a.src, b.src) || cmp(a.dst, b.dst) || cmp(a.kind, b.kind);
|
|
552
722
|
}
|
|
553
723
|
|
|
554
|
-
function cmp(a, b) {
|
|
555
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
556
|
-
}
|