Phase 3 -- Corporate Updates -- Specification
The contract for Phase 3 implementation. Each task lists its scope, file targets, AWS impact, and a STOP point where the implementer pauses for review before proceeding.
This specification is derived from requirements.md and analysis.md. The verification regime is in verification.md. The cross-phase exports Phase 3 produces are catalogued in exports.md. The execution plan is in plan/task-plan.md.
1. Working agreements
Section titled “1. Working agreements”1.1 Implementation skill domain
Section titled “1.1 Implementation skill domain”Phase 3 implementation is TypeScript / CDK / infra tooling on the infrastructure repository, plus Markdown / Starlight on the documentation repository. The full inventory of agent-execution skills (which Claude Code skills apply where, in what order) is orchestration metadata for the AI executor, not specification content; it lives in plan/task-plan.md § 1 under “Agent execution notes.” A human implementer or reviewer reading this spec does not need to know which skills apply.
1.2 AWS-impact discipline
Section titled “1.2 AWS-impact discipline”Per the project-level CLAUDE.md, every change is classified as None / Synth-only / Resource-touching. Each task below carries its impact tag. Tasks at Resource-touching require a cdk diff summary surfaced in the PR description and explicit user confirmation before deploy (Phase B).
The deploy targets the production Root account (Admin-Alpha1 profile, platformRoot AWS account); the Corporate Email stack and the Free Kanban Tool stack both deploy here for the foreseeable future per architecture-overview § 6.4.
1.3 API-over-GUI rule
Section titled “1.3 API-over-GUI rule”Wherever an API equivalent exists, the operator path uses the API via corporate-cli.ts (Postmark Account API, gh CLI, 1Password SDK, AWS CDK / aws CLI). The Postmark Console click-through is reserved for steps that have no API equivalent (sandbox-to-live approval is the only such step in Phase 3).
1.4 CFN stack-name immutability
Section titled “1.4 CFN stack-name immutability”The two new Corporate stacks have pinned CloudFormation stack names: CorporateMailDns and FreeKanbanToolMailDns. These literal strings are passed as the CDK id argument to each stack’s constructor. Once a stack is deployed, the name must not change — changing the id argument forces CloudFormation to delete and recreate the stack, destroying every resource it owns (the arda.ardamails.com zone, the NS-delegation Custom Resource, the IAM role, etc.). An inline source-code comment immediately above each constructor call documents the constraint, mirroring the Phase 2 "RootConfiguration" pattern.
The CDK class names match the CFN stack names: class CorporateMailDns extends cdk.Stack and class FreeKanbanToolMailDns extends cdk.Stack, in src/main/cdk/stacks/corporate/corporate-mail-dns.ts and src/main/cdk/stacks/corporate/free-kanban-tool-mail-dns.ts respectively.
1.5 Tests ship with code
Section titled “1.5 Tests ship with code”Every task that introduces or modifies executable code (constructs, stacks, app, instance config, CLI, drift driver, helpers, lambdas) ships with parallel tests in the same PR, and the tests must pass before the STOP gate clears. The “tests” criterion includes:
- Jest (or the repo’s chosen runner) unit tests with adequate coverage for new public surface.
- Integration / synth-only smoke tests where applicable (CDK Template-matcher +
npx cdk synthagainst fixture context). - Injected-dependency tests for code with external boundaries (Postmark client, 1Password SDK, filesystem,
dig). - The repo’s lint and type-check gates (
npm run lint,tsc --noEmit) green. - The repo’s CI matrix exercises the new code path.
A task whose scope is “add construct X” without adding X.test.ts (or an equivalent) does not clear its STOP gate. The same rule applies to any change to existing code that broadens or changes a public surface — the change ships with the test that locks the new shape.
1.6 Documentation quality review
Section titled “1.6 Documentation quality review”Every task that produces durable documentation (planning artifacts, design / decision-log entries, the operator runbook, current-system/ pages, byproducts) is reviewed by a specialized documentation reviewer before the STOP gate clears. The review is in addition to the implementer’s own self-review and the human reviewer’s PR-level review:
- The reviewer is a
technical-writer(orquality-reviewerwith a docs-specific brief) launched as a sub-agent. - The reviewer’s brief: assess structure, clarity, internal consistency, link integrity, frontmatter conformance, en_US locale (
artifact,behavior,analyze), and adherence to thedocument-writingandpath-conventionsskills. - Findings are returned in a structured report. The implementer addresses each finding before the STOP gate clears (or accepts and records the rationale inline if a finding is rejected).
This rule applies to:
- Tasks D1, D3, D4 (Phase A contract documentation).
- Tasks D2, D5, D6 (Phase D system documentation, byproducts, CHANGELOG amendment) — the operator runbook at
process/sre/runbooks/postmark-domain-verification.mdis included. - Any other task whose scope adds a
.md/.mdxfile underdocumentation/src/content/docs/.
1.7 Out of scope (restated)
Section titled “1.7 Out of scope (restated)”Per requirements.md § Out of scope of Phase 3:
- Per-partition mail sub-zones (Phase 4).
- Per-partition Postmark account-token + encryption-key Secrets Manager entries (Phase 4).
- Backend
ShopAccess/Emailmodule inoperations(Phase 5b). - Apex SPF / DMARC at
ardamails.com(out of project scope). - Tightening
AllowCreatingNSRecordsRole.allowedParentHostedZoneIds(out of project scope). - Migration of
PostmarkServerthin-wrapper internals to a Lambda-backed Custom Resource (deferred). - Helm-release equivalent of the IaC patterns (deferred).
2. Tasks
Section titled “2. Tasks”The tasks divide into four execution phases. The split follows the principle that documentation describing what to build comes first (it is the contract); documentation describing what was built comes last (it benefits from learnings, discoveries, and any deploy-time corrections to the contract).
Numbering scheme. Task IDs are content-typed, not execution-ordered:
D*for documentation,I*for infrastructure,O*for operator. Numbers are not sequential within a phase (e.g., Phase D contains tasks D2, D5, D6 — D2 is in Phase D because it is system documentation, D5 because it is byproducts, etc.). The companionplan/task-plan.mdprefixes every task withT-(e.g.,T-D1,T-I3,T-O0) for clarity in cross-document references;T-D1and bareD1denote the same task. Execution order is in § 3.
- Execution Phase A — Contract documentation (Tasks D1, D3, D4): the planning artifacts, the
phases.mdpatch, the docs-side CHANGELOG. Authored before implementation begins. The implementer reads these to know what to build. - Execution Phase B — Infrastructure code (Tasks I1-I10): CDK constructs, stacks, app, instance config, CLI, drift workflow. Authored against the Phase A contract.
- Execution Phase C — Operator deploy + verification (Tasks O0-O4): pre-deploy state assertion, mailbox prerequisite, Phase A run + Phase B deploy, post-deploy verification, drift-workflow first run.
- Execution Phase D — System documentation + byproducts (Tasks D2, D5, D6): the
current-system/oam/andcurrent-system/runtime/pages describing what was actually built; the long-lived operator runbook atprocess/sre/runbooks/postmark-domain-verification.md; the implementation byproducts (changelog,learnings,suggestions,alternatives,skipped,specification-postunder3-corporate-updates/implementation/). Authored after the deploy so any discoveries (e.g., a Postmark API quirk like the IMPORT-detour discovered in Phase 2) are reflected.
If a Phase D task surfaces a contradiction with the Phase A contract, the contract is amended (per the implementation-task skill’s specification-post.md byproduct convention) — the spec is not retroactively rewritten to hide the divergence.
The infrastructure tasks are the most numerous; they are deployed in dependency order within Phase B.
Task D1: Phase 3 planning artifacts on the documentation side
Section titled “Task D1: Phase 3 planning artifacts on the documentation side”Goal: publish the Phase 3 planning artifacts so reviewers can read the contract before the IaC tasks land.
Scope:
- Author
requirements.md,specification.md(this file),verification.md,exports.md, andplan/task-plan.mdunderroadmap/in-progress/email-integration/3-corporate-updates/. (analysis.mdand the operator-domain-verification-checklist stub are already in place from Pass 1.) - Patch
phases.md— the Phase 3 section’s deliverables table is extended with a row for the Postmark Sender Signature creation by Phase A (perDQ-R1-009) and a row for thecorporate-drift.ymlworkflow. - Patch
decision-log.md— the Round R1-Phase3 section already exists from Pass 1; confirm it is published and linked from the decision table at the top.
File targets:
- New:
3-corporate-updates/{requirements,specification,verification,exports}.md,3-corporate-updates/plan/task-plan.md. - Edit:
phases.md(Phase 3 deliverables table),decision-log.md(already complete).
AWS impact: None.
STOP — review after D1: make pr-checks passes. The five Pass-2 documents are linked from phases.md and from each other.
Task D3: Patch phases.md Phase 3 deliverables table
Section titled “Task D3: Patch phases.md Phase 3 deliverables table”Goal: keep the project-level phase plan aligned with the Phase 3 contract.
Scope:
- In
phases.md’s Phase 3 deliverables table, add a row for the Postmark Sender Signature registration (perDQ-R1-009) — previously the deliverables table only mentioned DNS records. - Add a row for the
corporate-drift.ymlworkflow. - Confirm the Phase 3 entry-criteria / recovery subsections still match the spec (they were authored in a prior commit).
File targets:
- Edit:
phases.md(Phase 3 deliverables table).
AWS impact: None.
STOP — review after D3: make pr-checks passes; reviewer confirms the patch reflects the spec’s deliverable list.
Task D4: Documentation CHANGELOG entry
Section titled “Task D4: Documentation CHANGELOG entry”Goal: every PR to a protected branch carries a CHANGELOG.md entry; this task folds the Pass-2 contract artifacts into the existing [0.31.0] entry.
Scope:
- Extend the existing
[0.31.0]entry with bullets for the Pass-2 artifacts (requirements.md, thisspecification.md,verification.md,exports.md,plan/task-plan.md). - The
current-system/pages bullet (D2) is added later when D2 lands in Phase D.
File targets:
- Edit:
documentation/CHANGELOG.md.
AWS impact: None.
STOP — review after D4: make clq passes.
Task I1: Rename route-53-hosted-zone.ts → dns-zone.ts and migrate callers
Section titled “Task I1: Rename route-53-hosted-zone.ts → dns-zone.ts and migrate callers”Goal: generalize the hosted-zone construct so it serves any registrable domain.
Scope:
-
git mvthe filesrc/main/cdk/constructs/xgress/route-53-hosted-zone.tstodns-zone.ts. -
Inside, rename the class
Route53HostedZonetoDnsZone. Drop thearda.cardsdefault foroverrideDomainName; the prop now accepts any zone name explicitly. -
Update
validateProps()to require thezoneName(no default). -
Migrate every caller to
DnsZoneand the new path. PerDQ-R1-011, all callers migrate in this PR. The caller list at the time this spec was written (verified viagrepagainstArda-cards/infrastructureHEAD onphase-2-infra):src/main/cdk/stacks/infrastructure/ingress-stack.ts— 5 instantiations: lines 194 (ioHostedZone), 203 (appHostedZone), 212 (authHostedZone), 221 (assetsHostedZone), 458 (per-partition / config-driven hosted zone). Plus the import at line 1.src/main/cdk/constructs/xgress/route-53-hosted-zone.ts— the construct itself (renamed in this task).knowledge-base/route53-and-dns.md— one prose mention of the construct name; update if still relevant after the rename.
Re-run
grep -rn "Route53HostedZone\|route-53-hosted-zone"against the actual HEAD at implementation time — if the count differs, reconcile with the caller-list above and update Task I1 inspecification-post.md. After migration, the same grep returns zero hits. -
Update the construct’s tests (
route-53-hosted-zone.test.ts→dns-zone.test.ts) to exercise the new shape.
File targets:
- Move + edit:
src/main/cdk/constructs/xgress/route-53-hosted-zone.ts→dns-zone.ts; corresponding test. - Edit: every caller of
Route53HostedZone(expected: a small number; identify exhaustively via grep).
AWS impact: Synth-only — the synthesized template per existing caller is unchanged once the rename is applied (zone names already explicit in the existing callers’ configs; the migration only removes the arda.cards default).
STOP — review after I1: npm run build and npm test pass on the infrastructure repo. cdk synth for every existing app produces identical templates to the pre-rename baseline (verified by template diff against a checkpoint).
Task I2: Add dns-email-records.ts xgress construct
Section titled “Task I2: Add dns-email-records.ts xgress construct”Goal: introduce a generic construct that publishes a sending sub-domain’s DKIM TXT record and Return-Path CNAME record.
Scope:
-
Author
src/main/cdk/constructs/xgress/dns-email-records.ts. Public shape:export interface Configuration {hostedZone: r53.IHostedZone;subdomain: string; // e.g., "freekanban"dkimSelector: string; // from PostmarkSendingDomain.BuiltdkimKey: string; // public DKIM key textreturnPathTarget: string; // e.g., "pm.mtasv.net"ttlSeconds?: number; // default 300}export interface Built {dkimRecord: r53.TxtRecord;returnPathRecord: r53.CnameRecord;} -
Implement the construct: emits one
r53.TxtRecordat<selector>._domainkey.<subdomain>.<zoneName>and oner53.CnameRecordatpm-bounces.<subdomain>.<zoneName>→returnPathTarget. TTL 300 seconds default. -
No environment-variable bridge; no file-artifact channel. Inputs come from props only.
-
validateProps()requires non-empty selector, key, target. -
Author
dns-email-records.test.ts— CDK Template assertions for the two records’ Names, Types, and TTLs.
File targets:
- New:
src/main/cdk/constructs/xgress/dns-email-records.ts,dns-email-records.test.ts.
AWS impact: Synth-only.
STOP — review after I2: tests pass. Construct synthesizes against a fixture hosted zone.
Task I3: Add Postmark thin-wrapper constructs
Section titled “Task I3: Add Postmark thin-wrapper constructs”Goal: introduce the IaC-side analogue of L1 protocol proxies for the Postmark Account API.
Scope:
- Create the directory
src/main/cdk/platform/constructs/postmark/. - Author
server.ts(PostmarkServerthin-wrapper):Configuration:accountToken: string(resolved at runtime from 1Password),serverName: string,colorOptional?: PostmarkServerColor,bounceHookUrl?: string, etc.Built: public values only —serverId: numberplus any other DNS-publishable metadata. The Postmark Server API token is NOT exposed onBuiltin either the interim or the target Custom-Resource mode. The token is a Phase-A side-effect persisted to 1Password (op://Arda-CorporateOAM/Free-Kanban-Generator-Postmark-Server/credential) and is the only place Phase 3 stores it. Stacks that need the token at runtime resolve it from 1Password through their own auth path (out of scope of CDK).- In the interim mechanism, the construct reads
serverId(and any other public values) fromcdk.context.json— the source of truth populated by Phase A of the Corporate CLI.
- Author
sending-domain.ts(PostmarkSendingDomainthin-wrapper):Configuration:accountToken: string,domainName: string(e.g.,"arda.ardamails.com"),returnPathDomainOptional?: string.Built:dkimSelector: string,dkimKey: string,returnPathTarget: string.- Same interim-mechanism pattern: read public values from
cdk.context.json.
- Both constructs follow the construct-line shape (
validateProps();ConfigurationandBuilttypes). Neither is a full CDKConstructsubclass for synth-time runtime behavior; they emit nothing into the CFN template directly but expose typedBuiltfields the stacks compose into other constructs (e.g.,DnsEmailRecords). - Tests at
server.test.tsandsending-domain.test.ts. Mock the Postmark API client at the HTTP boundary; assert idempotency (list-then-create),Builtshape, error handling for 4xx vs 5xx (per the rev1 design intent recorded in the cross-cutting design’s Postmark API surface notes).
File targets:
- New:
src/main/cdk/platform/constructs/postmark/{server,sending-domain}.tsand tests.
AWS impact: Synth-only.
STOP — review after I3: tests pass. The constructs synthesize cleanly against a fixture cdk.context.json.
Task I4: Reintroduce FREE_KANBAN_POSTMARK_ITEM; extend ari-configuration.ts; extend drift-check
Section titled “Task I4: Reintroduce FREE_KANBAN_POSTMARK_ITEM; extend ari-configuration.ts; extend drift-check”Goal: re-add the typed 1Password reference removed by Phase 1’s DQ-R1-007 (with the new vault); add the reserved-words list extension; extend the drift-check tool’s surface.
Scope:
-
In
src/main/cdk/platform/one-password.ts, add:export const FREE_KANBAN_POSTMARK_ITEM: OnePasswordItem = {vault: "Arda-CorporateOAM",title: "Free-Kanban-Generator-Postmark-Server",primaryField: "credential",reference: "op://Arda-CorporateOAM/Free-Kanban-Generator-Postmark-Server/credential",}; -
In
src/main/cdk/platform/ari-configuration.ts, add a constantMAIL_RESERVED_SLUGS_AT_MAIL_ROOT(or similar; name TBD by implementer with rationale in the PR description) holding["arda"](and an extension point for future Corporate-instance-group zone names). Wire into any partition-zone slug-validation that should reject these names. -
In
tools/drift-check.ts, extend theALL_OP_ITEMSarray withFREE_KANBAN_POSTMARK_ITEM. Important: the drift run fromexternal-resources-drift.yml(Phase 1’s workflow) authenticates withOP_SERVICE_ACCOUNT_TOKEN, which is scoped toArda-SystemsOAMonly. Do not assert the new item’s content from the Phase 1 workflow. Instead, the Phase 1 drift run only asserts the three Phase-1-resolvable items; the new item’s existence assertion lives in the Corporate drift workflow (Task I8) which authenticates differently. -
Update
tools/drift-check.test.ts— the test that asserts the count of items moves from 3 to 4 (item declarations); the test forOP_SERVICE_ACCOUNT_TOKEN-scoped resolution still asserts only the three Phase-1 items.
File targets:
- Edit:
src/main/cdk/platform/one-password.ts,src/main/cdk/platform/ari-configuration.ts,tools/drift-check.ts,tools/drift-check.test.ts.
AWS impact: Synth-only on one-password.ts and ari-configuration.ts. None on the drift-check changes.
STOP — review after I4: tests pass. tools/drift-check.ts --report (dry-run mode) returns four items.
Task I5: Add instances/Corporate/free-kanban-tool.ts
Section titled “Task I5: Add instances/Corporate/free-kanban-tool.ts”Goal: declare the Free Kanban Tool’s Phase-3 configuration in the rev1 declarative pattern.
Scope:
-
Create the directory
src/main/cdk/instances/Corporate/. -
Author
free-kanban-tool.ts(the asset-named instance config; not the stack file):import { POSTMARK_PROD_ACCOUNT } from "../../platform/postmark-service";import { FREE_KANBAN_POSTMARK_ITEM } from "../../platform/one-password";export const FREE_KANBAN_CONFIG = {postmarkAccount: POSTMARK_PROD_ACCOUNT,postmarkItem: FREE_KANBAN_POSTMARK_ITEM,sendingSubdomain: "freekanban",corporateZoneName: "arda.ardamails.com",postmarkPlanColor: PostmarkServerColor.GREEN, // or similar plan attribute} as const; -
Future Corporate consumers (HubSpot, marketing) add sibling files in this directory.
File targets:
- New:
src/main/cdk/instances/Corporate/free-kanban-tool.ts.
AWS impact: Synth-only.
STOP — review after I5: file imports resolve; npm run build passes.
Task I6: Add Corporate stacks (CorporateMailDns and FreeKanbanToolMailDns)
Section titled “Task I6: Add Corporate stacks (CorporateMailDns and FreeKanbanToolMailDns)”Goal: stand up the two Corporate stacks per the architecture-overview composition.
Scope:
- Create
src/main/cdk/stacks/corporate/. - Author
corporate-mail-dns.ts(CorporateMailDnsstack):- Owns the
arda.ardamails.comzone viaDnsZone. - Owns the SPF TXT record at apex (
v=spf1 include:spf.mtasv.net ~all). - Owns the DMARC TXT record at
_dmarc.arda.ardamails.com(initial monitoring policyv=DMARC1; p=quarantine; sp=quarantine; rua=mailto:dmarc-reports@arda.cards; alignment defaults to relaxed —adkim/aspftags omitted). - Instantiates
WriteNSRecordsToUpstreamDnswithsubdomain: "arda",nameServersfrom the new zone’shostedZoneNameServers,targetAccountIdset toplatformRoot’s account ID,createNsRoleArnreferencing the Phase 2 exportarda-allow-create-ns-record-role. - Exports
arda-corporate-mail-zone(zone ID). RemovalPolicy.RETAINon the zone.- Inline comment above the constructor call in
tools/cdk-corporate.ts:// CFN stack name MUST remain "CorporateMailDns" -- changing it would force CloudFormation to delete and recreate the stack.
- Owns the
- Author
free-kanban-tool-mail-dns.ts(FreeKanbanToolMailDnsstack):- Composes
PostmarkServer(thin-wrapper, reads fromcdk.context.json),PostmarkSendingDomain(thin-wrapper, reads fromcdk.context.json), andDnsEmailRecords. - The
PostmarkSendingDomain.Built.dkimSelector/dkimKeyandPostmarkServer.Built.serverIdplusreturnPathTargetare passed intoDnsEmailRecords. - The Corporate zone is consumed via the
arda-corporate-mail-zoneexport from theCorporateMailDnsstack (or via directFn.import_value(...)if the stack is in the same App, which it is). - Inline comment above the
FreeKanbanToolMailDnsconstructor call intools/cdk-corporate.ts:// CFN stack name MUST remain "FreeKanbanToolMailDns" -- changing it would force CloudFormation to delete and recreate the stack.
- Composes
- Tests for each stack — CDK Template assertions for the resources declared (zone, NS-write CR, SPF / DMARC TXT records, Free Kanban DKIM TXT, Return-Path CNAME).
File targets:
- New:
src/main/cdk/stacks/corporate/corporate-mail-dns.ts,free-kanban-tool-mail-dns.ts, and tests.
AWS impact: Synth-only (the resources will be deployed in Task O2; this task only synthesizes).
STOP — review after I6: tests pass. cdk synth via tools/cdk-corporate.ts (added in I7) produces a valid template against a fixture cdk.context.json populated with sentinel values.
Task I7: Add Corporate App class and entry script
Section titled “Task I7: Add Corporate App class and entry script”Goal: a reusable CorporateApp class plus a dedicated entry script that calls new cdk.App() and instantiates both Corporate stacks.
Scope:
-
Create
src/main/cdk/apps/Corporate/. -
Author
index.ts— exportsCorporateAppclass (nonew cdk.App()at module load; App class is reusable and side-effect-free per the pattern inapps/Al1x/). -
Author
src/main/cdk/tools/cdk-corporate.ts— the entry script:const app = new cdk.App();// CFN stack name MUST remain "CorporateMailDns" -- changing it would// force CloudFormation to delete and recreate the stack.const corporateMailDns = new CorporateMailDns(app, "CorporateMailDns", { ... });// CFN stack name MUST remain "FreeKanbanToolMailDns" -- changing it// would force CloudFormation to delete and recreate the stack.new FreeKanbanToolMailDns(app, "FreeKanbanToolMailDns", { ... });(Note: the CDK
idargument and the CFN stack name are the same literal string; using a single name reduces the chance of an inadvertent rename relative to Phase 2’s pattern of distinct values for “logical id” and “CFN name”.) -
Both constructor calls carry the inline CFN-stack-name preservation comments shown above (rule from § 1.4).
-
Wire the new App into CI synth.
tools/cdk-runner.jsis partition-instance-driven (itssrcRootissrc/main/cdk/instances), so it does not natively exercise non-partition apps. Phase 2 introducedtools/ci-root-check.jsfor the same reason (the Root app is also non-partition). Phase 3 mirrors that pattern:- Author
tools/ci-corporate-check.jsalong the same lines astools/ci-root-check.js. The check runscdk synth --app 'npx ts-node ... tools/cdk-corporate.ts'against a fixturecdk.context.jsonand asserts the synthesized template includes the expected stacks and resources. - Add a CI workflow step (or extend the existing one) that invokes
node tools/ci-corporate-check.js. - Out of scope for this task: a generalization of
cdk-runner.jsto admit non-partition apps — that is a follow-up tracked alongsidePDEV-440(the Phase-2-discovered test-coverage gap).
- Author
File targets:
- New:
src/main/cdk/apps/Corporate/index.ts. - New:
src/main/cdk/tools/cdk-corporate.ts. - New:
tools/ci-corporate-check.js. - Edit:
.github/workflows/<existing-CI-workflow>.yml— add theci-corporate-checkstep.
AWS impact: Synth-only.
STOP — review after I7: CI matrix run includes the Corporate App; cdk synth via cdk-corporate.ts passes against the fixture context.
Task I8: Add corporate-drift.yml workflow + driver
Section titled “Task I8: Add corporate-drift.yml workflow + driver”Goal: scheduled drift detection for the Corporate instance group.
Scope:
- Author
infrastructure/.github/workflows/corporate-drift.yml:schedule: cron: "0 9 1 * *"(monthly).workflow_dispatch:for manual trigger.- Permissions:
contents: read,issues: write. - Steps: checkout, setup Node, run
npm ci, runtools/corporate-drift.tswithOP_SERVICE_ACCOUNT_TOKENavailable, on failuregh issue createwith labelsdrift,phase-3,corporate.
- Author
tools/corporate-drift.ts:- Enumerate the assets declared in
instances/Corporate/. - For each asset:
- Resolve the Postmark account token via the 1Password SDK (
op://Arda-SystemsOAM/...); the Corporate item itself is not read here (out ofOP_SERVICE_ACCOUNT_TOKENscope). - Call the Postmark Account API to list servers; assert the asset’s server is present.
digthe asset’s DNS records; assert presence and shape.- For the 1Password item, only assert the declaration exists in
platform/one-password.ts(verified by the existing drift-check pattern).
- Resolve the Postmark account token via the 1Password SDK (
- Output: structured JSON report.
- Enumerate the assets declared in
- Tests for
corporate-drift.tsfollow thedrift-check.test.tspattern — injectable dependencies (PostmarkClient,httpGet,dig).
File targets:
- New:
infrastructure/.github/workflows/corporate-drift.yml. - New:
infrastructure/tools/corporate-drift.tsandcorporate-drift.test.ts.
AWS impact: None (CI YAML; tool only reads from external services).
STOP — review after I8: tests pass. A manual workflow_dispatch smoke run of the workflow succeeds against a populated state (postpone to after Task O2 deploy).
Task I9: Author tools/corporate-cli.ts
Section titled “Task I9: Author tools/corporate-cli.ts”Goal: the two-phase orchestrator the operator invokes.
Scope:
- Author
infrastructure/tools/corporate-cli.tswith two subcommands:corporate-cli prepare <asset>— Phase A. Steps in order:- Resolve the Postmark account token from 1Password (
Arda-SystemsOAMfor the Postmark account itself). - Conflict-check (
REQ-CLI-003): list existing Postmark Sender Signatures, list existing servers, list existing 1Password items inArda-CorporateOAM. If the asset’s name collides with a pre-existing entity that is not the one this invocation is reconciling, exit with a structured failure message. - Create-or-reconcile the Sender Signature for the asset’s domain (
arda.ardamails.comfor Free Kanban). Capture DKIM selector + key + Return-Path target. - Invoke
verifyDkimandverifyReturnPathagainst the Sender Signature. (May initially returnunverifiedif the DNS records don’t exist yet — that’s fine; Phase B writes them, then Task O3 re-verifies.) - Create-or-reconcile the Postmark server for the asset. Capture the Server API token in a process-local secret-handling buffer.
- Read
DeliveryTypefromGET /servers/{id}on the just-created (or reconciled) server. Emit a structured log event:infoforLive,warnforSandboxor unknown. This is the signal forREQ-OPS-002: Postmark has no account-level sandbox endpoint (GET /redirects;GET /accountreturns 404), soDeliveryTypeon the individual server is the correct and only API-surface indicator of whether mail will be delivered. The operator reads this log output and acts on it before proceeding to Phase B if approval is needed. - Write the 1Password item
Free-Kanban-Generator-Postmark-Server(inArda-CorporateOAM) with the server-token as thecredentialfield. Retries with exponential backoff. On permanent failure, exit with redacted summary; do not writecdk.context.json. - Write public values to
cdk.context.json:postmark.<asset>.serverId,postmark.<asset>.dkimSelector,postmark.<asset>.dkimKey,postmark.<asset>.returnPathTarget.
- Resolve the Postmark account token from 1Password (
corporate-cli deploy <asset>— Phase B. Steps:- Run
cdk diff apps/Corporate/. Display the diff. - On confirmation (interactive prompt or
--yesflag), runcdk deploy apps/Corporate/.
- Run
- Source comments at the top of each subcommand explicitly name the phase and the migration trigger (when Lambda-backed Custom Resources become a wider repo pattern,
prepareis retired). - Structured logging (per
REQ-CLI-007): JSON-line per event; redaction of token-shaped fields. - Tests at
corporate-cli.test.ts— inject mocks for the Postmark client, the 1Password SDK, the filesystem; cover idempotency, conflict-check failure path, in-memory token-buffer with retries, redaction.
File targets:
- New:
infrastructure/tools/corporate-cli.tsandcorporate-cli.test.ts.
AWS impact: None (the CLI runs externally and only invokes APIs / writes files; the cdk deploy invocation is what carries the Resource-touching impact, attributed to Task O2).
STOP — review after I9: tests pass. Dry-run of corporate-cli prepare free-kanban against a NonProd fixture succeeds.
Task I10: Infrastructure CHANGELOG entry
Section titled “Task I10: Infrastructure CHANGELOG entry”Goal: every PR to a protected branch carries a CHANGELOG.md entry; this task adds the infrastructure-side entry alongside the I1-I9 work.
Scope:
- Add an entry to
infrastructure/CHANGELOG.mdunder[2.30.0](or the next available minor) with:### Added: the Postmark thin-wrappers,dns-email-records.ts, the Corporate stacks, theapps/Corporate/app, theinstances/Corporate/free-kanban-tool.tsconfig, thecorporate-cli.tstool, thecorporate-drift.ymlworkflow + driver, the reintroducedFREE_KANBAN_POSTMARK_ITEMtyped reference, the reserved-words extension.### Changed: the renameroute-53-hosted-zone.ts→dns-zone.tswith caller migration.
File targets:
- Edit:
infrastructure/CHANGELOG.md.
AWS impact: None.
STOP — review after I10: clq validation passes via the infrastructure repo’s CI gate.
Task O0: Pre-deploy state assertion
Section titled “Task O0: Pre-deploy state assertion”Goal: confirm the live environment is in the state the spec assumes before any Phase C action that touches it. Catches drift, partial-deploys, and cross-phase merge-state surprises up front, not after a side-effect has been issued.
Scope:
-
Phase 1 / Phase 2 / Phase 3 merge state (default: strict; overridable — see below):
gh pr view 67 -R Arda-cards/documentation --json statereportsMERGED(or auto-merge armed and approval landed; see override).gh pr view 69 -R Arda-cards/documentation --json statereportsMERGED.gh pr view 70 -R Arda-cards/documentation --json statereportsMERGED.gh pr view 446 -R Arda-cards/infrastructure --json statereportsMERGED.gh pr view 448 -R Arda-cards/infrastructure --json statereportsMERGED.- The Phase 3 docs Wave-1 PR (this branch’s PR) is
MERGED. - The Phase 3 infrastructure PR is
MERGED— the deploy in Task O2 expects the code onmainofArda-cards/infrastructure.
Override (operator discretion): if a human reviewer has explicitly approved a PR and auto-merge is armed but the merge has not yet landed (e.g., a CI re-run is in flight), the operator may proceed past T-O0 with a documented exception captured in operator notes for Phase D’s runbook. The override exists because reviewer-delay shouldn’t block a deploy that is otherwise green; it does not waive the requirement that PRs be approved before deploy. Operators do not override around an unapproved or red-CI PR.
-
AWS state (
platformRootaccount,Admin-Alpha1profile):cdk diffforapps/Root/against the deployedRootConfigurationstack reports zero differences (Phase 2 baseline preserved).aws cloudformation describe-stacks --stack-name RootConfiguration --profile Admin-Alpha1reportsUPDATE_COMPLETE(orIMPORT_COMPLETE);Outputsincludesarda-ardamails-zoneandarda-allow-create-ns-record-role.- No
AWS::Route53::HostedZoneforarda.ardamails.comexists yet (the zone Phase 3 creates is not present from a previous attempt). - No NS record set named
arda.ardamails.com.exists in theardamails.comzone.
-
Postmark state (PostmarkProd):
- Account-token resolves via the operator’s 1Password DesktopAuth:
op read 'op://Arda-SystemsOAM/Postmark-Prod/credential'returns a valid token. - No existing Sender Signature for
arda.ardamails.com; no existing server matching the Free Kanban Tool’s configured name. (If either is found, the operator confirms whether to reconcile or to abort; idempotency is by design but a stale entity from a previous attempt deserves explicit acknowledgment.) - Note: sandbox-vs-live status cannot be probed at this pre-deploy stage because
DeliveryTypeis a per-server property returned byGET /servers/{id}— it exists only after server creation. Phase A surfaces it immediately after creating the server; see Task I9.REQ-OPS-002is answered during Phase A, not at pre-deploy time.
- Account-token resolves via the operator’s 1Password DesktopAuth:
-
1Password state:
Arda-CorporateOAMvault is reachable from the operator workstation (DesktopAuth).Free-Kanban-Generator-Postmark-Serveritem does not exist (Phase A creates it).- Phase 1 items in
Arda-SystemsOAMresolve underOP_SERVICE_ACCOUNT_TOKEN-scoped CI auth (the existingexternal-resources-drift.ymlworkflow’s last successful run confirms this; if the workflow has not run since the relevant change, trigger it viaworkflow_dispatch).
-
Operator-side prerequisites (recap):
- Local infrastructure repo on
jmpicnic/email-integration-phase-3(ininfrastructure);npm ciclean;npm testclean. - Phase 3 contract documentation merged to
mainindocumentation(Wave 1). - DMARC reporting mailbox (
dmarc-reports@arda.cards) NOT yet provisioned (that’s Task O1; pre-state assertion only confirms its absence is the expected starting state, not that it should already exist).
- Local infrastructure repo on
File targets: none (this is a state assertion, not a state mutation).
AWS impact: None (read-only assertions).
STOP — review after O0: every assertion above passes. If any fails, do not proceed; investigate (typically: a previous Phase 3 attempt left a partial state; cleanup is a separate operator decision). The Corporate CLI’s --dry-run mode (introduced in I9) is the recommended driver for the API-side assertions; the AWS-side assertions use aws CLI / cdk diff.
Task O1: Operator prerequisite — DMARC reporting mailbox
Section titled “Task O1: Operator prerequisite — DMARC reporting mailbox”Goal: ensure the DMARC rua destination resolves before Phase B deploy.
Scope:
- Operator provisions
dmarc-reports@arda.cardsin Arda’s Google Workspace as a real, reachable mailbox or distribution list. - Send a test email to the address; confirm receipt.
File targets: none (out-of-IaC operator action).
AWS impact: None.
STOP — review after O1: confirmation captured in the operator companion (Task O3).
Task O2: Phase A run + Phase B deploy
Section titled “Task O2: Phase A run + Phase B deploy”Goal: run the orchestrator against PostmarkProd; deploy the Corporate stacks.
Scope:
- Run Phase A in PostmarkNonProd first as a smoke test (use a fixture asset name that does not collide with PostmarkProd). Validate the structured output and the resulting
cdk.context.json(committed, but tagged so it does not pollute the prod context). - Run Phase A in PostmarkProd:
corporate-cli prepare free-kanban. The 1Password item is created inArda-CorporateOAM;cdk.context.jsonis updated with the prod values. - Commit the updated
cdk.context.json. PR review surfaces the diff. - Run Phase B:
corporate-cli deploy free-kanban. Thecdk diffis displayed; user confirmation is the gate. After confirmation,cdk deploy apps/Corporate/runs:- Creates the
arda.ardamails.comzone. - Writes SPF / DMARC records.
- Writes the NS-delegation record for
ardaintoardamails.comvia theWriteNSRecordsToUpstreamDnsCR. - Writes the Free Kanban DKIM TXT and Return-Path CNAME records.
- Creates the
File targets: none (this is the deploy task; the file deltas were produced by I1-I9 and the cdk.context.json update).
AWS impact: Resource-touching. Real production resources land in platformRoot. Postmark state (Sender Signature, server, item) lands in PostmarkProd / Arda-CorporateOAM.
STOP — review after O2: cdk diff against the deployed Corporate stacks reports zero differences. dig confirms the records resolve.
Task O3: Post-deploy verification + operator notes capture
Section titled “Task O3: Post-deploy verification + operator notes capture”Goal: verify end-to-end and capture the raw material the runbook (Phase D, Task D2) is authored from.
Scope:
- Run
corporate-cli verify free-kanban(or invoke theverifyDkim/verifyReturnPathAPI calls directly): the Sender Signature is now marked verified. Capture timestamps + any operator-visible quirks. - (If account is in sandbox) Submit the Postmark sandbox-to-live approval via the Postmark Console — the only manual UI step in Phase 3 (per
REQ-OPS-002). Wait for approval; capture submit / approval timestamps. - End-to-end smoke send: send a test email from the Free Kanban Tool to a non-owner address. Confirm
Authentication-Resultsshowsdkim=pass,spf=pass,dmarc=passaligned witharda.ardamails.com(perREQ-OPS-004). Capture the header text. - Stash the captured notes in operator scratch (a personal note, a Linear comment, or a working file outside source control). They feed Task D2 in Phase D, which authors the long-lived runbook.
The runbook is not authored in this task — it lives at process/sre/runbooks/... and is a Phase D deliverable so it benefits from the deploy experience and any discoveries made between O3 and the runbook write.
File targets: none in this task (the captured notes are operator scratch, not committed).
AWS impact: None on the IaC side; the smoke send produces real live mail.
STOP — review after O3: smoke send succeeded; operator notes captured for Phase D’s runbook authoring.
Task O4: First scheduled run of corporate-drift.yml
Section titled “Task O4: First scheduled run of corporate-drift.yml”Goal: confirm the drift workflow exercises the deployed Corporate state successfully and opens an issue on injected drift.
Scope:
- Trigger
corporate-drift.ymlviaworkflow_dispatch(manual run) immediately after T-O3 to validate the workflow fires correctly against the deployed state. - Wait for the next scheduled run (monthly) to confirm the cron-driven path works.
- Optionally inject a drift (e.g., temporarily delete a non-critical record in a test fixture or simulate via a test mode) to confirm the issue-on-failure path; back-out immediately.
File targets: none (this is a runtime confirmation; no file deltas).
AWS impact: None (the workflow only reads external state).
STOP — review after O4: workflow run reports success against the green deployed state; the drift-detection-failure path is confirmed by the optional injected-drift test (or deferred to first natural drift event).
Task D2: System documentation pages + operator runbook
Section titled “Task D2: System documentation pages + operator runbook”Goal: publish the Phase 3 long-lived documentation under current-system/ (describing what was built) and the operator runbook under process/sre/runbooks/ (describing the durable procedure). Both authored after deploy so they reflect any deploy-time discoveries.
Scope:
- Add new pages under
current-system/oam/:- A Postmark service overview for the Corporate consumer pattern (the operational view: how the parent Sender Signature works, how leaf sub-domains inherit DKIM, how the Phase A / Phase B split surfaces in operator workflows, what the actual
verifyDkim/verifyReturnPathround-trip looks like in production). - A Free Kanban Tool service page (per-asset OAM: where the server lives, where the token lives, how to rotate it, the smoke-send playbook from Task O3’s experience).
- A Corporate drift notes page (cadence, label conventions, escalation, real first-run results from Task O4).
- A Postmark service overview for the Corporate consumer pattern (the operational view: how the parent Sender Signature works, how leaf sub-domains inherit DKIM, how the Phase A / Phase B split surfaces in operator workflows, what the actual
- Add new pages under
current-system/runtime/:- A Corporate Resource Group structure page (instance-group definition; AWS-account-vs-instance-group decoupling; reserved-name discipline at
arda.ardamails.com; the actual deployed CFN stack names). - A Free Kanban Tool component placement page.
- A Corporate Resource Group structure page (instance-group definition; AWS-account-vs-instance-group decoupling; reserved-name discipline at
- Author the long-lived operator runbook at
process/sre/runbooks/postmark-domain-verification.md(or a similarly scoped name finalized at write time — the procedure generalizes beyond Free Kanban Tool: any future Corporate consumer or partition-level Postmark Sender Signature verification reuses it). The runbook is sourced from the operator notes captured in Task O3. The Phase 1 stub atroadmap/in-progress/email-integration/3-corporate-updates/operator-domain-verification-checklist.mdis superseded by the new runbook; D2 either rewrites the stub as a forward-pointer to the runbook or removes it (operator decision at authoring time). - No new pages under
current-system/functional/orcurrent-system/data-model/(Phase 3 is all IaC).
File targets:
- New:
current-system/oam/postmark-service/corporate-consumers.md(or similarly named). - New:
current-system/oam/corporate/free-kanban-tool.md,current-system/oam/corporate/drift-notes.md. - New:
current-system/runtime/corporate-resource-group.md,current-system/runtime/free-kanban-tool.md. - New:
process/sre/runbooks/postmark-domain-verification.md(the durable runbook). - Edit or remove:
roadmap/in-progress/email-integration/3-corporate-updates/operator-domain-verification-checklist.md(superseded by the runbook).
AWS impact: None.
STOP — review after D2: pages render in make preview; internal links resolve; make test-links returns zero broken links; the new runbook is reachable from the Free Kanban Tool service page and from the Corporate consumers OAM page. If any system-doc text contradicts the contract in specification.md, capture the divergence in implementation/specification-post.md (Task D5) and amend the spec only after user direction — the contract is the historical record of what was prescribed.
Task D5: Implementation byproducts
Section titled “Task D5: Implementation byproducts”Goal: produce the standard implementation byproducts under 3-corporate-updates/implementation/ per the implementation-task skill.
Scope:
- Author six files under
3-corporate-updates/implementation/:changelog.md— task-by-task summary of what landed, deviations from the spec, and any deferred follow-ups.learnings.md— non-obvious insights surfaced during implementation (the Phase-2 IMPORT-detour pattern is the model; if Phase 3 surfaces an analogous discovery, capture it here asL-N).suggestions.md— cross-cutting improvements that fell out of scope but should be tracked (S-N).alternatives.md— design choices considered and rejected during implementation (A-N).skipped.md— spec items intentionally not implemented, with rationale (SK-N).specification-post.md— spec deltas (D-N): each entry names a section ofspecification.mdthat was amended after deploy and the reason. Captures contract drift without rewriting history.
- Cross-link with the corresponding files in
1-external-resources/implementation/and2-root-updates/implementation/.
File targets:
- New:
3-corporate-updates/implementation/{changelog,learnings,suggestions,alternatives,skipped,specification-post}.md.
AWS impact: None.
STOP — review after D5: byproducts published; make pr-checks clean.
Task D6: Documentation CHANGELOG amendment (system docs + byproducts)
Section titled “Task D6: Documentation CHANGELOG amendment (system docs + byproducts)”Goal: capture the system documentation and byproducts in the documentation/CHANGELOG.md.
Scope:
- Open a follow-up docs PR (or extend an open one) carrying D2 + D5 + D6.
- Add a new top entry to
documentation/CHANGELOG.md(next available minor; the contract entry[0.31.0]is closed once the contract docs PR merges):### Added: thecurrent-system/oam/...andcurrent-system/runtime/...pages from D2; the3-corporate-updates/implementation/*.mdbyproducts from D5.
File targets:
- Edit:
documentation/CHANGELOG.md.
AWS impact: None.
STOP — review after D6: make clq passes.
3. Execution sequence
Section titled “3. Execution sequence”Phase A (contract docs PR): D1 -> D3 -> D4
Phase B (infra PR; can start as soon as Phase A is reviewable): I1 -> I2 -> I3 ----> I6 -> I7 -> I9 -> I10 \ / I4 -> I5 ------/ \ I8 (parallel; first run validates O2)
Phase C (operator deploy + verification, post-Phase-B-merge): O0 -> O1 -> O2 -> O3 -> O4 (O0 is a read-only state assertion; nothing in Phase C may execute until O0's assertions pass.)
Phase D (system-docs follow-up PR, post-O3): D2 -> D5 -> D6Phase relationships:
- Phase A is the contract. It must land first; Phase B implements against it; Phase C verifies it.
- Phase A and Phase B can run in parallel after Phase A’s PR is reviewable — Phase B’s implementer treats Phase A’s open PR as the contract.
- Phase A merges before Phase C (the deploy needs the contract published).
- Phase D explicitly lands last — after the deploy in Phase C produces real evidence — so the
current-system/pages, the byproducts, and the operator companion all benefit from learnings, deploy-time discoveries, and any contract drift captured inspecification-post.md.
Within Phase B:
- I1 (rename) blocks I2 / I3 only because it changes import paths. Otherwise the construct work is independent.
- I4 / I5 are independent and can land in either order.
- I6 (stacks) requires I1, I2, I3, I5.
- I7 (app) requires I6.
- I8 (drift workflow) is parallel to the deploy tasks; the workflow’s first scheduled run validates the deploy.
- I9 (CLI) requires I3 (it imports / interacts with the Postmark thin-wrapper internals’ shape).
- I10 (infra CHANGELOG) is the last Phase-B task before opening the infra PR.
Within Phase C:
- O0 (pre-deploy state assertion) is the first action; nothing else in Phase C runs until its assertions pass. Read-only; safe to re-run.
- O1 (mailbox) is an out-of-IaC prerequisite that can run anytime between O0 and O2.
- O2 (deploy) requires every I task complete, O0 cleared, and O1 confirmed.
- O3 (post-deploy verification + operator notes) requires O2.
- O4 (drift workflow first run) requires O2 deployed.
Within Phase D:
- D2 (system docs) draws on what was actually deployed and verified in Phase C.
- D5 (byproducts) draws on the implementation experience — what surprised, what was skipped, what alternatives surfaced.
- D6 (CHANGELOG amendment) folds D2 + D5 into a new top entry on a follow-up docs PR.
4. Out of scope of this specification
Section titled “4. Out of scope of this specification”Restated for clarity; identical to requirements.md § Out of scope of Phase 3.
5. Open Questions and Decisions
Section titled “5. Open Questions and Decisions”The eight Phase 3 design questions were resolved during Pass 1 of this planning. Each is recorded in decision-log.md under Round R1-Phase3 (DQ-R1-009..016). Restated here so the implementer has a single index in the spec:
| # | Question (short form) | Decision (short form) | Reference |
|---|---|---|---|
| OQ-1 | Postmark verification target — parent or leaf? | Parent (arda.ardamails.com); leaves inherit DKIM. | DQ-R1-009 |
| OQ-2 | NS-delegation write — through assume-role even when same-account? | Yes; uniform pattern; preserves invariance under future Corporate-account migration. | DQ-R1-010 |
| OQ-3 | route-53-hosted-zone.ts migration shape? | Rename in place; callers updated in same PR. | DQ-R1-011 |
| OQ-4 | Corporate drift-workflow filename and scope? | corporate-drift.yml; instance-group-scoped; data-driven over instances/Corporate/. | DQ-R1-012 |
| OQ-5 | Phase A failure ordering for the server token? | In-memory buffer + retries on the 1P write; fail loud; no auto-rollback via delete-server. | DQ-R1-013 |
| OQ-6 | cdk.context.json commit policy? | Commit (public values only; standard CDK convention). | DQ-R1-014 |
| OQ-7 | DMARC reporting mailbox? | dmarc-reports@arda.cards; operator provisions in Google Workspace before Phase B deploy. | DQ-R1-015 |
| OQ-8 | Reserved-name registry scope at arda.ardamails.com? | Documentation-only; CLI enforces locally via Phase-A conflict-check. | DQ-R1-016 |
If implementation surfaces new OQs, record them here with options + recommendation, then resolve to a DQ-R1-NNN entry continuing from DQ-R1-017.
6. References
Section titled “6. References”analysis.md— gap analysis.requirements.md— numbered requirements.verification.md— test catalogue with verification methods.exports.md— downstream contracts.plan/task-plan.md— execution plan with dependency graph and risk register.operator-domain-verification-checklist.md— operator companion (stub at Pass-1; expanded at Task O3).../goal.md— project intent.../architecture-overview.md— IaC code hierarchy, Corporate instance group, reserved-name discipline.../cross-cutting-design.md— Free Kanban Tool blast radius, secret inventory, drift detection.../phases.md— phase plan (Phase 3 section).../decision-log.md— decisions including newDQ-R1-009..016.
Copyright: © Arda Systems 2025-2026, All rights reserved