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
Accepted inputs are closed #18, #19, #136, and #173. This issue expands the one existing events.Event; it never creates another Event identity or changes the accepted /events/<positive-public-id>/<current-title-slug> canonical.
Outcome
Create a deliberately non-activating Event domain foundation that full #45 can consume:
forward-only schema on the existing Event identity for the checked source's type/schedule, registration window, lifecycle timestamps, revision, stable calendar UID, and calendar sequence;
an exact, atomic, network-free import of the checked 421-Event source envelope;
revision-guarded lifecycle, schedule, and registration-availability services owned by events; and
deterministic pure RFC 5545 serialization with no HTTP route, template, attachment, delivery, or provider action.
The accepted public pages continue to read content/public_projection/events.json. Landing this issue must therefore produce no public-page, route, sitemap, feed, cache, Studio, admin API, registration, email, Q&A, job, or provider behavior change.
Frozen source envelope
The groomed envelope on origin/mainface8e4808d65afbf0374d1ced7a88079950d663 is:
all 421 starts are timezone-aware Berlin instants (+01:00 or +02:00), 2 have an end, and every imported timezone name is Europe/Berlin.
Engineering must recompute this envelope before work. Any base/source/artifact/digest/count difference returns the issue to PM instead of silently widening or repairing source content here. The repository's current broader release/source recovery remains an operational start gate: implementation begins only from a clean current origin/main whose required base CI and source provenance gates are green.
Schema contract
Extend events.Event in place. Final state must contain:
event_type: required allowlisted value conference, podcast, webinar, or workshop for the checked import;
starts_at: required timezone-aware instant stored by Django in UTC;
ends_at: optional aware instant, strictly after starts_at when present;
timezone_name: required valid IANA zone; every checked legacy record imports as Europe/Berlin without reinterpreting its already-aware instant;
optional registration_opens_at and registration_closes_at; when both exist open is before close, and any close is no later than start;
positive monotonic revision, initialized to 1 for the imported unchanged row;
nullable published_at, completed_at, cancelled_at, and archived_at; imported legacy rows remain published and retain null where the historical timestamp is unknown rather than fabricating evidence;
immutable unique calendar_uid exactly event-<lowercase-event-uuid>@datatalks.club; and
non-negative calendar_sequence, initialized to RFC 5545 sequence 0.
Changing title/date/source data never changes UUID, public ID, calendar UID, source attachment, or aliases. New Events created through the identity service default to draft; the migration preserves all existing checked rows as published. No capacity or waitlist column, placeholder, serializer field, or availability branch is added.
Use forward migrations with historical models and deterministic data. Fresh migration and upgrade from the current schema must converge to the same 421 core rows on SQLite and PostgreSQL. Do not query a network, production system, current wall clock, request Host, or runtime model from a migration.
Deterministic import contract
Add a versioned Event-core manifest or equivalent immutable checked artifact derived only from the frozen source envelope. Its parser/importer must:
join each record by the exact existing UUID plus public ID and exact repository/revision/source key/checksum; title/date/slug/provider guessing is forbidden;
carry only UUID/public ID, source provenance/checksum, allowed event type, exact start/end instant, and retained timezone name needed by this child;
validate the complete set before a write: exact counts, unique identities, all 421 identity-manifest records present once, no extra record, allowed keys/types, aware timestamps, end ordering, timezone validity, and digest binding;
default to dry-run and require explicit apply; apply is one transaction and updates no alias, public projection, description, speaker, link, Q&A, historical aggregate, registration, audit, job, or provider row;
be byte/dataset deterministic across two clean builds, idempotent on exact replay, and report bounded counts/digests only; and
reject missing, duplicate, changed, ambiguous, renumbered, invalid-zone, naive-time, unsupported-type, checksum, or partial input before any mutation.
A later source revision is not accepted implicitly. It requires a reviewed manifest/envelope update under #45 or another groomed source issue.
Lifecycle and availability service contract
Application services in events are the only supported new mutation boundary. They use the existing immutable Event UUID, require an expected revision, lock/recheck the row, and either commit one result or make no change.
cancelled is reachable only from draft or published. No reverse, skip, self-transition, completion-after-cancellation, or physical delete behavior is introduced. A successful transition increments revision once and records only the matching known timestamp. Cancellation also increments calendar sequence once in the same transaction. Invalid/stale/concurrent transitions leave revision, timestamps, and sequence unchanged.
A schedule service accepts aware start/end values plus a valid IANA timezone and optional registration window, revalidates all ordering inside the lock, and increments revision once. A genuine start/end/timezone change increments calendar sequence once; an exact no-op does neither. This child does not own title, body, visibility, location, Person, relationship, notification-decision, or public rendering edits.
The pure availability query returns a typed result plus bounded reason from current persisted state and an injected aware now:
available only when lifecycle is published, now < starts_at, registration_opens_at is absent or now >= opens_at, and registration_closes_at is absent or now < closes_at;
unavailable reasons are exactly lifecycle-not-published, not-yet-open, registration-closed, or event-started, selected in that order after validating the record;
there is no capacity, waitlist, email preference, identity, cookie, or provider check.
Pure ICS contract
Implement a standard-library, side-effect-free serializer below events. It receives an already-resolved Event plus an explicit canonical detail URL from #173's builder and emits UTF-8 RFC 5545 bytes with CRLF endings, 75-octet UTF-8-safe folding, and correct escaping of backslash, comma, semicolon, CR/LF.
The stable ordered calendar/event fields are VERSION:2.0, PRODID:-//DataTalks.Club//Events//EN, CALSCALE:GREGORIAN, an explicit method enum (PUBLISH, REQUEST, or CANCEL), UID, SEQUENCE, UTC DTSTAMP, UTC DTSTART, optional UTC DTEND, escaped SUMMARY, optional escaped public description/location supplied through an explicitly public data object, canonical numeric Event URL, and STATUS:CANCELLED only for cancellation. Dates serialize as UTC YYYYMMDDTHHMMSSZ; sequence is the stored non-negative integer. Serialization is deterministic for identical inputs.
Protected join information is not a serializer input and must be absent from output, exceptions, logs, fixtures, and reports. This issue does not decide whether a future public download uses PUBLISH, nor does it attach REQUEST/CANCEL to email; those adapters remain #45/#46/#49 work.
Public ICS route authority decision
No authoritative source currently defines an Event ICS endpoint. Spec 02 requires future registration/calendar builders to emit the numeric/current-slug Event identity, but it does not choose among possible suffix/subpath routes. The existing course /<course>/calendar.ics compatibility route is course-owned and is not authority for Events.
Therefore this child adds no URL pattern, view, response headers, filename, cache class, template action, sitemap/feed entry, redirect, or browser link. Full #45 must separately record and groom the exact public Event ICS path/method/cache/alias contract before any route is implemented. The serializer's URL property is the already-approved Event detail canonical, not a new ICS endpoint.
Acceptance criteria
Forward migrations expand the one existing Event model with the exact fields/constraints/defaults above; 421 checked rows retain UUID/public ID/source/aliases, import as published with exact schedule/type/timezone, sequence 0, stable unique UID, revision 1, and no fabricated publication timestamp.
Fresh and upgrade migrations converge on SQLite/PostgreSQL; migration drift is empty; malformed persisted combinations are rejected by database constraints where portable and by service/import validation otherwise.
Dry-run/apply/replay of the frozen import is atomic, network-free, deterministic, complete, and idempotent; every named malformed/mismatched/partial case fails before writes with bounded non-sensitive diagnostics.
Revision-guarded lifecycle and schedule services implement exactly the transition/window/sequence rules, including concurrent stale writers, rollback, no-op, DST, ambiguous fold=0, leap day, already-aware source values, and timezone validation.
Availability boundary tests prove every exact state/window/start boundary and reason ordering with an injected clock and no capacity/waitlist behavior.
Exercise every allowed/forbidden lifecycle edge, stale revisions, two-writer publish/cancel and reschedule/cancel races, timestamp/sequence outcomes, exact no-op, and transaction rollback on SQLite plus the applicable separate-connection PostgreSQL cases.
Exercise availability immediately before/at/after open, close, and start for draft/published/completed/cancelled/archived records, including absent one/both window bounds.
Import ordinary winter/summer Berlin instants, DST ambiguity under the accepted fold=0 convention, leap day, explicit aware offsets, naive/malformed times, invalid zones, unsupported types, identity/checksum mismatch, duplicate/missing/extra rows, and changed source revision.
Serialize ordinary, Unicode/folded, escaped, optional-end, and cancelled Events twice byte-for-byte. Parse fixtures with an independent test parser or strict assertions and prove the output contains the numeric detail URL but no UUID route, protected join canary, registration address, provider identifier, or new endpoint.
Prove current public Event pages and all route/link/schema inventories are unchanged, no public ICS URL resolves, no worker/job/network seam is invoked, and importing does not create or modify Q&A or historical-total rows.
Verification and screenshots
This is a database/domain/internal-serialization change with no product-page render impact. Screenshot evidence is not_applicable unless the versioned verification graph finds an actual render path; unrelated public screenshots are not evidence. The independent tester still runs the graph-selected Playwright tier (normally smoke for a backend-only change), validates the frozen plan/envelope, and reports route non-activation.
No public/template/feed/sitemap/calendar-download route; no public-reader cutover; no Event description, summary, image, visibility, location, protected join data, recording/recap/course/cohort/external-event relationship, Person/speaker/host row, Studio/admin adapter or capability, registration/token/preference/attendance/export, notification decision, EmailDelivery, durable job, Q&A change, historical-total change, provider/network call, production/source access, capacity/waitlist, new package, commit, push, deployment, or issue closure outside the normal lifecycle.
Delivery convention
Follow _docs/PROCESS.md: engineer leaves the focused implementation uncommitted; a separate tester validates the frozen plan, focused SQLite/PostgreSQL tests, graph-selected broader gates, and route non-activation; PM accepts afterward. Only then may the engineer commit with body Closes #289, the orchestrator locally merge with --no-ff and push, and on-call alone observe CI. No pull request or production action belongs to this issue.
Parent: #45
Normative authority:
_docs/specs/05-events-registration-email.md, especially Event model, lifecycle, availability, Event changes, and calendar behavior;_docs/specs/02-url-link-seo-compatibility.md#canonical-public-event-identity, for the numeric public Event identity consumed as an ICSURLvalue only;_docs/architecture/app-boundaries.md, foreventsownership and presentation-to-service dependency direction;_docs/specs/07-security-privacy-operations.md, for protected join information, source redaction, and network-side-effect boundaries;_docs/specs/09-migration-rollout-roadmap.md#milestone-6---events-and-transactional-email; and_docs/specs/10-verification-strategy.md.Accepted inputs are closed #18, #19, #136, and #173. This issue expands the one existing
events.Event; it never creates another Event identity or changes the accepted/events/<positive-public-id>/<current-title-slug>canonical.Outcome
Create a deliberately non-activating Event domain foundation that full #45 can consume:
events; andThe accepted public pages continue to read
content/public_projection/events.json. Landing this issue must therefore produce no public-page, route, sitemap, feed, cache, Studio, admin API, registration, email, Q&A, job, or provider behavior change.Frozen source envelope
The groomed envelope on
origin/mainface8e4808d65afbf0374d1ced7a88079950d663is:content/public_projection/events.json: 421 records, SHA-256333c53c88445d9cbc6d91c26f9043a6998645346a84fdd3b2782ec0a989d7fc6;events/event_identity_manifest.json: schema 2, 421 UUID/public-ID bindings and 1,684 aliases, SHA-2561fd4ff474199cae9ce97a75ac8061aa4c8388edf66a1cb960377e0494559b6f5;DataTalksClub/datatalksclub.github.io@ee43d3fa0929faf691178d79f19528e6f15a83e5,_data/events.yaml;DataTalksClub/content@e29f56ce70bd997171a78a9f0facc9354797f421, green source run31365358459;conference6,podcast214,webinar132,workshop69;+01:00or+02:00), 2 have an end, and every imported timezone name isEurope/Berlin.Engineering must recompute this envelope before work. Any base/source/artifact/digest/count difference returns the issue to PM instead of silently widening or repairing source content here. The repository's current broader release/source recovery remains an operational start gate: implementation begins only from a clean current
origin/mainwhose required base CI and source provenance gates are green.Schema contract
Extend
events.Eventin place. Final state must contain:event_type: required allowlisted valueconference,podcast,webinar, orworkshopfor the checked import;starts_at: required timezone-aware instant stored by Django in UTC;ends_at: optional aware instant, strictly afterstarts_atwhen present;timezone_name: required valid IANA zone; every checked legacy record imports asEurope/Berlinwithout reinterpreting its already-aware instant;registration_opens_atandregistration_closes_at; when both exist open is before close, and any close is no later than start;revision, initialized to 1 for the imported unchanged row;published_at,completed_at,cancelled_at, andarchived_at; imported legacy rows remainpublishedand retain null where the historical timestamp is unknown rather than fabricating evidence;calendar_uidexactlyevent-<lowercase-event-uuid>@datatalks.club; andcalendar_sequence, initialized to RFC 5545 sequence 0.Changing title/date/source data never changes UUID, public ID, calendar UID, source attachment, or aliases. New Events created through the identity service default to
draft; the migration preserves all existing checked rows aspublished. Nocapacityor waitlist column, placeholder, serializer field, or availability branch is added.Use forward migrations with historical models and deterministic data. Fresh migration and upgrade from the current schema must converge to the same 421 core rows on SQLite and PostgreSQL. Do not query a network, production system, current wall clock, request Host, or runtime model from a migration.
Deterministic import contract
Add a versioned Event-core manifest or equivalent immutable checked artifact derived only from the frozen source envelope. Its parser/importer must:
A later source revision is not accepted implicitly. It requires a reviewed manifest/envelope update under #45 or another groomed source issue.
Lifecycle and availability service contract
Application services in
eventsare the only supported new mutation boundary. They use the existing immutable Event UUID, require an expected revision, lock/recheck the row, and either commit one result or make no change.Exact transitions are:
cancelledis reachable only fromdraftorpublished. No reverse, skip, self-transition, completion-after-cancellation, or physical delete behavior is introduced. A successful transition increments revision once and records only the matching known timestamp. Cancellation also increments calendar sequence once in the same transaction. Invalid/stale/concurrent transitions leave revision, timestamps, and sequence unchanged.A schedule service accepts aware start/end values plus a valid IANA timezone and optional registration window, revalidates all ordering inside the lock, and increments revision once. A genuine start/end/timezone change increments calendar sequence once; an exact no-op does neither. This child does not own title, body, visibility, location, Person, relationship, notification-decision, or public rendering edits.
The pure availability query returns a typed result plus bounded reason from current persisted state and an injected aware
now:published,now < starts_at,registration_opens_atis absent ornow >= opens_at, andregistration_closes_atis absent ornow < closes_at;Pure ICS contract
Implement a standard-library, side-effect-free serializer below
events. It receives an already-resolved Event plus an explicit canonical detail URL from #173's builder and emits UTF-8 RFC 5545 bytes with CRLF endings, 75-octet UTF-8-safe folding, and correct escaping of backslash, comma, semicolon, CR/LF.The stable ordered calendar/event fields are
VERSION:2.0,PRODID:-//DataTalks.Club//Events//EN,CALSCALE:GREGORIAN, an explicit method enum (PUBLISH,REQUEST, orCANCEL),UID,SEQUENCE, UTCDTSTAMP, UTCDTSTART, optional UTCDTEND, escapedSUMMARY, optional escaped public description/location supplied through an explicitly public data object, canonical numeric EventURL, andSTATUS:CANCELLEDonly for cancellation. Dates serialize as UTCYYYYMMDDTHHMMSSZ; sequence is the stored non-negative integer. Serialization is deterministic for identical inputs.Protected join information is not a serializer input and must be absent from output, exceptions, logs, fixtures, and reports. This issue does not decide whether a future public download uses
PUBLISH, nor does it attachREQUEST/CANCELto email; those adapters remain #45/#46/#49 work.Public ICS route authority decision
No authoritative source currently defines an Event ICS endpoint. Spec 02 requires future registration/calendar builders to emit the numeric/current-slug Event identity, but it does not choose among possible suffix/subpath routes. The existing course
/<course>/calendar.icscompatibility route is course-owned and is not authority for Events.Therefore this child adds no URL pattern, view, response headers, filename, cache class, template action, sitemap/feed entry, redirect, or browser link. Full #45 must separately record and groom the exact public Event ICS path/method/cache/alias contract before any route is implemented. The serializer's
URLproperty is the already-approved Event detail canonical, not a new ICS endpoint.Acceptance criteria
fold=0, leap day, already-aware source values, and timezone validation.uv-backed lint/format/type/migration checks, focused SQLite/PostgreSQL migration/service/concurrency/import/ICS tests, graph-selected broader Django/container checks, independent tester PASS, and PM acceptance exist before commit.Required test scenarios
fold=0convention, leap day, explicit aware offsets, naive/malformed times, invalid zones, unsupported types, identity/checksum mismatch, duplicate/missing/extra rows, and changed source revision.Verification and screenshots
This is a database/domain/internal-serialization change with no product-page render impact. Screenshot evidence is
not_applicableunless the versioned verification graph finds an actual render path; unrelated public screenshots are not evidence. The independent tester still runs the graph-selected Playwright tier (normally smoke for a backend-only change), validates the frozen plan/envelope, and reports route non-activation.Dependencies and ordering
Explicit non-goals
No public/template/feed/sitemap/calendar-download route; no public-reader cutover; no Event description, summary, image, visibility, location, protected join data, recording/recap/course/cohort/external-event relationship, Person/speaker/host row, Studio/admin adapter or capability, registration/token/preference/attendance/export, notification decision,
EmailDelivery, durable job, Q&A change, historical-total change, provider/network call, production/source access, capacity/waitlist, new package, commit, push, deployment, or issue closure outside the normal lifecycle.Delivery convention
Follow
_docs/PROCESS.md: engineer leaves the focused implementation uncommitted; a separate tester validates the frozen plan, focused SQLite/PostgreSQL tests, graph-selected broader gates, and route non-activation; PM accepts afterward. Only then may the engineer commit with bodyCloses #289, the orchestrator locally merge with--no-ffand push, and on-call alone observe CI. No pull request or production action belongs to this issue.