@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.
Files changed (79) hide show
  1. package/Authors.md +5 -0
  2. package/License.md +6 -0
  3. package/README.md +121 -14
  4. package/dist/src/adapter-entry.d.ts +9 -0
  5. package/dist/src/adapter-entry.d.ts.map +1 -0
  6. package/dist/src/adapter-entry.js +7 -0
  7. package/dist/src/adapter-entry.js.map +1 -0
  8. package/dist/src/adapter.d.ts +51 -0
  9. package/dist/src/adapter.d.ts.map +1 -0
  10. package/dist/src/adapter.js +3 -0
  11. package/dist/src/adapter.js.map +1 -0
  12. package/dist/src/backup.d.ts +23 -0
  13. package/dist/src/backup.d.ts.map +1 -0
  14. package/dist/src/backup.js +164 -0
  15. package/dist/src/backup.js.map +1 -0
  16. package/dist/src/index.d.ts +8 -0
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/index.js +5 -0
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/node-adapter-entry.d.ts +3 -0
  21. package/dist/src/node-adapter-entry.d.ts.map +1 -0
  22. package/dist/src/node-adapter-entry.js +3 -0
  23. package/dist/src/node-adapter-entry.js.map +1 -0
  24. package/dist/src/node-adapter.d.ts +4 -0
  25. package/dist/src/node-adapter.d.ts.map +1 -0
  26. package/dist/src/node-adapter.js +20 -0
  27. package/dist/src/node-adapter.js.map +1 -0
  28. package/dist/src/open.d.ts +3 -18
  29. package/dist/src/open.d.ts.map +1 -1
  30. package/dist/src/open.js +4 -52
  31. package/dist/src/open.js.map +1 -1
  32. package/dist/src/provenance.d.ts +24 -0
  33. package/dist/src/provenance.d.ts.map +1 -0
  34. package/dist/src/provenance.js +75 -0
  35. package/dist/src/provenance.js.map +1 -0
  36. package/dist/src/query-sql.d.ts +45 -0
  37. package/dist/src/query-sql.d.ts.map +1 -0
  38. package/dist/src/query-sql.js +77 -0
  39. package/dist/src/query-sql.js.map +1 -0
  40. package/dist/src/record.d.ts +29 -0
  41. package/dist/src/record.d.ts.map +1 -0
  42. package/dist/src/record.js +50 -0
  43. package/dist/src/record.js.map +1 -0
  44. package/dist/src/resolve.d.ts +2 -2
  45. package/dist/src/resolve.d.ts.map +1 -1
  46. package/dist/src/resolve.js +0 -0
  47. package/dist/src/resolve.js.map +1 -1
  48. package/dist/src/runtime.d.ts +16 -0
  49. package/dist/src/runtime.d.ts.map +1 -0
  50. package/dist/src/runtime.js +43 -0
  51. package/dist/src/runtime.js.map +1 -0
  52. package/dist/src/schema.d.ts +10 -6
  53. package/dist/src/schema.d.ts.map +1 -1
  54. package/dist/src/schema.js +111 -9
  55. package/dist/src/schema.js.map +1 -1
  56. package/dist/src/sensitivity.d.ts +24 -0
  57. package/dist/src/sensitivity.d.ts.map +1 -0
  58. package/dist/src/sensitivity.js +34 -0
  59. package/dist/src/sensitivity.js.map +1 -0
  60. package/dist/src/store.d.ts +37 -13
  61. package/dist/src/store.d.ts.map +1 -1
  62. package/dist/src/store.js +107 -72
  63. package/dist/src/store.js.map +1 -1
  64. package/package.json +25 -6
  65. package/src/adapter-entry.ts +18 -0
  66. package/src/adapter.ts +57 -0
  67. package/src/backup.ts +188 -0
  68. package/src/index.ts +8 -0
  69. package/src/node-adapter-entry.ts +3 -0
  70. package/src/node-adapter.ts +23 -0
  71. package/src/open.ts +7 -80
  72. package/src/provenance.ts +109 -0
  73. package/src/query-sql.ts +99 -0
  74. package/src/record.ts +82 -0
  75. package/src/resolve.ts +0 -0
  76. package/src/runtime.ts +78 -0
  77. package/src/schema.ts +135 -10
  78. package/src/sensitivity.ts +42 -0
  79. 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
- * builtin `node:sqlite`.
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 can stamp actor provenance (spec §9.5): pass `source` and every
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 = `alias_closure(name) AS (
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): stamp `@src:<source>` on every appended
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 stamping (spec §9.5): stamp `@src:<source>` even when the
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
- * stamp, so an authored `src:` context must not displace it — both are
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 = (path: string = ':memory:', options: { registry?: Canonical.Registry.t } = {}) => {
191
- const db = new DatabaseSync(path)
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
- Schema.init(db)
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 maxTx = db.prepare('SELECT MAX(tx) AS tx FROM cave_claim').get() as undefined | { tx: null | string }
199
- if (maxTx?.tx != null) {
200
- Uuidv7.observe(maxTx.tx)
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
- * Savepoint-based, so transactions nest: a caller can wrap several
252
- * appends or an append plus checks against the appended state — and
253
- * roll the whole group back by throwing (spec §20.3 write gating).
254
- * Rollback also restores the in-memory verb registry, so declarations
255
- * from rolled-back claims don't outlive their rows.
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
- db.exec(`SAVEPOINT ${savepoint}`)
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
- db.exec(`ROLLBACK TO ${savepoint}`)
269
- db.exec(`RELEASE ${savepoint}`)
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 claim = stampSource(entry.claim, options_.source, options_.lifecycle === true)
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 ${aliasEdgeSql}, ${aliasPairSql}, ${aliasSeedSql}\n` :
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
- /** Raw database handle used by `@cavelang/query`; treat as read-only. */
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 ${aliasEdgeSql}, ${aliasPairSql}\n` : ''
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 ${aliasEdgeSql}, ${aliasPairSql}\n` : ''
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 ? ORDER BY c.tx DESC`
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('SELECT * FROM cave_claim ORDER BY tx')
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) {