Repository navigation
feat(worker): Camera-labelled bursts through a BurstDetection work item (#297) - #300
Merged
Merged
Conversation
…em (#297) After a crawl, the photos of a storage config that share Apple's BurstUUID form one burst, with the earliest member as the stored cover. - BurstDetection work kind, one item per storage config with dedup key burst-detection:{id}. The config's Crawl enqueues it once the crawl ran; StorageConfigSchedules<TPayload>, now shared with Crawl (decision 3B), schedules it at the crawl interval as a fallback. - BurstChecks table: one row per checked photo (candidate flag, BurstUUID). Detection reads each photo's original once (1A + 2A); the first run checks the whole library. A missing root fails the item before any photo is marked, so an unmounted share cannot mark photos as non-candidates. - Each run repairs the config's bursts first (fewer than two active members: deleted; otherwise the earliest active member becomes cover; memberless bursts deleted), then checks unchecked Image and LivePhoto rows oldest first, in batches, joining or merging bursts by label. - Bursts.PrimaryAssetId is nullable with ON DELETE SET NULL instead of RESTRICT, so deleting a storage config or a cover no longer fails; the Burst query filter keeps coverless bursts visible. - ContentChangedWriter clears the burst mark of a file edited in place. - MetadataExtractor 2.9.3 yields an AppleMakernoteDirectory from the maintainer's sample HEICs (no BurstUUID in them), confirming the label read before building on it. - ExifDate.Parse is shared by the EXIF middleware and the reader; it relies on NodaTime returning a failed result for null text. - GLOSSARY.md with Burst and Cover, from docs/26-burst-design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#297) The members are moved after the merged bursts are removed because removing a burst sets its tracked members' BurstId to null immediately; the merge tests pin that order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nfig guard (#297) - IBurstRepository (Core) holds every burst query and the burst-checked mark, so the handler no longer touches AnichronDbContext and BurstChecks is written through one route (ContentChangedWriter clears the mark through it too). The includes for a burst's members live in one place. - ActiveStorageConfigs is the one "missing or inactive config" guard for per-config handlers; Crawl and BurstDetection both return StorageConfigSkipped. CrawlSkipped is gone: same JSON shape, one type. - BurstDetectionResult is the run's counter itself; the private Tally that mirrored it is gone. - WorkSchedule<TPayload> is no longer abstract, so StorageConfigSchedules builds it directly and the empty StorageConfigSchedule subclass is gone. - ExifDate.Parse handles a missing value explicitly instead of relying on NodaTime returning a failed parse for null; tests pin null, empty and ISO input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #297. Design: note on #296, decision (1A + 2A, 3B, cover
SET NULL).Summary
After a crawl, photos of one storage config sharing Apple's
BurstUUIDform one Burst; its stored Cover is the earliest member (GLOSSARY.md).ContentChangedWriterclears the mark (file edited in place is checked again); a moved file keeps mark and burst.WorkSchedule<TPayload>is no longer abstract;CrawlSkippedbecameStorageConfigSkipped(same JSON shape).ExifDate.Parseis shared byExifExtractionMiddlewareand the reader.Beyond the note: a missing root fails the run before any photo is marked, so an unmounted share cannot mark the library as non-candidates. Memberless bursts are loaded and deleted (not one statement); whichever config's run finds one counts it as dissolved.
Evidence
AppleMakernoteDirectory(noBurstUUIDin either), so the label read holds.FK_Bursts_MediaAssets_PrimaryAssetId(RESTRICT).After:
BurstDeleteBehaviourTests(integration): config delete cascades assets and checks, burst stays coverless; deleting the cover nulls it and keeps the other member.BurstDetectionTests, 38 cases), e.g.scripts/mutation-changed.sh100.00 %. A throwaway run of the handler on real PostgreSQL confirmed the queries translate and dissolve via the trackedSET NULLfix-up.Merge Danger
Door: one-way
Migration
BurstDetectionaddsBurstChecksand changesBursts.PrimaryAssetIdto nullableSET NULL; theBurstquery filter now keeps coverless bursts. Rows written by detection persist.Blast Radius: Worker
First run per config after deployment reads every photo's original once (cheaper than a crawl, which hashes every file). Crawl results now return
StorageConfigSkippedinstead ofCrawlSkipped(same{ reason }shape). No API change.🤖 Generated with Claude Code