From df2a9d1f77464c0f7d0375c0229192bc3b48bec9 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 3 May 2026 04:55:49 -0500 Subject: [PATCH] catalogd: BiometricCollection on Go SubjectManifest reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors lakehouse f1fa6e4 (Phase 1.6 Gate 3a). Adds: type BiometricCollection struct { DataPath string TemplateHash string CollectedAt time.Time ConsentVersionHash string (omitempty) Classifications json.RawMessage (omitempty) } SubjectManifest gains *BiometricCollection biometric_collection,omitempty field so manifests written by the Rust biometric upload endpoint round-trip through the Go reader without losing the new collection record. Cross-runtime parity probe (subject_audit_parity.sh) still 6/6 byte-identical after the field addition — the omitempty pointer means manifests without a collection record produce identical bytes to pre-Gate-3a manifests. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/catalogd/subject.go | 40 +++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/internal/catalogd/subject.go b/internal/catalogd/subject.go index 35e3e70..fbf8b65 100644 --- a/internal/catalogd/subject.go +++ b/internal/catalogd/subject.go @@ -37,19 +37,35 @@ const GenesisHash = "GENESIS" // SubjectManifest mirrors crates/shared/src/types.rs::SubjectManifest. // Keep field tags byte-identical so manifest JSON written by Rust round-trips. +// +// Vertical: omitempty matches Rust's serde-default deserializer accepting +// missing field (default Unknown). For the OTHER fields we deliberately +// avoid omitempty since Rust always emits them. type SubjectManifest struct { - Schema string `json:"schema"` - CandidateID string `json:"candidate_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Status string `json:"status"` - Vertical string `json:"vertical,omitempty"` - Consent SubjectConsent `json:"consent"` - Retention SubjectRetention `json:"retention"` - Datasets []SubjectDatasetRef `json:"datasets"` - SafeViews []string `json:"safe_views"` - AuditLogPath string `json:"audit_log_path"` - AuditLogChainRoot string `json:"audit_log_chain_root"` + Schema string `json:"schema"` + CandidateID string `json:"candidate_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Status string `json:"status"` + Vertical string `json:"vertical,omitempty"` + Consent SubjectConsent `json:"consent"` + Retention SubjectRetention `json:"retention"` + Datasets []SubjectDatasetRef `json:"datasets"` + SafeViews []string `json:"safe_views"` + AuditLogPath string `json:"audit_log_path"` + AuditLogChainRoot string `json:"audit_log_chain_root"` + BiometricCollection *BiometricCollection `json:"biometric_collection,omitempty"` +} + +// BiometricCollection mirrors crates/shared/src/types.rs::BiometricCollection. +// Written by the Phase 1.6 Gate 3 photo-upload endpoint. None until the +// first biometric upload; cleared on BIPA erasure. +type BiometricCollection struct { + DataPath string `json:"data_path"` + TemplateHash string `json:"template_hash"` + CollectedAt time.Time `json:"collected_at"` + ConsentVersionHash string `json:"consent_version_hash,omitempty"` + Classifications json.RawMessage `json:"classifications,omitempty"` } type SubjectConsent struct {