Goal: Email Integration Phase 5a -- Component Library Updates
Ship five additive helpers in common-module that the Phase 5b Email module (operations) will consume to build its L1/L2/L3/L4 surfaces correctly. Phase 5a is a library-only phase — no runtime infrastructure changes, no consumer wiring. All five helpers are designed to be reusable beyond the email use case; the email module is the first consumer but not the last.
This phase is the common-module-side counterpart of Phase 5b. Phase 5a’s outputs (a sequence of common-module releases) become Phase 5b’s inputs (a single gradle.properties bump). The two phases can be planned in parallel; the only hard sequencing is that Phase 5b’s implementation merge waits on Phase 5a’s GitHub Packages publish of the final release.
Tickets
Section titled “Tickets”- PDEV-201 — Notifications / E-mail Pipeline: Project umbrella covering Phase 5a (and the broader Email Integration project). All five Phase 5a PRs reference this ticket; manual transitions only (no auto-transition from PR bodies).
Repositories
Section titled “Repositories”| Repository | Role | Planned Changes |
|---|---|---|
common-module | Primary | Five additive PRs (four Added-only minors plus one Changed major). New packages: lib/api/headers/ (sanitizeHeader), lib/crypto/ (TokenCipher, Hmac), lib/runtime/idempotency/ (RawIdempotencyStore, IdempotencyStore, IdempotencyKeyMinter). Extended: lib/lang/errors/AppError.kt (third top-level branch). Reclassified: 62+ Internal.IncompatibleState construction sites swept against the new Application.ConflictingState / Invocation.GeneralValidation buckets. |
documentation | Primary | Phase 5a planning artefacts (this directory), decision-log entries DQ-R1-027 through DQ-R1-031, completion byproducts at phase end. |
operations | Out of scope for Phase 5a | Consumer-side adoption (lifting the new common-module version, applying the email-module-scoped IncompatibleState sweep, wiring the typed idempotency view) is Phase 5b’s responsibility. |
infrastructure | Out of scope for Phase 5a | All infrastructure dependencies are already satisfied by Phase 4’s deliverables. |
Success Criteria
Section titled “Success Criteria”Phase 5a is complete when all of the following are true:
- All five
common-modulereleases published. PRs #1, #3, #4, #5 land asAdded-only minor releases (9.2.0 -> 9.3.0 -> 9.4.0 -> 9.5.0); PR #2 (theIncompatibleStatesweep) lands last as aChangedmajor release (10.0.0). Each release is available on the workspace’s GitHub Packages repository for consumergradle.propertiesbumps. AppError.Applicationis live. The hierarchy gainssealed class Applicationwith three subtypes (PreconditionFailed,PolicyRejected,ConflictingState);reportable()returns empty list for the branch; existing tests pass; new tests assert the three subtype constructors and the empty-reportablecontract. (DQ-R1-027)IncompatibleStatesweep complete incommon-module. Every construction site ofInternal.IncompatibleStateincommon-module/lib/src/mainhas been audited and either kept (genuine bug-class invariant violation), reclassified toApplication.ConflictingState(recoverable application outcome), or reclassified toInvocation.GeneralValidation(caller error). The classification rationale is captured per-bucket in the implementation PR’s description. Existing test suites pass after the sweep. (DQ-R1-028)sanitizeHeaderis live. New packagelib/api/headers/exposessanitizeHeader(name, value): Result<String?>returning accept / silent-drop / hard-reject outcomes. Unit tests cover the value-rejection cases (control characters, oversize, charset). The composition pattern withHeadersAllowListis documented. (DQ-R1-029)TokenCipherandHmacare live. New packagelib/crypto/exposesTokenCipher(the two-axis envelope from DQ-R1-019) andHmac(HmacSHA256 wrapper). The two existing JDK-Maccall sites (OpaqueId.kt:67,S3AssetService.kt:143) are internally refactored to useHmac(no external API change). Unit tests cover encrypt/decrypt round-trip across material-version transitions, the auth-tag-mismatch ->Internal.IncompatibleStateclassification, and the unknown-versionId->Transient.FailoverFailedclassification. (DQ-R1-030)- Idempotency helpers are live. New package
lib/runtime/idempotency/exposesRawIdempotencyStore(nativeJsonElementboundary, symmetric Req/Res),IdempotencyStore<Req, Res>(typed wrapper viainline fun typedAs()),IdempotencyStoreFactory, andIdempotencyKeyMinter. ContainerizedPostgres integration tests cover the fullbegin/commit/failPermanent/failTransientlifecycle, theMismatchdetection path (withrecordedRequest: JsonElementcarried), theInFlightoutcome under concurrentbegin, andpurgeExpired. The Flyway migration creating theidempotency_recordtable is not shipped bycommon-module; it ships in Phase 5b’s consumer adoption. (DQ-R1-031) - All five decision-log entries recorded.
DQ-R1-027throughDQ-R1-031exist in the global decision-log.md, each with context, options, recommendation, decision, and applied-to sections. common-moduleCHANGELOG carries one entry per PR. Each release line inCHANGELOG.mdmatches the workspace direct-edit changelog convention (one new entry per PR; descriptive of intent and outcome; not a file enumeration).
Context
Section titled “Context”What exists today (post-Phase-4)
Section titled “What exists today (post-Phase-4)”- Per-partition mail surface live across all four active partitions (
dev,stage,demo,prod). Phase 4 delivered the platform-side: partition mail sub-zones, per-partitionpartition-emailstack, per-partitionEmailEncryptionKeySecrets Manager secret, per-partition DNS-provisioning + SM-fallback IAM roles,runtime-platform-driftworkflow. common-moduleat version 9.x.x — the workspace’s shared Kotlin library; seecommon-module/CHANGELOG.mdon origin. Already in production use byoperations. No Phase 5a-relevant API yet.- Existing helpers and patterns Phase 5a builds on:
cards.arda.common.lib.lang.errors.AppError— the existing two-branch hierarchy (Internal,Invocation) that Phase 5a extends.cards.arda.common.lib.lang.serialization.JsonConfig.standardJson— the canonical kotlinxJsoninstance Phase 5a’s idempotency helpers anderror_payloadprojection use. Refinable viaJsonConfig.refine { ... }for variants (e.g.,prettyPrint = falsefor canonical hashing).cards.arda.common.lib.persistence.keystore.DatabaseBackedMap— the factory shape pattern (inTransaction/inConnection/withTxreturningResult<T>) that the idempotency factory mirrors.cards.arda.common.lib.api.rest.server.service.Message— thecompanion inline operator fun <reified T : Any> invoke(...)factory pattern, reused for the idempotency-store typed wrapper but not forTokenCipher(non-generic; uses plaincompanion operator fun invokeinstead).cards.arda.common.lib.runtime.observability.HeadersAllowList— the deny-by-name observability filter thatsanitizeHeadercomposes downstream of.cards.arda.common.lib.runtime.observability.OpaqueIdandcards.arda.common.lib.infra.storage.S3AssetService— the two existing JDK-MacHmacSHA256 call sites that the newHmachelper DRYs.cards.arda.common.lib.testing.persistence.ContainerizedPostgres— the integration-test pattern (real PostgreSQL container) for any persistence-shaped helper, used by the idempotency store’s lifecycle tests.
Why Phase 5a is library-only
Section titled “Why Phase 5a is library-only”The Phase 5b Email module is the proximate consumer of these helpers, but each helper is independently useful. Shipping them in common-module (the workspace’s shared library) instead of inside the Email module (a Phase 5b runtime artifact) means:
- Future modules that need an idempotency store, encrypted-field primitive, sanitised-header reader, or
Application-class error reuse the same primitives without a rebuild. - Phase 5a and Phase 5b can be reviewed independently. Phase 5b reviewers focus on the Email module’s correctness, not on whether
TokenCipher’s envelope format is sound (that’s a Phase 5a review). - The interface contract between the two phases is a versioned
common-moduleartifact rather than internal Email-module code, making future cross-consumer compatibility easier to reason about.
Release sequencing
Section titled “Release sequencing”Five PRs are sequenced per task-plan.md. Four are Added-only minor bumps (PRs #1, #3, #4, #5 -> 9.2.0, 9.3.0, 9.4.0, 9.5.0) and can be reviewed in any order; the fifth (PR #2 — the IncompatibleState sweep) is Changed and lands last as a major bump (10.0.0). The major-last sequencing means Phase 5b’s consumer adoption PR sees one combined gradle.properties bump to 10.0.0 carrying the full Phase 5a payload.
Out of scope
Section titled “Out of scope”- Phase 5b consumer adoption — lifting
common-moduleto10.0.0, applying the email-module-scopedIncompatibleStatesweep, wiring the typed idempotency view to the L3 services in the Email module. Owned by Phase 5b. - The
idempotency_recordFlyway migration — ships in Phase 5b’s consumer adoption (operations), not incommon-module.common-moduledoes not own production-side migrations. - HKDF exposure as a standalone helper — deferred. HKDF stays internal to
TokenCipherin v1. The workbook tracks this asDT-003(deferred); promote when a non-TokenCipherconsumer surfaces. - AppError-Application decision-record extraction into per-helper files — the workbook keeps
5a/decisions/dt-002-*.mdfiles for local record-keeping; the canonical decisions live in this directory’s decision-log.md.
Reference Documents
Section titled “Reference Documents”decision-log.md— entriesDQ-R1-027throughDQ-R1-031for the Phase 5a decisions;DQ-R1-019for the encryption-envelope source design.design/index.md— canonical Phase 5a design (four helpers in §§ 1-4; § 5 pointer to idempotency).design/idempotency-design.md— carved-out implementation-shaped design for the idempotency helpers.task-plan.md— six PRs (one documentation promotion + fivecommon-modulereleases), sequencing, worktree strategy, and acceptance criteria.- Inherited constraints (DQ-201..208 application-layer set, DQ-012 per-tenant token storage, DQ-R1-019 per-partition encryption key) live in the global
decision-log.md; the new Phase 5a entries DQ-R1-027..031 cite them where they apply. The four constraints the design honours implicitly (DQ-204 STS, DQ-206 no-plaintext, DQ-208 tx-ownership, Cross-Universe rule) are restated indesign/index.md§ 6.4.
Copyright: © Arda Systems 2025-2026, All rights reserved