@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/runtime.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { Verb } from '@cavelang/core'
2
+ import * as Canonical from '@cavelang/canonical'
3
+ import type { Adapter } from './adapter.ts'
4
+ import * as QuerySql from './query-sql.ts'
5
+ import { open as openStore } from './store.ts'
6
+ import type { Store as BaseStore } from './store.ts'
7
+
8
+ export type Store = BaseStore & {
9
+ /**
10
+ * Verb registry reconstructed from the configured base registry plus
11
+ * declaration rows visible at the transaction-time boundary.
12
+ */
13
+ readonly registryAsOf: (asOf: string) => Canonical.Registry.t
14
+ }
15
+
16
+ export type OpenOptions = {
17
+ readonly registry?: Canonical.Registry.t
18
+ }
19
+
20
+ type Declaration = {
21
+ readonly subject: string
22
+ readonly verb: string
23
+ readonly object: string
24
+ }
25
+
26
+ const declarationsAsOf = (store: BaseStore, asOf: string): Declaration[] => {
27
+ const boundary = QuerySql.asOfBoundary(asOf)
28
+ if (boundary === undefined) {
29
+ throw new Error(`CAVE: cannot parse as-of boundary ${JSON.stringify(asOf)}`)
30
+ }
31
+ return store.db.prepare(`
32
+ SELECT subject, verb, object FROM cave_claim
33
+ WHERE tx ${boundary.operator} ?
34
+ AND negated = 0 AND object IS NOT NULL AND verb IN ('REVERSE', 'RENAMED-TO', 'IS')
35
+ AND id NOT IN (SELECT child_id FROM cave_edge WHERE role IN ('WHEN', 'VIA', 'BECAUSE'))
36
+ ORDER BY tx
37
+ `).all(boundary.tx) as Declaration[]
38
+ }
39
+
40
+ const applyDeclarations = (
41
+ baseRegistry: Canonical.Registry.t,
42
+ declarations: readonly Declaration[]
43
+ ): Canonical.Registry.t => {
44
+ let registry = baseRegistry
45
+ for (const declaration of declarations) {
46
+ if (!Verb.isVerbToken(declaration.subject)) continue
47
+ if (declaration.verb === 'REVERSE' && Verb.isVerbToken(declaration.object)) {
48
+ registry = Canonical.Registry.declareReverse(
49
+ registry,
50
+ declaration.subject,
51
+ declaration.object
52
+ ).registry
53
+ } else if (declaration.verb === 'RENAMED-TO' && Verb.isVerbToken(declaration.object)) {
54
+ registry = Canonical.Registry.declareRename(
55
+ registry,
56
+ declaration.subject,
57
+ declaration.object
58
+ ).registry
59
+ } else if (declaration.verb === 'IS' && declaration.object === 'verb') {
60
+ registry = Canonical.Registry.declareVerb(registry, declaration.subject)
61
+ }
62
+ }
63
+ return registry
64
+ }
65
+
66
+ /** Open a CAVE store with an explicitly selected SQLite implementation. */
67
+ export const openWith = (
68
+ adapter: Adapter,
69
+ path: string = ':memory:',
70
+ options: OpenOptions = {}
71
+ ): Store => {
72
+ const baseRegistry = options.registry ?? Canonical.standardRegistry
73
+ const store = openStore(adapter, path, options)
74
+ return Object.assign(store, {
75
+ registryAsOf: (asOf: string): Canonical.Registry.t =>
76
+ applyDeclarations(baseRegistry, declarationsAsOf(store, asOf))
77
+ })
78
+ }
package/src/schema.ts CHANGED
@@ -1,11 +1,11 @@
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
- */
1
+ /** Ordered, transactional storage schema migrations (spec §13). */
2
+
3
+ import type { Capabilities, Database } from './adapter.ts'
4
+ import * as Provenance from './provenance.ts'
5
5
 
6
- import type { DatabaseSync } from 'node:sqlite'
6
+ export const currentVersion = 1
7
7
 
8
- export const ddl = `
8
+ const ddlBeforeFts = `
9
9
  CREATE TABLE IF NOT EXISTS cave_claim (
10
10
  id TEXT PRIMARY KEY, -- UUIDv7
11
11
  tx TEXT NOT NULL, -- UUIDv7, lexicographic = transaction order
@@ -50,6 +50,15 @@ CREATE TABLE IF NOT EXISTS cave_context (
50
50
  CREATE INDEX IF NOT EXISTS idx_cave_context ON cave_context (context);
51
51
  CREATE INDEX IF NOT EXISTS idx_cave_context_claim ON cave_context (claim_id, context);
52
52
 
53
+ CREATE TABLE IF NOT EXISTS cave_provenance (
54
+ claim_id TEXT NOT NULL,
55
+ dimension TEXT NOT NULL CHECK (dimension IN ('actor', 'source', 'run', 'domain')),
56
+ value TEXT NOT NULL,
57
+ PRIMARY KEY (claim_id, dimension, value),
58
+ FOREIGN KEY (claim_id) REFERENCES cave_claim(id)
59
+ );
60
+ CREATE INDEX IF NOT EXISTS idx_cave_provenance_lookup ON cave_provenance (dimension, value, claim_id);
61
+
53
62
  CREATE TABLE IF NOT EXISTS cave_tag (
54
63
  claim_id TEXT NOT NULL,
55
64
  key TEXT NOT NULL,
@@ -70,12 +79,128 @@ CREATE INDEX IF NOT EXISTS idx_cave_edge_parent ON cave_edge (parent_id);
70
79
  CREATE INDEX IF NOT EXISTS idx_cave_edge_child ON cave_edge (child_id);
71
80
  CREATE INDEX IF NOT EXISTS idx_cave_edge_role ON cave_edge (role);
72
81
 
73
- CREATE VIRTUAL TABLE IF NOT EXISTS cave_fts USING fts5(
82
+ `
83
+
84
+ const ftsDdl = (fullText: Capabilities['fullText']): string => `
85
+ CREATE VIRTUAL TABLE IF NOT EXISTS cave_fts USING ${fullText}(
74
86
  claim_id, subject, verb, object, attribute, value_text, comment, raw_line
75
87
  );
76
88
  `
77
89
 
78
- /** Creates all tables and indexes. */
79
- export const init = (db: DatabaseSync): void => {
80
- db.exec(ddl)
90
+ /** Default Node/FTS5 schema SQL retained for callers that inspect the DDL. */
91
+ export const ddl = ddlBeforeFts + ftsDdl('fts5')
92
+
93
+ const ddlFor = (capabilities: Capabilities): string =>
94
+ ddlBeforeFts + ftsDdl(capabilities.fullText)
95
+
96
+ type Migration = {
97
+ readonly version: number
98
+ readonly up: (db: Database, capabilities: Capabilities) => void
99
+ }
100
+
101
+ const migrations: readonly Migration[] = [
102
+ {
103
+ version: 1,
104
+ up: (db, capabilities) => {
105
+ db.exec(ddlFor(capabilities))
106
+ Provenance.backfill(db)
107
+ }
108
+ }
109
+ ]
110
+
111
+ const versionOf = (db: Database): number =>
112
+ (db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
113
+
114
+ const requiredTables = [
115
+ 'cave_claim', 'cave_context', 'cave_provenance', 'cave_tag', 'cave_edge', 'cave_fts'
116
+ ] as const
117
+
118
+ const requiredIndexes = [
119
+ 'idx_cave_claim_key_tx', 'idx_cave_subject', 'idx_cave_verb', 'idx_cave_object',
120
+ 'idx_cave_attribute', 'idx_cave_conf', 'idx_cave_context', 'idx_cave_context_claim',
121
+ 'idx_cave_provenance_lookup', 'idx_cave_tag_key', 'idx_cave_tag_claim',
122
+ 'idx_cave_edge_parent', 'idx_cave_edge_child', 'idx_cave_edge_role'
123
+ ] as const
124
+
125
+ const requiredColumns: Readonly<Record<string, readonly string[]>> = {
126
+ cave_claim: [
127
+ 'id', 'tx', 'subject', 'verb', 'negated', 'object', 'attribute', 'value_text',
128
+ 'value_num', 'value_unit', 'value_approx', 'delta_text', 'delta_num', 'delta_unit',
129
+ 'sigma_level', 'conf', 'importance', 'comment', 'raw_line', 'claim_key'
130
+ ],
131
+ cave_context: ['claim_id', 'context'],
132
+ cave_provenance: ['claim_id', 'dimension', 'value'],
133
+ cave_tag: ['claim_id', 'key', 'value'],
134
+ cave_edge: ['parent_id', 'role', 'child_id'],
135
+ cave_fts: ['claim_id', 'subject', 'verb', 'object', 'attribute', 'value_text', 'comment', 'raw_line']
136
+ }
137
+
138
+ export const validate = (db: Database, version: number, schema = 'main'): void => {
139
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schema)) {
140
+ throw new Error(`CAVE: invalid SQLite schema name ${JSON.stringify(schema)}`)
141
+ }
142
+ const objects = new Map(
143
+ (db.prepare(`SELECT name, type FROM ${schema}.sqlite_schema WHERE name LIKE 'cave_%' OR name LIKE 'idx_cave_%'`)
144
+ .all() as { name: string, type: string }[]).map(row => [row.name, row.type])
145
+ )
146
+ const problems = [
147
+ ...requiredTables.flatMap(name => objects.has(name) ? [] : [`missing table ${name}`]),
148
+ ...requiredIndexes.flatMap(name => objects.get(name) === 'index' ? [] : [`missing index ${name}`]),
149
+ ...Object.entries(requiredColumns).flatMap(([table, required]) => {
150
+ if (!objects.has(table)) {
151
+ return []
152
+ }
153
+ const columns = new Set(
154
+ (db.prepare(`PRAGMA ${schema}.table_info(${table})`).all() as { name: string }[]).map(row => row.name)
155
+ )
156
+ return required.flatMap(column => columns.has(column) ? [] : [`missing column ${table}.${column}`])
157
+ })
158
+ ]
159
+ if (problems.length > 0) {
160
+ throw new Error(`CAVE: schema version ${version} is incompatible: ${problems.join(', ')}`)
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Upgrade every supported older version in order. Each step owns one SQLite
166
+ * transaction, including its `user_version` update, so interruption leaves
167
+ * either the old version or the complete next version and reopen can resume.
168
+ */
169
+ export const init = (
170
+ db: Database,
171
+ capabilities: Capabilities = {
172
+ transactions: { immediate: true, savepoints: true },
173
+ fullText: 'fts5',
174
+ }
175
+ ): void => {
176
+ let version = versionOf(db)
177
+ if (version > currentVersion) {
178
+ throw new Error(
179
+ `CAVE: schema version ${version} is newer than this runtime supports (${currentVersion}); upgrade CAVE`)
180
+ }
181
+ for (const migration of migrations) {
182
+ if (migration.version <= version) {
183
+ continue
184
+ }
185
+ if (migration.version !== version + 1) {
186
+ throw new Error(`CAVE: no schema migration path from version ${version} to ${migration.version}`)
187
+ }
188
+ db.exec('BEGIN IMMEDIATE')
189
+ try {
190
+ migration.up(db, capabilities)
191
+ db.exec(`PRAGMA user_version = ${migration.version}`)
192
+ validate(db, migration.version)
193
+ db.exec('COMMIT')
194
+ version = migration.version
195
+ } catch (error) {
196
+ try {
197
+ db.exec('ROLLBACK')
198
+ } catch {
199
+ // SQLite may already have rolled back a failed statement.
200
+ }
201
+ const detail = error instanceof Error ? error.message : String(error)
202
+ throw new Error(`CAVE: schema migration ${version} -> ${migration.version} failed: ${detail}`)
203
+ }
204
+ }
205
+ validate(db, version)
81
206
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Sensitivity labels and row visibility (spec §9.7).
3
+ *
4
+ * Claims opt into a level with `#sensitivity:<level>`. Unlabelled rows are
5
+ * `internal`; malformed, flat, or unknown labels fail closed as `restricted`.
6
+ * Several labels on one row resolve to the most restrictive one.
7
+ */
8
+
9
+ export const levels = ['public', 'internal', 'confidential', 'restricted'] as const
10
+ export type Level = (typeof levels)[number]
11
+
12
+ export const defaultLevel: Level = 'internal'
13
+ export const defaultMaximum: Level = 'internal'
14
+
15
+ export const parse = (value: string): undefined | Level =>
16
+ levels.includes(value as Level) ? value as Level : undefined
17
+
18
+ export const rank = (level: Level): number => levels.indexOf(level)
19
+
20
+ /**
21
+ * SQL predicate that allows rows at or below `maximum`. `alias` must be a
22
+ * trusted SQL identifier supplied by CAVE code, never user input.
23
+ */
24
+ export const sql = (alias: string, maximum: Level = defaultMaximum): string =>
25
+ `COALESCE((SELECT MAX(CASE s.value ` +
26
+ `WHEN 'public' THEN 0 WHEN 'internal' THEN 1 WHEN 'confidential' THEN 2 WHEN 'restricted' THEN 3 ELSE 3 END) ` +
27
+ `FROM cave_tag s WHERE s.claim_id = ${alias}.id AND s.key = 'sensitivity'), ${rank(defaultLevel)}) <= ${rank(maximum)}`
28
+
29
+ /** Most restrictive recognized label; malformed labels are `restricted`. */
30
+ export const ofTags = (tags: readonly { key: string, value?: null | string }[]): Level => {
31
+ let effective: undefined | Level
32
+ for (const tag of tags) {
33
+ if (tag.key !== 'sensitivity') {
34
+ continue
35
+ }
36
+ const level = tag.value === undefined || tag.value === null ? 'restricted' : parse(tag.value) ?? 'restricted'
37
+ if (effective === undefined || rank(level) > rank(effective)) {
38
+ effective = level
39
+ }
40
+ }
41
+ return effective ?? defaultLevel
42
+ }