@cavelang/store 0.27.14 → 0.29.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/Authors.md +5 -0
- package/License.md +6 -0
- package/README.md +121 -14
- package/dist/src/adapter-entry.d.ts +9 -0
- package/dist/src/adapter-entry.d.ts.map +1 -0
- package/dist/src/adapter-entry.js +7 -0
- package/dist/src/adapter-entry.js.map +1 -0
- package/dist/src/adapter.d.ts +51 -0
- package/dist/src/adapter.d.ts.map +1 -0
- package/dist/src/adapter.js +3 -0
- package/dist/src/adapter.js.map +1 -0
- package/dist/src/backup.d.ts +23 -0
- package/dist/src/backup.d.ts.map +1 -0
- package/dist/src/backup.js +164 -0
- package/dist/src/backup.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/node-adapter-entry.d.ts +3 -0
- package/dist/src/node-adapter-entry.d.ts.map +1 -0
- package/dist/src/node-adapter-entry.js +3 -0
- package/dist/src/node-adapter-entry.js.map +1 -0
- package/dist/src/node-adapter.d.ts +4 -0
- package/dist/src/node-adapter.d.ts.map +1 -0
- package/dist/src/node-adapter.js +20 -0
- package/dist/src/node-adapter.js.map +1 -0
- package/dist/src/open.d.ts +3 -18
- package/dist/src/open.d.ts.map +1 -1
- package/dist/src/open.js +4 -52
- package/dist/src/open.js.map +1 -1
- package/dist/src/provenance.d.ts +24 -0
- package/dist/src/provenance.d.ts.map +1 -0
- package/dist/src/provenance.js +75 -0
- package/dist/src/provenance.js.map +1 -0
- package/dist/src/query-sql.d.ts +45 -0
- package/dist/src/query-sql.d.ts.map +1 -0
- package/dist/src/query-sql.js +77 -0
- package/dist/src/query-sql.js.map +1 -0
- package/dist/src/record.d.ts +29 -0
- package/dist/src/record.d.ts.map +1 -0
- package/dist/src/record.js +50 -0
- package/dist/src/record.js.map +1 -0
- package/dist/src/resolve.d.ts +2 -2
- package/dist/src/resolve.d.ts.map +1 -1
- package/dist/src/resolve.js +0 -0
- package/dist/src/resolve.js.map +1 -1
- package/dist/src/runtime.d.ts +16 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +43 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/schema.d.ts +10 -6
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +111 -9
- package/dist/src/schema.js.map +1 -1
- package/dist/src/sensitivity.d.ts +24 -0
- package/dist/src/sensitivity.d.ts.map +1 -0
- package/dist/src/sensitivity.js +34 -0
- package/dist/src/sensitivity.js.map +1 -0
- package/dist/src/store.d.ts +37 -13
- package/dist/src/store.d.ts.map +1 -1
- package/dist/src/store.js +107 -72
- package/dist/src/store.js.map +1 -1
- package/package.json +25 -6
- package/src/adapter-entry.ts +18 -0
- package/src/adapter.ts +57 -0
- package/src/backup.ts +188 -0
- package/src/index.ts +8 -0
- package/src/node-adapter-entry.ts +3 -0
- package/src/node-adapter.ts +23 -0
- package/src/open.ts +7 -80
- package/src/provenance.ts +109 -0
- package/src/query-sql.ts +99 -0
- package/src/record.ts +82 -0
- package/src/resolve.ts +0 -0
- package/src/runtime.ts +78 -0
- package/src/schema.ts +135 -10
- package/src/sensitivity.ts +42 -0
- package/src/store.ts +134 -80
package/src/store.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The CAVE store (spec §13) — append-only claim persistence on the Node.js
|
|
3
|
-
*
|
|
3
|
+
* through an explicit synchronous SQLite adapter.
|
|
4
4
|
*
|
|
5
5
|
* - one row per fact, canonical direction; inverses are query-time views
|
|
6
6
|
* over existing indexes, never materialized rows (spec §13.3);
|
|
7
7
|
* - current belief = latest tx per claim key (spec §9.1, §13.5);
|
|
8
8
|
* - the verb registry is rebuilt from stored in-band declaration claims on
|
|
9
|
-
* open, so a reopened database keeps its inverse vocabulary;
|
|
9
|
+
* open, so a reopened database keeps its inverse and lifecycle vocabulary;
|
|
10
10
|
* - full-text search over subjects, objects, values, comments and raw
|
|
11
11
|
* lines via FTS5;
|
|
12
|
-
* - appends
|
|
12
|
+
* - appends record actor provenance and keep compatibility stamps (spec §9.5): pass `source` and every
|
|
13
13
|
* claim without a `src:` context gets `@src:<actor>` — applied before the
|
|
14
14
|
* claim key is computed, so different actors keep separate belief series;
|
|
15
15
|
* - contradiction resolution (spec §26): coexisting series about one fact
|
|
@@ -18,65 +18,27 @@
|
|
|
18
18
|
* traversal opt-in.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { DatabaseSync } from 'node:sqlite'
|
|
22
21
|
import { Claim, Context, Key, Uuidv7, Verb } from '@cavelang/core'
|
|
23
22
|
import * as Canonical from '@cavelang/canonical'
|
|
23
|
+
import type { Adapter } from './adapter.ts'
|
|
24
|
+
import * as QuerySql from './query-sql.ts'
|
|
24
25
|
import * as Resolve from './resolve.ts'
|
|
25
26
|
import * as Row from './row.ts'
|
|
26
27
|
import * as Schema from './schema.ts'
|
|
28
|
+
import * as Sensitivity from './sensitivity.ts'
|
|
29
|
+
import * as Provenance from './provenance.ts'
|
|
30
|
+
import * as Record from './record.ts'
|
|
27
31
|
|
|
28
|
-
const currentSql =
|
|
29
|
-
SELECT c.* FROM cave_claim c
|
|
30
|
-
JOIN (
|
|
31
|
-
SELECT claim_key, MAX(tx) AS max_tx
|
|
32
|
-
FROM cave_claim GROUP BY claim_key
|
|
33
|
-
) latest ON c.claim_key = latest.claim_key AND c.tx = latest.max_tx
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Alias edges (spec §13.6): current positive `ALIAS` claims read as
|
|
38
|
-
* undirected (`ALIAS` has no `REVERSE`, so each written direction is its
|
|
39
|
-
* own claim key — both assert the same link). Retraction unmerges:
|
|
40
|
-
* `a ALIAS b @ 0%` drops that direction's edge. Both endpoints must be
|
|
41
|
-
* entity-form — the closure is defined for entity terms only, so a row
|
|
42
|
-
* naming a `"…"`/`` `…` `` literal contributes no edge and two entities
|
|
43
|
-
* aliasing one literal never link through it.
|
|
44
|
-
*/
|
|
45
|
-
const aliasEdgeSql = `alias_edge(a, b) AS (
|
|
46
|
-
SELECT c.subject, c.object FROM (${currentSql}) c
|
|
47
|
-
WHERE c.verb = 'ALIAS' AND c.negated = 0 AND c.conf > 0 AND c.object IS NOT NULL
|
|
48
|
-
AND ${Row.entityTermSql('c.subject')} AND ${Row.entityTermSql('c.object')}
|
|
49
|
-
UNION
|
|
50
|
-
SELECT c.object, c.subject FROM (${currentSql}) c
|
|
51
|
-
WHERE c.verb = 'ALIAS' AND c.negated = 0 AND c.conf > 0 AND c.object IS NOT NULL
|
|
52
|
-
AND ${Row.entityTermSql('c.subject')} AND ${Row.entityTermSql('c.object')}
|
|
53
|
-
)`
|
|
32
|
+
const currentSql = QuerySql.current()
|
|
54
33
|
|
|
55
34
|
/**
|
|
56
35
|
* The closure walked from a seed entity — the seed is the query's first
|
|
57
36
|
* positional parameter. Requires `alias_edge` in scope.
|
|
58
37
|
*/
|
|
59
|
-
const aliasSeedSql =
|
|
60
|
-
SELECT ?
|
|
61
|
-
UNION
|
|
62
|
-
SELECT e.b FROM alias_closure s JOIN alias_edge e ON e.a = s.name
|
|
63
|
-
)`
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The full transitive closure as ordered pairs — every two names currently
|
|
67
|
-
* believed to denote one entity. Resolution grouping widens through it
|
|
68
|
-
* (spec §26.1). Requires `alias_edge` in scope.
|
|
69
|
-
*/
|
|
70
|
-
const aliasPairSql = `alias_pair(a, b) AS (
|
|
71
|
-
SELECT a, b FROM alias_edge
|
|
72
|
-
UNION
|
|
73
|
-
SELECT p.a, e.b FROM alias_pair p JOIN alias_edge e ON e.a = p.b
|
|
74
|
-
)`
|
|
38
|
+
const aliasSeedSql = QuerySql.aliasSeed()
|
|
75
39
|
|
|
76
40
|
/** Seeded alias closure (spec §13.6), ready to prefix a SELECT. */
|
|
77
|
-
const aliasClosureSql =
|
|
78
|
-
WITH RECURSIVE ${aliasEdgeSql}, ${aliasSeedSql}
|
|
79
|
-
`
|
|
41
|
+
const aliasClosureSql = QuerySql.aliasClosure(currentSql)
|
|
80
42
|
|
|
81
43
|
export type IngestResult = {
|
|
82
44
|
/** ids of the batch's claim rows, in document order — for a row skipped as already present (`ids` replay, spec §28.1), the existing id. */
|
|
@@ -90,7 +52,8 @@ export type IngestResult = {
|
|
|
90
52
|
|
|
91
53
|
export type AppendOptions = {
|
|
92
54
|
/**
|
|
93
|
-
* Actor provenance (spec §9.5):
|
|
55
|
+
* Actor provenance (spec §9.5): record actor `<source>` and stamp
|
|
56
|
+
* `@src:<source>` on every appended
|
|
94
57
|
* claim that carries no `src:` context — e.g. `cli`, `agent/claude-code`,
|
|
95
58
|
* `ingest/93a01c626b3f`. Applied before the claim key is computed, so the
|
|
96
59
|
* stamp is part of claim identity; claims that already name a source keep
|
|
@@ -99,15 +62,23 @@ export type AppendOptions = {
|
|
|
99
62
|
*/
|
|
100
63
|
readonly source?: string
|
|
101
64
|
/**
|
|
102
|
-
* Lifecycle
|
|
65
|
+
* Lifecycle ownership (spec §9.5.1): record run `<source>` and stamp
|
|
66
|
+
* `@src:<source>` even when the
|
|
103
67
|
* claim already names a source. Connect records, rule conclusions and
|
|
104
|
-
* action effects are found for retraction and attribution by their
|
|
105
|
-
*
|
|
106
|
-
* kept (multi-source rows resolve per §26.3). The exact stamp context
|
|
68
|
+
* action effects are found for retraction and attribution by their run,
|
|
69
|
+
* so an authored `src:` context must not displace it — both contexts are
|
|
70
|
+
* kept for compatibility (multi-source rows resolve per §26.3). The exact stamp context
|
|
107
71
|
* is never duplicated. Without this flag an authored source suppresses
|
|
108
72
|
* the stamp.
|
|
109
73
|
*/
|
|
110
74
|
readonly lifecycle?: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Additional structured contexts applied to every row before keying. Used
|
|
77
|
+
* by connectors to attach source spans without rewriting generated text.
|
|
78
|
+
*/
|
|
79
|
+
readonly contexts?: readonly Context.t[]
|
|
80
|
+
/** Explicit provenance dimensions; compact contexts remain unchanged. */
|
|
81
|
+
readonly provenance?: Provenance.Input
|
|
111
82
|
/**
|
|
112
83
|
* Explicit row identity (spec §28.1), index-aligned with the result's
|
|
113
84
|
* claims: a claim with an id here is replayed under it — inserted with
|
|
@@ -133,6 +104,11 @@ const stampSource = (claim: Claim.t, source: undefined | string, lifecycle: bool
|
|
|
133
104
|
{ ...claim, contexts: [...claim.contexts, context] }
|
|
134
105
|
}
|
|
135
106
|
|
|
107
|
+
const addContexts = (claim: Claim.t, contexts: readonly Context.t[] = []): Claim.t => {
|
|
108
|
+
const combined = Context.dedupe([...claim.contexts, ...contexts])
|
|
109
|
+
return combined.length === claim.contexts.length ? claim : { ...claim, contexts: combined }
|
|
110
|
+
}
|
|
111
|
+
|
|
136
112
|
export type ForwardFact = {
|
|
137
113
|
/** Canonical (primary) verb. */
|
|
138
114
|
readonly verb: string
|
|
@@ -187,18 +163,34 @@ export const defaultDbPath = (): string =>
|
|
|
187
163
|
* already stored; pass `Canonical.Registry.empty` for a declaration-free
|
|
188
164
|
* start.
|
|
189
165
|
*/
|
|
190
|
-
export const open = (
|
|
191
|
-
|
|
166
|
+
export const open = (
|
|
167
|
+
adapter: Adapter,
|
|
168
|
+
path: string = ':memory:',
|
|
169
|
+
options: { registry?: Canonical.Registry.t } = {}
|
|
170
|
+
) => {
|
|
171
|
+
const db = adapter.open(path)
|
|
172
|
+
// Concurrent writers wait for the database allocation lock instead of
|
|
173
|
+
// failing immediately with SQLITE_BUSY.
|
|
174
|
+
db.exec('PRAGMA busy_timeout = 5000')
|
|
192
175
|
db.exec('PRAGMA foreign_keys = ON')
|
|
193
|
-
|
|
176
|
+
try {
|
|
177
|
+
Schema.init(db, adapter.capabilities)
|
|
178
|
+
} catch (error) {
|
|
179
|
+
db.close()
|
|
180
|
+
throw error
|
|
181
|
+
}
|
|
194
182
|
|
|
195
183
|
// The receive rule (spec §28.2): the store, not the wall clock, is the
|
|
196
184
|
// monotonic authority — every append outsorts every tx already stored,
|
|
197
185
|
// merged history from fast-clocked origins included.
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
186
|
+
const selectMaxTx = db.prepare('SELECT MAX(tx) AS tx FROM cave_claim')
|
|
187
|
+
const observeMaxTx = (): void => {
|
|
188
|
+
const maxTx = selectMaxTx.get() as undefined | { tx: null | string }
|
|
189
|
+
if (maxTx?.tx != null) {
|
|
190
|
+
Uuidv7.observe(maxTx.tx)
|
|
191
|
+
}
|
|
201
192
|
}
|
|
193
|
+
observeMaxTx()
|
|
202
194
|
|
|
203
195
|
const baseRegistry = options.registry ?? Canonical.standardRegistry
|
|
204
196
|
let registry = baseRegistry
|
|
@@ -212,6 +204,8 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
212
204
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
213
205
|
`)
|
|
214
206
|
const insertContext = db.prepare('INSERT INTO cave_context (claim_id, context) VALUES (?, ?)')
|
|
207
|
+
const insertProvenance = db.prepare(
|
|
208
|
+
'INSERT OR IGNORE INTO cave_provenance (claim_id, dimension, value) VALUES (?, ?, ?)')
|
|
215
209
|
const insertTag = db.prepare('INSERT INTO cave_tag (claim_id, key, value) VALUES (?, ?, ?)')
|
|
216
210
|
const insertEdge = db.prepare('INSERT INTO cave_edge (parent_id, role, child_id) VALUES (?, ?, ?)')
|
|
217
211
|
const insertFts = db.prepare(`
|
|
@@ -230,7 +224,7 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
230
224
|
const rebuildRegistry = (): void => {
|
|
231
225
|
const declarations = db.prepare(`
|
|
232
226
|
SELECT subject, verb, object FROM cave_claim
|
|
233
|
-
WHERE negated = 0 AND object IS NOT NULL AND verb IN ('REVERSE', 'IS')
|
|
227
|
+
WHERE negated = 0 AND object IS NOT NULL AND verb IN ('REVERSE', 'RENAMED-TO', 'IS')
|
|
234
228
|
AND id NOT IN (SELECT child_id FROM cave_edge WHERE role IN ('WHEN', 'VIA', 'BECAUSE'))
|
|
235
229
|
ORDER BY tx
|
|
236
230
|
`).all() as { subject: string, verb: string, object: string }[]
|
|
@@ -240,6 +234,8 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
240
234
|
}
|
|
241
235
|
if (declaration.verb === 'REVERSE' && Verb.isVerbToken(declaration.object)) {
|
|
242
236
|
registry = Canonical.Registry.declareReverse(registry, declaration.subject, declaration.object).registry
|
|
237
|
+
} else if (declaration.verb === 'RENAMED-TO' && Verb.isVerbToken(declaration.object)) {
|
|
238
|
+
registry = Canonical.Registry.declareRename(registry, declaration.subject, declaration.object).registry
|
|
243
239
|
} else if (declaration.verb === 'IS' && declaration.object === 'verb') {
|
|
244
240
|
registry = Canonical.Registry.declareVerb(registry, declaration.subject)
|
|
245
241
|
}
|
|
@@ -248,25 +244,34 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
248
244
|
rebuildRegistry()
|
|
249
245
|
|
|
250
246
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
247
|
+
* The outer transaction takes SQLite's write-reservation lock before any
|
|
248
|
+
* transaction id is allocated, serializing concurrent processes. Nested
|
|
249
|
+
* transactions use savepoints, so a caller can still wrap several appends
|
|
250
|
+
* and checks and roll the group back by throwing (spec §20.3 write gating).
|
|
251
|
+
* Rollback also restores the in-memory verb registry.
|
|
256
252
|
*/
|
|
257
253
|
let transactionDepth = 0
|
|
258
254
|
const transaction = <T>(body: () => T): T => {
|
|
255
|
+
const outer = transactionDepth === 0
|
|
259
256
|
const savepoint = `cave_tx_${transactionDepth}`
|
|
260
257
|
transactionDepth += 1
|
|
261
258
|
const savedRegistry = registry
|
|
262
|
-
|
|
259
|
+
let started = false
|
|
263
260
|
try {
|
|
261
|
+
db.exec(outer ? 'BEGIN IMMEDIATE' : `SAVEPOINT ${savepoint}`)
|
|
262
|
+
started = true
|
|
264
263
|
const result = body()
|
|
265
|
-
db.exec(`RELEASE ${savepoint}`)
|
|
264
|
+
db.exec(outer ? 'COMMIT' : `RELEASE ${savepoint}`)
|
|
266
265
|
return result
|
|
267
266
|
} catch (error) {
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
if (started) {
|
|
268
|
+
if (outer) {
|
|
269
|
+
db.exec('ROLLBACK')
|
|
270
|
+
} else {
|
|
271
|
+
db.exec(`ROLLBACK TO ${savepoint}`)
|
|
272
|
+
db.exec(`RELEASE ${savepoint}`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
270
275
|
registry = savedRegistry
|
|
271
276
|
throw error
|
|
272
277
|
} finally {
|
|
@@ -283,6 +288,12 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
283
288
|
const ids: string[] = []
|
|
284
289
|
const replay = options_.ids !== undefined
|
|
285
290
|
let skipped = 0
|
|
291
|
+
// BEGIN IMMEDIATE makes this read and all following inserts one
|
|
292
|
+
// database-serialized allocation step. A process that opened before a
|
|
293
|
+
// fast-clock peer wrote now observes that peer before minting (§28.2).
|
|
294
|
+
if (result.claims.some((_, index) => options_.ids?.[index] === undefined)) {
|
|
295
|
+
observeMaxTx()
|
|
296
|
+
}
|
|
286
297
|
result.claims.forEach((entry, index) => {
|
|
287
298
|
const explicit = options_.ids?.[index]
|
|
288
299
|
if (explicit !== undefined) {
|
|
@@ -295,7 +306,14 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
295
306
|
return
|
|
296
307
|
}
|
|
297
308
|
}
|
|
298
|
-
const
|
|
309
|
+
const authored = addContexts(entry.claim, options_.contexts)
|
|
310
|
+
const claim = stampSource(authored, options_.source, options_.lifecycle === true)
|
|
311
|
+
const provided = options_.provenance ?? {}
|
|
312
|
+
const provenance = Provenance.entries(authored.contexts, {
|
|
313
|
+
...provided,
|
|
314
|
+
actor: provided.actor ?? options_.source,
|
|
315
|
+
run: provided.run ?? (options_.lifecycle === true ? options_.source : undefined)
|
|
316
|
+
})
|
|
299
317
|
const id = explicit ?? Uuidv7.next()
|
|
300
318
|
const columns = Row.toColumns(claim)
|
|
301
319
|
const rawLine = columns.rawLine === '' ? Canonical.emitClaim(claim) : columns.rawLine
|
|
@@ -311,6 +329,9 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
311
329
|
for (const context of claim.contexts) {
|
|
312
330
|
insertContext.run(id, context)
|
|
313
331
|
}
|
|
332
|
+
for (const entry of provenance) {
|
|
333
|
+
insertProvenance.run(id, entry.dimension, entry.value)
|
|
334
|
+
}
|
|
314
335
|
for (const tag of claim.tags) {
|
|
315
336
|
insertTag.run(id, tag.key, tag.value ?? null)
|
|
316
337
|
}
|
|
@@ -347,6 +368,18 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
347
368
|
const toClaim = (row: Row.t): Claim.t =>
|
|
348
369
|
Row.toClaim(row, contextsOf(row.id), tagsOf(row.id))
|
|
349
370
|
|
|
371
|
+
const provenanceOf = (row: Row.t | string): Provenance.t => {
|
|
372
|
+
const id = typeof row === 'string' ? row : row.id
|
|
373
|
+
const entries = db.prepare(`
|
|
374
|
+
SELECT dimension, value FROM cave_provenance WHERE claim_id = ? ORDER BY dimension, value
|
|
375
|
+
`).all(id) as Provenance.Entry[]
|
|
376
|
+
return Provenance.fromEntries(entries)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Stable, versioned public representation of one storage row. */
|
|
380
|
+
const recordOf = (row: Row.t): Record.t =>
|
|
381
|
+
Record.of(row, toClaim(row), provenanceOf(row))
|
|
382
|
+
|
|
350
383
|
const traversalFilter = (options: TraverseOptions): string =>
|
|
351
384
|
(options.negated === true ? '' : ' AND negated = 0') +
|
|
352
385
|
(options.retracted === true ? '' : ' AND conf > 0')
|
|
@@ -364,7 +397,7 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
364
397
|
options.aliases === true ?
|
|
365
398
|
// Resolution grouping needs the pair closure too (spec §26.1).
|
|
366
399
|
(options.resolve === true ?
|
|
367
|
-
`\nWITH RECURSIVE ${
|
|
400
|
+
`\nWITH RECURSIVE ${QuerySql.aliasPairs(currentSql)}, ${aliasSeedSql}\n` :
|
|
368
401
|
aliasClosureSql) :
|
|
369
402
|
''
|
|
370
403
|
|
|
@@ -381,12 +414,18 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
381
414
|
currentSql
|
|
382
415
|
|
|
383
416
|
return {
|
|
384
|
-
/**
|
|
417
|
+
/** Adapter that owns this database connection and its capabilities. */
|
|
418
|
+
adapter,
|
|
419
|
+
|
|
420
|
+
/** Raw adapter database handle — used by `@cavelang/query`; treat as read-only. */
|
|
385
421
|
db,
|
|
386
422
|
|
|
387
423
|
/** Current verb registry (input registry + stored + ingested declarations). */
|
|
388
424
|
registry: (): Canonical.Registry.t => registry,
|
|
389
425
|
|
|
426
|
+
/** Configured registry before any in-band declarations are applied. */
|
|
427
|
+
baseRegistry: (): Canonical.Registry.t => baseRegistry,
|
|
428
|
+
|
|
390
429
|
/**
|
|
391
430
|
* Rebuilds the registry from the base plus stored declarations — after
|
|
392
431
|
* rows arrive outside `ingest`/`insertResult` (a §28 merge writes
|
|
@@ -462,7 +501,7 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
462
501
|
*/
|
|
463
502
|
resolvedBeliefs(options_: { aliases?: boolean } = {}): Row.t[] {
|
|
464
503
|
const aliases = options_.aliases === true
|
|
465
|
-
const prefix = aliases ? `WITH RECURSIVE ${
|
|
504
|
+
const prefix = aliases ? `WITH RECURSIVE ${QuerySql.aliasPairs(currentSql)}\n` : ''
|
|
466
505
|
return rows(`${prefix}SELECT * FROM (${Resolve.resolvedSql(readPolicy(), currentSql, { aliases })}) ORDER BY tx`)
|
|
467
506
|
},
|
|
468
507
|
|
|
@@ -475,7 +514,7 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
475
514
|
*/
|
|
476
515
|
contested(options_: { aliases?: boolean } = {}): Resolve.Contested[] {
|
|
477
516
|
const aliases = options_.aliases === true
|
|
478
|
-
const prefix = aliases ? `WITH RECURSIVE ${
|
|
517
|
+
const prefix = aliases ? `WITH RECURSIVE ${QuerySql.aliasPairs(currentSql)}\n` : ''
|
|
479
518
|
const ranked = rows(
|
|
480
519
|
`${prefix}SELECT * FROM (${Resolve.rankedSql(readPolicy(), currentSql, { aliases })}) ORDER BY res_group, res_rank`
|
|
481
520
|
) as Resolve.Ranked[]
|
|
@@ -556,6 +595,19 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
556
595
|
WHERE ctx.context = ? ORDER BY c.tx DESC`, context)
|
|
557
596
|
},
|
|
558
597
|
|
|
598
|
+
/** Rows carrying one explicit provenance dimension, newest first. */
|
|
599
|
+
byProvenance(dimension: Provenance.Dimension, value: string): Row.t[] {
|
|
600
|
+
return rows(`
|
|
601
|
+
SELECT c.* FROM cave_claim c JOIN cave_provenance p ON c.id = p.claim_id
|
|
602
|
+
WHERE p.dimension = ? AND p.value = ? ORDER BY c.tx DESC`, dimension, value)
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
/** Explicit actor, physical source, lifecycle run, and domain metadata. */
|
|
606
|
+
provenanceOf,
|
|
607
|
+
|
|
608
|
+
/** Map a database-shaped row to the stable `cave.claim/v1` contract. */
|
|
609
|
+
recordOf,
|
|
610
|
+
|
|
559
611
|
/** Members of a topic — forward `CONTAINS` traversal (spec §11.2). */
|
|
560
612
|
topicMembers(topic: string, options_: TraverseOptions = {}): string[] {
|
|
561
613
|
return rows(
|
|
@@ -579,11 +631,12 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
579
631
|
* FTS5 MATCH syntax. `limit` caps the rows inside the query itself,
|
|
580
632
|
* so a broad search never materializes more than the caller reads.
|
|
581
633
|
*/
|
|
582
|
-
search(query: string, options_: { raw?: boolean, limit?: number } = {}): Row.t[] {
|
|
634
|
+
search(query: string, options_: { raw?: boolean, limit?: number, maxSensitivity?: Sensitivity.Level } = {}): Row.t[] {
|
|
583
635
|
const match = options_.raw === true ? query : `"${query.replaceAll('"', '""')}"`
|
|
584
636
|
const sql = `
|
|
585
637
|
SELECT c.* FROM cave_claim c JOIN cave_fts f ON c.id = f.claim_id
|
|
586
|
-
WHERE cave_fts MATCH ?
|
|
638
|
+
WHERE cave_fts MATCH ?${options_.maxSensitivity === undefined ? '' : ` AND ${Sensitivity.sql('c', options_.maxSensitivity)}`}
|
|
639
|
+
ORDER BY c.tx DESC`
|
|
587
640
|
return options_.limit === undefined ?
|
|
588
641
|
rows(sql, match) :
|
|
589
642
|
rows(`${sql} LIMIT ?`, match, options_.limit)
|
|
@@ -629,11 +682,12 @@ export const open = (path: string = ':memory:', options: { registry?: Canonical.
|
|
|
629
682
|
* endpoint resolves to the *current row of its claim key*, and the
|
|
630
683
|
* resulting edges are deduplicated.
|
|
631
684
|
*/
|
|
632
|
-
exportText(options_: { current?: boolean, tx?: boolean } = {}): string {
|
|
685
|
+
exportText(options_: { current?: boolean, tx?: boolean, maxSensitivity?: Sensitivity.Level } = {}): string {
|
|
633
686
|
const current = options_.current === true
|
|
687
|
+
const maximum = options_.maxSensitivity ?? Sensitivity.defaultMaximum
|
|
634
688
|
const claimRows = current ?
|
|
635
|
-
rows(`${currentSql} ORDER BY c.tx`) :
|
|
636
|
-
rows(
|
|
689
|
+
rows(`${currentSql} WHERE ${Sensitivity.sql('c', maximum)} ORDER BY c.tx`) :
|
|
690
|
+
rows(`SELECT c.* FROM cave_claim c WHERE ${Sensitivity.sql('c', maximum)} ORDER BY c.tx`)
|
|
637
691
|
const indexById = new Map(claimRows.map((row, index) => [row.id, index]))
|
|
638
692
|
let resolve = (id: string): undefined | number => indexById.get(id)
|
|
639
693
|
if (current) {
|