@cavelang/store 0.1.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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * The CAVE store (spec §13) — append-only claim persistence on the Node.js
3
+ * builtin `node:sqlite`.
4
+ *
5
+ * - one row per fact, canonical direction; inverses are query-time views
6
+ * over existing indexes, never materialized rows (spec §13.3);
7
+ * - current belief = latest tx per claim key (spec §9.1, §13.5);
8
+ * - the verb registry is rebuilt from stored in-band declaration claims on
9
+ * open, so a reopened database keeps its inverse vocabulary;
10
+ * - full-text search over subjects, objects, values, comments and raw
11
+ * lines via FTS5.
12
+ */
13
+ import { DatabaseSync } from 'node:sqlite';
14
+ import { Claim, Key, Uuidv7, Verb } from '@cavelang/core';
15
+ import * as Canonical from '@cavelang/canonical';
16
+ import * as Row from "./row.js";
17
+ import * as Schema from "./schema.js";
18
+ const currentSql = `
19
+ SELECT c.* FROM cave_claim c
20
+ JOIN (
21
+ SELECT claim_key, MAX(tx) AS max_tx
22
+ FROM cave_claim GROUP BY claim_key
23
+ ) latest ON c.claim_key = latest.claim_key AND c.tx = latest.max_tx
24
+ `;
25
+ /**
26
+ * Opens (creating if necessary) a CAVE store. The registry defaults to the
27
+ * standard §5.5 prelude pairs and is extended by any declaration claims
28
+ * already stored; pass `Canonical.Registry.empty` for a declaration-free
29
+ * start.
30
+ */
31
+ export const open = (path = ':memory:', options = {}) => {
32
+ const db = new DatabaseSync(path);
33
+ db.exec('PRAGMA foreign_keys = ON');
34
+ Schema.init(db);
35
+ let registry = options.registry ?? Canonical.standardRegistry;
36
+ const insertClaim = db.prepare(`
37
+ INSERT INTO cave_claim (
38
+ id, tx, subject, verb, negated, object, attribute,
39
+ value_text, value_num, value_unit, value_approx,
40
+ delta_text, delta_num, delta_unit, sigma_level,
41
+ conf, importance, comment, raw_line, claim_key
42
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
43
+ `);
44
+ const insertContext = db.prepare('INSERT INTO cave_context (claim_id, context) VALUES (?, ?)');
45
+ const insertTag = db.prepare('INSERT INTO cave_tag (claim_id, key, value) VALUES (?, ?, ?)');
46
+ const insertEdge = db.prepare('INSERT INTO cave_edge (parent_id, role, child_id) VALUES (?, ?, ?)');
47
+ const insertFts = db.prepare(`
48
+ INSERT INTO cave_fts (claim_id, subject, verb, object, attribute, value_text, comment, raw_line)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
50
+ `);
51
+ /**
52
+ * Rebuilds in-band declarations from stored claims (ordered by tx),
53
+ * mirroring `@cavelang/canonical`'s `applyDeclarations` predicate exactly —
54
+ * the registry after reopen must equal the registry at close. Qualifier
55
+ * condition rows (children of WHEN/VIA/BECAUSE edges) never declared
56
+ * in-session, so they are excluded here too; `X IS verb` needs a
57
+ * verb-shaped plain-entity subject.
58
+ */
59
+ const rebuildRegistry = () => {
60
+ const declarations = db.prepare(`
61
+ SELECT subject, verb, object FROM cave_claim
62
+ WHERE negated = 0 AND object IS NOT NULL AND verb IN ('REVERSE', 'IS')
63
+ AND id NOT IN (SELECT child_id FROM cave_edge WHERE role IN ('WHEN', 'VIA', 'BECAUSE'))
64
+ ORDER BY tx
65
+ `).all();
66
+ for (const declaration of declarations) {
67
+ if (!Verb.isVerbToken(declaration.subject)) {
68
+ continue;
69
+ }
70
+ if (declaration.verb === 'REVERSE' && Verb.isVerbToken(declaration.object)) {
71
+ registry = Canonical.Registry.declareReverse(registry, declaration.subject, declaration.object).registry;
72
+ }
73
+ else if (declaration.verb === 'IS' && declaration.object === 'verb') {
74
+ registry = Canonical.Registry.declareVerb(registry, declaration.subject);
75
+ }
76
+ }
77
+ };
78
+ rebuildRegistry();
79
+ const transaction = (body) => {
80
+ db.exec('BEGIN');
81
+ try {
82
+ const result = body();
83
+ db.exec('COMMIT');
84
+ return result;
85
+ }
86
+ catch (error) {
87
+ db.exec('ROLLBACK');
88
+ throw error;
89
+ }
90
+ };
91
+ /** Appends a canonicalization result — one row per claim, per-row tx. */
92
+ const insertResult = (result) => transaction(() => {
93
+ const ids = [];
94
+ for (const entry of result.claims) {
95
+ const claim = entry.claim;
96
+ const id = Uuidv7.next();
97
+ const columns = Row.toColumns(claim);
98
+ const rawLine = columns.rawLine === '' ? Canonical.emitClaim(claim) : columns.rawLine;
99
+ insertClaim.run(id, id, columns.subject, columns.verb, columns.negated, columns.object, columns.attribute, columns.valueText, columns.valueNum, columns.valueUnit, columns.valueApprox, columns.deltaText, columns.deltaNum, columns.deltaUnit, columns.sigmaLevel, columns.conf, columns.importance, columns.comment, rawLine, Key.of(claim));
100
+ for (const context of claim.contexts) {
101
+ insertContext.run(id, context);
102
+ }
103
+ for (const tag of claim.tags) {
104
+ insertTag.run(id, tag.key, tag.value ?? null);
105
+ }
106
+ insertFts.run(id, columns.subject, columns.verb, columns.object, columns.attribute, columns.valueText, columns.comment, rawLine);
107
+ ids.push(id);
108
+ }
109
+ for (const edge of result.edges) {
110
+ insertEdge.run(ids[edge.parent], edge.role, ids[edge.child]);
111
+ }
112
+ registry = result.registry;
113
+ return { ids, edges: result.edges.length, problems: result.problems };
114
+ });
115
+ const rows = (sql, ...params) => db.prepare(sql).all(...params);
116
+ const contextsOf = (id) => db.prepare('SELECT context FROM cave_context WHERE claim_id = ?').all(id)
117
+ .map(row => row.context);
118
+ const tagsOf = (id) => db.prepare('SELECT key, value FROM cave_tag WHERE claim_id = ?').all(id);
119
+ const toClaim = (row) => Row.toClaim(row, contextsOf(row.id), tagsOf(row.id));
120
+ const traversalFilter = (options) => (options.negated === true ? '' : ' AND negated = 0') +
121
+ (options.retracted === true ? '' : ' AND conf > 0');
122
+ return {
123
+ /** Raw database handle — used by `@cavelang/query`; treat as read-only. */
124
+ db,
125
+ /** Current verb registry (input registry + stored + ingested declarations). */
126
+ registry: () => registry,
127
+ /**
128
+ * Parses, canonicalizes and appends CAVE text. Lenient by default —
129
+ * problems are returned, valid lines still land (spec §1.6); pass
130
+ * `strict` to throw instead.
131
+ */
132
+ ingest(text, options_ = {}) {
133
+ const result = Canonical.canonicalizeText(text, registry);
134
+ if (options_.strict === true && result.problems.length > 0) {
135
+ const detail = result.problems.map(problem => ` line ${problem.line}: ${problem.message}`).join('\n');
136
+ throw new Error(`CAVE ingest failed with ${result.problems.length} problem(s):\n${detail}`);
137
+ }
138
+ return insertResult(result);
139
+ },
140
+ /** Appends an already-canonicalized result. */
141
+ insertResult,
142
+ /** Latest row per claim key (spec §13.5), oldest first. */
143
+ currentBeliefs(options_ = {}) {
144
+ return options_.minConf === undefined ?
145
+ rows(`${currentSql} ORDER BY c.tx`) :
146
+ rows(`${currentSql} WHERE c.conf >= ? ORDER BY c.tx`, options_.minConf);
147
+ },
148
+ /** Current belief for one claim key, `undefined` if the fact is unknown. */
149
+ currentBelief(claimKey) {
150
+ const row = db.prepare('SELECT * FROM cave_claim WHERE claim_key = ? ORDER BY tx DESC LIMIT 1').get(claimKey);
151
+ return row;
152
+ },
153
+ /** Full belief series of one claim key, oldest first (spec §9.1). */
154
+ history(claimKey) {
155
+ return rows('SELECT * FROM cave_claim WHERE claim_key = ? ORDER BY tx', claimKey);
156
+ },
157
+ /** All rows about an entity, both directions, newest first (spec §13.5). */
158
+ claimsAbout(entity) {
159
+ return rows('SELECT * FROM cave_claim WHERE subject = ? OR object = ? ORDER BY tx DESC', entity, entity);
160
+ },
161
+ /** Forward reads: current relational facts with `entity` as subject (spec §13.3). */
162
+ forward(entity, options_ = {}) {
163
+ return rows(`SELECT * FROM (${currentSql}) WHERE subject = ? AND object IS NOT NULL${traversalFilter(options_)} ORDER BY tx`, entity).map(row => ({ verb: row.verb, target: row.object, row }));
164
+ },
165
+ /**
166
+ * Inverse reads (spec §13.3): current relational facts
167
+ * with `entity` as object, relation named via the registry's inverse
168
+ * when one is declared.
169
+ */
170
+ reverse(entity, options_ = {}) {
171
+ return rows(`SELECT * FROM (${currentSql}) WHERE object = ? AND object IS NOT NULL${traversalFilter(options_)} ORDER BY tx`, entity).map(row => {
172
+ const rel = Canonical.Registry.inverseOf(registry, row.verb);
173
+ return { verb: row.verb, ...rel === undefined ? {} : { rel }, source: row.subject, row };
174
+ });
175
+ },
176
+ /** Flat tag (`value` omitted → `value IS NULL`) or scoped tag rows (spec §13.5). */
177
+ byTag(key, value) {
178
+ return value === undefined ?
179
+ rows(`
180
+ SELECT c.* FROM cave_claim c JOIN cave_tag t ON c.id = t.claim_id
181
+ WHERE t.key = ? AND t.value IS NULL ORDER BY c.tx`, key) :
182
+ rows(`
183
+ SELECT c.* FROM cave_claim c JOIN cave_tag t ON c.id = t.claim_id
184
+ WHERE t.key = ? AND t.value = ? ORDER BY c.tx`, key, value);
185
+ },
186
+ /** Rows carrying a context (spec §13.5), newest first. */
187
+ byContext(context) {
188
+ return rows(`
189
+ SELECT c.* FROM cave_claim c JOIN cave_context ctx ON c.id = ctx.claim_id
190
+ WHERE ctx.context = ? ORDER BY c.tx DESC`, context);
191
+ },
192
+ /** Members of a topic — forward `CONTAINS` traversal (spec §11.2). */
193
+ topicMembers(topic, options_ = {}) {
194
+ return rows(`SELECT * FROM (${currentSql}) WHERE subject = ? AND verb = 'CONTAINS' AND object IS NOT NULL${traversalFilter(options_)} ORDER BY tx`, topic).map(row => row.object);
195
+ },
196
+ /** Topics containing an entity — the inverse `CONTAINS` read (spec §11.2). */
197
+ topicsOf(entity, options_ = {}) {
198
+ return rows(`SELECT * FROM (${currentSql}) WHERE object = ? AND verb = 'CONTAINS'${traversalFilter(options_)} ORDER BY tx`, entity).map(row => row.subject);
199
+ },
200
+ /**
201
+ * Full-text search, newest first. The query is treated as a literal
202
+ * phrase by default (safe for terms like `token-expiry`, which FTS5
203
+ * would otherwise parse as a column filter); pass `raw` to use full
204
+ * FTS5 MATCH syntax.
205
+ */
206
+ search(query, options_ = {}) {
207
+ const match = options_.raw === true ? query : `"${query.replaceAll('"', '""')}"`;
208
+ return rows(`
209
+ SELECT c.* FROM cave_claim c JOIN cave_fts f ON c.id = f.claim_id
210
+ WHERE cave_fts MATCH ? ORDER BY c.tx DESC`, match);
211
+ },
212
+ /** Qualifier/grouping edges of a claim row (spec §13.2). */
213
+ edgesOf(parentId) {
214
+ return db.prepare(`
215
+ SELECT e.role AS role, c.* FROM cave_edge e JOIN cave_claim c ON c.id = e.child_id
216
+ WHERE e.parent_id = ?`).all(parentId).map(({ role, ...child }) => ({ role, child: child }));
217
+ },
218
+ /** Reconstructs the full canonical claim of a row (side tables included). */
219
+ toClaim,
220
+ /**
221
+ * Emits the store as canonical CAVE text — all rows in transaction
222
+ * order, or only current beliefs with `current`.
223
+ *
224
+ * In current-only export an edge endpoint may be a superseded row;
225
+ * dropping such edges would silently un-condition current claims and
226
+ * promote orphaned WHEN conditions to top-level facts. Instead each
227
+ * endpoint resolves to the *current row of its claim key*, and the
228
+ * resulting edges are deduplicated.
229
+ */
230
+ exportText(options_ = {}) {
231
+ const current = options_.current === true;
232
+ const claimRows = current ?
233
+ rows(`${currentSql} ORDER BY c.tx`) :
234
+ rows('SELECT * FROM cave_claim ORDER BY tx');
235
+ const indexById = new Map(claimRows.map((row, index) => [row.id, index]));
236
+ let resolve = (id) => indexById.get(id);
237
+ if (current) {
238
+ const indexByKey = new Map(claimRows.map((row, index) => [row.claim_key, index]));
239
+ const keyById = new Map(db.prepare('SELECT id, claim_key FROM cave_claim').all()
240
+ .map(row => [row.id, row.claim_key]));
241
+ resolve = id => indexById.get(id) ?? indexByKey.get(keyById.get(id) ?? '');
242
+ }
243
+ const claims = claimRows.map(row => ({ claim: toClaim(row), line: 0 }));
244
+ const edgeRows = db.prepare('SELECT parent_id, role, child_id FROM cave_edge').all();
245
+ const seen = new Set();
246
+ const edges = edgeRows.flatMap(edge => {
247
+ const parent = resolve(edge.parent_id);
248
+ const child = resolve(edge.child_id);
249
+ if (parent === undefined || child === undefined || parent === child) {
250
+ return [];
251
+ }
252
+ const dedupe = `${parent}|${edge.role}|${child}`;
253
+ if (seen.has(dedupe)) {
254
+ return [];
255
+ }
256
+ seen.add(dedupe);
257
+ return [{ parent, role: edge.role, child }];
258
+ });
259
+ return Canonical.emit({ claims, edges });
260
+ },
261
+ close() {
262
+ db.close();
263
+ }
264
+ };
265
+ };
266
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cavelang/store",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CAVE persistence on node:sqlite — §13 storage model, append-only belief series, inverse-aware reads.",
6
+ "license": "CC0-1.0",
7
+ "author": "Mirek Rusin (https://github.com/mirek)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mirek/cave.git",
11
+ "directory": "packages/store"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.18"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/src/index.d.ts",
19
+ "default": "./dist/src/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/test",
25
+ "!dist/tsconfig.tsbuildinfo",
26
+ "src",
27
+ "!src/**/*.test.ts",
28
+ "README.md",
29
+ "License.md"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@cavelang/core": "0.1.0",
36
+ "@cavelang/canonical": "0.1.0"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -b",
40
+ "test": "node --disable-warning=ExperimentalWarning --test 'test/*.test.ts'",
41
+ "typecheck": "tsc -b"
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@cavelang/store` — CAVE persistence on the Node.js builtin `node:sqlite`
3
+ * (spec §13).
4
+ *
5
+ * ```ts
6
+ * import { open } from '@cavelang/store'
7
+ *
8
+ * const store = open('knowledge.db')
9
+ * store.ingest('monorepo CONTAINS packages/api')
10
+ * store.reverse('packages/api')
11
+ * // → [{ verb: 'CONTAINS', rel: 'PART-OF', source: 'monorepo', … }]
12
+ * ```
13
+ */
14
+
15
+ export * as Row from './row.ts'
16
+ export * as Schema from './schema.ts'
17
+ export { open } from './store.ts'
18
+ export type { ForwardFact, IngestResult, ReverseFact, Store, TraverseOptions } from './store.ts'
package/src/row.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Row mapping — canonical claims ↔ `cave_claim` rows.
3
+ *
4
+ * Terms are stored *formatted* (delimiters preserved): entities as plain
5
+ * text so §13.5 queries like `WHERE subject = 'auth/middleware'` work, code
6
+ * literals as `` `<=` `` and text literals as `"…"` so a literal never
7
+ * collides with a same-spelled entity. Values are stored as written
8
+ * (`value_text`, incl. `~`, multiplier letters and delimiters) alongside
9
+ * the normalized numeric value and unit (spec §13.4 step 9).
10
+ */
11
+
12
+ import { Claim, Value } from '@cavelang/core'
13
+
14
+ /** A `cave_claim` row. */
15
+ export type Row = {
16
+ readonly id: string
17
+ readonly tx: string
18
+ readonly subject: string
19
+ readonly verb: string
20
+ readonly negated: number
21
+ readonly object: null | string
22
+ readonly attribute: null | string
23
+ readonly value_text: null | string
24
+ readonly value_num: null | number
25
+ readonly value_unit: null | string
26
+ readonly value_approx: number
27
+ readonly delta_text: null | string
28
+ readonly delta_num: null | number
29
+ readonly delta_unit: null | string
30
+ readonly sigma_level: null | number
31
+ readonly conf: number
32
+ readonly importance: number
33
+ readonly comment: null | string
34
+ readonly raw_line: string
35
+ readonly claim_key: string
36
+ }
37
+
38
+ export type t = Row
39
+
40
+ /** @returns term parsed back from its stored formatted text. */
41
+ export const parseTerm = (text: string): Claim.Term => {
42
+ if (text.length >= 2 && text.startsWith('`') && text.endsWith('`')) {
43
+ return Claim.code(text.slice(1, -1))
44
+ }
45
+ if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
46
+ return Claim.text(text.slice(1, -1))
47
+ }
48
+ return Claim.entity(text)
49
+ }
50
+
51
+ /** @returns value parsed back from its stored as-written text. */
52
+ export const parseValue = (text: string): Value.t => {
53
+ if (text.length >= 2 && text.startsWith('`') && text.endsWith('`')) {
54
+ return Value.ofCode(text.slice(1, -1))
55
+ }
56
+ if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
57
+ return Value.ofText(text.slice(1, -1))
58
+ }
59
+ return Value.parse(text)
60
+ }
61
+
62
+ /** Column values of a claim, in `cave_claim` insert order (id/tx excluded). */
63
+ export const toColumns = (claim: Claim.t): {
64
+ subject: string
65
+ verb: string
66
+ negated: number
67
+ object: null | string
68
+ attribute: null | string
69
+ valueText: null | string
70
+ valueNum: null | number
71
+ valueUnit: null | string
72
+ valueApprox: number
73
+ deltaText: null | string
74
+ deltaNum: null | number
75
+ deltaUnit: null | string
76
+ sigmaLevel: number
77
+ conf: number
78
+ importance: number
79
+ comment: null | string
80
+ rawLine: string
81
+ claimKey: string
82
+ } => {
83
+ const payload = claim.payload
84
+ const value =
85
+ payload.kind === 'attribute' ? payload.value :
86
+ payload.kind === 'metric' ? payload.value :
87
+ undefined
88
+ return {
89
+ subject: Claim.formatTerm(claim.subject),
90
+ verb: claim.verb,
91
+ negated: claim.negated ? 1 : 0,
92
+ object: payload.kind === 'relation' ? Claim.formatTerm(payload.object) : null,
93
+ attribute: payload.kind === 'attribute' ? payload.attribute : null,
94
+ valueText: value === undefined ? null : Value.format(value),
95
+ valueNum: value?.num ?? null,
96
+ valueUnit: value?.unit ?? null,
97
+ valueApprox: value?.approx === true ? 1 : 0,
98
+ deltaText: claim.delta === undefined ? null : Value.format(claim.delta),
99
+ deltaNum: claim.delta?.num ?? null,
100
+ deltaUnit: claim.delta?.unit ?? null,
101
+ sigmaLevel: claim.sigmaLevel ?? 2,
102
+ conf: claim.conf,
103
+ importance: claim.importance ? 1 : 0,
104
+ comment: claim.comment ?? null,
105
+ rawLine: claim.raw,
106
+ claimKey: '' // filled by the store with Key.of on the canonical claim
107
+ }
108
+ }
109
+
110
+ /**
111
+ * @returns canonical claim reconstructed from a row plus its side tables.
112
+ * `sigmaLevel` collapses to `undefined` at the semantic default of 2.
113
+ */
114
+ export const toClaim = (
115
+ row: Row,
116
+ contexts: readonly string[],
117
+ tags: readonly { key: string, value: null | string }[]
118
+ ): Claim.t =>
119
+ Claim.of({
120
+ subject: parseTerm(row.subject),
121
+ verb: row.verb,
122
+ negated: row.negated !== 0,
123
+ payload:
124
+ row.object !== null ? Claim.relation(parseTerm(row.object)) :
125
+ row.attribute !== null && row.value_text !== null ? Claim.attribute(row.attribute, parseValue(row.value_text)) :
126
+ row.value_text !== null ? Claim.metric(parseValue(row.value_text)) :
127
+ Claim.none,
128
+ contexts,
129
+ tags: tags.map(tag => tag.value === null ? { key: tag.key } : { key: tag.key, value: tag.value }),
130
+ conf: row.conf,
131
+ importance: row.importance !== 0,
132
+ ...row.delta_text !== null ? { delta: parseValue(row.delta_text) } : {},
133
+ ...row.sigma_level !== null && row.sigma_level !== 2 ? { sigmaLevel: row.sigma_level } : {},
134
+ ...row.comment !== null ? { comment: row.comment } : {},
135
+ raw: row.raw_line
136
+ })
package/src/schema.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Storage schema (spec §13.1, §13.2) — verbatim from the specification,
3
+ * with `IF NOT EXISTS` added so opening an existing database is idempotent.
4
+ */
5
+
6
+ import type { DatabaseSync } from 'node:sqlite'
7
+
8
+ export const ddl = `
9
+ CREATE TABLE IF NOT EXISTS cave_claim (
10
+ id TEXT PRIMARY KEY, -- UUIDv7
11
+ tx TEXT NOT NULL, -- UUIDv7, lexicographic = transaction order
12
+
13
+ subject TEXT NOT NULL, -- canonical (primary) direction
14
+ verb TEXT NOT NULL, -- canonical (primary) verb
15
+ negated INTEGER NOT NULL DEFAULT 0,
16
+
17
+ object TEXT, -- relation object, entity, literal
18
+ attribute TEXT, -- for HAS attr: value
19
+ value_text TEXT, -- value as written (incl. ~, multiplier, delimiters)
20
+ value_num REAL, -- parsed numeric value when possible
21
+ value_unit TEXT, -- normalized unit string
22
+ value_approx INTEGER NOT NULL DEFAULT 0,
23
+
24
+ delta_text TEXT,
25
+ delta_num REAL,
26
+ delta_unit TEXT,
27
+ sigma_level REAL DEFAULT 2.0,
28
+
29
+ conf REAL NOT NULL DEFAULT 1.0,
30
+ importance INTEGER NOT NULL DEFAULT 0,
31
+
32
+ comment TEXT,
33
+ raw_line TEXT NOT NULL, -- exactly as written, incl. inverse form
34
+
35
+ claim_key TEXT NOT NULL -- normalized key; shared by forward/inverse readings
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_cave_claim_key_tx ON cave_claim (claim_key, tx);
39
+ CREATE INDEX IF NOT EXISTS idx_cave_subject ON cave_claim (subject);
40
+ CREATE INDEX IF NOT EXISTS idx_cave_verb ON cave_claim (verb);
41
+ CREATE INDEX IF NOT EXISTS idx_cave_object ON cave_claim (object);
42
+ CREATE INDEX IF NOT EXISTS idx_cave_attribute ON cave_claim (attribute);
43
+ CREATE INDEX IF NOT EXISTS idx_cave_conf ON cave_claim (conf);
44
+
45
+ CREATE TABLE IF NOT EXISTS cave_context (
46
+ claim_id TEXT NOT NULL,
47
+ context TEXT NOT NULL,
48
+ FOREIGN KEY (claim_id) REFERENCES cave_claim(id)
49
+ );
50
+ CREATE INDEX IF NOT EXISTS idx_cave_context ON cave_context (context);
51
+
52
+ CREATE TABLE IF NOT EXISTS cave_tag (
53
+ claim_id TEXT NOT NULL,
54
+ key TEXT NOT NULL,
55
+ value TEXT, -- NULL for flat tags (spec §13.2)
56
+ FOREIGN KEY (claim_id) REFERENCES cave_claim(id)
57
+ );
58
+ CREATE INDEX IF NOT EXISTS idx_cave_tag_key ON cave_tag (key, value);
59
+
60
+ CREATE TABLE IF NOT EXISTS cave_edge (
61
+ parent_id TEXT NOT NULL,
62
+ role TEXT NOT NULL, -- WHEN, VIA, BECAUSE, QUALIFIES
63
+ child_id TEXT NOT NULL,
64
+ FOREIGN KEY (parent_id) REFERENCES cave_claim(id),
65
+ FOREIGN KEY (child_id) REFERENCES cave_claim(id)
66
+ );
67
+ CREATE INDEX IF NOT EXISTS idx_cave_edge_parent ON cave_edge (parent_id);
68
+ CREATE INDEX IF NOT EXISTS idx_cave_edge_child ON cave_edge (child_id);
69
+ CREATE INDEX IF NOT EXISTS idx_cave_edge_role ON cave_edge (role);
70
+
71
+ CREATE VIRTUAL TABLE IF NOT EXISTS cave_fts USING fts5(
72
+ claim_id, subject, verb, object, attribute, value_text, comment, raw_line
73
+ );
74
+ `
75
+
76
+ /** Creates all tables and indexes. */
77
+ export const init = (db: DatabaseSync): void => {
78
+ db.exec(ddl)
79
+ }