Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
29bc60d
test(result): require durable consent reference integrity
seonghobae Aug 20, 2026
1ed1e6a
fix(result): enforce durable consent reference integrity
seonghobae Aug 20, 2026
14d9d37
test(result): reject control-bearing consent references
seonghobae Aug 20, 2026
ad6730f
fix(result): reject control-bearing consent references
seonghobae Aug 20, 2026
ea0af17
merge(main): reconcile result consent integrity after #258
seonghobae Aug 20, 2026
38f10cc
test(result): require durable reference parity
seonghobae Aug 20, 2026
ef2d945
test(result): keep parity regression compile-safe
seonghobae Aug 20, 2026
5dc1aa3
fix(result): enforce Rust-equivalent reference parity
seonghobae Aug 20, 2026
c126491
test(result): satisfy parity lint checks
seonghobae Aug 20, 2026
89bf03d
merge(main): reconcile result reference integrity with protected head
seonghobae Aug 21, 2026
0d4a188
test(result): cover ASCII numeric reference parity
seonghobae Aug 21, 2026
c4c6621
test(result): prove failed migration reapply preserves checks
seonghobae Aug 21, 2026
0ed3248
test(result): prove reference canonicalization boundary
seonghobae Aug 21, 2026
a3fa959
test(result): fence noncanonical whitespace aliases in storage
seonghobae Aug 21, 2026
8b4262b
refactor(result): delegate consent refs to scalar validator
seonghobae Aug 21, 2026
5da80b7
chore(result): reconcile durable consent refs with current main
seonghobae Aug 21, 2026
26489b1
Merge branch 'main' into fix/result-consent-array-integrity-20260821
opencode-agent[bot] Aug 24, 2026
7a787ff
Merge branch 'main' into fix/result-consent-array-integrity-20260821
seonghobae Aug 25, 2026
f7cf439
Merge branch 'main' into fix/result-consent-array-integrity-20260821
opencode-agent[bot] Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 148 additions & 85 deletions migrations/0007_result_snapshot.sql
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,95 +1,108 @@
-- Immutable result provenance uses the same opaque-reference boundary as the Rust domain.
-- Rust 1.97 `char::is_numeric` includes Unicode Nd/No/Nl characters that PostgreSQL's POSIX
-- digit class does not cover. PostgreSQL 18 UTF-8 with pg_unicode_fast supplies matching
-- whitespace/control classification; the generated int4multirange is Rust 1.97 Unicode 17.
CREATE OR REPLACE FUNCTION result_snapshot_reference_is_valid(reference_text TEXT)
RETURNS BOOLEAN
LANGUAGE sql
IMMUTABLE
STRICT
PARALLEL SAFE
SET search_path = pg_catalog
AS $result_snapshot_reference$
WITH reference_character AS (
SELECT substr(reference_text, character_index, 1) AS character_text
FROM generate_series(1, character_length(reference_text)) AS character_index
),
reference_classification AS (
SELECT
character_text,
ascii(character_text) <@ '{[48,58),[178,180),[185,186),[188,191),[1632,1642),[1776,1786),[1984,1994),[2406,2416),[2534,2544),[2548,2554),[2662,2672),[2790,2800),[2918,2928),[2930,2936),[3046,3059),[3174,3184),[3192,3199),[3302,3312),[3416,3423),[3430,3449),[3558,3568),[3664,3674),[3792,3802),[3872,3892),[4160,4170),[4240,4250),[4969,4989),[5870,5873),[6112,6122),[6128,6138),[6160,6170),[6470,6480),[6608,6619),[6784,6794),[6800,6810),[6992,7002),[7088,7098),[7232,7242),[7248,7258),[8304,8305),[8308,8314),[8320,8330),[8528,8579),[8581,8586),[9312,9372),[9450,9472),[10102,10132),[11517,11518),[12295,12296),[12321,12330),[12344,12347),[12690,12694),[12832,12842),[12872,12880),[12881,12896),[12928,12938),[12977,12992),[42528,42538),[42726,42736),[43056,43062),[43216,43226),[43264,43274),[43472,43482),[43504,43514),[43600,43610),[44016,44026),[65296,65306),[65799,65844),[65856,65913),[65930,65932),[66273,66300),[66336,66340),[66369,66370),[66378,66379),[66513,66518),[66720,66730),[67672,67680),[67705,67712),[67751,67760),[67835,67840),[67862,67868),[68028,68030),[68032,68048),[68050,68096),[68160,68169),[68221,68223),[68253,68256),[68331,68336),[68440,68448),[68472,68480),[68521,68528),[68858,68864),[68912,68922),[68928,68938),[69216,69247),[69405,69415),[69457,69461),[69573,69580),[69714,69744),[69872,69882),[69942,69952),[70096,70106),[70113,70133),[70384,70394),[70736,70746),[70864,70874),[71248,71258),[71360,71370),[71376,71396),[71472,71484),[71904,71923),[72016,72026),[72688,72698),[72784,72813),[73040,73050),[73120,73130),[73184,73194),[73552,73562),[73664,73685),[74752,74863),[90416,90426),[92768,92778),[92864,92874),[93008,93018),[93019,93026),[93552,93562),[93824,93847),[94196,94199),[118000,118010),[119488,119508),[119520,119540),[119648,119673),[120782,120832),[123200,123210),[123632,123642),[124144,124154),[124401,124411),[125127,125136),[125264,125274),[126065,126124),[126125,126128),[126129,126133),[126209,126254),[126255,126270),[127232,127245),[130032,130042)}'::int4multirange AS is_numeric
FROM reference_character
)
SELECT
reference_text <> ''
AND reference_text COLLATE "pg_unicode_fast" !~ '(^[[:space:]])|([[:space:]]$)'
AND reference_text COLLATE "pg_unicode_fast" !~ '[[:cntrl:]]'
Comment thread
seonghobae marked this conversation as resolved.
AND NOT COALESCE(
bool_or(is_numeric)
AND bool_and(
is_numeric
OR character_text = ANY (
ARRAY['+', '-', '.', ',', 'e', 'E', U&'\066B', U&'\066C', U&'\FF0E', U&'\FF0C']
)
),
FALSE
)
Comment thread
seonghobae marked this conversation as resolved.
FROM reference_classification;
Comment thread
seonghobae marked this conversation as resolved.
$result_snapshot_reference$;

CREATE OR REPLACE FUNCTION result_snapshot_consent_refs_are_valid(reference_array TEXT[])
RETURNS BOOLEAN
LANGUAGE sql
IMMUTABLE
STRICT
PARALLEL SAFE
-- Capture the migration's current schema so this array validator delegates to
-- the scalar validator created beside it without hard-coding `public`.
SET search_path FROM CURRENT
Comment thread
seonghobae marked this conversation as resolved.
AS $result_snapshot_consent_refs$
WITH consent_reference AS (
SELECT reference_text
FROM unnest(reference_array) AS consent_reference(reference_text)
)
SELECT
count(*) = count(DISTINCT reference_text)
AND COALESCE(
bool_and(
reference_text IS NOT NULL
AND result_snapshot_reference_is_valid(reference_text)
),
TRUE
)
FROM consent_reference;
$result_snapshot_consent_refs$;
Comment thread
seonghobae marked this conversation as resolved.

CREATE TABLE IF NOT EXISTS result_snapshot (
result_snapshot_ref TEXT CONSTRAINT result_snapshot_ref_not_null NOT NULL
CONSTRAINT result_snapshot_ref_format_check CHECK (
result_snapshot_ref = btrim(result_snapshot_ref)
AND result_snapshot_ref <> ''
AND NOT (
result_snapshot_ref ~ '[[:digit:]]'
AND result_snapshot_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(result_snapshot_ref)
),
participant_ref TEXT CONSTRAINT result_snapshot_participant_ref_not_null NOT NULL
CONSTRAINT result_snapshot_participant_ref_format_check CHECK (
participant_ref = btrim(participant_ref)
AND participant_ref <> ''
AND NOT (
participant_ref ~ '[[:digit:]]'
AND participant_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(participant_ref)
),
scoring_result_ref TEXT CONSTRAINT result_snapshot_scoring_result_ref_not_null NOT NULL
CONSTRAINT result_snapshot_scoring_result_ref_format_check CHECK (
scoring_result_ref = btrim(scoring_result_ref)
AND scoring_result_ref <> ''
AND NOT (
scoring_result_ref ~ '[[:digit:]]'
AND scoring_result_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(scoring_result_ref)
),
session_ref TEXT CONSTRAINT result_snapshot_session_ref_not_null NOT NULL
CONSTRAINT result_snapshot_session_ref_format_check CHECK (
session_ref = btrim(session_ref)
AND session_ref <> ''
AND NOT (
session_ref ~ '[[:digit:]]'
AND session_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(session_ref)
),
response_snapshot_ref TEXT CONSTRAINT result_snapshot_response_ref_not_null NOT NULL
CONSTRAINT result_snapshot_response_ref_format_check CHECK (
response_snapshot_ref = btrim(response_snapshot_ref)
AND response_snapshot_ref <> ''
AND NOT (
response_snapshot_ref ~ '[[:digit:]]'
AND response_snapshot_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(response_snapshot_ref)
),
assessment_spec_ref TEXT CONSTRAINT result_snapshot_spec_ref_not_null NOT NULL
CONSTRAINT result_snapshot_spec_ref_format_check CHECK (
assessment_spec_ref = btrim(assessment_spec_ref)
AND assessment_spec_ref <> ''
AND NOT (
assessment_spec_ref ~ '[[:digit:]]'
AND assessment_spec_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(assessment_spec_ref)
),
instrument_version_ref TEXT CONSTRAINT result_snapshot_instrument_ref_not_null NOT NULL
CONSTRAINT result_snapshot_instrument_ref_format_check CHECK (
instrument_version_ref = btrim(instrument_version_ref)
AND instrument_version_ref <> ''
AND NOT (
instrument_version_ref ~ '[[:digit:]]'
AND instrument_version_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(instrument_version_ref)
),
scoring_version_ref TEXT CONSTRAINT result_snapshot_scoring_ref_not_null NOT NULL
CONSTRAINT result_snapshot_scoring_ref_format_check CHECK (
scoring_version_ref = btrim(scoring_version_ref)
AND scoring_version_ref <> ''
AND NOT (
scoring_version_ref ~ '[[:digit:]]'
AND scoring_version_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(scoring_version_ref)
),
calibration_reference TEXT CONSTRAINT result_snapshot_calibration_ref_not_null NOT NULL
CONSTRAINT result_snapshot_calibration_ref_format_check CHECK (
calibration_reference = btrim(calibration_reference)
AND calibration_reference <> ''
AND NOT (
calibration_reference ~ '[[:digit:]]'
AND calibration_reference ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(calibration_reference)
),
norm_version_ref TEXT
CONSTRAINT result_snapshot_norm_ref_format_check CHECK (
norm_version_ref IS NULL OR (
norm_version_ref = btrim(norm_version_ref)
AND norm_version_ref <> ''
AND NOT (
norm_version_ref ~ '[[:digit:]]'
AND norm_version_ref ~ '^[[:digit:]+,.eE-]+$'
)
)
norm_version_ref IS NULL OR result_snapshot_reference_is_valid(norm_version_ref)
),
requested_output_schema_version INTEGER
CONSTRAINT result_snapshot_schema_version_not_null NOT NULL
Expand All @@ -98,16 +111,14 @@ CREATE TABLE IF NOT EXISTS result_snapshot (
),
narrative_version_ref TEXT CONSTRAINT result_snapshot_narrative_ref_not_null NOT NULL
CONSTRAINT result_snapshot_narrative_ref_format_check CHECK (
narrative_version_ref = btrim(narrative_version_ref)
AND narrative_version_ref <> ''
AND NOT (
narrative_version_ref ~ '[[:digit:]]'
AND narrative_version_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(narrative_version_ref)
),
consent_snapshot_refs TEXT[] CONSTRAINT result_snapshot_consent_refs_not_null NOT NULL
CONSTRAINT result_snapshot_consent_refs_not_empty_check CHECK (
cardinality(consent_snapshot_refs) > 0
)
CONSTRAINT result_snapshot_consent_refs_integrity_check CHECK (
result_snapshot_consent_refs_are_valid(consent_snapshot_refs)
),
engine_artifact_digest TEXT CONSTRAINT result_snapshot_engine_digest_not_null NOT NULL
CONSTRAINT result_snapshot_engine_digest_format_check CHECK (
Expand All @@ -119,13 +130,8 @@ CREATE TABLE IF NOT EXISTS result_snapshot (
supersedes_ref TEXT
CONSTRAINT result_snapshot_supersedes_ref_format_check CHECK (
supersedes_ref IS NULL OR (
supersedes_ref = btrim(supersedes_ref)
AND supersedes_ref <> ''
AND supersedes_ref <> result_snapshot_ref
AND NOT (
supersedes_ref ~ '[[:digit:]]'
AND supersedes_ref ~ '^[[:digit:]+,.eE-]+$'
)
supersedes_ref <> result_snapshot_ref
AND result_snapshot_reference_is_valid(supersedes_ref)
)
),
created_at TIMESTAMPTZ CONSTRAINT result_snapshot_created_at_not_null NOT NULL
Expand All @@ -142,12 +148,7 @@ CREATE TABLE IF NOT EXISTS result_snapshot_observation (
),
construct_ref TEXT CONSTRAINT result_snapshot_observation_construct_ref_not_null NOT NULL
CONSTRAINT result_snapshot_observation_construct_ref_format_check CHECK (
construct_ref = btrim(construct_ref)
AND construct_ref <> ''
AND NOT (
construct_ref ~ '[[:digit:]]'
AND construct_ref ~ '^[[:digit:]+,.eE-]+$'
)
result_snapshot_reference_is_valid(construct_ref)
),
observation_disposition TEXT
CONSTRAINT result_snapshot_observation_disposition_not_null NOT NULL
Expand Down Expand Up @@ -191,9 +192,71 @@ CREATE TABLE IF NOT EXISTS result_snapshot_observation (
)
);

-- Reapplying this migration must also strengthen a schema created by an earlier
-- revision of this not-yet-released migration. PostgreSQL's CREATE TABLE IF NOT
-- EXISTS does not reconcile changed CHECK definitions on an existing table.
-- Reapplying this migration must strengthen historical CHECK definitions as well as fresh tables.
-- PostgreSQL's CREATE TABLE IF NOT EXISTS does not reconcile changed constraints on an existing
-- schema, so every reference predicate is dropped and recreated, forcing existing rows through
-- the Rust-equivalent validator before the migration can succeed.
ALTER TABLE result_snapshot_observation DROP CONSTRAINT IF EXISTS result_snapshot_observation_construct_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_supersedes_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_consent_refs_integrity_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_narrative_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_norm_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_calibration_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_scoring_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_instrument_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_spec_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_response_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_session_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_scoring_result_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_participant_ref_format_check;
ALTER TABLE result_snapshot DROP CONSTRAINT IF EXISTS result_snapshot_ref_format_check;

ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_ref_format_check CHECK (
result_snapshot_reference_is_valid(result_snapshot_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_participant_ref_format_check CHECK (
result_snapshot_reference_is_valid(participant_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_scoring_result_ref_format_check CHECK (
result_snapshot_reference_is_valid(scoring_result_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_session_ref_format_check CHECK (
result_snapshot_reference_is_valid(session_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_response_ref_format_check CHECK (
result_snapshot_reference_is_valid(response_snapshot_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_spec_ref_format_check CHECK (
result_snapshot_reference_is_valid(assessment_spec_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_instrument_ref_format_check CHECK (
result_snapshot_reference_is_valid(instrument_version_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_scoring_ref_format_check CHECK (
result_snapshot_reference_is_valid(scoring_version_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_calibration_ref_format_check CHECK (
result_snapshot_reference_is_valid(calibration_reference)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_norm_ref_format_check CHECK (
norm_version_ref IS NULL OR result_snapshot_reference_is_valid(norm_version_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_narrative_ref_format_check CHECK (
result_snapshot_reference_is_valid(narrative_version_ref)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_consent_refs_integrity_check CHECK (
result_snapshot_consent_refs_are_valid(consent_snapshot_refs)
);
ALTER TABLE result_snapshot ADD CONSTRAINT result_snapshot_supersedes_ref_format_check CHECK (
supersedes_ref IS NULL OR (
supersedes_ref <> result_snapshot_ref
AND result_snapshot_reference_is_valid(supersedes_ref)
)
);
ALTER TABLE result_snapshot_observation ADD CONSTRAINT result_snapshot_observation_construct_ref_format_check CHECK (
result_snapshot_reference_is_valid(construct_ref)
);

Comment thread
seonghobae marked this conversation as resolved.
ALTER TABLE result_snapshot
DROP CONSTRAINT IF EXISTS result_snapshot_engine_digest_format_check;
ALTER TABLE result_snapshot
Expand Down Expand Up @@ -256,4 +319,4 @@ DROP TRIGGER IF EXISTS result_snapshot_observation_truncate_guard
CREATE TRIGGER result_snapshot_observation_truncate_guard
BEFORE TRUNCATE ON result_snapshot_observation
FOR EACH STATEMENT
EXECUTE FUNCTION reject_result_snapshot_evidence_mutation();
EXECUTE FUNCTION reject_result_snapshot_evidence_mutation();
Loading
Loading