simt-dev 0.6.3 → 0.7.1
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 +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +31 -14
- package/src/projection/manifest.mjs +89 -53
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
package/src/index/persist.mjs
CHANGED
|
@@ -4,18 +4,80 @@
|
|
|
4
4
|
// rows. This file is the format; store.mjs is the walk that fills it. They are
|
|
5
5
|
// split because the walk and the format together crossed the review trigger.
|
|
6
6
|
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
7
8
|
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
8
9
|
import { dirname, join } from 'node:path';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export const
|
|
11
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
12
|
+
|
|
13
|
+
export const SQLITE_FILE = join(LOCAL_DIR, 'index.sqlite');
|
|
14
|
+
export const JSONL_FILE = join(LOCAL_DIR, 'index.jsonl');
|
|
15
|
+
const SQLITE_FILE_LEGACY = '.simt/index.sqlite';
|
|
16
|
+
const JSONL_FILE_LEGACY = '.simt/index.jsonl';
|
|
17
|
+
export const SCHEMA_V = '4';
|
|
18
|
+
|
|
19
|
+
// The FTS tables carry their tokenizer in their definition, so `if not exists`
|
|
20
|
+
// cannot migrate them: a v3 database would keep tokenizing new rows the v3 way
|
|
21
|
+
// (ASCII fold, kind and ref searchable). The 3 -> 4 migration below recreates
|
|
22
|
+
// them, which is the versioned replacement for the per-write wipe (Q44).
|
|
23
|
+
export const FTS_SCHEMA = 'create virtual table fts using fts5(kind UNINDEXED, ref UNINDEXED, body, tokenize = "unicode61 remove_diacritics 2")';
|
|
24
|
+
export const FTS_TRI_SCHEMA = 'create virtual table fts_tri using fts5(kind UNINDEXED, ref UNINDEXED, body, tokenize = "trigram")';
|
|
25
|
+
|
|
26
|
+
// A database written by a *newer* simt is never opened for write, and never
|
|
27
|
+
// read blind (review 0003 Q44): the mismatched canonical file stays with its
|
|
28
|
+
// owner, and this version reads and writes a sidecar named after itself, so
|
|
29
|
+
// two pinned versions stop rewriting one file past each other (the ADR-059
|
|
30
|
+
// flip-flop). An *older* database is read and written in place, and the
|
|
31
|
+
// migration brings it current.
|
|
32
|
+
export function indexFileFor(root, backend, version) {
|
|
33
|
+
const base = backend === 'sqlite' ? SQLITE_FILE : JSONL_FILE;
|
|
34
|
+
if (version === '' || version === SCHEMA_V) return join(root, base);
|
|
35
|
+
const dot = base.lastIndexOf('.');
|
|
36
|
+
return join(root, `${base.slice(0, dot)}.v${SCHEMA_V}${base.slice(dot)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The canonical file is opened for write when the stored version is this one
|
|
40
|
+
// or older - the ordered migration upgrades the older one in place. A newer
|
|
41
|
+
// version owns its file, and opening it here would demote the stamp.
|
|
42
|
+
function canonicalForWrite(version) {
|
|
43
|
+
const at = Number(version);
|
|
44
|
+
return Number.isInteger(at) && at <= Number(SCHEMA_V);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Ordered schema migrations: each step upgrades a database written at `from`
|
|
48
|
+
// to `to`, applied in list order. A database at a version past the last step
|
|
49
|
+
// is not opened for write at all (indexFileFor routes it to a sidecar).
|
|
50
|
+
const MIGRATIONS = [
|
|
51
|
+
{
|
|
52
|
+
from: 3,
|
|
53
|
+
to: 4,
|
|
54
|
+
apply: (db) => {
|
|
55
|
+
db.exec('drop table if exists fts');
|
|
56
|
+
db.exec(FTS_SCHEMA);
|
|
57
|
+
db.exec('drop table if exists fts_tri');
|
|
58
|
+
db.exec(FTS_TRI_SCHEMA);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Bring an open database up to the current schema and stamp it. `user_version`
|
|
65
|
+
* 0 means "written before the stamp existed": those databases carried the
|
|
66
|
+
* version in meta only, so every step at or below the current schema runs -
|
|
67
|
+
* each is idempotent on an already-migrated table set.
|
|
68
|
+
*/
|
|
69
|
+
function migrateDb(db) {
|
|
70
|
+
const at = Number(db.prepare('pragma user_version').get()?.user_version ?? 0);
|
|
71
|
+
for (const step of MIGRATIONS) {
|
|
72
|
+
if (at === 0 || (at >= step.from && at < step.to)) step.apply(db);
|
|
73
|
+
}
|
|
74
|
+
db.exec(`pragma user_version = ${Number(SCHEMA_V)}`);
|
|
75
|
+
}
|
|
13
76
|
|
|
14
77
|
const SCHEMA = `
|
|
15
78
|
create table if not exists files(path text primary key, blob text, lang text, bytes int, pkg text);
|
|
16
79
|
create table if not exists symbols(id integer primary key, path text, name text, kind text, line int, sig text, exported int);
|
|
17
80
|
create table if not exists edges(src text, dst text, kind text, weight real);
|
|
18
|
-
create virtual table if not exists fts using fts5(kind, ref, body);
|
|
19
81
|
create table if not exists embeddings(path text primary key, blob text, model text, dims int, vec blob);
|
|
20
82
|
create table if not exists meta(key text primary key, value text);
|
|
21
83
|
`;
|
|
@@ -52,9 +114,51 @@ function tmpFor(file) {
|
|
|
52
114
|
return `${file}.${process.pid}.${tmpSeq}.tmp`;
|
|
53
115
|
}
|
|
54
116
|
|
|
55
|
-
|
|
117
|
+
/**
|
|
118
|
+
* What the index was built from, so a reader can tell a current tree from an
|
|
119
|
+
* old one without re-reading any blob (review 0003 Q42).
|
|
120
|
+
*
|
|
121
|
+
* The digest covers the `git ls-files -s` listing the build consumed: same
|
|
122
|
+
* bytes in, same digest out. Stored beside `schema` in `meta`, written on every
|
|
123
|
+
* build including the no-change one, so a build that changed nothing still
|
|
124
|
+
* vouches for the tree it saw.
|
|
125
|
+
*/
|
|
126
|
+
export function treeDigest(listed) {
|
|
127
|
+
const rows = [...(listed ?? [])]
|
|
128
|
+
.map((entry) => `${entry.blob ?? ''} ${entry.path ?? ''}`)
|
|
129
|
+
.sort()
|
|
130
|
+
.join('\n');
|
|
131
|
+
return createHash('sha1').update(rows, 'utf8').digest('hex');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function writeSqlite(root, next, removed, symbols, edges, fts, embeddings = [], tree = '') {
|
|
56
135
|
const { DatabaseSync } = await import('node:sqlite');
|
|
57
|
-
|
|
136
|
+
// The canonical file is written when it is this version's or older (the
|
|
137
|
+
// migration upgrades the older one in place, review 0003 Q44): a database at
|
|
138
|
+
// a newer version stays with its owner, and the build goes to the sidecar
|
|
139
|
+
// this version owns.
|
|
140
|
+
let file = join(root, SQLITE_FILE);
|
|
141
|
+
try {
|
|
142
|
+
await stat(file);
|
|
143
|
+
} catch {
|
|
144
|
+
file = join(root, SQLITE_FILE_LEGACY);
|
|
145
|
+
try {
|
|
146
|
+
await stat(file);
|
|
147
|
+
} catch {
|
|
148
|
+
file = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (file) {
|
|
152
|
+
let version = SCHEMA_V;
|
|
153
|
+
try {
|
|
154
|
+
version = await storedVersion(file);
|
|
155
|
+
} catch {
|
|
156
|
+
version = SCHEMA_V; // unreadable: rebuild it in place, as a cold build would
|
|
157
|
+
}
|
|
158
|
+
file = canonicalForWrite(version) ? file : indexFileFor(root, 'sqlite', version);
|
|
159
|
+
} else {
|
|
160
|
+
file = join(root, SQLITE_FILE);
|
|
161
|
+
}
|
|
58
162
|
await mkdir(dirname(file), { recursive: true });
|
|
59
163
|
// A cold build goes to a temp file that is renamed over the target: three
|
|
60
164
|
// concurrent cold runs used to interleave schema creation and leave a locked,
|
|
@@ -83,6 +187,7 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts, embe
|
|
|
83
187
|
db.exec('pragma synchronous = normal');
|
|
84
188
|
db.exec(SCHEMA);
|
|
85
189
|
db.exec('begin immediate');
|
|
190
|
+
migrateDb(db);
|
|
86
191
|
const upsert = db.prepare(`insert into files(path, blob, lang, bytes, pkg) values (?, ?, ?, ?, ?)
|
|
87
192
|
on conflict(path) do update set blob = excluded.blob, lang = excluded.lang, bytes = excluded.bytes, pkg = excluded.pkg`);
|
|
88
193
|
const dropSymbols = db.prepare('delete from symbols where path = ?');
|
|
@@ -92,11 +197,13 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts, embe
|
|
|
92
197
|
const dropEdges = db.prepare('delete from edges where src = ?');
|
|
93
198
|
const dropIncoming = db.prepare('delete from edges where dst = ?');
|
|
94
199
|
const dropFts = db.prepare('delete from fts where ref = ?');
|
|
200
|
+
const dropTri = db.prepare('delete from fts_tri where ref = ?');
|
|
95
201
|
const dropEmbed = db.prepare('delete from embeddings where path = ?');
|
|
96
202
|
const dropFile = db.prepare('delete from files where path = ?');
|
|
97
203
|
const addSymbol = db.prepare('insert into symbols(path, name, kind, line, sig, exported) values (?, ?, ?, ?, ?, ?)');
|
|
98
204
|
const addEdge = db.prepare('insert into edges(src, dst, kind, weight) values (?, ?, ?, ?)');
|
|
99
205
|
const addFts = db.prepare('insert into fts(kind, ref, body) values (?, ?, ?)');
|
|
206
|
+
const addTri = db.prepare('insert into fts_tri(kind, ref, body) values (?, ?, ?)');
|
|
100
207
|
const upsertEmbed = db.prepare(`insert into embeddings(path, blob, model, dims, vec) values (?, ?, ?, ?, ?)
|
|
101
208
|
on conflict(path) do update set blob = excluded.blob, model = excluded.model, dims = excluded.dims, vec = excluded.vec`);
|
|
102
209
|
for (const row of next) {
|
|
@@ -104,6 +211,7 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts, embe
|
|
|
104
211
|
dropSymbols.run(row.path);
|
|
105
212
|
dropEdges.run(row.path);
|
|
106
213
|
dropFts.run(row.path);
|
|
214
|
+
dropTri.run(row.path);
|
|
107
215
|
dropEmbed.run(row.path);
|
|
108
216
|
}
|
|
109
217
|
for (const path of removed) {
|
|
@@ -112,17 +220,27 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts, embe
|
|
|
112
220
|
dropEdges.run(path);
|
|
113
221
|
dropIncoming.run(path);
|
|
114
222
|
dropFts.run(path);
|
|
223
|
+
dropTri.run(path);
|
|
115
224
|
dropEmbed.run(path);
|
|
116
225
|
}
|
|
117
226
|
for (const row of symbols) addSymbol.run(row.path, row.name, row.kind, row.line, row.sig, row.exported);
|
|
118
227
|
for (const row of edges) addEdge.run(row.src, row.dst, row.kind, row.weight);
|
|
119
|
-
for (const row of fts)
|
|
228
|
+
for (const row of fts) {
|
|
229
|
+
addFts.run(row.kind, row.ref, row.body);
|
|
230
|
+
// The trigram table carries folded raw text for CJK-bearing sections
|
|
231
|
+
// only; an ASCII-only section is already reachable through `fts`.
|
|
232
|
+
if (row.raw) addTri.run(row.kind, row.ref, row.raw);
|
|
233
|
+
}
|
|
120
234
|
for (const row of embeddings) {
|
|
121
235
|
const vecBlob = Buffer.isBuffer(row.vec) || row.vec instanceof Uint8Array ? row.vec : encodeVectorBlob(row.vec);
|
|
122
236
|
const dims = Number.isInteger(row.dims) ? row.dims : Math.floor(vecBlob.byteLength / 4);
|
|
123
237
|
upsertEmbed.run(row.path, row.blob ?? '', row.model ?? '', dims, vecBlob);
|
|
124
238
|
}
|
|
125
|
-
|
|
239
|
+
const upsertMeta = db.prepare(
|
|
240
|
+
'insert into meta(key, value) values (?, ?) on conflict(key) do update set value = excluded.value',
|
|
241
|
+
);
|
|
242
|
+
upsertMeta.run('schema', SCHEMA_V);
|
|
243
|
+
upsertMeta.run('tree', tree);
|
|
126
244
|
db.exec('commit');
|
|
127
245
|
} catch (error) {
|
|
128
246
|
try { db.exec('rollback'); } catch { /* a failed begin has nothing to undo */ }
|
|
@@ -133,6 +251,32 @@ export async function writeSqlite(root, next, removed, symbols, edges, fts, embe
|
|
|
133
251
|
if (cold) await rename(target, file);
|
|
134
252
|
}
|
|
135
253
|
|
|
254
|
+
/**
|
|
255
|
+
* The version a database declares: `PRAGMA user_version` when stamped, else the
|
|
256
|
+
* meta `schema` row databases written before the stamp carried it in. Throws
|
|
257
|
+
* when the file cannot be read as a database - the caller decides whether that
|
|
258
|
+
* is a cold start or a corrupt one.
|
|
259
|
+
*/
|
|
260
|
+
async function storedVersion(file) {
|
|
261
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
262
|
+
const db = new DatabaseSync(file);
|
|
263
|
+
try {
|
|
264
|
+
const user = Number(db.prepare('pragma user_version').get()?.user_version ?? 0);
|
|
265
|
+
if (user > 0) return String(user);
|
|
266
|
+
return db.prepare("select value from meta where key = 'schema'").get()?.value ?? '';
|
|
267
|
+
} finally {
|
|
268
|
+
db.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A damaged index file is named, not silent (review 0003 Q44). */
|
|
273
|
+
function corruptIndexError(cause) {
|
|
274
|
+
const corrupt = new Error('the index file is corrupt; run the index build to replace it');
|
|
275
|
+
corrupt.code = 'INDEX_CORRUPT';
|
|
276
|
+
corrupt.cause = cause;
|
|
277
|
+
return corrupt;
|
|
278
|
+
}
|
|
279
|
+
|
|
136
280
|
/**
|
|
137
281
|
* A contended index write is a retryable race, not a broken repository: name it
|
|
138
282
|
* so the command maps its own remedy instead of blaming the git checkout (Q08).
|
|
@@ -145,10 +289,46 @@ function asIndexError(error) {
|
|
|
145
289
|
busy.cause = error;
|
|
146
290
|
return busy;
|
|
147
291
|
}
|
|
292
|
+
if (/file is not a database|database disk image|malformed|corrupt/i.test(error?.message ?? '')) {
|
|
293
|
+
return corruptIndexError(error);
|
|
294
|
+
}
|
|
148
295
|
return error;
|
|
149
296
|
}
|
|
150
297
|
|
|
151
|
-
|
|
298
|
+
/**
|
|
299
|
+
* The jsonl file this version writes to: the canonical file when it is this
|
|
300
|
+
* version's or unreadable, the sidecar when it belongs to another version
|
|
301
|
+
* (review 0003 Q44).
|
|
302
|
+
*/
|
|
303
|
+
async function jsonlTargetFile(root) {
|
|
304
|
+
const base = join(root, JSONL_FILE);
|
|
305
|
+
let text = null;
|
|
306
|
+
try {
|
|
307
|
+
text = await readFile(base, 'utf8');
|
|
308
|
+
} catch {
|
|
309
|
+
try {
|
|
310
|
+
text = await readFile(join(root, JSONL_FILE_LEGACY), 'utf8');
|
|
311
|
+
} catch {
|
|
312
|
+
return base; // no file: cold build goes to the canonical path
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
for (const line of text.split('\n')) {
|
|
317
|
+
if (line === '') continue;
|
|
318
|
+
const row = JSON.parse(line);
|
|
319
|
+
if (row.table !== 'meta' || row.key !== 'schema') continue;
|
|
320
|
+
const value = row.value ?? '';
|
|
321
|
+
// Older files are rewritten in place at the current schema; a newer one
|
|
322
|
+
// stays with its owner and the build goes to the sidecar.
|
|
323
|
+
return canonicalForWrite(value) ? base : indexFileFor(root, 'jsonl', value);
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
return base; // unreadable: rebuild it in place
|
|
327
|
+
}
|
|
328
|
+
return base;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function writeJsonl(root, previous, next, removed, symbols, edges, fts, force, embeddings = [], tree = '') {
|
|
152
332
|
const gone = new Set(removed);
|
|
153
333
|
const replaced = new Map(next.map((row) => [row.path, row]));
|
|
154
334
|
const drop = new Set([...replaced.keys(), ...gone]);
|
|
@@ -160,25 +340,26 @@ export async function writeJsonl(root, previous, next, removed, symbols, edges,
|
|
|
160
340
|
}
|
|
161
341
|
for (const row of replaced.values()) files.push(row);
|
|
162
342
|
files.sort(byPath);
|
|
163
|
-
const keptSymbols = force ? [] : (await readJsonl(root, 'symbols')).filter((row) => !drop.has(row.path));
|
|
343
|
+
const keptSymbols = force ? [] : (await readJsonl(root, 'symbols')).rows.filter((row) => !drop.has(row.path));
|
|
164
344
|
// Edge ownership mirrors the sqlite backend: a changed file loses only the edges
|
|
165
345
|
// it owns (src), while a removed file also drops the edges that pointed at it.
|
|
166
|
-
const keptEdges = force ? [] : (await readJsonl(root, 'edges')).filter((row) => !drop.has(row.src) && !gone.has(row.dst));
|
|
167
|
-
const keptFts = force ? [] : (await readJsonl(root, 'fts')).filter((row) => !drop.has(row.ref));
|
|
346
|
+
const keptEdges = force ? [] : (await readJsonl(root, 'edges')).rows.filter((row) => !drop.has(row.src) && !gone.has(row.dst));
|
|
347
|
+
const keptFts = force ? [] : (await readJsonl(root, 'fts')).rows.filter((row) => !drop.has(row.ref));
|
|
168
348
|
const newEmbedPaths = new Set(embeddings.map((row) => row.path));
|
|
169
349
|
const keptEmbeddings = force
|
|
170
350
|
? []
|
|
171
|
-
: (await readJsonl(root, 'embeddings')).filter((row) => !drop.has(row.path) && !newEmbedPaths.has(row.path));
|
|
351
|
+
: (await readJsonl(root, 'embeddings')).rows.filter((row) => !drop.has(row.path) && !newEmbedPaths.has(row.path));
|
|
172
352
|
const allSymbols = [...keptSymbols, ...symbols].sort(bySymbol);
|
|
173
353
|
const allEdges = [...keptEdges, ...edges].sort(byEdge);
|
|
174
354
|
const allFts = [...keptFts, ...fts].sort((a, b) => cmp(a.ref, b.ref));
|
|
175
355
|
const allEmbeddings = [...keptEmbeddings, ...embeddings].sort(byPath);
|
|
176
|
-
const file =
|
|
356
|
+
const file = await jsonlTargetFile(root);
|
|
177
357
|
await mkdir(dirname(file), { recursive: true });
|
|
178
358
|
// Written atomically: a concurrent reader never sees a half-written file.
|
|
179
359
|
const tmp = tmpFor(file);
|
|
180
360
|
const lines = [
|
|
181
361
|
JSON.stringify({ table: 'meta', key: 'schema', value: SCHEMA_V }),
|
|
362
|
+
JSON.stringify({ table: 'meta', key: 'tree', value: tree }),
|
|
182
363
|
...files.map((row) => JSON.stringify({ table: 'files', path: row.path, blob: row.blob, lang: row.lang, bytes: row.bytes, pkg: row.pkg })),
|
|
183
364
|
...allSymbols.map((row) => JSON.stringify({ table: 'symbols', path: row.path, name: row.name, kind: row.kind, line: row.line, sig: row.sig, exported: row.exported })),
|
|
184
365
|
...allEdges.map((row) => JSON.stringify({ table: 'edges', src: row.src, dst: row.dst, kind: row.kind, weight: row.weight })),
|
|
@@ -202,58 +383,127 @@ export async function writeJsonl(root, previous, next, removed, symbols, edges,
|
|
|
202
383
|
}
|
|
203
384
|
|
|
204
385
|
export async function readJsonl(root, table) {
|
|
386
|
+
// The pre-move path is a fallback for one release (review 0003 Q19).
|
|
205
387
|
let text;
|
|
206
388
|
try {
|
|
207
389
|
text = await readFile(join(root, JSONL_FILE), 'utf8');
|
|
208
390
|
} catch {
|
|
209
|
-
|
|
391
|
+
try {
|
|
392
|
+
text = await readFile(join(root, JSONL_FILE_LEGACY), 'utf8');
|
|
393
|
+
} catch {
|
|
394
|
+
return { rows: [], error: null };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
let schema = '';
|
|
398
|
+
try {
|
|
399
|
+
for (const line of text.split('\n')) {
|
|
400
|
+
if (line === '') continue;
|
|
401
|
+
const row = JSON.parse(line);
|
|
402
|
+
if (row.table === 'meta' && row.key === 'schema') schema = row.value ?? '';
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
return { rows: [], error: corruptIndexError(error) };
|
|
406
|
+
}
|
|
407
|
+
// A file a newer version owns is not read blind (review 0003 Q44): this
|
|
408
|
+
// version's sidecar is read instead, and a sidecar that has not been built
|
|
409
|
+
// yet is cold, not broken. An older file is read in place; the next write
|
|
410
|
+
// rewrites it at the current schema.
|
|
411
|
+
if (!canonicalForWrite(schema)) {
|
|
412
|
+
try {
|
|
413
|
+
text = await readFile(indexFileFor(root, 'jsonl', schema), 'utf8');
|
|
414
|
+
} catch {
|
|
415
|
+
return { rows: [], error: null };
|
|
416
|
+
}
|
|
210
417
|
}
|
|
211
418
|
const rows = [];
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
419
|
+
try {
|
|
420
|
+
for (const line of text.split('\n')) {
|
|
421
|
+
if (line === '') continue;
|
|
422
|
+
const row = JSON.parse(line);
|
|
423
|
+
if (row.table !== table) continue;
|
|
424
|
+
if (table === 'meta') rows.push({ key: row.key, value: row.value });
|
|
425
|
+
else if (table === 'files') rows.push({ path: row.path, blob: row.blob, lang: row.lang, bytes: row.bytes, pkg: row.pkg });
|
|
426
|
+
else if (table === 'symbols') rows.push({ path: row.path, name: row.name, kind: row.kind, line: row.line, sig: row.sig, exported: row.exported });
|
|
427
|
+
else if (table === 'edges') rows.push({ src: row.src, dst: row.dst, kind: row.kind, weight: row.weight });
|
|
428
|
+
else if (table === 'fts') rows.push({ kind: row.kind, ref: row.ref, body: row.body });
|
|
429
|
+
else if (table === 'embeddings') {
|
|
430
|
+
const vec = decodeVectorBlob(row.vec);
|
|
431
|
+
rows.push({
|
|
432
|
+
path: row.path,
|
|
433
|
+
blob: row.blob ?? '',
|
|
434
|
+
model: row.model ?? '',
|
|
435
|
+
dims: Number.isInteger(row.dims) ? row.dims : vec.length,
|
|
436
|
+
vec,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
230
439
|
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
return { rows: [], error: corruptIndexError(error) };
|
|
231
442
|
}
|
|
232
443
|
if (table === 'symbols') rows.sort(bySymbol);
|
|
233
444
|
else if (table === 'edges') rows.sort(byEdge);
|
|
234
445
|
else if (table === 'fts') rows.sort((a, b) => cmp(a.ref, b.ref));
|
|
235
446
|
else rows.sort(byPath);
|
|
236
|
-
return rows;
|
|
447
|
+
return { rows, error: null };
|
|
237
448
|
}
|
|
238
449
|
|
|
450
|
+
/**
|
|
451
|
+
* The sqlite file a read opens: the canonical file when it is this version's
|
|
452
|
+
* or older, the sidecar when it belongs to a newer one (review 0003 Q44),
|
|
453
|
+
* null when no index for this version exists. Throws when a present file
|
|
454
|
+
* cannot be read as a database - corrupt, not cold.
|
|
455
|
+
*/
|
|
456
|
+
async function resolveSqliteFile(root) {
|
|
457
|
+
// The pre-move path is a fallback for one release (review 0003 Q19).
|
|
458
|
+
let file = join(root, SQLITE_FILE);
|
|
459
|
+
try {
|
|
460
|
+
await stat(file);
|
|
461
|
+
} catch {
|
|
462
|
+
file = join(root, SQLITE_FILE_LEGACY);
|
|
463
|
+
try {
|
|
464
|
+
await stat(file);
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const version = await storedVersion(file);
|
|
470
|
+
// An older database is readable in place: its tables carry the same columns,
|
|
471
|
+
// and the next write migrates it. Only a newer one is routed to a sidecar.
|
|
472
|
+
if (canonicalForWrite(version)) return file;
|
|
473
|
+
const sidecar = indexFileFor(root, 'sqlite', version);
|
|
474
|
+
try {
|
|
475
|
+
await stat(sidecar);
|
|
476
|
+
return sidecar;
|
|
477
|
+
} catch {
|
|
478
|
+
return null; // this version has not built its sidecar yet: cold
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Read rows from the index. Returns `{ rows, error }`: a missing index is
|
|
484
|
+
* `{ rows: [], error: null }` (cold), a present index that cannot be read is
|
|
485
|
+
* an empty row set with a named error - "locked", "no such column" and
|
|
486
|
+
* "corrupt" used to all look like "no results" (review 0003 Q44).
|
|
487
|
+
*/
|
|
239
488
|
export async function readSqliteRows(root, sql, params = []) {
|
|
240
489
|
let DatabaseSync;
|
|
241
490
|
try {
|
|
242
491
|
({ DatabaseSync } = await import('node:sqlite'));
|
|
243
492
|
} catch {
|
|
244
|
-
return [];
|
|
493
|
+
return { rows: [], error: null };
|
|
245
494
|
}
|
|
246
|
-
|
|
495
|
+
let file;
|
|
247
496
|
try {
|
|
248
|
-
await
|
|
249
|
-
} catch {
|
|
250
|
-
return [];
|
|
497
|
+
file = await resolveSqliteFile(root);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
return { rows: [], error: asIndexError(error) };
|
|
251
500
|
}
|
|
501
|
+
if (!file) return { rows: [], error: null };
|
|
252
502
|
const db = new DatabaseSync(file);
|
|
253
503
|
try {
|
|
254
|
-
return db.prepare(sql).all(...params);
|
|
255
|
-
} catch {
|
|
256
|
-
return [];
|
|
504
|
+
return { rows: db.prepare(sql).all(...params), error: null };
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return { rows: [], error: asIndexError(error) };
|
|
257
507
|
} finally {
|
|
258
508
|
db.close();
|
|
259
509
|
}
|
|
@@ -261,21 +511,31 @@ export async function readSqliteRows(root, sql, params = []) {
|
|
|
261
511
|
|
|
262
512
|
export async function readSchema(root, backend) {
|
|
263
513
|
if (backend === 'sqlite') {
|
|
264
|
-
const rows = await readSqliteRows(root, "select value from meta where key = 'schema'");
|
|
514
|
+
const { rows } = await readSqliteRows(root, "select value from meta where key = 'schema'");
|
|
265
515
|
return rows[0]?.value ?? '';
|
|
266
516
|
}
|
|
267
|
-
const rows = await readJsonl(root, 'meta');
|
|
517
|
+
const { rows } = await readJsonl(root, 'meta');
|
|
268
518
|
return rows.find((row) => row.key === 'schema')?.value ?? '';
|
|
269
519
|
}
|
|
270
520
|
|
|
521
|
+
/** The tree the stored index was built from, or '' when no build has vouched for one. */
|
|
522
|
+
export async function readTreeDigest(root, backend) {
|
|
523
|
+
if (backend === 'sqlite') {
|
|
524
|
+
const { rows } = await readSqliteRows(root, "select value from meta where key = 'tree'");
|
|
525
|
+
return rows[0]?.value ?? '';
|
|
526
|
+
}
|
|
527
|
+
const { rows } = await readJsonl(root, 'meta');
|
|
528
|
+
return rows.find((row) => row.key === 'tree')?.value ?? '';
|
|
529
|
+
}
|
|
530
|
+
|
|
271
531
|
export async function countSqlite(root) {
|
|
272
532
|
const symbols = await readSqliteRows(root, 'select count(*) as n from symbols');
|
|
273
533
|
const edges = await readSqliteRows(root, 'select count(*) as n from edges');
|
|
274
534
|
const embeddings = await readSqliteRows(root, 'select count(*) as n from embeddings');
|
|
275
535
|
return {
|
|
276
|
-
symbols: Number(symbols[0]?.n ?? 0),
|
|
277
|
-
edges: Number(edges[0]?.n ?? 0),
|
|
278
|
-
embeddings: Number(embeddings[0]?.n ?? 0),
|
|
536
|
+
symbols: Number(symbols.rows[0]?.n ?? 0),
|
|
537
|
+
edges: Number(edges.rows[0]?.n ?? 0),
|
|
538
|
+
embeddings: Number(embeddings.rows[0]?.n ?? 0),
|
|
279
539
|
};
|
|
280
540
|
}
|
|
281
541
|
|