You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
_docs/architecture/app-boundaries.md, especially domains -> email_app -> jobs -> Relay and Datamailer as read-only migration/history/reconciliation input;
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:
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.
scripts/verify_datamailer_source_surface.py — standard-library-only, read-only validator/CLI; and
core/tests/test_datamailer_source_surface.py — SimpleTestCase 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:
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.
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.
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:
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:evolution — data/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"]);
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:
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:
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:
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.
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.
After that exact main state is green, PM must re-audit and freeze the website commit/tree, CMP pin, adoption-manifest digests, discovered Datamailer path set and counts, and whole-manifest receipt. The face8e4 candidate is reconstruction evidence only; it is not tester-ready after base movement.
The graph-selected smoke run occurs only after that reconstruction and only while holding the repository Playwright lease. Unrelated or overlapping browser work invalidates its evidence.
A later source change intentionally updates this manifest only through a separately groomed issue or the exact owning retirement issue; silently accepting drift is forbidden.
Corrected semantic-registry checksum and engineering resume
The corrected manifest has exactly 16 sorted semantic registries and 179 leaves:
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.
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_sha2562b370a174dff803ad8af8079ad5574afa5616797b280aabec7af1493efba3899, 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.
Parent: #50
Normative authority:
_docs/specs/README.md, fixed application/email boundary;_docs/architecture/app-boundaries.md, especiallydomains -> email_app -> jobs -> Relayand Datamailer as read-only migration/history/reconciliation input;_docs/specs/04-courses-and-cohorts.md, Datamailer characterization and send-disabled migration boundary;_docs/specs/05-events-registration-email.md, course/cohort communication and the no-new-Datamailer-send rule;_docs/specs/07-security-privacy-operations.md, minimization, redaction, retention, and protected-source rules;_docs/specs/09-migration-rollout-roadmap.md, repeatable source-pinned migration evidence, outbound-disabled rehearsal, one-sender cutover, and rollback;_docs/specs/10-verification-strategy.md, source/adoption drift, no-provider-path, redaction, and test-safety requirements;Source-only slice decision; and_docs/PROCESS.md.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:
face8e4808d65afbf0374d1ced7a88079950d663;c8496fb2a37529eb7c63e1eeb9e35e29a2aadada;https://github.com/DataTalksClub/course-management-platform.git;98a235283904b4ef9ad29e196298540756cf1bccfrom_docs/adoption/course-platform/source-pin.json;71145a6b28ed0ea89e984b9c6bda5151549d3597d37c75d4c6b369ef4fbaf1b3;31a0ef0439b68477b905d9d084bfe9c0998dd8a7168ee3c99bc98d72542ea6e2;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:
_docs/inventory/datamailer-source-surface.json— canonical static inventory;scripts/verify_datamailer_source_surface.py— standard-library-only, read-only validator/CLI; andcore/tests/test_datamailer_source_surface.py—SimpleTestCasecontract 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 thecore.testscontract; the CLI is also run directly withuv 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:
content_sha256is SHA-256 of the canonical JSON object with that field omitted. Unknown, missing, duplicate, incorrectly typed, unsorted, or non-canonical data fails.website_sourcecontains exactlycommit,tree, andpath_set_sha256.cmp_sourcecontains exactlyrepository,commit,source_pin_path,source_pin_sha256,copied_files_path,copied_files_sha256,patched_files_path, andpatched_files_sha256, with the values frozen above.scan_contractcontains exactly:roots: sorted exact setaccounts,api,core,course_management,courses,data,jobs,studio_courses,website;extensions: sorted exact set.html,.md,.py;match: case-insensitive literaldatamailerin 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; andexpected_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
filesrow contains exactly:surface_kindsvocabulary is exactlycaller,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_overlaytakes precedence when the target path is inintegration-patched-files.tsv; otherwisepinned_cmpapplies when it is a copied target incopied-files.tsv; otherwise it istarget_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 trustHEADalone.Exact semantic entries
semantic_surfacescontains exactly the following sorted registries. Every leaf is a structured entry withname,source_path,symbol_or_locator,owner_issue, anddisposition; 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
_VALUEsymbol 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:initial—data/migrations/0001_initial.py, exact operationCreateModel(name="DatamailerContactEvent");data.DatamailerContactEvent:evolution—data/migrations/0004_datamailercontactevent_duplicate_count_and_more.py, whose complete operation set is exactly the two contact-only additionsAddField(model_name="datamailercontactevent", name="duplicate_count")andAddField(model_name="datamailercontactevent", name="last_seen_at"). This registry has one leaf, whose exactsymbol_or_locatorstring isAddField(model_name="datamailercontactevent", names=["duplicate_count", "last_seen_at"]);data.DatamailerOutboxEvent:initial—data/migrations/0002_datamaileroutboxevent.py, exact operationCreateModel(name="DatamailerOutboxEvent");data.DatamailerOutboxDispatchRun—data/migrations/0003_datamaileroutboxdispatchrun.py, exact operationCreateModel(name="DatamailerOutboxDispatchRun"); anddata.DatamailerSendAudit—data/migrations/0005_datamailersendaudit.py, exact operationCreateModel(name="DatamailerSendAudit").DatamailerOutboxEventhas no evolution entry in0004: that migration depends on0003but owns only the twoDatamailerContactEventfields above. The fourTextChoicesclasses belong todata/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: outboxpending,processing,acked,retrying,failed,dead; dispatch-runsuccess,failed; send typetransactional,recipient_list,transient_recipient_list; send-audit statussucceeded,failed.Record identifier-bearing field names, never values: contact
event_id,event_type,audience,client,preference_key; outboxevent_id,event_type,idempotency_key,ordering_key; send auditidempotency_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/events—api_datamailer_events;/api/datamailer/send-audits—api_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 ofDATAMAILER_CAMPAIGN_ACTION_HANDLERS(for exampleDATAMAILER_CAMPAIGN_ACTION_HANDLERS["cancel"]), not nonexistentaction == "…"comparisons. Record the four outbox event typescontact.erase,recipient_list.member_remove,recipient_list.member_upsert,recipient_list.members_bulk_upsert; and the 21 client path templates currently declared undercourse_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_disabledunder #50; callback/audit/status/history inspection surfaces arecandidate_read_only_historyunder #50/#49; actual freeze/drain/retirement isoperator_only_cutoverunder #74.Template, preference, list, and identity surface
Record the eight copied Datamailer template keys at the files that actually define them;
definitions/registry.pyonly aggregates imported dictionaries and is not a valid key locator:certificate-availability-notification—course_management/datamailer_templates/definitions/certificates.py;deadline-reminder—course_management/datamailer_templates/definitions/reminders.py;homework-score-notificationandproject-score-notification—course_management/datamailer_templates/definitions/scores.py;peer-review-assignment—course_management/datamailer_templates/definitions/peer_review.py; andhomework-submission-confirmation,project-submission-confirmation, andregistration-confirmation—course_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; andcourse_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; andpayloads/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; andsync/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_authoritysemantic registry. The frozen tree has no freeze/drain/retire implementation to inventory:dispatch_datamailer_outbox_eventis 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
requests, SDKs, or provider clients; open a database; read environment variables; resolve DNS; open sockets; or run a management command.DATAMAILER_API_KEYandDATAMAILER_WEBHOOK_TOKENare identifiers, not values.Acceptance criteria
any(token in source)or bare substring fallback is forbidden.uv run --frozen python scripts/verify_datamailer_source_surface.py, focusedcore.tests.test_datamailer_source_surface, formatting/lint/type/migration checks,make test-ci, and every component selected by one recomputed frozen-candidate plan pass, includingmake test-playwright-smokewhen the accepted graph continues to selectprofile=full/reason=test_infrastructurewithbrowser_profile=smoke.not_applicableonly 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_applicableonly 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 nodesshared.core,surface.documentation, andsurface.scripts, withshared_runtime/test_infrastructurerisk, overallprofile=full/reason=test_infrastructure, andbrowser_profile=smoke. The required browser command is thereforemake test-playwright-smoke, not the blocking fullmake test-playwrightsuite.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
98a2352....Corrected semantic-registry checksum and engineering resume
The corrected manifest has exactly 16 sorted semantic registries and 179 leaves:
For the canonical two-space-indented, key-sorted, LF-terminated
semantic_surfacesobject, SHA-256 is03de633a2f382946513f4376707c02a1bb601f628ef6ec7049b524942a7055df. With the already-audited 212 file rows unchanged and this corrected semantic object, the exact manifestcontent_sha256is2b370a174dff803ad8af8079ad5574afa5616797b280aabec7af1493efba3899and the canonical manifest is 123,784 bytes.The exact canonical JSON for all five
migration_ownershipleaves 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-25688aa06d1576b130dfa5901647faf3256a6ff6b499ada12383442c952aa5eda4e.[ { "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_surfacesobject is published verbatim in the PM canonical semantic artifact, SHA-25603de633a2f382946513f4376707c02a1bb601f628ef6ec7049b524942a7055df. 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_sha2562b370a174dff803ad8af8079ad5574afa5616797b280aabec7af1493efba3899, 123,784 full canonical bytes, and full-file SHA-256d7270c3a79a2fcb8bd04eeab70f1e0e36b3e2b9a23e47a0bd9a91224ae5fb116.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-engineercandidate atface8e4is 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 digest20842c0128ba7ceb20d546cd5435fbf6f48ebc97fdff8e9c9dede9ef9a290ec2, 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:
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.