@cavelang/store 0.27.14 → 0.29.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/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/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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
+
export const currentVersion = 1
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
79
|
-
export const
|
|
80
|
-
|
|
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
|
+
}
|