From 09ff6d30c8c04097f2a2aa4c95e8d9a18d5a26b7 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Thu, 3 Sep 2026 22:53:59 -0400 Subject: [PATCH] Keep source-metadata heads from moving to older generations --- internal/store/source_metadata.go | 20 +++++--- internal/store/source_metadata_test.go | 71 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/internal/store/source_metadata.go b/internal/store/source_metadata.go index c6e73cdd..70250df4 100644 --- a/internal/store/source_metadata.go +++ b/internal/store/source_metadata.go @@ -78,7 +78,8 @@ func sourceMetadataGenerationID(source, contract, fingerprint, checksum string) } // PublishSourceMetadata validates and atomically publishes one immutable -// generation. Retrying the identical publication is idempotent. +// generation. Retrying the identical publication is idempotent; an older +// stored generation cannot move the active head backward. func (s *Store) PublishSourceMetadata( ctx context.Context, sourceSHA256, extractorFingerprint string, canonical []byte, ) (SourceMetadataGeneration, error) { @@ -102,13 +103,19 @@ func (s *Store) PublishSourceMetadata( generation.GenerationID = sourceMetadataGenerationID(sourceSHA256, metadata.ContractVersion, extractorFingerprint, checksum) err = s.withStorageTx(ctx, func(tx *sql.Tx) error { - if _, execErr := tx.ExecContext(ctx, `INSERT INTO source_metadata_generations( + result, execErr := tx.ExecContext(ctx, `INSERT INTO source_metadata_generations( generation_id,source_sha256,contract_version,extractor_fingerprint,canonical_json,checksum,created_at ) VALUES(?,?,?,?,?,?,?) ON CONFLICT(source_sha256,contract_version,extractor_fingerprint) DO NOTHING`, generation.GenerationID, sourceSHA256, metadata.ContractVersion, extractorFingerprint, - canonical, checksum, generation.CreatedAt); execErr != nil { + canonical, checksum, generation.CreatedAt) + if execErr != nil { return fmt.Errorf("recording source metadata: %w", execErr) } + inserted, rowsErr := result.RowsAffected() + if rowsErr != nil { + return fmt.Errorf("recording source metadata: %w", rowsErr) + } + recorded := inserted > 0 var stored SourceMetadataGeneration if scanErr := tx.QueryRowContext(ctx, `SELECT generation_id,source_sha256,contract_version, extractor_fingerprint,canonical_json,checksum,created_at FROM source_metadata_generations @@ -123,10 +130,11 @@ func (s *Store) PublishSourceMetadata( return errors.New("source metadata extractor identity already has different evidence") } generation = stored - _, execErr := tx.ExecContext(ctx, `INSERT INTO source_metadata_heads(source_sha256,generation_id,published_at) + _, execErr = tx.ExecContext(ctx, `INSERT INTO source_metadata_heads(source_sha256,generation_id,published_at) VALUES(?,?,?) ON CONFLICT(source_sha256) DO UPDATE SET - generation_id=excluded.generation_id,published_at=excluded.published_at`, - sourceSHA256, generation.GenerationID, nowRFC3339()) + generation_id=excluded.generation_id,published_at=excluded.published_at + WHERE ? OR source_metadata_heads.generation_id=excluded.generation_id`, + sourceSHA256, generation.GenerationID, nowRFC3339(), recorded) return execErr }) return generation, err diff --git a/internal/store/source_metadata_test.go b/internal/store/source_metadata_test.go index c60bbcb6..b795f657 100644 --- a/internal/store/source_metadata_test.go +++ b/internal/store/source_metadata_test.go @@ -56,6 +56,77 @@ func TestSourceMetadataGenerationsAreImmutableAndAttachmentFactsStayJoined(t *te require.Error(t, err, "a head must not select another original's evidence") } +// Coverage guard: this exercises the immutable-insert provenance used by the +// source-metadata head conflict predicate. +func TestSourceMetadataHeadAdvancesOnFirstRecordingAndHoldsOnRepublication(t *testing.T) { + s := newTestStore(t) + ctx := t.Context() + sourceSHA256 := fakeHash("a1") + _, err := s.CreateFile(ctx, s.RootID(), "source-a.pdf", sourceSHA256, 1, "application/pdf") + require.NoError(t, err) + metadataA := document.SourceMetadataV1{ContractVersion: document.SourceMetadataContractV1, + Fields: []document.SourceMetadataFieldV1{{Key: "title", Namespace: "pdf.info", SourceField: "Title", + Value: document.SourceMetadataValueV1{Kind: document.SourceMetadataString, String: new("A")}}}} + canonicalA, _, err := document.MarshalSourceMetadataV1(metadataA) + require.NoError(t, err) + generationA, err := s.PublishSourceMetadata(ctx, sourceSHA256, fakeHash("f1"), canonicalA) + require.NoError(t, err) + + metadataB := document.SourceMetadataV1{ContractVersion: document.SourceMetadataContractV1, + Fields: []document.SourceMetadataFieldV1{{Key: "title", Namespace: "pdf.info", SourceField: "Title", + Value: document.SourceMetadataValueV1{Kind: document.SourceMetadataString, String: new("B")}}}} + canonicalB, _, err := document.MarshalSourceMetadataV1(metadataB) + require.NoError(t, err) + generationB, err := s.PublishSourceMetadata(ctx, sourceSHA256, fakeHash("f2"), canonicalB) + require.NoError(t, err) + + active, activeMetadata, err := s.ActiveSourceMetadata(ctx, sourceSHA256) + require.NoError(t, err) + assert.Equal(t, generationB.GenerationID, active.GenerationID) + assert.Equal(t, "B", *activeMetadata.Fields[0].Value.String) + + currentRetry, err := s.PublishSourceMetadata(ctx, sourceSHA256, fakeHash("f2"), canonicalB) + require.NoError(t, err) + assert.Equal(t, generationB.GenerationID, currentRetry.GenerationID) + + replayed, err := s.PublishSourceMetadata(ctx, sourceSHA256, fakeHash("f1"), canonicalA) + require.NoError(t, err) + assert.Equal(t, generationA.GenerationID, replayed.GenerationID) + + active, activeMetadata, err = s.ActiveSourceMetadata(ctx, sourceSHA256) + require.NoError(t, err) + assert.Equal(t, generationB.GenerationID, active.GenerationID) + assert.Equal(t, "B", *activeMetadata.Fields[0].Value.String) + var generationCount int + require.NoError(t, s.db.QueryRowContext(ctx, + `SELECT COUNT(*) FROM source_metadata_generations WHERE source_sha256=?`, sourceSHA256).Scan(&generationCount)) + assert.Equal(t, 2, generationCount) +} + +func TestSourceMetadataHeadAppearsWhenMissingForRecordedEvidence(t *testing.T) { + s := newTestStore(t) + ctx := t.Context() + sourceSHA256 := fakeHash("a1") + _, err := s.CreateFile(ctx, s.RootID(), "source-a.pdf", sourceSHA256, 1, "application/pdf") + require.NoError(t, err) + canonical, _, err := document.MarshalSourceMetadataV1(document.SourceMetadataV1{ + ContractVersion: document.SourceMetadataContractV1, + Fields: []document.SourceMetadataFieldV1{{Key: "title", Namespace: "pdf.info", SourceField: "Title", + Value: document.SourceMetadataValueV1{Kind: document.SourceMetadataString, String: new("A")}}}}) + require.NoError(t, err) + generation, err := s.PublishSourceMetadata(ctx, sourceSHA256, fakeHash("f1"), canonical) + require.NoError(t, err) + _, err = s.db.ExecContext(ctx, `DELETE FROM source_metadata_heads WHERE source_sha256=?`, sourceSHA256) + require.NoError(t, err) + + republished, err := s.PublishSourceMetadata(ctx, sourceSHA256, fakeHash("f1"), canonical) + require.NoError(t, err) + assert.Equal(t, generation.GenerationID, republished.GenerationID) + active, _, err := s.ActiveSourceMetadata(ctx, sourceSHA256) + require.NoError(t, err) + assert.Equal(t, generation.GenerationID, active.GenerationID) +} + func TestHistoricalSourceMetadataOmitsCurrentAttachmentFacts(t *testing.T) { s := newTestStore(t) ctx := t.Context()