@gnolith/taproot 0.1.0-rc.0 → 0.3.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/COMPATIBILITY.md +15 -3
  3. package/LICENSE +21 -21
  4. package/README.md +122 -46
  5. package/SECURITY.md +10 -3
  6. package/SUPPORT.md +6 -1
  7. package/dist/authorization-maintenance.d.ts +55 -0
  8. package/dist/authorization-maintenance.d.ts.map +1 -0
  9. package/dist/authorization-maintenance.js +686 -0
  10. package/dist/authorization-maintenance.js.map +1 -0
  11. package/dist/authorization.d.ts +98 -0
  12. package/dist/authorization.d.ts.map +1 -0
  13. package/dist/authorization.js +1137 -0
  14. package/dist/authorization.js.map +1 -0
  15. package/dist/canonical.d.ts +2 -1
  16. package/dist/canonical.d.ts.map +1 -1
  17. package/dist/canonical.js +9 -1
  18. package/dist/canonical.js.map +1 -1
  19. package/dist/errors.d.ts +10 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +10 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +127 -50
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +352 -44
  26. package/dist/index.js.map +1 -1
  27. package/dist/migrations.d.ts +42 -0
  28. package/dist/migrations.d.ts.map +1 -0
  29. package/dist/migrations.js +550 -0
  30. package/dist/migrations.js.map +1 -0
  31. package/dist/rdf.js +1 -1
  32. package/dist/rdf.js.map +1 -1
  33. package/dist/repository.d.ts +22 -11
  34. package/dist/repository.d.ts.map +1 -1
  35. package/dist/repository.js +596 -63
  36. package/dist/repository.js.map +1 -1
  37. package/dist/schema.d.ts +41 -7
  38. package/dist/schema.d.ts.map +1 -1
  39. package/dist/schema.js +722 -83
  40. package/dist/schema.js.map +1 -1
  41. package/dist/types.d.ts +59 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/docs/api.md +70 -18
  44. package/docs/architecture.md +21 -8
  45. package/docs/authorization.md +82 -0
  46. package/docs/completion-audit.md +22 -16
  47. package/docs/operations.md +48 -9
  48. package/docs/product-scope.md +11 -4
  49. package/docs/release-policy.md +4 -0
  50. package/docs/testing.md +31 -5
  51. package/docs/threat-model.md +38 -6
  52. package/examples/d1-diamond-interop/README.md +12 -0
  53. package/examples/d1-diamond-interop/demo.ts +281 -0
  54. package/migrations/0002_audit_operations.sql +4 -3
  55. package/migrations/0003_canonical_statement_text.sql +31 -0
  56. package/migrations/0004_canonical_authorization_policy.sql +213 -0
  57. package/package.json +12 -7
  58. package/examples/codex-site/.openai/hosting.json +0 -3
  59. package/examples/codex-site/README.md +0 -13
  60. package/examples/codex-site/demo.ts +0 -152
@@ -1,20 +1,27 @@
1
1
  # Product scope
2
2
 
3
- Taproot exists so a Codex Site can own a Wikibase-style knowledge graph in D1
3
+ Taproot exists so a D1-backed application can own a Wikibase-style knowledge graph
4
4
  and query it through Diamond without operating MediaWiki or another external
5
5
  knowledge service.
6
6
 
7
- Version 0.1 is complete for the package's declared entity domain: Wikibase
7
+ Version 0.3 covers the package's declared entity domain: Wikibase
8
8
  core Items and Properties, canonical JSON interchange, statements and all
9
9
  their components, supported datatypes, revision/lifecycle history,
10
10
  attribution and audit, search, RDF/SPARQL projection, bulk workflows,
11
- migration, integrity, repair, policy hooks, and operational documentation.
11
+ migration, integrity, repair, canonical entity authorization policy, and
12
+ operational documentation.
12
13
 
13
14
  The following are separate products or future compatibility domains rather
14
15
  than missing Taproot implementation: MCP transport, agent orchestration,
15
- authentication/authorization, UI, wiki pages, media bytes, MediaWiki APIs,
16
+ authentication, principal/membership/session management, UI, wiki pages,
17
+ media bytes, MediaWiki APIs,
16
18
  Lexeme/Form/Sense, MediaInfo, and EntitySchema entity storage.
17
19
 
20
+ Complete-Site assembly, resource provisioning, remote migration execution,
21
+ deployment, and production acceptance are also outside this package. The Codex
22
+ agent creating a Gnolith Site owns those responsibilities. Taproot qualifies
23
+ only its published package and supported local runtime contracts.
24
+
18
25
  Adding an extension entity type changes IDs, JSON, commands, RDF, migration,
19
26
  and compatibility fixtures. It must be designed and versioned as an explicit
20
27
  new domain rather than accepted as unvalidated JSON.
@@ -11,6 +11,10 @@ inspects the packed artifact, publishes to npm through OIDC trusted publishing,
11
11
  and attaches the tarball, SBOM, checksums, and build attestation to a GitHub
12
12
  Release.
13
13
 
14
+ A Taproot release qualifies the published package and its supported local
15
+ runtime contracts. It does not assemble, provision, deploy, or accept a complete
16
+ Gnolith Site; the Codex agent creating a Site owns those responsibilities.
17
+
14
18
  The current minor line receives fixes. Security support follows `SECURITY.md`.
15
19
  Deprecations are announced in the changelog before removal whenever a safe
16
20
  migration path exists.
package/docs/testing.md CHANGED
@@ -11,11 +11,37 @@ projection repair, shared RDF values, and injected RDF failures. The injected
11
11
  failure proves that canonical JSON, revisions, audit events, search terms,
12
12
  ownership, and Diamond quads roll back as one D1 batch.
13
13
 
14
- `test/example.test.ts` runs the documented Codex Site example and queries its
15
- Taproot-generated graph through Diamond SPARQL. `consumer:check` packs the
14
+ `test/interop.test.ts` runs the documented local D1 and Diamond interoperability
15
+ example and queries its Taproot-generated graph through Diamond SPARQL.
16
+
17
+ `test/persistence.test.ts` exercises Diamond's Node SQLite adapter in memory
18
+ and on disk, including read-only planning, immutable identity, close/reopen,
19
+ same-adapter enforcement, late-batch rollback, and concurrent writers.
20
+ `consumer:check` packs the
16
21
  package, installs the tarball into a fresh temporary project with registry
17
22
  Diamond and Miniflare dependencies, initializes a new D1 database, writes an
18
- entity, and verifies the RDF through SPARQL using only public package exports.
23
+ entity, and verifies the RDF through privileged package-test SPARQL using only
24
+ public package exports. This proves RDF interoperability, not authorization of
25
+ a user-facing SPARQL endpoint.
26
+
27
+ `npm run check` is the complete local package release-quality gate. Coverage
28
+ thresholds are enforced in `vitest.config.ts`; lowering them requires an
29
+ explained review. Miniflare/Workerd and packed-consumer success verifies the
30
+ package's supported D1 runtime contract, not assembly, provisioning, deployment,
31
+ or acceptance of a complete Gnolith Site. The Codex agent creating a Site owns
32
+ those checks.
33
+
34
+ ## Miniflare dependency security
35
+
36
+ Miniflare is a development-only dependency used to qualify Taproot against its
37
+ supported Workerd D1 runtime. Taproot applies an exact root override of
38
+ `sharp@0.35.3` because Miniflare currently declares `sharp@0.34.5`, which is
39
+ affected by GHSA-f88m-g3jw-g9cj. This is a temporary upstream-compatibility
40
+ exception tracked in [Cloudflare workers-sdk PR #14493](https://github.com/cloudflare/workers-sdk/pull/14493).
41
+ Remove the override once a Miniflare release in Taproot's supported range
42
+ declares a non-vulnerable Sharp version and the full D1, interop,
43
+ packed-consumer, Node 22/24, and audit gates pass without it.
19
44
 
20
- `npm run check` is the complete local release-quality gate. Coverage thresholds
21
- are enforced in `vitest.config.ts`; lowering them requires an explained review.
45
+ The override is qualified for Taproot's Miniflare D1 test use. It is not a claim
46
+ of general Cloudflare Images compatibility, nor a compatibility claim for
47
+ macOS, ARM, or musl environments.
@@ -8,9 +8,19 @@ association between an edit and its claimed attribution metadata.
8
8
 
9
9
  ## Trust boundaries
10
10
 
11
- The host application is trusted to authenticate and authorize callers, select
12
- the D1 binding, supply an absolute site-owned base IRI, and protect mutation
13
- routes. Attribution is a stored claim, not proof of identity. Diamond executes
11
+ Taproot assumes its consumer supplies a SQLite/D1 binding and absolute base IRI.
12
+ Hosts create `AuthorizationContext` values only from authenticated state;
13
+ Taproot loads persisted canonical policies and checks them before and after
14
+ hydration. The normal package export has no raw canonical repository. Every
15
+ public mutation requires a runtime-branded installation guard, a current
16
+ context with exact `knowledge:write`, current target visibility, and canonical
17
+ policy. Policy changes require exact `knowledge:policy`. Invalid guards or
18
+ contexts fail before database work or observers. Mutation receipts do not
19
+ disclose canonical bodies. Host assembly withholds the database binding,
20
+ host capability, guard, and keys from request, user, agent, and MCP code. The
21
+ host capability is bootstrap/maintenance authority, while ordinary mutations
22
+ remain per-entity policy authorized.
23
+ Attribution is a stored claim, not proof of identity. Diamond executes
14
24
  the RDF patch prepared by Taproot and exposes read-only SPARQL querying; hosts
15
25
  must not give untrusted callers an independent write path to Taproot or Diamond
16
26
  tables.
@@ -19,22 +29,44 @@ tables.
19
29
 
20
30
  - Lost updates are blocked by expected-revision guards inside the same D1 batch
21
31
  as all canonical and projected writes.
32
+ - Authorization ABA races are blocked by a shared counter CAS plus a unique
33
+ durable advance ID asserted inside the same ordered batch.
22
34
  - Partial writes are blocked by D1 batch atomicity; injected Workerd failures
23
35
  verify rollback across Taproot and Diamond.
24
36
  - History tampering is deterred and detected by immutable-table triggers,
25
37
  SHA-256 content/parent chains, schema inspection, and integrity APIs.
26
38
  - Resource exhaustion is bounded by entity, command, page, bulk, redirect, and
27
- RDF patch limits. Hosts still need request-size, authentication, and rate
28
- limits at the network boundary.
39
+ RDF patch limits. Request-size, authentication, and network rate limits are
40
+ outside the package boundary.
29
41
  - RDF projection confusion is constrained by strict entity validation,
30
42
  deterministic mapping, site-owned namespaces, and per-entity quad ownership.
43
+ - Application/search canonical reads fail closed on absent policy,
44
+ cross-installation identity, stale authorization revision, policy changes
45
+ during hydration, and malformed CNF visibility. Denials have a generic body.
46
+ - Search administration requires the exact `search:admin` capability; personas
47
+ and generic administrator labels do not imply it.
48
+ - Page cursors require a non-extractable host AES-GCM key, use fixed-size
49
+ plaintext padding, and bind caller, grants, operation, query/filter, auth
50
+ revision, and search generation.
31
51
  - Dependency and release compromise is reduced through locked installs, pinned
32
52
  GitHub Actions, dependency review, CodeQL, secret scanning, license checks,
33
53
  OIDC npm publishing, provenance, SBOMs, checksums, and attestations.
34
54
 
35
55
  ## Out of scope
36
56
 
37
- Taproot does not provide authentication, authorization, agent/MCP isolation,
57
+ Taproot does not provide authentication, principal/workspace persistence, agent/MCP isolation,
38
58
  network rate limiting, D1 backup custody, UI sanitization, or arbitrary SPARQL
39
59
  Update. A compromised host with direct D1 access can bypass library controls;
40
60
  restore from a trusted backup and verify audit chains after such an incident.
61
+
62
+ Taproot's local Workerd tests validate its package controls under the supported
63
+ D1 contract. They do not qualify a deployed host. The Codex agent creating a
64
+ Site owns assembly, provisioning, route and security wiring, deployment, backup
65
+ policy, and production acceptance.
66
+
67
+ Diamond's database-level SPARQL handler sees Taproot's complete RDF projection
68
+ and is therefore privileged host maintenance/debug infrastructure, not a
69
+ normal user, agent, MCP, or search endpoint. Workshop must construct an
70
+ authorization-scoped dataset (with a final canonical recheck) before exposing
71
+ SPARQL results. Possession of the SQLite/D1 binding itself remains privileged:
72
+ a compromised host can query tables outside any package API.
@@ -0,0 +1,12 @@
1
+ # Local D1 and Diamond interoperability example
2
+
3
+ Pass a D1-compatible database to `runTaprootInteropDemo(db)`. The demo
4
+ initializes Taproot and Diamond in the same database, creates Properties and an
5
+ Item, applies revision-guarded edits, and queries Taproot's RDF projection
6
+ through Diamond SPARQL. The SPARQL call is a package interoperability probe
7
+ over a privileged database binding, not an authorization-safe endpoint.
8
+
9
+ The automated test runs this example locally with Miniflare's Workerd D1
10
+ implementation. It verifies the published package's D1 and Diamond contracts;
11
+ it does not assemble, provision, deploy, or accept a complete Gnolith Site.
12
+ Those responsibilities belong to the Codex agent creating that Site.
@@ -0,0 +1,281 @@
1
+ import {
2
+ RevisionConflictError,
3
+ SEARCH_ADMIN_CAPABILITY,
4
+ KNOWLEDGE_WRITE_CAPABILITY,
5
+ KNOWLEDGE_POLICY_CAPABILITY,
6
+ addStatement,
7
+ bootstrapTaprootAuthorization,
8
+ createAuthorizationCursorCodec,
9
+ createAuthorizedTaproot,
10
+ createItem,
11
+ createProperty,
12
+ createTaprootHostWriteCapability,
13
+ createInstallationAuthorizationGuard,
14
+ exportEntityJson,
15
+ initializeTaproot,
16
+ setLabel,
17
+ setStatementRank,
18
+ type CanonicalAuthorizationPolicyInput,
19
+ type D1DatabaseLike,
20
+ type Statement,
21
+ type AuthorizationContext,
22
+ } from '@gnolith/taproot';
23
+ import { createSparqlHandler } from '@gnolith/diamond';
24
+
25
+ export async function runTaprootInteropDemo(db: D1DatabaseLike) {
26
+ await initializeTaproot(db, { baseIri: 'https://knowledge.example' });
27
+ const options = { baseIri: 'https://knowledge.example' };
28
+ const writeCapability = createTaprootHostWriteCapability(
29
+ db,
30
+ options,
31
+ await crypto.subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, false, [
32
+ 'sign',
33
+ ]),
34
+ );
35
+ await bootstrapTaprootAuthorization(
36
+ db,
37
+ options,
38
+ writeCapability,
39
+ 'demo-installation',
40
+ );
41
+ const authorizationGuard = await createInstallationAuthorizationGuard(
42
+ db,
43
+ options,
44
+ writeCapability,
45
+ );
46
+
47
+ await createProperty(db, options, authorizationGuard, writer(1), {
48
+ datatype: 'string',
49
+ labels: { en: { language: 'en', value: 'occupation' } },
50
+ authorization: policy(1),
51
+ });
52
+ await createProperty(db, options, authorizationGuard, writer(2), {
53
+ datatype: 'time',
54
+ labels: { en: { language: 'en', value: 'point in time' } },
55
+ authorization: policy(2),
56
+ });
57
+ await createProperty(db, options, authorizationGuard, writer(3), {
58
+ datatype: 'url',
59
+ labels: { en: { language: 'en', value: 'reference URL' } },
60
+ authorization: policy(3),
61
+ });
62
+
63
+ const created = await createItem(db, options, authorizationGuard, writer(4), {
64
+ labels: { en: { language: 'en', value: 'Ada Lovelace' } },
65
+ descriptions: {
66
+ en: { language: 'en', value: 'English mathematician' },
67
+ },
68
+ attribution: {
69
+ id: 'agent:demo-curator',
70
+ kind: 'agent',
71
+ tool: 'gnolith-mcp',
72
+ },
73
+ requestId: 'demo-request',
74
+ authorization: policy(4),
75
+ });
76
+ const occupation: Statement = {
77
+ id: `${created.entityId}$occupation`,
78
+ type: 'statement',
79
+ text: 'Ada Lovelace worked as a computer programmer in 1843.',
80
+ rank: 'normal',
81
+ mainsnak: {
82
+ snaktype: 'value',
83
+ property: 'P1',
84
+ datatype: 'string',
85
+ datavalue: { type: 'string', value: 'computer programmer' },
86
+ },
87
+ qualifiers: {
88
+ P2: [
89
+ {
90
+ snaktype: 'value',
91
+ property: 'P2',
92
+ datatype: 'time',
93
+ datavalue: {
94
+ type: 'time',
95
+ value: {
96
+ time: '+1843-01-01T00:00:00Z',
97
+ timezone: 0,
98
+ before: 0,
99
+ after: 0,
100
+ precision: 9,
101
+ calendarmodel: 'http://www.wikidata.org/entity/Q1985727',
102
+ },
103
+ },
104
+ },
105
+ ],
106
+ },
107
+ 'qualifiers-order': ['P2'],
108
+ references: [
109
+ {
110
+ hash: 'demo-source',
111
+ snaks: {
112
+ P3: [
113
+ {
114
+ snaktype: 'value',
115
+ property: 'P3',
116
+ datatype: 'url',
117
+ datavalue: {
118
+ type: 'string',
119
+ value: 'https://example.test/source',
120
+ },
121
+ },
122
+ ],
123
+ },
124
+ 'snaks-order': ['P3'],
125
+ },
126
+ ],
127
+ };
128
+ const added = await addStatement(
129
+ db,
130
+ options,
131
+ authorizationGuard,
132
+ writer(5, true),
133
+ created.entityId,
134
+ occupation,
135
+ {
136
+ expectedRevision: created.newRevision,
137
+ authorization: policy(5, { [occupation.id]: [] }),
138
+ },
139
+ );
140
+ const preferred = await setStatementRank(
141
+ db,
142
+ options,
143
+ authorizationGuard,
144
+ writer(6),
145
+ created.entityId,
146
+ occupation.id,
147
+ 'preferred',
148
+ 'Ada Lovelace worked as a computer programmer in 1843 (preferred).',
149
+ {
150
+ expectedRevision: added.newRevision,
151
+ authorization: policy(6, { [occupation.id]: [] }),
152
+ },
153
+ );
154
+ const unknownOccupation: Statement = {
155
+ id: `${created.entityId}$unknown-occupation`,
156
+ type: 'statement',
157
+ text: 'Ada Lovelace had another occupation whose value is unknown.',
158
+ rank: 'normal',
159
+ mainsnak: {
160
+ snaktype: 'somevalue',
161
+ property: 'P1',
162
+ datatype: 'string',
163
+ },
164
+ qualifiers: {},
165
+ 'qualifiers-order': [],
166
+ references: [],
167
+ };
168
+ await addStatement(
169
+ db,
170
+ options,
171
+ authorizationGuard,
172
+ writer(7, true),
173
+ created.entityId,
174
+ unknownOccupation,
175
+ {
176
+ expectedRevision: preferred.newRevision,
177
+ authorization: policy(7, {
178
+ [occupation.id]: [],
179
+ [unknownOccupation.id]: [],
180
+ }),
181
+ },
182
+ );
183
+
184
+ const sparql = createSparqlHandler({ db });
185
+ const query = `SELECT ?occupation WHERE {
186
+ <https://knowledge.example/entity/${created.entityId}>
187
+ <https://knowledge.example/prop/direct/P1> ?occupation
188
+ }`;
189
+ const response = await sparql(
190
+ new Request(
191
+ `https://interop.example/sparql?query=${encodeURIComponent(query)}`,
192
+ { headers: { accept: 'application/sparql-results+json' } },
193
+ ),
194
+ );
195
+
196
+ let staleRevisionRejected = false;
197
+ try {
198
+ await setLabel(
199
+ db,
200
+ options,
201
+ authorizationGuard,
202
+ writer(8),
203
+ created.entityId,
204
+ 'en',
205
+ 'stale edit',
206
+ {
207
+ expectedRevision: created.newRevision,
208
+ authorization: policy(8, {
209
+ [occupation.id]: [],
210
+ [unknownOccupation.id]: [],
211
+ }),
212
+ },
213
+ );
214
+ } catch (cause) {
215
+ staleRevisionRejected = cause instanceof RevisionConflictError;
216
+ }
217
+
218
+ const sparqlResults: unknown = await response.json();
219
+ const reader = createAuthorizedTaproot(
220
+ db,
221
+ options,
222
+ {
223
+ installationId: 'demo-installation',
224
+ principalId: 'demo-principal',
225
+ activeWorkspaceId: null,
226
+ workspaceIds: [],
227
+ capabilities: [SEARCH_ADMIN_CAPABILITY],
228
+ authorizationRevision: 8,
229
+ },
230
+ {
231
+ cursorCodec: createAuthorizationCursorCodec(
232
+ await crypto.subtle.generateKey(
233
+ { name: 'AES-GCM', length: 256 },
234
+ false,
235
+ ['encrypt', 'decrypt'],
236
+ ),
237
+ ),
238
+ },
239
+ );
240
+ const audit = await reader.listAuditEvents({ entityId: created.entityId });
241
+ const integrity = await reader.inspectEntityIntegrity(created.entityId);
242
+ const canonical = await reader.getEntity(created.entityId);
243
+ return {
244
+ entityJson: exportEntityJson(canonical.entity),
245
+ sparqlResults,
246
+ staleRevisionRejected,
247
+ audit: audit.items,
248
+ integrity,
249
+ };
250
+ }
251
+
252
+ function policy(
253
+ expectedAuthorizationRevision: number,
254
+ statementRestrictions: CanonicalAuthorizationPolicyInput['statementRestrictions'] = {},
255
+ ): CanonicalAuthorizationPolicyInput {
256
+ return {
257
+ installationId: 'demo-installation',
258
+ workspaceId: null,
259
+ ownerPrincipalId: 'demo-principal',
260
+ visibility: { version: 1, clauses: [] },
261
+ statementRestrictions,
262
+ expectedAuthorizationRevision,
263
+ };
264
+ }
265
+
266
+ function writer(
267
+ authorizationRevision: number,
268
+ policyAuthority = false,
269
+ ): AuthorizationContext {
270
+ return {
271
+ installationId: 'demo-installation',
272
+ principalId: 'demo-principal',
273
+ activeWorkspaceId: null,
274
+ workspaceIds: [],
275
+ capabilities: [
276
+ KNOWLEDGE_WRITE_CAPABILITY,
277
+ ...(policyAuthority ? [KNOWLEDGE_POLICY_CAPABILITY] : []),
278
+ ],
279
+ authorizationRevision,
280
+ };
281
+ }
@@ -1,6 +1,7 @@
1
- -- Structural half of the v1 -> v2 migration. initializeTaproot() must run
2
- -- immediately afterward to SHA-256 backfill legacy revisions, create their
3
- -- audit events, install immutability triggers, and verify version metadata.
1
+ -- Historical 0.1 structural migration retained as package documentation.
2
+ -- Taproot 0.2 hosts must use the exported migration/initializer APIs rather
3
+ -- than applying this file directly; checksummed adoption and JavaScript
4
+ -- backfills cannot be represented by this SQL file alone.
4
5
  ALTER TABLE taproot_entity_revisions ADD COLUMN attribution_json TEXT CHECK (attribution_json IS NULL OR json_valid(attribution_json));
5
6
  ALTER TABLE taproot_entity_revisions ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json));
6
7
  ALTER TABLE taproot_entity_revisions ADD COLUMN event_id TEXT;
@@ -0,0 +1,31 @@
1
+ -- Documentation copy of canonical JSON migration 3.
2
+ -- Hosts must use applyTaprootMigrations() so the package migration ledger is
3
+ -- updated atomically. No fallback statement prose is generated.
4
+ INSERT INTO taproot_assertions(assertion_key)
5
+ WITH canonical_json(entity_json) AS (
6
+ SELECT entity_json FROM taproot_entities
7
+ UNION ALL
8
+ SELECT entity_json FROM taproot_entity_revisions
9
+ )
10
+ SELECT NULL
11
+ FROM canonical_json AS source,
12
+ json_each(source.entity_json, '$.claims') AS claim,
13
+ json_each(claim.value) AS statement
14
+ WHERE json_type(statement.value, '$.text') IS NOT 'text'
15
+ OR trim(json_extract(statement.value, '$.text')) = ''
16
+ LIMIT 1;
17
+
18
+ UPDATE taproot_metadata SET metadata_value = '2'
19
+ WHERE metadata_key = 'canonical_json_version'
20
+ AND metadata_value = '1';
21
+
22
+ INSERT INTO taproot_assertions(assertion_key)
23
+ SELECT NULL WHERE NOT EXISTS (
24
+ SELECT 1 FROM taproot_metadata
25
+ WHERE metadata_key = 'canonical_json_version'
26
+ AND metadata_value = '2'
27
+ );
28
+
29
+ INSERT INTO taproot_migrations(version, name)
30
+ VALUES (3, 'canonical-statement-text')
31
+ ON CONFLICT(version) DO NOTHING;