ygl-trees 0.66.0 → 0.69.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 +22 -0
- package/dist/apps/cli/src/bin.js +47 -4
- package/dist/apps/cli/src/profile-boot.js +11 -3
- package/dist/bundle/bin.cjs +159 -124
- package/dist/bundle/mcp-C27lIHd1.cjs +276 -0
- package/dist/bundle/rolldown-runtime-C96Ew50Q.cjs +64 -0
- package/dist/packages/core/database/src/db.js +421 -379
- package/dist/packages/core/database/src/plugin.js +5 -3
- package/dist/packages/core/database/src/types.js +8 -2
- package/dist/packages/ops/tools/src/doctor.js +19 -7
- package/dist/packages/ops/tools/src/index.js +4 -1
- package/dist/packages/ops/tools/src/mcp.js +233 -0
- package/dist/packages/ops/tools/src/plugins.js +10 -2
- package/dist/packages/research/pipeline/src/entities.js +6 -0
- package/dist/packages/research/pipeline/src/writes.js +1 -1
- package/dist/packages/techtree/model/src/writes.js +14 -14
- package/llms.txt +1 -1
- package/package.json +3 -3
|
@@ -46,351 +46,351 @@ const fs = __importStar(require("node:fs"));
|
|
|
46
46
|
const path = __importStar(require("node:path"));
|
|
47
47
|
const text_1 = require("../../text/src/text");
|
|
48
48
|
exports.SCHEMA_VERSION = 9;
|
|
49
|
-
exports.SCHEMA_SQL = `
|
|
50
|
-
-- ---------------------------------------------------------------- meta
|
|
51
|
-
-- runtime state the tool writes itself (current selections, bookkeeping);
|
|
52
|
-
-- user-tunable settings live in config
|
|
53
|
-
CREATE TABLE IF NOT EXISTS meta (
|
|
54
|
-
key TEXT PRIMARY KEY,
|
|
55
|
-
value TEXT NOT NULL
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
-- ---------------------------------------------------------------- config (v6)
|
|
59
|
-
-- user-tunable settings; registry (defaults/types/notes) in src/config.ts.
|
|
60
|
-
-- Reads fall back to defaults — an empty table is a valid state.
|
|
61
|
-
CREATE TABLE IF NOT EXISTS config (
|
|
62
|
-
key TEXT PRIMARY KEY,
|
|
63
|
-
value TEXT NOT NULL DEFAULT '',
|
|
64
|
-
note TEXT DEFAULT '',
|
|
65
|
-
updated TEXT NOT NULL
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
-- ---------------------------------------------------------------- skills (L1/L2/L3)
|
|
69
|
-
CREATE TABLE IF NOT EXISTS skills (
|
|
70
|
-
id TEXT PRIMARY KEY,
|
|
71
|
-
name TEXT NOT NULL UNIQUE,
|
|
72
|
-
version TEXT DEFAULT '0.1.0',
|
|
73
|
-
description TEXT NOT NULL DEFAULT '', -- L1: what + when, always in context
|
|
74
|
-
body TEXT NOT NULL DEFAULT '', -- L2: instructions, loaded on activation
|
|
75
|
-
tags TEXT DEFAULT '',
|
|
76
|
-
origin TEXT DEFAULT '', -- where it came from (path/repo)
|
|
77
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
78
|
-
hits INTEGER NOT NULL DEFAULT 0, -- how often an agent matched it
|
|
79
|
-
created TEXT NOT NULL,
|
|
80
|
-
updated TEXT NOT NULL
|
|
81
|
-
);
|
|
82
|
-
CREATE TABLE IF NOT EXISTS skill_refs ( -- L3: loaded on demand
|
|
83
|
-
id TEXT PRIMARY KEY,
|
|
84
|
-
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
|
|
85
|
-
path TEXT NOT NULL,
|
|
86
|
-
content TEXT NOT NULL DEFAULT ''
|
|
87
|
-
);
|
|
88
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
89
|
-
id UNINDEXED, name, description, tags, ngram, tokenize='unicode61'
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
-- ---------------------------------------------------------------- projects
|
|
93
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
94
|
-
id TEXT PRIMARY KEY,
|
|
95
|
-
name TEXT NOT NULL UNIQUE,
|
|
96
|
-
question TEXT NOT NULL DEFAULT '',
|
|
97
|
-
inclusion TEXT DEFAULT '',
|
|
98
|
-
exclusion TEXT DEFAULT '',
|
|
99
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
100
|
-
created TEXT NOT NULL,
|
|
101
|
-
updated TEXT NOT NULL
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
-- ---------------------------------------------------------------- research: queries
|
|
105
|
-
CREATE TABLE IF NOT EXISTS queries (
|
|
106
|
-
id TEXT PRIMARY KEY,
|
|
107
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
108
|
-
text TEXT NOT NULL,
|
|
109
|
-
engine TEXT DEFAULT '',
|
|
110
|
-
status TEXT NOT NULL DEFAULT 'pending', -- pending|done
|
|
111
|
-
note TEXT DEFAULT '',
|
|
112
|
-
content_hash TEXT NOT NULL DEFAULT '', -- sha1(text): idempotent writes
|
|
113
|
-
created TEXT NOT NULL
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
-- ---------------------------------------------------------------- research: sources
|
|
117
|
-
-- query <-> source association: which search query recalled which source
|
|
118
|
-
-- (schema v4; pure additive, no migration needed beyond CREATE IF NOT EXISTS)
|
|
119
|
-
CREATE TABLE IF NOT EXISTS query_sources (
|
|
120
|
-
query_id TEXT NOT NULL REFERENCES queries(id) ON DELETE CASCADE,
|
|
121
|
-
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
122
|
-
note TEXT DEFAULT '',
|
|
123
|
-
created TEXT NOT NULL,
|
|
124
|
-
PRIMARY KEY (query_id, source_id)
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
CREATE TABLE IF NOT EXISTS sources (
|
|
128
|
-
id TEXT PRIMARY KEY,
|
|
129
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
130
|
-
url TEXT DEFAULT '',
|
|
131
|
-
title TEXT NOT NULL DEFAULT '',
|
|
132
|
-
url_hash TEXT NOT NULL,
|
|
133
|
-
content_hash TEXT NOT NULL DEFAULT '',
|
|
134
|
-
abstract TEXT DEFAULT '',
|
|
135
|
-
content TEXT DEFAULT '',
|
|
136
|
-
engine TEXT DEFAULT '',
|
|
137
|
-
relevance INTEGER NOT NULL DEFAULT 0, -- 0..5 screening score
|
|
138
|
-
confidence TEXT NOT NULL DEFAULT 'B', -- A|B|C source trust grade
|
|
139
|
-
tags TEXT DEFAULT '',
|
|
140
|
-
created TEXT NOT NULL DEFAULT '',
|
|
141
|
-
retrieved_at TEXT NOT NULL,
|
|
142
|
-
UNIQUE(project_id, url_hash)
|
|
143
|
-
);
|
|
144
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS sources_fts USING fts5(
|
|
145
|
-
id UNINDEXED, title, abstract, content, ngram, tokenize='unicode61'
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
-- ---------------------------------------------------------------- research: evidence
|
|
149
|
-
CREATE TABLE IF NOT EXISTS evidence (
|
|
150
|
-
id TEXT PRIMARY KEY,
|
|
151
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
152
|
-
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
153
|
-
quote TEXT NOT NULL,
|
|
154
|
-
note TEXT DEFAULT '',
|
|
155
|
-
locator TEXT DEFAULT '', -- page / section / offset
|
|
156
|
-
content_hash TEXT NOT NULL DEFAULT '', -- sha1(quote): idempotent writes
|
|
157
|
-
created TEXT NOT NULL
|
|
158
|
-
);
|
|
159
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS evidence_fts USING fts5(
|
|
160
|
-
id UNINDEXED, quote, ngram, tokenize='unicode61'
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
-- ---------------------------------------------------------------- research: claims
|
|
164
|
-
CREATE TABLE IF NOT EXISTS claims (
|
|
165
|
-
id TEXT PRIMARY KEY,
|
|
166
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
167
|
-
text TEXT NOT NULL,
|
|
168
|
-
status TEXT NOT NULL DEFAULT 'unverified', -- unverified|supported|refuted|contested
|
|
169
|
-
confidence TEXT NOT NULL DEFAULT 'B',
|
|
170
|
-
content_hash TEXT NOT NULL DEFAULT '', -- sha1(text): idempotent writes
|
|
171
|
-
created TEXT NOT NULL,
|
|
172
|
-
updated TEXT NOT NULL DEFAULT ''
|
|
173
|
-
);
|
|
174
|
-
CREATE TABLE IF NOT EXISTS claim_evidence (
|
|
175
|
-
claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
|
|
176
|
-
evidence_id TEXT NOT NULL REFERENCES evidence(id) ON DELETE CASCADE,
|
|
177
|
-
stance TEXT NOT NULL DEFAULT 'supports', -- supports|refutes
|
|
178
|
-
PRIMARY KEY (claim_id, evidence_id)
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
-- ---------------------------------------------------------------- production
|
|
182
|
-
CREATE TABLE IF NOT EXISTS artifacts (
|
|
183
|
-
id TEXT PRIMARY KEY,
|
|
184
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
185
|
-
kind TEXT NOT NULL DEFAULT 'section', -- outline|section|report|note|spec|design|task
|
|
186
|
-
title TEXT NOT NULL DEFAULT '',
|
|
187
|
-
format TEXT NOT NULL DEFAULT 'markdown',
|
|
188
|
-
content TEXT NOT NULL DEFAULT '',
|
|
189
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
190
|
-
created TEXT NOT NULL,
|
|
191
|
-
updated TEXT NOT NULL DEFAULT '',
|
|
192
|
-
meta TEXT NOT NULL DEFAULT '' -- v8 JSON envelope: task {"status"}, design {"design_type"}
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
-- ---------------------------------------------------------------- knowledge base
|
|
196
|
-
CREATE TABLE IF NOT EXISTS kb_notes (
|
|
197
|
-
id TEXT PRIMARY KEY,
|
|
198
|
-
title TEXT NOT NULL,
|
|
199
|
-
body TEXT NOT NULL DEFAULT '',
|
|
200
|
-
source_url TEXT DEFAULT '',
|
|
201
|
-
origin TEXT DEFAULT '', -- 'artifact:<id>' | 'note' | 'skill:<id>'
|
|
202
|
-
tags TEXT DEFAULT '',
|
|
203
|
-
confidence TEXT NOT NULL DEFAULT 'B',
|
|
204
|
-
reuse INTEGER NOT NULL DEFAULT 0,
|
|
205
|
-
content_hash TEXT NOT NULL DEFAULT '', -- sha1(title+body): idempotent writes
|
|
206
|
-
created TEXT NOT NULL,
|
|
207
|
-
updated TEXT NOT NULL
|
|
208
|
-
);
|
|
209
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS kb_fts USING fts5(
|
|
210
|
-
id UNINDEXED, title, tags, ngram, tokenize='unicode61'
|
|
211
|
-
);
|
|
212
|
-
CREATE TABLE IF NOT EXISTS kb_links (
|
|
213
|
-
from_id TEXT NOT NULL REFERENCES kb_notes(id) ON DELETE CASCADE,
|
|
214
|
-
to_id TEXT NOT NULL REFERENCES kb_notes(id) ON DELETE CASCADE,
|
|
215
|
-
rel TEXT NOT NULL DEFAULT 'related',
|
|
216
|
-
PRIMARY KEY (from_id, to_id, rel)
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
-- ================================================================ TECH TREE MODEL
|
|
220
|
-
-- A node model covering four things:
|
|
221
|
-
-- 1. parent/child structure -> node_edges (adjacency, source of truth)
|
|
222
|
-
-- 2. associations per node -> node_edges.rel + relations (multi-kind)
|
|
223
|
-
-- 3. required vs optional gates -> node_edges.required + node_conditions
|
|
224
|
-
-- 4. indexes for fast queries -> node_closure (transitive), node_fts, idx_*
|
|
225
|
-
CREATE TABLE IF NOT EXISTS trees (
|
|
226
|
-
id TEXT PRIMARY KEY,
|
|
227
|
-
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
228
|
-
name TEXT NOT NULL UNIQUE,
|
|
229
|
-
note TEXT DEFAULT '',
|
|
230
|
-
created TEXT NOT NULL
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
CREATE TABLE IF NOT EXISTS nodes (
|
|
234
|
-
id TEXT PRIMARY KEY,
|
|
235
|
-
tree_id TEXT NOT NULL REFERENCES trees(id) ON DELETE CASCADE,
|
|
236
|
-
key TEXT NOT NULL, -- stable id used in save files
|
|
237
|
-
name TEXT NOT NULL,
|
|
238
|
-
kind TEXT NOT NULL DEFAULT 'tech', -- tech|skill|upgrade|building|era
|
|
239
|
-
tier INTEGER NOT NULL DEFAULT 0, -- era / depth band for layout
|
|
240
|
-
cost REAL NOT NULL DEFAULT 0,
|
|
241
|
-
cost_unit TEXT DEFAULT '',
|
|
242
|
-
payload TEXT DEFAULT '', -- JSON: effects, unlocks, metadata
|
|
243
|
-
tags TEXT DEFAULT '',
|
|
244
|
-
created TEXT NOT NULL,
|
|
245
|
-
UNIQUE(tree_id, key)
|
|
246
|
-
);
|
|
247
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
|
248
|
-
id UNINDEXED, key, name, tags, ngram, tokenize='unicode61'
|
|
249
|
-
);
|
|
250
|
-
CREATE INDEX IF NOT EXISTS idx_nodes_tree ON nodes(tree_id);
|
|
251
|
-
CREATE INDEX IF NOT EXISTS idx_nodes_tier ON nodes(tree_id, tier);
|
|
252
|
-
|
|
253
|
-
-- Adjacency list: the faithful source of truth for the DAG.
|
|
254
|
-
-- rel parent->child semantics: prereq | unlock | excludes | enables
|
|
255
|
-
-- mode how multiple parents combine: AND | OR | XOR | NOT
|
|
256
|
-
-- required 1 = must pass (gating) 0 = optional (bonus / discount only)
|
|
257
|
-
CREATE TABLE IF NOT EXISTS node_edges (
|
|
258
|
-
id TEXT PRIMARY KEY,
|
|
259
|
-
tree_id TEXT NOT NULL REFERENCES trees(id) ON DELETE CASCADE,
|
|
260
|
-
parent_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
261
|
-
child_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
262
|
-
rel TEXT NOT NULL DEFAULT 'prereq',
|
|
263
|
-
mode TEXT NOT NULL DEFAULT 'AND',
|
|
264
|
-
required INTEGER NOT NULL DEFAULT 1,
|
|
265
|
-
min_level INTEGER NOT NULL DEFAULT 1,
|
|
266
|
-
weight REAL NOT NULL DEFAULT 1,
|
|
267
|
-
note TEXT DEFAULT '',
|
|
268
|
-
created TEXT NOT NULL DEFAULT '',
|
|
269
|
-
UNIQUE(parent_id, child_id, rel)
|
|
270
|
-
);
|
|
271
|
-
CREATE INDEX IF NOT EXISTS idx_edge_child ON node_edges(child_id);
|
|
272
|
-
CREATE INDEX IF NOT EXISTS idx_edge_parent ON node_edges(parent_id);
|
|
273
|
-
CREATE INDEX IF NOT EXISTS idx_edge_req ON node_edges(child_id, required);
|
|
274
|
-
|
|
275
|
-
-- Closure table: transitive index for ancestor/descendant queries.
|
|
276
|
-
-- depth 0 = self row, 1 = direct parent, n = n-th ancestor.
|
|
277
|
-
CREATE TABLE IF NOT EXISTS node_closure (
|
|
278
|
-
tree_id TEXT NOT NULL,
|
|
279
|
-
ancestor TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
280
|
-
descendant TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
281
|
-
depth INTEGER NOT NULL,
|
|
282
|
-
PRIMARY KEY (ancestor, descendant)
|
|
283
|
-
);
|
|
284
|
-
CREATE INDEX IF NOT EXISTS idx_closure_desc ON node_closure(descendant);
|
|
285
|
-
CREATE INDEX IF NOT EXISTS idx_closure_anc ON node_closure(ancestor);
|
|
286
|
-
|
|
287
|
-
-- Non-structural gates: conditions that are not other nodes.
|
|
288
|
-
-- required=1 blocks unlock; required=0 only modifies cost / grants bonus.
|
|
289
|
-
CREATE TABLE IF NOT EXISTS node_conditions (
|
|
290
|
-
id TEXT PRIMARY KEY,
|
|
291
|
-
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
292
|
-
name TEXT NOT NULL,
|
|
293
|
-
kind TEXT NOT NULL DEFAULT 'flag', -- flag|resource|level|quest|time|expr
|
|
294
|
-
expr TEXT NOT NULL DEFAULT '', -- evaluated against a runtime state
|
|
295
|
-
op TEXT NOT NULL DEFAULT '>=', -- >= | == | <= | != | has
|
|
296
|
-
value TEXT NOT NULL DEFAULT '',
|
|
297
|
-
required INTEGER NOT NULL DEFAULT 1,
|
|
298
|
-
effect TEXT NOT NULL DEFAULT 'gate', -- gate | cost_mul | bonus
|
|
299
|
-
weight REAL NOT NULL DEFAULT 1,
|
|
300
|
-
note TEXT DEFAULT '',
|
|
301
|
-
created TEXT NOT NULL DEFAULT ''
|
|
302
|
-
);
|
|
303
|
-
CREATE INDEX IF NOT EXISTS idx_cond_node ON node_conditions(node_id);
|
|
304
|
-
CREATE INDEX IF NOT EXISTS idx_cond_req ON node_conditions(node_id, required);
|
|
305
|
-
-- (node_conditions.created is added by upgrade8to9 for existing DBs; new DBs get it from upgrade8to9 too since SCHEMA_SQL doesn't declare it inline)
|
|
306
|
-
|
|
307
|
-
-- Runtime state per context (save slot / player / run).
|
|
308
|
-
CREATE TABLE IF NOT EXISTS node_state (
|
|
309
|
-
tree_id TEXT NOT NULL REFERENCES trees(id) ON DELETE CASCADE,
|
|
310
|
-
context TEXT NOT NULL DEFAULT 'default',
|
|
311
|
-
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
312
|
-
status TEXT NOT NULL DEFAULT 'locked', -- locked|available|unlocked
|
|
313
|
-
level INTEGER NOT NULL DEFAULT 0,
|
|
314
|
-
progress REAL NOT NULL DEFAULT 0,
|
|
315
|
-
updated TEXT NOT NULL,
|
|
316
|
-
PRIMARY KEY (tree_id, context, node_id)
|
|
317
|
-
);
|
|
318
|
-
CREATE INDEX IF NOT EXISTS idx_state_ctx ON node_state(tree_id, context);
|
|
319
|
-
CREATE INDEX IF NOT EXISTS idx_state_status ON node_state(tree_id, context, status);
|
|
320
|
-
|
|
321
|
-
-- Denormalized index on nodes, refreshed by the 'node index' command.
|
|
322
|
-
-- topo_order lets "can I research this" skip recursive traversal.
|
|
323
|
-
CREATE TABLE IF NOT EXISTS node_index (
|
|
324
|
-
tree_id TEXT PRIMARY KEY REFERENCES trees(id) ON DELETE CASCADE,
|
|
325
|
-
topo_order TEXT NOT NULL DEFAULT '', -- JSON array of node ids
|
|
326
|
-
tiers TEXT NOT NULL DEFAULT '', -- JSON: tier -> [node ids]
|
|
327
|
-
in_degree TEXT NOT NULL DEFAULT '', -- JSON: node id -> required parent count
|
|
328
|
-
built_at TEXT NOT NULL,
|
|
329
|
-
dirty INTEGER NOT NULL DEFAULT 1
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
-- ---------------------------------------------------------------- generic graph
|
|
333
|
-
CREATE TABLE IF NOT EXISTS relations (
|
|
334
|
-
id TEXT PRIMARY KEY,
|
|
335
|
-
src_type TEXT NOT NULL, src_id TEXT NOT NULL,
|
|
336
|
-
dst_type TEXT NOT NULL, dst_id TEXT NOT NULL,
|
|
337
|
-
rel TEXT NOT NULL DEFAULT 'related',
|
|
338
|
-
weight REAL NOT NULL DEFAULT 1,
|
|
339
|
-
created TEXT NOT NULL DEFAULT '',
|
|
340
|
-
UNIQUE(src_type, src_id, dst_type, dst_id, rel)
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
-- ---------------------------------------------------------------- scheduling
|
|
344
|
-
CREATE TABLE IF NOT EXISTS jobs (
|
|
345
|
-
id TEXT PRIMARY KEY,
|
|
346
|
-
title TEXT NOT NULL,
|
|
347
|
-
command TEXT NOT NULL,
|
|
348
|
-
every_minutes INTEGER DEFAULT 0,
|
|
349
|
-
at TEXT DEFAULT '', -- one-shot ISO datetime
|
|
350
|
-
enabled INTEGER NOT NULL DEFAULT 1,
|
|
351
|
-
runs INTEGER NOT NULL DEFAULT 0,
|
|
352
|
-
last_run TEXT DEFAULT '',
|
|
353
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
354
|
-
created TEXT NOT NULL
|
|
355
|
-
);
|
|
356
|
-
CREATE TABLE IF NOT EXISTS runs (
|
|
357
|
-
id TEXT PRIMARY KEY,
|
|
358
|
-
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
359
|
-
started TEXT NOT NULL,
|
|
360
|
-
finished TEXT DEFAULT '',
|
|
361
|
-
rc INTEGER,
|
|
362
|
-
output TEXT DEFAULT ''
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
-- ---------------------------------------------------------------- audit log
|
|
366
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
367
|
-
id TEXT PRIMARY KEY,
|
|
368
|
-
ts TEXT NOT NULL,
|
|
369
|
-
actor TEXT NOT NULL DEFAULT 'cli',
|
|
370
|
-
kind TEXT NOT NULL,
|
|
371
|
-
entity TEXT DEFAULT '',
|
|
372
|
-
entity_id TEXT DEFAULT '',
|
|
373
|
-
payload TEXT DEFAULT '',
|
|
374
|
-
level TEXT NOT NULL DEFAULT 'info' -- v7 telemetry severity: info|warn|error
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
CREATE INDEX IF NOT EXISTS idx_sources_project ON sources(project_id);
|
|
378
|
-
CREATE INDEX IF NOT EXISTS idx_evidence_project ON evidence(project_id);
|
|
379
|
-
CREATE INDEX IF NOT EXISTS idx_claims_project ON claims(project_id);
|
|
380
|
-
CREATE INDEX IF NOT EXISTS idx_artifacts_proj ON artifacts(project_id);
|
|
381
|
-
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
|
|
382
|
-
CREATE INDEX IF NOT EXISTS idx_jobs_enabled ON jobs(enabled);
|
|
383
|
-
|
|
384
|
-
-- idempotent-write uniqueness (schema v3; named indexes serve both fresh
|
|
385
|
-
-- installs and v2 upgrades — keep CREATE TABLEs free of inline UNIQUE)
|
|
386
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uq_queries_content ON queries(project_id, content_hash);
|
|
387
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uq_evidence_content ON evidence(source_id, content_hash);
|
|
388
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uq_claims_content ON claims(project_id, content_hash);
|
|
389
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_content ON kb_notes(content_hash);
|
|
390
|
-
|
|
391
|
-
-- v5: skill refs add upserts ON CONFLICT(skill_id, path) — the unique index
|
|
392
|
-
-- it depends on was missing since v1 (the command errored on first use)
|
|
393
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
49
|
+
exports.SCHEMA_SQL = `
|
|
50
|
+
-- ---------------------------------------------------------------- meta
|
|
51
|
+
-- runtime state the tool writes itself (current selections, bookkeeping);
|
|
52
|
+
-- user-tunable settings live in config
|
|
53
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
54
|
+
key TEXT PRIMARY KEY,
|
|
55
|
+
value TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
-- ---------------------------------------------------------------- config (v6)
|
|
59
|
+
-- user-tunable settings; registry (defaults/types/notes) in src/config.ts.
|
|
60
|
+
-- Reads fall back to defaults — an empty table is a valid state.
|
|
61
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
62
|
+
key TEXT PRIMARY KEY,
|
|
63
|
+
value TEXT NOT NULL DEFAULT '',
|
|
64
|
+
note TEXT DEFAULT '',
|
|
65
|
+
updated TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
-- ---------------------------------------------------------------- skills (L1/L2/L3)
|
|
69
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
name TEXT NOT NULL UNIQUE,
|
|
72
|
+
version TEXT DEFAULT '0.1.0',
|
|
73
|
+
description TEXT NOT NULL DEFAULT '', -- L1: what + when, always in context
|
|
74
|
+
body TEXT NOT NULL DEFAULT '', -- L2: instructions, loaded on activation
|
|
75
|
+
tags TEXT DEFAULT '',
|
|
76
|
+
origin TEXT DEFAULT '', -- where it came from (path/repo)
|
|
77
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
78
|
+
hits INTEGER NOT NULL DEFAULT 0, -- how often an agent matched it
|
|
79
|
+
created TEXT NOT NULL,
|
|
80
|
+
updated TEXT NOT NULL
|
|
81
|
+
);
|
|
82
|
+
CREATE TABLE IF NOT EXISTS skill_refs ( -- L3: loaded on demand
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
|
|
85
|
+
path TEXT NOT NULL,
|
|
86
|
+
content TEXT NOT NULL DEFAULT ''
|
|
87
|
+
);
|
|
88
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
89
|
+
id UNINDEXED, name, description, tags, ngram, tokenize='unicode61'
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
-- ---------------------------------------------------------------- projects
|
|
93
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
94
|
+
id TEXT PRIMARY KEY,
|
|
95
|
+
name TEXT NOT NULL UNIQUE,
|
|
96
|
+
question TEXT NOT NULL DEFAULT '',
|
|
97
|
+
inclusion TEXT DEFAULT '',
|
|
98
|
+
exclusion TEXT DEFAULT '',
|
|
99
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
100
|
+
created TEXT NOT NULL,
|
|
101
|
+
updated TEXT NOT NULL
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
-- ---------------------------------------------------------------- research: queries
|
|
105
|
+
CREATE TABLE IF NOT EXISTS queries (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
108
|
+
text TEXT NOT NULL,
|
|
109
|
+
engine TEXT DEFAULT '',
|
|
110
|
+
status TEXT NOT NULL DEFAULT 'pending', -- pending|done
|
|
111
|
+
note TEXT DEFAULT '',
|
|
112
|
+
content_hash TEXT NOT NULL DEFAULT '', -- sha1(text): idempotent writes
|
|
113
|
+
created TEXT NOT NULL
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- ---------------------------------------------------------------- research: sources
|
|
117
|
+
-- query <-> source association: which search query recalled which source
|
|
118
|
+
-- (schema v4; pure additive, no migration needed beyond CREATE IF NOT EXISTS)
|
|
119
|
+
CREATE TABLE IF NOT EXISTS query_sources (
|
|
120
|
+
query_id TEXT NOT NULL REFERENCES queries(id) ON DELETE CASCADE,
|
|
121
|
+
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
122
|
+
note TEXT DEFAULT '',
|
|
123
|
+
created TEXT NOT NULL,
|
|
124
|
+
PRIMARY KEY (query_id, source_id)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
128
|
+
id TEXT PRIMARY KEY,
|
|
129
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
130
|
+
url TEXT DEFAULT '',
|
|
131
|
+
title TEXT NOT NULL DEFAULT '',
|
|
132
|
+
url_hash TEXT NOT NULL,
|
|
133
|
+
content_hash TEXT NOT NULL DEFAULT '',
|
|
134
|
+
abstract TEXT DEFAULT '',
|
|
135
|
+
content TEXT DEFAULT '',
|
|
136
|
+
engine TEXT DEFAULT '',
|
|
137
|
+
relevance INTEGER NOT NULL DEFAULT 0, -- 0..5 screening score
|
|
138
|
+
confidence TEXT NOT NULL DEFAULT 'B', -- A|B|C source trust grade
|
|
139
|
+
tags TEXT DEFAULT '',
|
|
140
|
+
created TEXT NOT NULL DEFAULT '',
|
|
141
|
+
retrieved_at TEXT NOT NULL,
|
|
142
|
+
UNIQUE(project_id, url_hash)
|
|
143
|
+
);
|
|
144
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sources_fts USING fts5(
|
|
145
|
+
id UNINDEXED, title, abstract, content, ngram, tokenize='unicode61'
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
-- ---------------------------------------------------------------- research: evidence
|
|
149
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
150
|
+
id TEXT PRIMARY KEY,
|
|
151
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
152
|
+
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
153
|
+
quote TEXT NOT NULL,
|
|
154
|
+
note TEXT DEFAULT '',
|
|
155
|
+
locator TEXT DEFAULT '', -- page / section / offset
|
|
156
|
+
content_hash TEXT NOT NULL DEFAULT '', -- sha1(quote): idempotent writes
|
|
157
|
+
created TEXT NOT NULL
|
|
158
|
+
);
|
|
159
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS evidence_fts USING fts5(
|
|
160
|
+
id UNINDEXED, quote, ngram, tokenize='unicode61'
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
-- ---------------------------------------------------------------- research: claims
|
|
164
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
165
|
+
id TEXT PRIMARY KEY,
|
|
166
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
167
|
+
text TEXT NOT NULL,
|
|
168
|
+
status TEXT NOT NULL DEFAULT 'unverified', -- unverified|supported|refuted|contested
|
|
169
|
+
confidence TEXT NOT NULL DEFAULT 'B',
|
|
170
|
+
content_hash TEXT NOT NULL DEFAULT '', -- sha1(text): idempotent writes
|
|
171
|
+
created TEXT NOT NULL,
|
|
172
|
+
updated TEXT NOT NULL DEFAULT ''
|
|
173
|
+
);
|
|
174
|
+
CREATE TABLE IF NOT EXISTS claim_evidence (
|
|
175
|
+
claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
|
|
176
|
+
evidence_id TEXT NOT NULL REFERENCES evidence(id) ON DELETE CASCADE,
|
|
177
|
+
stance TEXT NOT NULL DEFAULT 'supports', -- supports|refutes
|
|
178
|
+
PRIMARY KEY (claim_id, evidence_id)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
-- ---------------------------------------------------------------- production
|
|
182
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
185
|
+
kind TEXT NOT NULL DEFAULT 'section', -- outline|section|report|note|spec|design|task
|
|
186
|
+
title TEXT NOT NULL DEFAULT '',
|
|
187
|
+
format TEXT NOT NULL DEFAULT 'markdown',
|
|
188
|
+
content TEXT NOT NULL DEFAULT '',
|
|
189
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
190
|
+
created TEXT NOT NULL,
|
|
191
|
+
updated TEXT NOT NULL DEFAULT '',
|
|
192
|
+
meta TEXT NOT NULL DEFAULT '' -- v8 JSON envelope: task {"status"}, design {"design_type"}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
-- ---------------------------------------------------------------- knowledge base
|
|
196
|
+
CREATE TABLE IF NOT EXISTS kb_notes (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
title TEXT NOT NULL,
|
|
199
|
+
body TEXT NOT NULL DEFAULT '',
|
|
200
|
+
source_url TEXT DEFAULT '',
|
|
201
|
+
origin TEXT DEFAULT '', -- 'artifact:<id>' | 'note' | 'skill:<id>'
|
|
202
|
+
tags TEXT DEFAULT '',
|
|
203
|
+
confidence TEXT NOT NULL DEFAULT 'B',
|
|
204
|
+
reuse INTEGER NOT NULL DEFAULT 0,
|
|
205
|
+
content_hash TEXT NOT NULL DEFAULT '', -- sha1(title+body): idempotent writes
|
|
206
|
+
created TEXT NOT NULL,
|
|
207
|
+
updated TEXT NOT NULL
|
|
208
|
+
);
|
|
209
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS kb_fts USING fts5(
|
|
210
|
+
id UNINDEXED, title, tags, ngram, tokenize='unicode61'
|
|
211
|
+
);
|
|
212
|
+
CREATE TABLE IF NOT EXISTS kb_links (
|
|
213
|
+
from_id TEXT NOT NULL REFERENCES kb_notes(id) ON DELETE CASCADE,
|
|
214
|
+
to_id TEXT NOT NULL REFERENCES kb_notes(id) ON DELETE CASCADE,
|
|
215
|
+
rel TEXT NOT NULL DEFAULT 'related',
|
|
216
|
+
PRIMARY KEY (from_id, to_id, rel)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
-- ================================================================ TECH TREE MODEL
|
|
220
|
+
-- A node model covering four things:
|
|
221
|
+
-- 1. parent/child structure -> node_edges (adjacency, source of truth)
|
|
222
|
+
-- 2. associations per node -> node_edges.rel + relations (multi-kind)
|
|
223
|
+
-- 3. required vs optional gates -> node_edges.required + node_conditions
|
|
224
|
+
-- 4. indexes for fast queries -> node_closure (transitive), node_fts, idx_*
|
|
225
|
+
CREATE TABLE IF NOT EXISTS trees (
|
|
226
|
+
id TEXT PRIMARY KEY,
|
|
227
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
228
|
+
name TEXT NOT NULL UNIQUE,
|
|
229
|
+
note TEXT DEFAULT '',
|
|
230
|
+
created TEXT NOT NULL
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
234
|
+
id TEXT PRIMARY KEY,
|
|
235
|
+
tree_id TEXT NOT NULL REFERENCES trees(id) ON DELETE CASCADE,
|
|
236
|
+
key TEXT NOT NULL, -- stable id used in save files
|
|
237
|
+
name TEXT NOT NULL,
|
|
238
|
+
kind TEXT NOT NULL DEFAULT 'tech', -- tech|skill|upgrade|building|era
|
|
239
|
+
tier INTEGER NOT NULL DEFAULT 0, -- era / depth band for layout
|
|
240
|
+
cost REAL NOT NULL DEFAULT 0,
|
|
241
|
+
cost_unit TEXT DEFAULT '',
|
|
242
|
+
payload TEXT DEFAULT '', -- JSON: effects, unlocks, metadata
|
|
243
|
+
tags TEXT DEFAULT '',
|
|
244
|
+
created TEXT NOT NULL,
|
|
245
|
+
UNIQUE(tree_id, key)
|
|
246
|
+
);
|
|
247
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
|
248
|
+
id UNINDEXED, key, name, tags, ngram, tokenize='unicode61'
|
|
249
|
+
);
|
|
250
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_tree ON nodes(tree_id);
|
|
251
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_tier ON nodes(tree_id, tier);
|
|
252
|
+
|
|
253
|
+
-- Adjacency list: the faithful source of truth for the DAG.
|
|
254
|
+
-- rel parent->child semantics: prereq | unlock | excludes | enables
|
|
255
|
+
-- mode how multiple parents combine: AND | OR | XOR | NOT
|
|
256
|
+
-- required 1 = must pass (gating) 0 = optional (bonus / discount only)
|
|
257
|
+
CREATE TABLE IF NOT EXISTS node_edges (
|
|
258
|
+
id TEXT PRIMARY KEY,
|
|
259
|
+
tree_id TEXT NOT NULL REFERENCES trees(id) ON DELETE CASCADE,
|
|
260
|
+
parent_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
261
|
+
child_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
262
|
+
rel TEXT NOT NULL DEFAULT 'prereq',
|
|
263
|
+
mode TEXT NOT NULL DEFAULT 'AND',
|
|
264
|
+
required INTEGER NOT NULL DEFAULT 1,
|
|
265
|
+
min_level INTEGER NOT NULL DEFAULT 1,
|
|
266
|
+
weight REAL NOT NULL DEFAULT 1,
|
|
267
|
+
note TEXT DEFAULT '',
|
|
268
|
+
created TEXT NOT NULL DEFAULT '',
|
|
269
|
+
UNIQUE(parent_id, child_id, rel)
|
|
270
|
+
);
|
|
271
|
+
CREATE INDEX IF NOT EXISTS idx_edge_child ON node_edges(child_id);
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_edge_parent ON node_edges(parent_id);
|
|
273
|
+
CREATE INDEX IF NOT EXISTS idx_edge_req ON node_edges(child_id, required);
|
|
274
|
+
|
|
275
|
+
-- Closure table: transitive index for ancestor/descendant queries.
|
|
276
|
+
-- depth 0 = self row, 1 = direct parent, n = n-th ancestor.
|
|
277
|
+
CREATE TABLE IF NOT EXISTS node_closure (
|
|
278
|
+
tree_id TEXT NOT NULL,
|
|
279
|
+
ancestor TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
280
|
+
descendant TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
281
|
+
depth INTEGER NOT NULL,
|
|
282
|
+
PRIMARY KEY (ancestor, descendant)
|
|
283
|
+
);
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_closure_desc ON node_closure(descendant);
|
|
285
|
+
CREATE INDEX IF NOT EXISTS idx_closure_anc ON node_closure(ancestor);
|
|
286
|
+
|
|
287
|
+
-- Non-structural gates: conditions that are not other nodes.
|
|
288
|
+
-- required=1 blocks unlock; required=0 only modifies cost / grants bonus.
|
|
289
|
+
CREATE TABLE IF NOT EXISTS node_conditions (
|
|
290
|
+
id TEXT PRIMARY KEY,
|
|
291
|
+
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
292
|
+
name TEXT NOT NULL,
|
|
293
|
+
kind TEXT NOT NULL DEFAULT 'flag', -- flag|resource|level|quest|time|expr
|
|
294
|
+
expr TEXT NOT NULL DEFAULT '', -- evaluated against a runtime state
|
|
295
|
+
op TEXT NOT NULL DEFAULT '>=', -- >= | == | <= | != | has
|
|
296
|
+
value TEXT NOT NULL DEFAULT '',
|
|
297
|
+
required INTEGER NOT NULL DEFAULT 1,
|
|
298
|
+
effect TEXT NOT NULL DEFAULT 'gate', -- gate | cost_mul | bonus
|
|
299
|
+
weight REAL NOT NULL DEFAULT 1,
|
|
300
|
+
note TEXT DEFAULT '',
|
|
301
|
+
created TEXT NOT NULL DEFAULT ''
|
|
302
|
+
);
|
|
303
|
+
CREATE INDEX IF NOT EXISTS idx_cond_node ON node_conditions(node_id);
|
|
304
|
+
CREATE INDEX IF NOT EXISTS idx_cond_req ON node_conditions(node_id, required);
|
|
305
|
+
-- (node_conditions.created is added by upgrade8to9 for existing DBs; new DBs get it from upgrade8to9 too since SCHEMA_SQL doesn't declare it inline)
|
|
306
|
+
|
|
307
|
+
-- Runtime state per context (save slot / player / run).
|
|
308
|
+
CREATE TABLE IF NOT EXISTS node_state (
|
|
309
|
+
tree_id TEXT NOT NULL REFERENCES trees(id) ON DELETE CASCADE,
|
|
310
|
+
context TEXT NOT NULL DEFAULT 'default',
|
|
311
|
+
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
312
|
+
status TEXT NOT NULL DEFAULT 'locked', -- locked|available|unlocked
|
|
313
|
+
level INTEGER NOT NULL DEFAULT 0,
|
|
314
|
+
progress REAL NOT NULL DEFAULT 0,
|
|
315
|
+
updated TEXT NOT NULL,
|
|
316
|
+
PRIMARY KEY (tree_id, context, node_id)
|
|
317
|
+
);
|
|
318
|
+
CREATE INDEX IF NOT EXISTS idx_state_ctx ON node_state(tree_id, context);
|
|
319
|
+
CREATE INDEX IF NOT EXISTS idx_state_status ON node_state(tree_id, context, status);
|
|
320
|
+
|
|
321
|
+
-- Denormalized index on nodes, refreshed by the 'node index' command.
|
|
322
|
+
-- topo_order lets "can I research this" skip recursive traversal.
|
|
323
|
+
CREATE TABLE IF NOT EXISTS node_index (
|
|
324
|
+
tree_id TEXT PRIMARY KEY REFERENCES trees(id) ON DELETE CASCADE,
|
|
325
|
+
topo_order TEXT NOT NULL DEFAULT '', -- JSON array of node ids
|
|
326
|
+
tiers TEXT NOT NULL DEFAULT '', -- JSON: tier -> [node ids]
|
|
327
|
+
in_degree TEXT NOT NULL DEFAULT '', -- JSON: node id -> required parent count
|
|
328
|
+
built_at TEXT NOT NULL,
|
|
329
|
+
dirty INTEGER NOT NULL DEFAULT 1
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
-- ---------------------------------------------------------------- generic graph
|
|
333
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
334
|
+
id TEXT PRIMARY KEY,
|
|
335
|
+
src_type TEXT NOT NULL, src_id TEXT NOT NULL,
|
|
336
|
+
dst_type TEXT NOT NULL, dst_id TEXT NOT NULL,
|
|
337
|
+
rel TEXT NOT NULL DEFAULT 'related',
|
|
338
|
+
weight REAL NOT NULL DEFAULT 1,
|
|
339
|
+
created TEXT NOT NULL DEFAULT '',
|
|
340
|
+
UNIQUE(src_type, src_id, dst_type, dst_id, rel)
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
-- ---------------------------------------------------------------- scheduling
|
|
344
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
345
|
+
id TEXT PRIMARY KEY,
|
|
346
|
+
title TEXT NOT NULL,
|
|
347
|
+
command TEXT NOT NULL,
|
|
348
|
+
every_minutes INTEGER DEFAULT 0,
|
|
349
|
+
at TEXT DEFAULT '', -- one-shot ISO datetime
|
|
350
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
351
|
+
runs INTEGER NOT NULL DEFAULT 0,
|
|
352
|
+
last_run TEXT DEFAULT '',
|
|
353
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
354
|
+
created TEXT NOT NULL
|
|
355
|
+
);
|
|
356
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
357
|
+
id TEXT PRIMARY KEY,
|
|
358
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
359
|
+
started TEXT NOT NULL,
|
|
360
|
+
finished TEXT DEFAULT '',
|
|
361
|
+
rc INTEGER,
|
|
362
|
+
output TEXT DEFAULT ''
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
-- ---------------------------------------------------------------- audit log
|
|
366
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
367
|
+
id TEXT PRIMARY KEY,
|
|
368
|
+
ts TEXT NOT NULL,
|
|
369
|
+
actor TEXT NOT NULL DEFAULT 'cli',
|
|
370
|
+
kind TEXT NOT NULL,
|
|
371
|
+
entity TEXT DEFAULT '',
|
|
372
|
+
entity_id TEXT DEFAULT '',
|
|
373
|
+
payload TEXT DEFAULT '',
|
|
374
|
+
level TEXT NOT NULL DEFAULT 'info' -- v7 telemetry severity: info|warn|error
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
CREATE INDEX IF NOT EXISTS idx_sources_project ON sources(project_id);
|
|
378
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_project ON evidence(project_id);
|
|
379
|
+
CREATE INDEX IF NOT EXISTS idx_claims_project ON claims(project_id);
|
|
380
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_proj ON artifacts(project_id);
|
|
381
|
+
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
|
|
382
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_enabled ON jobs(enabled);
|
|
383
|
+
|
|
384
|
+
-- idempotent-write uniqueness (schema v3; named indexes serve both fresh
|
|
385
|
+
-- installs and v2 upgrades — keep CREATE TABLEs free of inline UNIQUE)
|
|
386
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_queries_content ON queries(project_id, content_hash);
|
|
387
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_evidence_content ON evidence(source_id, content_hash);
|
|
388
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_claims_content ON claims(project_id, content_hash);
|
|
389
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_content ON kb_notes(content_hash);
|
|
390
|
+
|
|
391
|
+
-- v5: skill refs add upserts ON CONFLICT(skill_id, path) — the unique index
|
|
392
|
+
-- it depends on was missing since v1 (the command errored on first use)
|
|
393
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
394
394
|
`;
|
|
395
395
|
class DB {
|
|
396
396
|
db;
|
|
@@ -401,34 +401,65 @@ class DB {
|
|
|
401
401
|
const useLock = opts?.lock === true; // opt-in: only the CLI entry point locks
|
|
402
402
|
// P0-2 (real-user incident): single-writer mutex. node-sqlite3-wasm has no
|
|
403
403
|
// concurrent-write protection; two CLI processes writing simultaneously
|
|
404
|
-
// corrupted the DB ("malformed").
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
|
|
404
|
+
// corrupted the DB ("malformed"). App-level dir lock with pid file +
|
|
405
|
+
// stale steal + friendly error.
|
|
406
|
+
//
|
|
407
|
+
// NAME COLLISION (0.65.0 P0): node-sqlite3-wasm's Node VFS implements
|
|
408
|
+
// sqlite's own file-lock protocol with exactly `${file}.lock` (mkdir on
|
|
409
|
+
// lock, EEXIST → SQLITE_BUSY, rmdir on unlock). An app lock using that
|
|
410
|
+
// name made every locked open fail BUSY on itself. Ours is `.ygllock` —
|
|
411
|
+
// the wasm protocol dir stays untouched.
|
|
412
|
+
const legacyLockDir = `${file}.lock`;
|
|
413
|
+
if (fs.existsSync(path.join(legacyLockDir, "pid"))) {
|
|
414
|
+
// orphaned 0.65.0 app-layer lock (wasm protocol dirs are empty — a pid
|
|
415
|
+
// file marks it as ours) would block wasm forever; remove it.
|
|
416
|
+
try {
|
|
417
|
+
fs.rmSync(legacyLockDir, { recursive: true, force: true });
|
|
418
|
+
}
|
|
419
|
+
catch { /* */ }
|
|
420
|
+
}
|
|
421
|
+
const lockDir = `${file}.ygllock`;
|
|
408
422
|
if (useLock && fs.existsSync(lockDir)) {
|
|
409
423
|
let holder = "unknown";
|
|
410
424
|
try {
|
|
411
425
|
holder = fs.readFileSync(path.join(lockDir, "pid"), "utf8").trim();
|
|
412
426
|
}
|
|
413
427
|
catch { /* */ }
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
428
|
+
// Same-process re-entry backstop (0.65.0 self-deadlock lesson).
|
|
429
|
+
const mine = holder === String(process.pid);
|
|
430
|
+
// PID liveness (P1 cold-user report): the 15-minute mtime window was a
|
|
431
|
+
// blunt proxy — on Windows the exit-time rmSync can silently fail (EBUSY
|
|
432
|
+
// from an un-released handle), leaving an orphaned lock whose mtime is
|
|
433
|
+
// recent. A dead holder means the lock is garbage regardless of mtime;
|
|
434
|
+
// a live holder means it is real regardless of mtime.
|
|
435
|
+
// signal 0 = pure existence check (does NOT kill the process)
|
|
436
|
+
// EPERM = exists but permission-denied (Windows ACL) — still alive
|
|
437
|
+
// ESRCH = no such process — dead, steal the lock
|
|
438
|
+
let holderAlive;
|
|
439
|
+
if (mine)
|
|
440
|
+
holderAlive = false; // our own stale entry — always steal
|
|
441
|
+
else {
|
|
442
|
+
try {
|
|
443
|
+
process.kill(Number(holder), 0);
|
|
444
|
+
holderAlive = true;
|
|
419
445
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
` lock: ${lockDir}\n` +
|
|
423
|
-
` if no other ygl-trees is running, remove it: rmdir "${lockDir}"\n` +
|
|
424
|
-
` (stale locks auto-clear after 15 minutes)`);
|
|
446
|
+
catch (e) {
|
|
447
|
+
holderAlive = e.code === "EPERM"; // EPERM = alive (Windows), ESRCH = dead
|
|
425
448
|
}
|
|
426
449
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
450
|
+
if (!holderAlive) {
|
|
451
|
+
// Orphaned lock (holder crashed / exit cleanup failed) — steal now.
|
|
452
|
+
try {
|
|
453
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
454
|
+
}
|
|
455
|
+
catch { /* */ }
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
throw new Error(`database is locked by another process (pid: ${holder}).\n` +
|
|
459
|
+
` lock: ${lockDir}\n` +
|
|
460
|
+
` if this is a webui serve dashboard, it shares WAL safely — CLI reads work,\n` +
|
|
461
|
+
` write commands need the serve process stopped (or YGL_TEST=1 to bypass)\n` +
|
|
462
|
+
` if no other ygl-trees is running, remove it: rmdir "${lockDir}"`);
|
|
432
463
|
}
|
|
433
464
|
}
|
|
434
465
|
// acquire the lock (best-effort; crash cleanup via stale detection above)
|
|
@@ -436,12 +467,23 @@ class DB {
|
|
|
436
467
|
try {
|
|
437
468
|
fs.mkdirSync(lockDir, { recursive: true });
|
|
438
469
|
fs.writeFileSync(path.join(lockDir, "pid"), String(process.pid));
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
470
|
+
// P1 cold-user report (Windows): `process.on("exit")` rmSync can silently
|
|
471
|
+
// fail when the SQLite handle hasn't been released yet (EBUSY on the dir).
|
|
472
|
+
// Two mitigations:
|
|
473
|
+
// 1. beforeExit fires earlier (while the event loop is still alive)
|
|
474
|
+
// 2. PID liveness check (above) makes any residual lock self-healing —
|
|
475
|
+
// the NEXT process sees a dead PID and clears it immediately.
|
|
476
|
+
const cleanup = () => {
|
|
477
|
+
for (let i = 0; i < 3; i++) { // retry: Windows may need a tick
|
|
478
|
+
try {
|
|
479
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
catch { /* */ }
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
process.on("beforeExit", cleanup);
|
|
486
|
+
process.on("exit", cleanup);
|
|
445
487
|
}
|
|
446
488
|
catch { /* lock creation failure is non-fatal (read-only scenarios) */ }
|
|
447
489
|
this.db = new node_sqlite3_wasm_1.Database(file);
|
|
@@ -456,7 +498,7 @@ class DB {
|
|
|
456
498
|
/** Release the write lock (called by close() and process exit). */
|
|
457
499
|
releaseLock() {
|
|
458
500
|
try {
|
|
459
|
-
const lockDir = `${this.path}.
|
|
501
|
+
const lockDir = `${this.path}.ygllock`;
|
|
460
502
|
if (fs.existsSync(lockDir))
|
|
461
503
|
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
462
504
|
}
|
|
@@ -465,7 +507,7 @@ class DB {
|
|
|
465
507
|
/** Static: force-release a lock (for cleanup hooks). */
|
|
466
508
|
static releaseLockFor(file) {
|
|
467
509
|
try {
|
|
468
|
-
const lockDir = `${file}.
|
|
510
|
+
const lockDir = `${file}.ygllock`;
|
|
469
511
|
if (fs.existsSync(lockDir))
|
|
470
512
|
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
471
513
|
}
|
|
@@ -478,19 +520,19 @@ class DB {
|
|
|
478
520
|
}
|
|
479
521
|
catch (e) {
|
|
480
522
|
if (String(e.message).includes("malformed") || String(e.message).includes("not a database")) {
|
|
481
|
-
throw new Error(`database corrupted: ${this.path}
|
|
523
|
+
throw new Error(`database corrupted: ${this.path}
|
|
482
524
|
` +
|
|
483
|
-
` ${e.message}
|
|
525
|
+
` ${e.message}
|
|
484
526
|
` +
|
|
485
|
-
`
|
|
527
|
+
`
|
|
486
528
|
` +
|
|
487
|
-
` 自愈路径(库可随时从源文件重建):
|
|
529
|
+
` 自愈路径(库可随时从源文件重建):
|
|
488
530
|
` +
|
|
489
|
-
` 1. 备份损坏库: cp "${this.path}" "${this.path}.corrupt.bak"
|
|
531
|
+
` 1. 备份损坏库: cp "${this.path}" "${this.path}.corrupt.bak"
|
|
490
532
|
` +
|
|
491
|
-
` 2. 重置: ygl-trees db reset
|
|
533
|
+
` 2. 重置: ygl-trees db reset
|
|
492
534
|
` +
|
|
493
|
-
` 3. 重新导入: ygl-trees db init && ygl-trees kb import <源目录>
|
|
535
|
+
` 3. 重新导入: ygl-trees db init && ygl-trees kb import <源目录>
|
|
494
536
|
` +
|
|
495
537
|
` (kb 笔记全部有源 .md 文件在盘上,库只是索引)`);
|
|
496
538
|
}
|