Skip to content

Pin the adopted Datamailer source surface in a static inventory #290

Description

@alexeygrigorev

Parent: #50

Normative authority:

PM disposition

GROOMED / P0 / SOURCE-ONLY / NON-ACTIVATING / BLOCKED ON CLEAN-BASE RECONSTRUCTION — CORRECTED 2026-08-30.

This correction supersedes the original semantic-registry assumptions and the 2026-08-30 grooming comment. The first engineer blocker was valid, and the PM re-audit also found stale campaign locators, template-source locators, and a non-factual cutover-source entry. Only the corrected contract below is authoritative.

This is the one pre-runtime child permitted by #50. It records and statically verifies the adopted Datamailer source surface. It is useful before #46/#47/#48/#49 because it describes current checked-in source only; it neither defines nor calls their future runtime interfaces.

It supplies inventory evidence to #50 but cannot satisfy #50, authorize a sender, prove a queue frozen/drained, or act as migration/cutover evidence. No database, Django runtime, provider, Relay, AWS, production, credential, or protected-source access is part of this issue. The only browser activity is the repository-local, offline, graph-selected Playwright smoke verification required by the lifecycle; it is test evidence, not product scope or external access.

Frozen source identity

The initial candidate is based on these immutable identities:

  • website commit face8e4808d65afbf0374d1ced7a88079950d663;
  • website tree c8496fb2a37529eb7c63e1eeb9e35e29a2aadada;
  • adopted CMP repository https://github.com/DataTalksClub/course-management-platform.git;
  • adopted CMP commit 98a235283904b4ef9ad29e196298540756cf1bcc from _docs/adoption/course-platform/source-pin.json;
  • source-pin SHA-256 71145a6b28ed0ea89e984b9c6bda5151549d3597d37c75d4c6b369ef4fbaf1b3;
  • copied-files SHA-256 31a0ef0439b68477b905d9d084bfe9c0998dd8a7168ee3c99bc98d72542ea6e2;
  • integration-patched-files SHA-256 db285bab70aef0035235b68e17e63be55e4c133cfe514e0bd9026e2b76ac6c95.

If current origin/main, the source pin, either adoption manifest, or any discovered Datamailer path changes before engineering freezes its base, return this issue to PM for a new identity and expected set. Do not silently regenerate around drift.

Exact implementation files

Only these three tracked files may change:

  1. _docs/inventory/datamailer-source-surface.json — canonical static inventory;
  2. scripts/verify_datamailer_source_surface.py — standard-library-only, read-only validator/CLI; and
  3. core/tests/test_datamailer_source_surface.pySimpleTestCase contract and negative drift/redaction tests.

Do not change Makefile, CI workflows, settings, application/runtime modules, models, migrations, routes, templates, copied/adoption manifests, or existing tests. Existing Django discovery runs the core.tests contract; the CLI is also run directly with uv run --frozen python scripts/verify_datamailer_source_surface.py.

Manifest schema

The JSON is UTF-8, LF-terminated, two-space-indented, key-sorted canonical JSON. It has exactly these top-level keys:

schema_version       integer, exactly 1
repository           exactly "DataTalksClub/website"
website_source       object
cmp_source           object
scan_contract        object
files                array
semantic_surfaces    object
content_sha256       lowercase SHA-256

content_sha256 is SHA-256 of the canonical JSON object with that field omitted. Unknown, missing, duplicate, incorrectly typed, unsorted, or non-canonical data fails.

website_source contains exactly commit, tree, and path_set_sha256. cmp_source contains exactly repository, commit, source_pin_path, source_pin_sha256, copied_files_path, copied_files_sha256, patched_files_path, and patched_files_sha256, with the values frozen above.

scan_contract contains exactly:

  • roots: sorted exact set accounts, api, core, course_management, courses, data, jobs, studio_courses, website;
  • extensions: sorted exact set .html, .md, .py;
  • match: case-insensitive literal datamailer in either the repository-relative path or decoded file bytes;
  • excluded_paths: only the three implementation files named above, preventing self-reference;
  • expected_file_count: 212; and
  • expected_root_counts: accounts=8, api=8, core=10, course_management=69, courses=73, data=20, jobs=0, studio_courses=21, website=3.

The exact sorted newline-terminated baseline path set has SHA-256 6559493ecd4628625a94a30056bc05217c3fbc94ee314edb2afa9f55f11ba8ff. Of the 212 entries, 124 match by both path and content, 19 by path only, and 69 by content only. Those three membership counts and each row's booleans are part of the contract.

Every files row contains exactly:

path                 normalized repository-relative POSIX path
size_bytes           non-negative integer
sha256               lowercase SHA-256 of exact bytes
path_match           boolean
content_match        boolean
provenance           pinned_cmp | target_overlay | target_owned
surface_kinds        sorted non-empty subset of the controlled vocabulary below

surface_kinds vocabulary is exactly caller, client, command, configuration, history, migration, model, observability, outbox, payload, preference, presentation, route, template, test, webhook. Multi-role files list every applicable kind; classification is review metadata and never permission to run the code.

Provenance is deterministic: target_overlay takes precedence when the target path is in integration-patched-files.tsv; otherwise pinned_cmp applies when it is a copied target in copied-files.tsv; otherwise it is target_owned. Every file path is unique, sorted bytewise, regular, non-symlink, inside one scan root, and matches by path or content. The validator compares current working-tree bytes and discovery membership to every row; it does not contact GitHub or trust HEAD alone.

Exact semantic entries

semantic_surfaces contains exactly the following sorted registries. Every leaf is a structured entry with name, source_path, symbol_or_locator, owner_issue, and disposition; it contains names/locators only, never a captured runtime value or source snippet.

Configuration symbols

Record the 16 Python symbols in course_management/settings.py:

DATAMAILER_API_KEY, DATAMAILER_AUDIENCE, DATAMAILER_CLIENT, DATAMAILER_FROM_EMAIL, DATAMAILER_IMPORT_S3_BUCKET, DATAMAILER_IMPORT_S3_PREFIX, DATAMAILER_IMPORT_S3_REGION, DATAMAILER_IMPORT_URL_EXPIRES_SECONDS, DATAMAILER_IMPORT_URL_EXPIRES_SECONDS_VALUE, DATAMAILER_OUTBOX_DISPATCH_IMMEDIATELY, DATAMAILER_STRICT, DATAMAILER_SYNC_ON_USER_CREATE, DATAMAILER_TIMEOUT_SECONDS, DATAMAILER_TRANSACTIONAL_DRY_RUN, DATAMAILER_URL, DATAMAILER_WEBHOOK_TOKEN.

The intermediate _VALUE symbol is not an environment key; all other listed names map to their same-named environment key. Record no setting value, default, credential, endpoint host, bucket, prefix, token, email, audience, client, or sender value.

Database/history surface

Record the four Django models and exactly five migration-ownership entries:

  • data.DatamailerContactEvent:initialdata/migrations/0001_initial.py, exact operation CreateModel(name="DatamailerContactEvent");
  • data.DatamailerContactEvent:evolutiondata/migrations/0004_datamailercontactevent_duplicate_count_and_more.py, whose complete operation set is exactly the two contact-only additions AddField(model_name="datamailercontactevent", name="duplicate_count") and AddField(model_name="datamailercontactevent", name="last_seen_at"). This registry has one leaf, whose exact symbol_or_locator string is AddField(model_name="datamailercontactevent", names=["duplicate_count", "last_seen_at"]);
  • data.DatamailerOutboxEvent:initialdata/migrations/0002_datamaileroutboxevent.py, exact operation CreateModel(name="DatamailerOutboxEvent");
  • data.DatamailerOutboxDispatchRundata/migrations/0003_datamaileroutboxdispatchrun.py, exact operation CreateModel(name="DatamailerOutboxDispatchRun"); and
  • data.DatamailerSendAuditdata/migrations/0005_datamailersendaudit.py, exact operation CreateModel(name="DatamailerSendAudit").

DatamailerOutboxEvent has no evolution entry in 0004: that migration depends on 0003 but owns only the two DatamailerContactEvent fields above. The four TextChoices classes belong to data/models.py; the corresponding migrations serialize their choice values but do not create separate enum models.

Record exact controlled states and use class/member locators such as DatamailerOutboxStatus.ACKED, proving that each member maps to the exact persisted value: outbox pending, processing, acked, retrying, failed, dead; dispatch-run success, failed; send type transactional, recipient_list, transient_recipient_list; send-audit status succeeded, failed.

Record identifier-bearing field names, never values: contact event_id, event_type, audience, client, preference_key; outbox event_id, event_type, idempotency_key, ordering_key; send audit idempotency_key, template_key, category_tag, list_key, source, event.

Entry points and outbound surface

Record all 13 management commands by basename and path:

audit_datamailer_recipient_lists, datamailer_campaign, datamailer_status, preview_peer_review_email, send_deadline_reminders, sync_datamailer_contacts, sync_datamailer_recipient_lists, upsert_datamailer_templates, datamailer_callback_status, datamailer_outbox_status, datamailer_send_status, monitoring_datamailer_health, process_datamailer_outbox.

Record the four mounted website routes and names:

  • /api/datamailer/eventsapi_datamailer_events;
  • /api/datamailer/send-auditsapi_datamailer_send_audits;
  • /studio/courses/datamailer/datamailer_operations; and
  • /studio/courses/datamailer/events/datamailer_events.

Record the five campaign POST action values sync, preview, test_send, queue, cancel. Their source locators are the corresponding keys of DATAMAILER_CAMPAIGN_ACTION_HANDLERS (for example DATAMAILER_CAMPAIGN_ACTION_HANDLERS["cancel"]), not nonexistent action == "…" comparisons. Record the four outbox event types contact.erase, recipient_list.member_remove, recipient_list.member_upsert, recipient_list.members_bulk_upsert; and the 21 client path templates currently declared under course_management/datamailer/client*.py:

/api/campaigns/{external_key}, /api/campaigns/{external_key}/cancel, /api/campaigns/{external_key}/preview, /api/campaigns/{external_key}/queue, /api/campaigns/{external_key}/test-send, /api/contacts, /api/contacts/erase, /api/contacts/imports, /api/contacts/preferences, /api/contacts/status, /api/contacts/{contact_id}/history, /api/recipient-lists/{list_key}/imports, /api/recipient-lists/{list_key}/imports/{job_id}, /api/recipient-lists/{list_key}/members, /api/recipient-lists/{list_key}/members/bulk-upsert, /api/recipient-lists/{list_key}/members/reconcile, /api/recipient-lists/{list_key}/members/{source_object_key}, /api/recipient-lists/{list_key}/transactional-send, /api/transactional/messages/{message_id}, /api/transactional/send, /api/transient-recipient-lists/transactional-send.

These are inventory facts, not approved endpoints. Mark network submit/queue/test/upsert/remove/erase/import/requeue surfaces new_work_must_be_disabled under #50; callback/audit/status/history inspection surfaces are candidate_read_only_history under #50/#49; actual freeze/drain/retirement is operator_only_cutover under #74.

Template, preference, list, and identity surface

Record the eight copied Datamailer template keys at the files that actually define them; definitions/registry.py only aggregates imported dictionaries and is not a valid key locator:

  • certificate-availability-notificationcourse_management/datamailer_templates/definitions/certificates.py;
  • deadline-remindercourse_management/datamailer_templates/definitions/reminders.py;
  • homework-score-notification and project-score-notificationcourse_management/datamailer_templates/definitions/scores.py;
  • peer-review-assignmentcourse_management/datamailer_templates/definitions/peer_review.py; and
  • homework-submission-confirmation, project-submission-confirmation, and registration-confirmationcourse_management/datamailer_templates/definitions/submissions.py.

Record preference field-to-tag pairs: email_submission_confirmations -> submission-results, email_deadline_reminders -> deadline-reminders, email_course_updates -> course-updates.

Record recipient-list kinds: registrations, enrollments, homework, project, project-passed, graduates.

The 15 identity-constructor entries are exactly:

  • course_management/datamailer/keys.py: course_enrolled_list_key, course_graduates_list_key, datamailer_ordering_key, homework_submitters_list_key, project_passed_list_key, project_submitters_list_key, registration_campaign_external_key, registration_list_key;
  • course_management/datamailer/recipient_list_imports.py: import_idempotency_key, import_object_key;
  • course_management/datamailer/payloads/send.py: datamailer_send_list_key, transient_recipient_list_key;
  • course_management/datamailer/payloads/registration_confirmations.py: registration_confirmation_delivery_fields;
  • course_management/datamailer/sync/certificates.py: certificate_graduate_outcome_idempotency_key; and
  • course_management/datamailer/sync/contacts.py: contact_erase_ordering_key.

The 21 selected top-level payload-builder entries are exactly:

  • payloads/base.py: contact_payload_for_user, enrollment_recipient_list_payload, recipient_list_member_payload, recipient_list_send_member_payload, removed_recipient_list_member_payload;
  • payloads/bulk.py: bulk_recipient_list_payload;
  • payloads/certificate_availability.py: certificate_availability_notification_payload;
  • payloads/course_graduates.py: course_graduate_recipient_list_payload;
  • payloads/homework_scores.py: homework_score_notification_payload;
  • payloads/peer_review.py: peer_review_assignment_notification_payload;
  • payloads/peer_review_members.py: peer_review_assignment_recipient_list_payload;
  • payloads/project_outcomes.py: project_passed_recipient_list_payload;
  • payloads/project_scores.py: project_score_notification_payload;
  • payloads/registration_campaigns.py: registration_campaign_datamailer_payload;
  • payloads/registration_confirmations.py: registration_confirmation_payload;
  • payloads/registration_contacts.py: registration_contact_payload;
  • payloads/registration_members.py: registration_recipient_list_payload;
  • payloads/send.py: recipient_list_member_sync_payload, recipient_list_send_payload; and
  • payloads/submissions.py: homework_submission_recipient_list_payload, project_submission_recipient_list_payload.

The 22 selected top-level sync/outbound entries are exactly:

  • sync/bulk.py: bulk_upsert_recipient_list_members_before_send, enqueue_recipient_list_bulk_upsert;
  • sync/certificates.py: send_certificate_availability_notification;
  • sync/contacts.py: erase_contact_from_datamailer, sync_contact;
  • sync/membership_events.py: remove_recipient_list_memberships, sync_contact_and_membership;
  • sync/membership_removals.py: remove_enrollment_from_datamailer, remove_homework_submission_from_datamailer, remove_project_submission_from_datamailer, remove_registration_from_datamailer;
  • sync/memberships.py: sync_enrollment_to_datamailer, sync_homework_submission_to_datamailer, sync_project_passed_outcome_to_datamailer, sync_project_submission_to_datamailer, sync_registration_to_datamailer;
  • sync/notifications.py: send_registration_confirmation_email;
  • sync/peer_review_notifications.py: send_peer_review_assignment_notification;
  • sync/score_notifications.py: send_homework_score_notification, send_project_score_notification; and
  • sync/transactional.py: send_transactional_and_audit, send_transactional_email.

These are the deliberately bounded semantic registries for this source-only child, not an assertion that every private helper is an independent entry. Record source path and qualified symbol only; do not enumerate real generated keys, emails, user IDs, object IDs, list members, metadata, payloads, rendered content, or provider responses.

Ownership is descriptive and fail-closed: #48 owns future Relay template management, #49 owns future website delivery/reconciliation, #227 owns canonical preferences, #50 owns later routing and Datamailer source retirement preparation, and #74 alone owns authorized production freeze/drain/retirement. The inventory grants none of those capabilities.

Do not create a cutover_authority semantic registry. The frozen tree has no freeze/drain/retire implementation to inventory: dispatch_datamailer_outbox_event is an outbound dispatch function, not a cutover operation. #74 authority remains normative issue metadata and an explicit non-goal, not a fabricated source locator.

Redaction and safety contract

  • The verifier uses only the Python standard library and local tracked files. It does not import Django settings, models, application modules, requests, SDKs, or provider clients; open a database; read environment variables; resolve DNS; open sockets; or run a management command.
  • The manifest contains only fixed repository/CMP identities, paths, sizes, hashes, controlled classifications, symbol/field names, route/path templates, and issue numbers.
  • The schema has no free-form source snippet, value/default/example, URL query, body, payload, header, email/recipient, token, cookie, authorization, credential, secret, provider response/ID value, row/count, or production-data field.
  • The sole allowed full URL is the public CMP repository URL above. Setting names such as DATAMAILER_API_KEY and DATAMAILER_WEBHOOK_TOKEN are identifiers, not values.
  • Tests seed synthetic canaries for email addresses, bearer material, private keys, AWS/account-like IDs, query credentials, webhook tokens, recipient values, and provider payloads and prove validation rejects them without echoing the canary in the error.

Acceptance criteria

  • The diff contains exactly the three allowed files and no runtime/config/model/migration/route/template/CI change.
  • The manifest validates against the exact frozen website/CMP identities and adoption-manifest digests.
  • Discovery yields exactly 212 unique sorted paths, the exact root and match-mode counts above, the exact path-set digest, and one byte-size/SHA-256/provenance/classification row per path.
  • Every exact semantic registry above is present once, points to an existing inventoried source file/symbol or locator, has a valid owner/disposition, and has no unclassified extra or omission. Semantic validation is source-specific and syntax-aware: every locator segment and relationship must be proved conjunctively (class + member + persisted value; class + field; operation + model + field; handler map + key; route mount + child route + name; definition dictionary + key). An any(token in source) or bare substring fallback is forbidden.
  • Two consecutive validator runs produce the same canonical inventory digest and no file mutation.
  • Add/delete/rename/content, path/content-match, source identity, source-pin/adoption-manifest, provenance, semantic-symbol, enum, route, command, model/migration, template, preference, list-kind, client-path, and overall digest drift each fail closed with a bounded non-sensitive reason.
  • Duplicate, unsorted, traversal, absolute, symlink, non-regular, out-of-root, oversized, unknown-field, wrong-type, invalid-enum, and non-canonical JSON cases fail closed.
  • Redaction canaries fail without echo; the inventory contains no secret, PII, row value, body, payload, captured provider identifier/value, or production datum.
  • Static inspection and tests prove the verifier has no database, Django/runtime import, environment read, network/provider/Relay/AWS, subprocess management-command, queue, or write path.
  • uv run --frozen python scripts/verify_datamailer_source_surface.py, focused core.tests.test_datamailer_source_surface, formatting/lint/type/migration checks, make test-ci, and every component selected by one recomputed frozen-candidate plan pass, including make test-playwright-smoke when the accepted graph continues to select profile=full / reason=test_infrastructure with browser_profile=smoke.
  • The engineer and independent tester each provide a valid four-bucket report that classifies every component exactly once, has no required skip, records exact base/head/tree/graph/plan/report and artifact digests, and binds a terminal smoke summary with complete flake-policy counts to the frozen candidate.
  • Screenshots are not_applicable only when the recomputed plan proves render inputs unchanged and has an empty required-capture set; browser smoke evidence does not make unrelated product screenshots applicable.

Verification disposition

This changes no rendered page or runtime behavior. Screenshots are explicitly not_applicable only when the recomputed graph proves no render impact and no required captures. Browser verification is not N/A: under _docs/PROCESS.md, a backend/internal source-and-test change receives the applicable Playwright tier. The current accepted graph classifies these three paths as direct nodes shared.core, surface.documentation, and surface.scripts, with shared_runtime / test_infrastructure risk, overall profile=full / reason=test_infrastructure, and browser_profile=smoke. The required browser command is therefore make test-playwright-smoke, not the blocking full make test-playwright suite.

No issue-specific ownership or planner exception may be added to force browser N/A. #252 owns capacity and terminal evidence for the full Playwright suite and is not a dependency of this smoke selection. The independent tester must recompute the plan, verify all components are classified exactly once, run the selected smoke tier under the repository Playwright lease, validate the terminal pytest and flake-policy counts, and independently confirm that screenshots remain N/A.

The engineer handoff and tester-final report must record exact base, HEAD, tree, worktree status, changed paths and file/diff digests; plan, graph, selection, report, environment, input, output, and artifact digests; the complete rerun/reused/skipped/not_applicable buckets; exact commands and counts; and an explicit reason for every reuse or N/A disposition. A required skip, missing canonical report, partial or nonterminal smoke result, lease/contention invalidation, graph drift, or pending screenshot disposition fails the gate.

The smoke run may start only after the ordered clean-base recovery below is complete, #290 has been reconstructed and refrozen on that exact base, the recomputed plan still selects it, and the current browser-lease owner has released the exclusive project lease. Engineering and testing must not poll, overlap, steal, kill, or bypass another browser owner.

Dependencies and delivery order

Corrected semantic-registry checksum and engineering resume

The corrected manifest has exactly 16 sorted semantic registries and 179 leaves:

campaign_actions=5                 client_path_templates=21
configuration_symbols=16           controlled_states=13
database_models=8                  identifier_fields=15
identity_constructors=15           management_commands=13
migration_ownership=5              mounted_routes=4
outbox_event_types=4               payload_builders=21
preference_fields=3                recipient_list_kinds=6
sync_entry_points=22               template_keys=8

For the canonical two-space-indented, key-sorted, LF-terminated semantic_surfaces object, SHA-256 is 03de633a2f382946513f4376707c02a1bb601f628ef6ec7049b524942a7055df. With the already-audited 212 file rows unchanged and this corrected semantic object, the exact manifest content_sha256 is 2b370a174dff803ad8af8079ad5574afa5616797b280aabec7af1493efba3899 and the canonical manifest is 123,784 bytes.

The exact canonical JSON for all five migration_ownership leaves is below. The bytes inside the fence are UTF-8, two-space-indented, key-sorted, and terminated by one LF; the fence is not part of the artifact. The array is 1,437 bytes with SHA-256 88aa06d1576b130dfa5901647faf3256a6ff6b499ada12383442c952aa5eda4e.

[
  {
    "disposition": "candidate_read_only_history",
    "name": "data.DatamailerContactEvent:evolution",
    "owner_issue": "#50",
    "source_path": "data/migrations/0004_datamailercontactevent_duplicate_count_and_more.py",
    "symbol_or_locator": "AddField(model_name=\"datamailercontactevent\", names=[\"duplicate_count\", \"last_seen_at\"])"
  },
  {
    "disposition": "candidate_read_only_history",
    "name": "data.DatamailerContactEvent:initial",
    "owner_issue": "#50",
    "source_path": "data/migrations/0001_initial.py",
    "symbol_or_locator": "CreateModel(name=\"DatamailerContactEvent\")"
  },
  {
    "disposition": "candidate_read_only_history",
    "name": "data.DatamailerOutboxDispatchRun",
    "owner_issue": "#50",
    "source_path": "data/migrations/0003_datamaileroutboxdispatchrun.py",
    "symbol_or_locator": "CreateModel(name=\"DatamailerOutboxDispatchRun\")"
  },
  {
    "disposition": "candidate_read_only_history",
    "name": "data.DatamailerOutboxEvent:initial",
    "owner_issue": "#50",
    "source_path": "data/migrations/0002_datamaileroutboxevent.py",
    "symbol_or_locator": "CreateModel(name=\"DatamailerOutboxEvent\")"
  },
  {
    "disposition": "candidate_read_only_history",
    "name": "data.DatamailerSendAudit",
    "owner_issue": "#50",
    "source_path": "data/migrations/0005_datamailersendaudit.py",
    "symbol_or_locator": "CreateModel(name=\"DatamailerSendAudit\")"
  }
]

The complete 47,860-byte canonical semantic_surfaces object is published verbatim in the PM canonical semantic artifact, SHA-256 03de633a2f382946513f4376707c02a1bb601f628ef6ec7049b524942a7055df. That artifact is the deterministic generator input; no leaf is defined only by checksum or must be recovered from an implementation candidate.

Whole-manifest reconstruction from that exact object yields content_sha256 2b370a174dff803ad8af8079ad5574afa5616797b280aabec7af1493efba3899, 123,784 full canonical bytes, and full-file SHA-256 d7270c3a79a2fcb8bd04eeab70f1e0e36b3e2b9a23e47a0bd9a91224ae5fb116.

The alternative semicolon-separated contact-evolution serialization is factually equivalent but non-canonical; its 2d8716f4... / c258163a... / 123,835-byte artifact remains superseded.

The corrected semantic object and whole-manifest receipt above remain authoritative reconstruction inputs. The existing .tmp/issue-290-engineer candidate at face8e4 is frozen historical/reconstruction evidence only and must not proceed to browser or tester work. Once the ordered clean-base recovery is complete, PM freezes the new exact source identity; engineering then reconstructs the same three-file contract on that base, regenerates the inventory and verification plan, reruns every required engineering gate from scratch, and posts the complete report defined above. Do not reuse the original semantic digest 20842c0128ba7ceb20d546cd5435fbf6f48ebc97fdff8e9c9dede9ef9a290ec2, any superseded grooming assumption, or an envelope from another source/graph/environment identity.

After engineer, independent tester, and PM acceptance, the focused commit body is:

Closes #290
Refs #50

Explicit non-goals

No runtime registry/adapter/feature flag; no model or migration; no import or row inspection; no database or protected source; no settings/environment read or write; no command execution beyond the local static verifier/tests and the graph-selected repository-local offline smoke gate; no live, remote, or provider browser access; no queue/outbox callback or status mutation; no network, provider, Relay, AWS, production, credential, sender/domain, recipient, email, or canary access; no campaign/template/list/contact/message operation; no send/requeue/disable/freeze/drain/classify-live-work/retire/delete action; no preference or identity migration; no claim of one active sender, migration completeness, release readiness, or #50/#74 acceptance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Must-have or release-blockingdata-migrationArea: data-migrationemailArea: emailenhancementNew feature or requestintegrationArea: integrationoperationsArea: operationssecurityArea: securitytestingArea: testing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions