@hraness/peopleblade 0.1.1 → 0.2.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.
@@ -0,0 +1,205 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ -- Schema v13 admitted same-identity guards only to reject them later. Purge
4
+ -- those impossible predecessors while the v14 invalidation trigger still
5
+ -- recognizes them, before metadata-only successors become valid.
6
+ DELETE FROM source_binding_transition_guards
7
+ WHERE NOT EXISTS (
8
+ SELECT 1 FROM source_binding_incarnations successor
9
+ WHERE successor.source_binding_id=source_binding_transition_guards.source_binding_id
10
+ AND successor.ordinal=source_binding_transition_guards.next_ordinal
11
+ )
12
+ AND EXISTS (
13
+ SELECT 1 FROM source_binding_incarnations predecessor
14
+ WHERE predecessor.id=source_binding_transition_guards.retiring_incarnation_id
15
+ AND predecessor.source_binding_id=source_binding_transition_guards.source_binding_id
16
+ AND predecessor.authority=source_binding_transition_guards.authority
17
+ AND predecessor.retired_at IS NULL
18
+ AND predecessor.subject_sha256=source_binding_transition_guards.next_subject_sha256
19
+ AND predecessor.auth_sha256=source_binding_transition_guards.next_auth_sha256
20
+ );
21
+
22
+ DROP TRIGGER source_binding_transition_guards_snapshot_insert;
23
+
24
+ CREATE TRIGGER source_binding_transition_guards_snapshot_insert
25
+ BEFORE INSERT ON source_binding_transition_guards FOR EACH ROW
26
+ WHEN NOT EXISTS (
27
+ SELECT 1 FROM source_binding_incarnations incarnation
28
+ JOIN source_bindings binding ON binding.id=incarnation.source_binding_id
29
+ WHERE incarnation.id=NEW.retiring_incarnation_id
30
+ AND incarnation.source_binding_id=NEW.source_binding_id
31
+ AND incarnation.retired_at IS NULL
32
+ AND incarnation.ordinal+1=NEW.next_ordinal
33
+ AND incarnation.authority=NEW.authority
34
+ AND julianday(NEW.observed_at)>=julianday(incarnation.last_seen_at)
35
+ AND incarnation.last_seen_at=NEW.expected_last_seen_at
36
+ AND incarnation.metadata_json=NEW.expected_metadata_json
37
+ AND binding.authority=NEW.authority
38
+ AND binding.subject_sha256=incarnation.subject_sha256
39
+ AND binding.auth_sha256=incarnation.auth_sha256
40
+ AND binding.last_seen_at=NEW.expected_last_seen_at
41
+ AND binding.metadata_json=NEW.expected_metadata_json
42
+ AND binding.complete_realm_inventory_observed_at
43
+ IS NEW.expected_complete_realm_inventory_observed_at
44
+ AND binding.complete_realm_inventory_sha256
45
+ IS NEW.expected_complete_realm_inventory_sha256
46
+ AND (
47
+ (
48
+ (incarnation.subject_sha256<>NEW.next_subject_sha256
49
+ OR incarnation.auth_sha256<>NEW.next_auth_sha256)
50
+ AND NOT EXISTS (
51
+ SELECT 1 FROM source_binding_incarnations owner
52
+ WHERE owner.authority=NEW.authority
53
+ AND owner.subject_sha256=NEW.next_subject_sha256
54
+ AND owner.source_binding_id<>NEW.source_binding_id
55
+ )
56
+ )
57
+ OR (
58
+ incarnation.subject_sha256=NEW.next_subject_sha256
59
+ AND incarnation.auth_sha256=NEW.next_auth_sha256
60
+ AND incarnation.metadata_json<>NEW.metadata_json
61
+ AND NEW.expected_complete_realm_inventory_observed_at IS NOT NULL
62
+ AND NEW.expected_complete_realm_inventory_sha256 IS NOT NULL
63
+ AND NEW.complete_realm_inventory_sha256
64
+ =NEW.expected_complete_realm_inventory_sha256
65
+ )
66
+ )
67
+ )
68
+ BEGIN SELECT RAISE(ABORT, 'source binding transition guard does not match current reviewed state'); END;
69
+
70
+ DROP TRIGGER source_binding_incarnations_chain_insert;
71
+
72
+ CREATE TRIGGER source_binding_incarnations_chain_insert
73
+ BEFORE INSERT ON source_binding_incarnations FOR EACH ROW
74
+ WHEN NEW.transition_kind='explicit-rebind' AND NOT EXISTS (
75
+ SELECT 1 FROM source_binding_incarnations previous
76
+ WHERE previous.id=NEW.previous_incarnation_id
77
+ AND previous.source_binding_id=NEW.source_binding_id
78
+ AND previous.authority=NEW.authority
79
+ AND previous.ordinal=NEW.ordinal-1
80
+ AND previous.retired_at IS NOT NULL
81
+ AND (
82
+ previous.subject_sha256<>NEW.subject_sha256
83
+ OR previous.auth_sha256<>NEW.auth_sha256
84
+ OR previous.metadata_json<>NEW.metadata_json
85
+ )
86
+ AND EXISTS (
87
+ SELECT 1 FROM source_bindings binding
88
+ WHERE binding.id=NEW.source_binding_id AND binding.authority=NEW.authority
89
+ )
90
+ )
91
+ BEGIN SELECT RAISE(ABORT, 'source binding incarnation predecessor is invalid'); END;
92
+
93
+ -- Schema v13 mirrored an older writer's root metadata into the current
94
+ -- incarnation. Preserve that compatibility only while the writer is not
95
+ -- attempting to overwrite an already-installed successor. The atomic
96
+ -- transition trigger inserts the successor before updating the root shadow, so
97
+ -- its target metadata already equals the one current incarnation here.
98
+ CREATE TRIGGER source_bindings_current_incarnation_metadata_guard
99
+ BEFORE UPDATE OF metadata_json ON source_bindings FOR EACH ROW
100
+ WHEN EXISTS (
101
+ SELECT 1 FROM source_binding_incarnations current
102
+ WHERE current.source_binding_id=NEW.id
103
+ AND current.authority=NEW.authority
104
+ AND current.retired_at IS NULL
105
+ AND current.metadata_json<>NEW.metadata_json
106
+ )
107
+ BEGIN SELECT RAISE(ABORT, 'source binding metadata must shadow its current incarnation'); END;
108
+
109
+ DROP TRIGGER source_binding_transition_guards_consumed_delete;
110
+
111
+ CREATE TRIGGER source_binding_transition_guards_consumed_delete
112
+ BEFORE DELETE ON source_binding_transition_guards FOR EACH ROW
113
+ WHEN NOT (
114
+ (
115
+ EXISTS (
116
+ SELECT 1 FROM source_binding_incarnations predecessor
117
+ WHERE predecessor.id=OLD.retiring_incarnation_id
118
+ AND predecessor.source_binding_id=OLD.source_binding_id
119
+ AND predecessor.authority=OLD.authority
120
+ AND predecessor.ordinal=OLD.next_ordinal-1
121
+ AND predecessor.retired_at=OLD.observed_at
122
+ AND predecessor.last_seen_at=OLD.expected_last_seen_at
123
+ AND predecessor.metadata_json=OLD.expected_metadata_json
124
+ )
125
+ AND EXISTS (
126
+ SELECT 1 FROM source_binding_incarnations successor
127
+ WHERE successor.source_binding_id=OLD.source_binding_id
128
+ AND successor.authority=OLD.authority
129
+ AND successor.ordinal=OLD.next_ordinal
130
+ AND successor.subject_sha256=OLD.next_subject_sha256
131
+ AND successor.auth_sha256=OLD.next_auth_sha256
132
+ AND successor.previous_incarnation_id=OLD.retiring_incarnation_id
133
+ AND successor.transition_kind='explicit-rebind'
134
+ AND successor.transition_evidence_sha256=OLD.transition_evidence_sha256
135
+ AND successor.transition_evidence_json=OLD.transition_evidence_json
136
+ AND successor.first_seen_at=OLD.observed_at
137
+ AND successor.last_seen_at=OLD.observed_at
138
+ AND successor.retired_at IS NULL
139
+ AND successor.metadata_json=OLD.metadata_json
140
+ )
141
+ AND EXISTS (
142
+ SELECT 1 FROM source_bindings binding
143
+ WHERE binding.id=OLD.source_binding_id
144
+ AND binding.authority=OLD.authority
145
+ AND binding.subject_sha256=OLD.next_subject_sha256
146
+ AND binding.auth_sha256=OLD.next_auth_sha256
147
+ AND binding.last_seen_at=OLD.observed_at
148
+ AND binding.complete_realm_inventory_observed_at=OLD.observed_at
149
+ AND binding.complete_realm_inventory_sha256=OLD.complete_realm_inventory_sha256
150
+ AND binding.metadata_json=OLD.metadata_json
151
+ )
152
+ )
153
+ OR (
154
+ NOT EXISTS (
155
+ SELECT 1 FROM source_binding_incarnations successor
156
+ WHERE successor.source_binding_id=OLD.source_binding_id
157
+ AND successor.ordinal=OLD.next_ordinal
158
+ )
159
+ AND EXISTS (
160
+ SELECT 1 FROM source_binding_incarnations predecessor
161
+ JOIN source_bindings binding ON binding.id=predecessor.source_binding_id
162
+ WHERE predecessor.id=OLD.retiring_incarnation_id
163
+ AND predecessor.source_binding_id=OLD.source_binding_id
164
+ AND predecessor.authority=OLD.authority
165
+ AND predecessor.ordinal=OLD.next_ordinal-1
166
+ AND predecessor.retired_at IS NULL
167
+ AND binding.authority=OLD.authority
168
+ AND binding.subject_sha256=predecessor.subject_sha256
169
+ AND binding.auth_sha256=predecessor.auth_sha256
170
+ AND (
171
+ (
172
+ predecessor.subject_sha256=OLD.next_subject_sha256
173
+ AND predecessor.auth_sha256=OLD.next_auth_sha256
174
+ AND predecessor.metadata_json=OLD.metadata_json
175
+ )
176
+ OR julianday(OLD.observed_at)<julianday(predecessor.last_seen_at)
177
+ OR predecessor.last_seen_at IS NOT OLD.expected_last_seen_at
178
+ OR predecessor.metadata_json IS NOT OLD.expected_metadata_json
179
+ OR binding.last_seen_at IS NOT OLD.expected_last_seen_at
180
+ OR binding.metadata_json IS NOT OLD.expected_metadata_json
181
+ OR binding.complete_realm_inventory_observed_at
182
+ IS NOT OLD.expected_complete_realm_inventory_observed_at
183
+ OR binding.complete_realm_inventory_sha256
184
+ IS NOT OLD.expected_complete_realm_inventory_sha256
185
+ OR (
186
+ predecessor.subject_sha256=OLD.next_subject_sha256
187
+ AND predecessor.auth_sha256=OLD.next_auth_sha256
188
+ AND (
189
+ OLD.expected_complete_realm_inventory_observed_at IS NULL
190
+ OR OLD.expected_complete_realm_inventory_sha256 IS NULL
191
+ OR OLD.complete_realm_inventory_sha256
192
+ IS NOT OLD.expected_complete_realm_inventory_sha256
193
+ )
194
+ )
195
+ OR EXISTS (
196
+ SELECT 1 FROM source_binding_incarnations owner
197
+ WHERE owner.authority=OLD.authority
198
+ AND owner.subject_sha256=OLD.next_subject_sha256
199
+ AND owner.source_binding_id<>OLD.source_binding_id
200
+ )
201
+ )
202
+ )
203
+ )
204
+ )
205
+ BEGIN SELECT RAISE(ABORT, 'source binding transition guard can only be deleted after exact consumption or proven invalidation'); END;
@@ -0,0 +1,151 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ -- User edits are overlays; original provider content and attribution stay immutable.
4
+ CREATE TABLE person_note_content_revisions (
5
+ note_id INTEGER NOT NULL REFERENCES person_notes(id) ON DELETE RESTRICT,
6
+ revision INTEGER NOT NULL CHECK(revision BETWEEN 1 AND 9007199254740991),
7
+ expected_revision INTEGER NOT NULL CHECK(expected_revision BETWEEN 0 AND 9007199254740990),
8
+ request_id TEXT NOT NULL UNIQUE CHECK(length(request_id) = 36),
9
+ expected_context_sha256 TEXT NOT NULL CHECK(
10
+ length(expected_context_sha256) = 64
11
+ AND expected_context_sha256 NOT GLOB '*[^0-9a-f]*'
12
+ ),
13
+ context_json TEXT NOT NULL CHECK(json_valid(context_json)),
14
+ title TEXT CHECK(title IS NULL OR (length(title) BETWEEN 1 AND 1024 AND instr(title, char(0)) = 0)),
15
+ body TEXT NOT NULL CHECK(length(CAST(body AS BLOB)) BETWEEN 1 AND 65536 AND instr(body, char(0)) = 0),
16
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
17
+ PRIMARY KEY(note_id, revision),
18
+ CHECK(revision = expected_revision + 1)
19
+ ) STRICT;
20
+
21
+ CREATE TRIGGER person_note_content_revision_valid
22
+ BEFORE INSERT ON person_note_content_revisions FOR EACH ROW
23
+ BEGIN
24
+ SELECT CASE WHEN
25
+ EXISTS (SELECT 1 FROM person_note_content_revisions prior
26
+ WHERE prior.request_id = NEW.request_id
27
+ OR (prior.note_id = NEW.note_id AND prior.revision = NEW.revision))
28
+ OR NEW.expected_revision IS NOT coalesce(
29
+ (SELECT max(prior.revision) FROM person_note_content_revisions prior WHERE prior.note_id = NEW.note_id), 0)
30
+ OR NOT EXISTS (SELECT 1 FROM person_notes WHERE id = NEW.note_id)
31
+ OR json_extract(NEW.context_json, '$.schemaVersion') IS NOT 1
32
+ OR json_extract(NEW.context_json, '$.databaseInstanceId') IS NOT
33
+ (SELECT instance_id FROM local_state WHERE singleton = 1)
34
+ OR json_extract(NEW.context_json, '$.noteId') IS NOT NEW.note_id
35
+ OR json_extract(NEW.context_json, '$.revision') IS NOT NEW.expected_revision
36
+ OR json_extract(NEW.context_json, '$.originalPersonId') IS NOT
37
+ (SELECT person_id FROM person_notes WHERE id = NEW.note_id)
38
+ OR json_extract(NEW.context_json, '$.attributedPersonId') IS NOT
39
+ (SELECT person_id FROM person_notes_current WHERE id = NEW.note_id)
40
+ OR json_extract(NEW.context_json, '$.attributionRevisionId') IS NOT coalesce(
41
+ (SELECT max(id) FROM person_note_attribution_revisions WHERE note_id = NEW.note_id), 0)
42
+ OR json_extract(NEW.context_json, '$.identityRevision') IS NOT
43
+ (SELECT identity_revision FROM local_revisions WHERE singleton = 1)
44
+ OR json_extract(NEW.context_json, '$.canonicalPersonId') IS NOT
45
+ (SELECT component.canonical_person_id FROM person_identity_components component
46
+ JOIN person_notes_current note ON note.person_id = component.person_id WHERE note.id = NEW.note_id)
47
+ OR json_extract(NEW.context_json, '$.componentPersonIds') IS NOT (
48
+ SELECT json_group_array(person_id) FROM (
49
+ SELECT member.person_id FROM person_identity_components member
50
+ WHERE member.canonical_person_id = (
51
+ SELECT component.canonical_person_id FROM person_identity_components component
52
+ JOIN person_notes_current note ON note.person_id = component.person_id WHERE note.id = NEW.note_id
53
+ ) ORDER BY member.person_id
54
+ )
55
+ )
56
+ THEN RAISE(ABORT, 'person note revision context is not current') END;
57
+ END;
58
+
59
+ CREATE TABLE person_note_creation_requests (
60
+ request_id TEXT PRIMARY KEY CHECK(length(request_id) = 36),
61
+ note_id INTEGER NOT NULL UNIQUE REFERENCES person_notes(id) ON DELETE RESTRICT,
62
+ requested_person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
63
+ expected_context_sha256 TEXT NOT NULL CHECK(
64
+ length(expected_context_sha256) = 64 AND expected_context_sha256 NOT GLOB '*[^0-9a-f]*'
65
+ ),
66
+ context_json TEXT NOT NULL CHECK(json_valid(context_json)),
67
+ occurred_at_input TEXT NOT NULL CHECK(length(occurred_at_input) BETWEEN 1 AND 80)
68
+ ) STRICT;
69
+
70
+ CREATE TRIGGER person_note_creation_request_valid
71
+ BEFORE INSERT ON person_note_creation_requests FOR EACH ROW
72
+ BEGIN
73
+ SELECT CASE WHEN
74
+ EXISTS (SELECT 1 FROM person_note_creation_requests prior
75
+ WHERE prior.request_id = NEW.request_id OR prior.note_id = NEW.note_id)
76
+ OR json_extract(NEW.context_json, '$.schemaVersion') IS NOT 1
77
+ OR json_extract(NEW.context_json, '$.databaseInstanceId') IS NOT
78
+ (SELECT instance_id FROM local_state WHERE singleton = 1)
79
+ OR json_extract(NEW.context_json, '$.personId') IS NOT NEW.requested_person_id
80
+ OR json_extract(NEW.context_json, '$.identityRevision') IS NOT
81
+ (SELECT identity_revision FROM local_revisions WHERE singleton = 1)
82
+ OR json_extract(NEW.context_json, '$.canonicalPersonId') IS NOT
83
+ (SELECT canonical_person_id FROM person_identity_components WHERE person_id = NEW.requested_person_id)
84
+ OR json_extract(NEW.context_json, '$.componentPersonIds') IS NOT (
85
+ SELECT json_group_array(person_id) FROM (
86
+ SELECT member.person_id FROM person_identity_components member
87
+ WHERE member.canonical_person_id = (
88
+ SELECT canonical_person_id FROM person_identity_components WHERE person_id = NEW.requested_person_id
89
+ ) ORDER BY member.person_id
90
+ )
91
+ )
92
+ OR NOT EXISTS (SELECT 1 FROM person_notes note
93
+ WHERE note.id = NEW.note_id AND note.source = 'manual' AND note.source_id = NEW.request_id
94
+ AND note.person_id = json_extract(NEW.context_json, '$.canonicalPersonId'))
95
+ THEN RAISE(ABORT, 'person note creation context is not current') END;
96
+ END;
97
+
98
+ CREATE TRIGGER person_note_creation_requests_immutable_update
99
+ BEFORE UPDATE ON person_note_creation_requests FOR EACH ROW
100
+ BEGIN SELECT RAISE(ABORT, 'person note creation requests are immutable'); END;
101
+ CREATE TRIGGER person_note_creation_requests_immutable_delete
102
+ BEFORE DELETE ON person_note_creation_requests FOR EACH ROW
103
+ BEGIN SELECT RAISE(ABORT, 'person note creation requests are immutable'); END;
104
+
105
+ CREATE TRIGGER person_note_content_revisions_immutable_update
106
+ BEFORE UPDATE ON person_note_content_revisions FOR EACH ROW
107
+ BEGIN SELECT RAISE(ABORT, 'person note content revisions are immutable'); END;
108
+ CREATE TRIGGER person_note_content_revisions_immutable_delete
109
+ BEFORE DELETE ON person_note_content_revisions FOR EACH ROW
110
+ BEGIN SELECT RAISE(ABORT, 'person note content revisions are immutable'); END;
111
+
112
+ CREATE VIEW person_notes_effective AS
113
+ SELECT note.id, note.person_id, note.occurred_at, note.source, note.source_id,
114
+ CASE WHEN revision.revision IS NULL THEN note.title ELSE revision.title END AS title,
115
+ coalesce(revision.body, note.body) AS body,
116
+ note.metadata_json, note.created_at,
117
+ coalesce(revision.revision, 0) AS revision,
118
+ revision.created_at AS revised_at
119
+ FROM person_notes_current note
120
+ LEFT JOIN person_note_content_revisions revision ON revision.note_id = note.id
121
+ AND revision.revision = (
122
+ SELECT max(latest.revision) FROM person_note_content_revisions latest WHERE latest.note_id = note.id
123
+ );
124
+
125
+ CREATE VIRTUAL TABLE person_notes_effective_fts USING fts5(
126
+ title, body, content='person_notes_effective', content_rowid='id',
127
+ tokenize='unicode61 remove_diacritics 2'
128
+ );
129
+ INSERT INTO person_notes_effective_fts(rowid, title, body)
130
+ SELECT id, coalesce(title, ''), body FROM person_notes_effective;
131
+
132
+ CREATE TRIGGER person_notes_effective_fts_original_insert
133
+ AFTER INSERT ON person_notes FOR EACH ROW
134
+ BEGIN
135
+ INSERT INTO person_notes_effective_fts(rowid, title, body)
136
+ VALUES (NEW.id, coalesce(NEW.title, ''), NEW.body);
137
+ END;
138
+
139
+ CREATE TRIGGER person_notes_effective_fts_revision_insert
140
+ AFTER INSERT ON person_note_content_revisions FOR EACH ROW
141
+ BEGIN
142
+ INSERT INTO person_notes_effective_fts(person_notes_effective_fts, rowid, title, body)
143
+ SELECT 'delete', NEW.note_id, coalesce(note.title, ''), note.body
144
+ FROM person_notes note WHERE note.id = NEW.note_id AND NEW.expected_revision = 0;
145
+ INSERT INTO person_notes_effective_fts(person_notes_effective_fts, rowid, title, body)
146
+ SELECT 'delete', NEW.note_id, coalesce(prior.title, ''), prior.body
147
+ FROM person_note_content_revisions prior
148
+ WHERE prior.note_id = NEW.note_id AND prior.revision = NEW.expected_revision;
149
+ INSERT INTO person_notes_effective_fts(rowid, title, body)
150
+ VALUES (NEW.note_id, coalesce(NEW.title, ''), NEW.body);
151
+ END;