docs backend evidence community safety CS10A_PROMOTION_CONTRACT - AlexandreZanata/Abastevo GitHub Wiki
CS10A — Promotion domain and worker contract
Date: 2026-10-11. Status: LOCAL_DONE. Scope: pure domain +
application contract only — no migration, inventory SQL, sanitizer
adapter, worker scheduling, storage purge, soak or APK (CS10B owns
persistence/wiring, CS10C the wall-clock 24 h proof). Head:
maintained dev + this task.
Plan: phased plan
(CS10, AT03), consent contract.
Rules: B-BR-CS06, BUC-CS05.
Validation: PASS
Frozen decisions
- Promotion re-checks the current consent at commit time: a revocation landing between queueing and commit refuses with no inventory write; stale offline grants never resurrect selection.
- Future-only per-image scope carries over: pre-grant captures and
opt-out images refuse (
ErrImageNotCovered); grants never authorize old photos retroactively, including across a revoke→fresh-grant cycle. - Minimization verdict is authoritative and fail-closed: FAIL and
UNCERTAIN refuse identically (
ErrNotSanitizable), no original-photo fallback. Sanitizer outages refuse with no write. - Derivative rows stay identifier-only (reflect pins): references plus source/derivative hashes that bind bytes, never pixels, GPS, keys or labels. Source bytes never persist in the promotion path; hashes bind them at offer time.
- Inventory converges on the image-plus-revision key: identical re-proposals replay, divergent hashes conflict. Owner revocation flips ACTIVE→REVOKED tombstones (restore filter reads them) and repeats converge to zero; cross-owner rows are untouched.
SelectableForTrainingis the single shared filter for worker commits and restore replay: active receipt plus currently granted consent on current terms, plain denial otherwise.
Added files (owning module privacy, no cross-module imports)
backend/internal/modules/privacy/domain/promotion.go:PromotionCandidate,Derivative(ACTIVE/REVOKED),PromotionReceipt, frozenPASS/FAIL/UNCERTAINverdicts,ProposeDerivative/RevokeDerivative/SelectableForTraining,ErrConsentRequired/ErrImageNotCovered/ErrNotSanitizable. The sanitizer port stays an interface so the real adapter (evidence media) wires in CS10B without an import inversion.backend/internal/modules/privacy/application/promote.go:Sanitizer+PromotionInventoryports,PromotePorts(embeds the consent ports),Propose(commit-time consent re-read → fail-closed minimize → converged write) andRevokeOwner(per-owner flip count for the purge signal).contracts/testdata/privacy-consent/promotion-*.json(4 golden vectors: eligible candidate, pre-grant refusal, uncertain refusal, active receipt) pinned byapplication/promote_fixture_test.gothrough the domain.- Incidental: the CS09A fixture glob narrowed to
consent-*.jsonso phase vectors coexist in one directory.
Validation (actual)
go test -race -count=1 ./internal/modules/privacy/...: all packages green, including 15 new promotion tests (domain 8, application 6, fixture 1), 0 failures. RED observed before GREEN (undefined-type build failures plus two contract mismatches the tests forced: three-value vs receipt return, embedded ports literal).go build ./...,go vet,gofmt -l,git diff --checkclean;scan-secrets.shpassed. No PII/GPS/secrets in code, fixtures (synthetic IDs/hashes only) or this file.- No PostGIS run: pure phase, no SQL touched. Persistence + worker wiring proofs are CS10B work; the wall-clock soak is CS10C.
Limits / owed
- Derivative inventory migration + guarded store + real-PostGIS revision/ownership/replay/concurrency proofs: CS10B.
- Sanitizer adapter over the evidence media pipeline, worker scheduling/job kinds, private-namespace storage: CS10B.
- Physical 24-hour deletion proof (controlled real soak, periodic inventory, restore drill, SLO measure): CS10C — a virtual-clock test is explicitly not claimed as wall-clock evidence here.
- Erasure fan-out into derivatives: CS10B/C. Collection stays OFF.
Next microtask: CS10B — derivative persistence + worker wiring.