DataTalksClub / DataTalksClub/website

Implement deterministic direct-upsert runner and partial recovery

Open
#274 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

content enhancement integration needs grooming P0 security testing
Dominant language
Python
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Parent epic: #38
Owner architecture decision: #226 (closed)
Schema/reconciliation prerequisite: #273
Source/projection prerequisite: #253
Existing network-free adapter baseline: #103 (closed)

Product outcome

Implement Slice B of #38: a deterministic, network-free application service that consumes one already authenticated and verified immutable checkout, dispatches it through a code-owned source adapter, and directly upserts source-owned current rows. Accepted record aggregates commit independently; an incomplete run is reported truthfully as partial or failed; a source-scoped stale-row sweep runs only for a fully accepted ownership partition; and replay of the same immutable commit converges without duplicate rows, relations, assets, logs, audits, or public identities.

This issue does not perform GitHub ingress, clone/fetch/checkout, durable-job scheduling, source locking/follow-up orchestration, public-reader cutover, or content management. It provides the pure verified-input-to-database runner and recovery contract that those later slices call.

PM status — blocked contract / do not dispatch

Keep needs grooming. The architecture and failure boundaries below are settled by #226/#38, but two implementation interfaces do not yet exist in an accepted form:

  1. #273 must land the exact direct-row models, ContentSource status/fence fields, SyncLog fields/constraints, historical reconciliation, and service-level transition API that this runner mutates.
  2. #253 must land one repaired, reproducible DataTalksClub/content source/projection identity. Its final commit/tree/CI/attestation, adapter/schema/parser/rendering versions, bundle/projection digests, exact partition inventories, and accepted parity transform must replace the stale hard-coded #103 pin before this issue can name an engineer-ready first-source fixture.

No engineer may implement against guessed #273 models, bless e29f56..., b023542..., or face8e4 as the new direct-sync source identity, or turn the existing staged ContentRelease preparation service into this runner. After both prerequisites are accepted, PM must make one final bounded grooming pass that inserts the exact model/service names and final #253 identities, verifies the adapter partition mapping below, and removes needs grooming.

Normative authority

  • _docs/PROCESS.md: dependency order, migrations, focused tests, change-selective verification, independent tester/PM gates, and no commit before acceptance.
  • _docs/specs/open-decisions.md, decision 1 as resolved by #12/#226, and #38: direct upsert with truthful partial recovery; no ordinary candidate/activation/rollback graph.
  • _docs/specs/01-platform-architecture.md and _docs/architecture/app-boundaries.md: content-owned business mutations in shared application services, backend-portable Django transactions, scalar durable-job boundaries, and no network work inside a database transaction.
  • _docs/specs/03-github-content-and-people.md: source ownership, bounded parsing/sanitization/provenance, route/link/asset safety, and draft/unpublished public filtering. Its staged release/activation wording is superseded where it conflicts with #226/#38.
  • _docs/specs/07-security-privacy-operations.md: last-known-safe behavior, bounded/redacted diagnostics, no secret or production-data exposure, and observable partial/failure state.
  • _docs/specs/09-migration-rollout-roadmap.md and _docs/specs/10-verification-strategy.md: expand/reconcile ordering, portable database tests, idempotency, concurrency, crash/fault injection, and exact release evidence.
  • #103 supplies the current dtc-content verifier, immutable VerifiedCheckout, CandidateBundle, bounded parsers, sanitizer, media checks, exact-person relation policy, and parity machinery. Reuse/refactor those pure contracts; do not retain its staged ContentRelease preparation wrapper or stale accepted pin.
  • #272 is a downstream evidence consumer. It does not define storage or authorize public cutover.

Exact runner boundary

Trusted input

The public application-service entry point receives only scalar database identity plus one immutable in-process verified value:

  • sync_log_id, source_id, expected source revision, and the held runner fence/lease token supplied by the trusted caller;
  • the database alias and a normal redacted service/audit context; and
  • a frozen VerifiedCheckout produced by the registered verifier, never a filesystem path, repository URL, branch, arbitrary SHA, payload, or provider response.

For the first adapter, VerifiedCheckout retains the #103 shape: exact lowercase 40-hex commit, exact tree, normalized allowlisted origin, immutable CandidateBundle, and the final #253 projection/parity evidence. Before any content mutation, the runner rebinds all of these to the locked ContentSource: stable source UUID/key, repository, branch, adapter ID, mount/path/resource policy, enabled state, expected revision, commit/tree, contract/schema/parser/rendering/selection-policy versions, bundle digest, source/projection provenance, and counts. Any missing, stale, contradictory, disabled, wrong-source, wrong-fence, wrong-version, or digest mismatch terminates failed with zero content mutation.

The runner and direct-upsert adapter perform no clone, fetch, GitHub API call, webhook parsing, source repository script, subprocess, arbitrary-path read, remote asset write, or network call. Parsing, sanitization, classification, exhaustive inventory construction, relation resolution, and global collision validation finish before the first direct-row transaction. No database transaction spans checkout, parsing, provider work, or asset I/O.

Code-owned adapter protocol

The source row's allowlisted adapter ID resolves through a closed code registry; request input cannot select a class or widen ownership. The adapter converts the frozen verified bundle into one immutable, deterministically ordered DirectSyncPlan containing:

  • exact source/commit/tree/version/policy/provenance identities and one canonical plan digest;
  • a stable ordered tuple of exhaustive ownership partitions;
  • for each partition, its kind/namespace, inventory count and digest, complete ordered record aggregates, collision result, and stale-transition policy;
  • for each aggregate, (content_kind, stable_record_key), normalized source-path digest, source checksum, intended published/draft state, owned scalar/structured fields, owned dependent relations, and owned asset metadata; and
  • bounded safe diagnostics and exact accepted/rejected classification, never raw source content.

The initial dtc-content mapping is four source-owned namespaces, subject only to confirmation against the accepted #253 adapter:

  1. article: one article plus its owned relations and referenced article asset metadata;
  2. podcast: one podcast plus its zero-or-one transcript dependent document, ordered relations, and referenced podcast asset metadata as one record-atomic aggregate;
  3. book: one book plus ordered author/discussion relations and referenced book asset metadata; and
  4. media: the exhaustive source-owned media inventory for any accepted assets not uniquely owned by one document aggregate, with exact stable public-path collision checks.

An asset identity may appear in dependency references but has one owning aggregate/partition write. The plan must reject double ownership, missing dependency ownership, duplicate stable keys, duplicate source paths, route claims, stable asset paths, unresolved required relations, non-exhaustive inventory, or any cross-source claim before the conflicting write. Person/account/member/course identities use only accepted exact source keys; display name and email are never join keys.

SyncLog lifecycle and source status

The accepted #273 transition service must enforce these exact states:

  • queued -> running only when the same queued log, source, input identity, expected revision, and runner fence are acquired; do not create a second log at start;
  • running -> success only after every partition has zero rejected records, every eligible stale sweep completed, final visible/draft/ownership digests match the plan, and terminal evidence commits;
  • running -> partial when one or more bounded record/partition operations fail after at least one direct content mutation committed;
  • running -> failed for pre-mutation input/global validation, no accepted mutation plus record/transaction/system failure, or an explicitly fenced interruption/watchdog transition supplied later by Slice C;
  • queued -> skipped only for an already-proved complete identical input or a separately classified coalesced request under #38; and
  • no other forward, backward, terminal-to-terminal, or unknown transition.

success, partial, failed, and skipped are immutable terminal logs. ContentSource.last_sync_status and last-log reference mirror the newest accepted transition. Attempted commit/time advance for the accepted attempt; successful commit/time advance only with success. A later queued/running/partial/failed run must not be masked by the older successful commit. A verified unchanged-head skip may retain complete evidence; a coalesced skip may not.

This slice validates and uses a supplied source fence but does not create provider ingress, durable jobs, lock coalescing, follow-up scheduling, remote-head observation, reconciliation scheduling, or stale-worker watchdogs; those belong to Slice C. Tests use the accepted #273 seam to simulate their inputs and reject a stale/wrong fence.

Record atomicity and partition sweep

  1. Acquire and validate the one running log/source/fence, build and globally validate the complete plan, then process partitions and records in stable adapter order.
  2. Each record aggregate and all direct rows it owns—document/current row, owned dependent transcript, relations, asset metadata, provenance, and its bounded audit/event evidence—commit in one portable transaction.atomic(using=...) block.
  3. A rejected existing aggregate remains byte-for-byte/public-state unchanged. A rejected new aggregate creates no current/public row. Failure input, parsed body, HTML, YAML, Markdown, exception, stack, or temporary path is never persisted.
  4. A valid aggregate may commit even if a later aggregate fails. This accepted mixed-version state is explicit partial; it is not rolled back, relabelled success, or hidden behind the prior status.
  5. The partition stale sweep runs in its own atomic service step only after the adapter proves the inventory exhaustive and every aggregate in that partition reached created, updated, or unchanged. Any global classification, collision, parse, resolution, apply, or post-state error suppresses that partition's entire sweep.
  6. A successful sweep transitions only previously source-owned rows absent from that exact complete inventory through the model's accepted non-public state (is_published=false, status=draft, or the explicit #273 soft-delete field). It never hard-deletes and never changes another source's row, historical ContentRelease child, manually owned operational field, or cross-source identity.
  7. Public-safe filtering is a model/query invariant supplied by #273 and later cutover work: raw direct rows, partial diagnostics, disabled-source rows, and draft/soft-deleted rows are non-public by default. #274 does not change current public reads.

Replay, idempotency, recovery, and checkpoints

  • (source, content_kind, stable_record_key) plus the accepted #273 ownership key is stable and unique. Exact replay updates/reuses the same aggregate and replaces owned child sets deterministically; it never appends duplicate relations/assets/audits.
  • An exact retry of the same request/idempotency identity returns the original log and safe terminal result, or conflicts if the identity is reused with different source/commit/tree/plan bytes.
  • Recovery from partial/failed is a new queued reconcile, manual, or later follow_up log linked safely to the prior log, with a new request/idempotency identity but the exact same immutable commit/tree/plan. The old terminal log is never reopened.
  • Recovery always starts the deterministic plan from the beginning. Already applied aggregates compare unchanged; failed aggregates retry; suppressed sweeps run only after their partition becomes complete. A later accepted branch-head commit is a different input/run. No arbitrary older-SHA parameter, restored release, pointer, or operator override may move direct rows backward.
  • There is no authoritative mutable per-record resume cursor. The durable checkpoint is the committed source-owned aggregate provenance plus each terminal log's bounded partition-completion/count/state digests. A crash may lose in-process progress reporting but cannot lose or duplicate an already committed aggregate; replay recomputes from the immutable input.
  • A crash before any record commit leaves running for Slice C's fenced watchdog/retry path. A crash after one or more record commits but before terminal evidence also leaves running; the watchdog must first terminalize that stale log as failed, then a new recovery log performs full replay. The runner never silently overwrites a stale log or fabricates success from database rows alone.

Deterministic evidence

For every terminal run, persist only #273's bounded fields and one canonical SHA-256 evidence digest over versioned canonical JSON. Evidence binds source/run/input identity, trigger, commit/tree, adapter/schema/parser/rendering/selection versions, plan/bundle/provenance digests, requested/started/finished UTC times, bounded created/updated/unchanged/drafted/rejected counts, per-partition completion/count digests, final visible/draft/ownership-state digests, safe reason/diagnostic codes, and prior-recovery link where applicable.

Counts and diagnostic arrays enforce accepted limits. Paths/field pointers use a normalized source-relative allowlisted grammar and are length-bounded. Evidence, logs, audits, metrics, and exceptions exclude raw body/signature/delivery ID/provider payload, repository content, rendered/source text, secret reference/value, token, credential, checkout path, absolute path, URL query/header/cookie/IP, email/profile/member/course activity, SQL, exception/stack text, and unbounded per-record values. Commit, path, delivery, and error values are not high-cardinality metric labels.

Canonical ordering is by partition order then content kind/stable key/source-path digest; relations/assets use their accepted stable order. Repeating the same plan against the same pre-state produces the same plan/inventory/evidence-state digests; replay action counts may truthfully change from created/updated to unchanged and therefore produce a distinct run-evidence digest.

Acceptance criteria (blocked until exact prerequisite identities are inserted)

  • #273 is accepted and this body names its exact models, constraints, transition/direct-row services, ownership/non-public fields, and recovery-link/fence interface; migrations and historical reconciliation remain outside this issue.
  • #253 is accepted and this body pins its one green immutable source commit/tree/CI/attestation, final adapter/schema/parser/rendering/selection versions, exact counts/partition inventories, bundle/projection/plan digests, and parity transform.
  • The runner accepts only the frozen verified value and scalar database/service identities above; arbitrary paths/URLs/branches/SHAs, stale source revision, disabled source, wrong adapter/version/digest, and stale/wrong fence fail before content mutation.
  • The initial dtc-content adapter emits the exact four confirmed ownership namespaces, stable aggregate order, exhaustive inventories, accepted transcript/relation/asset ownership, and byte-identical plans/digests across two clean full-corpus adaptations.
  • Global parse/classification/resource/relation/collision failure produces failed, zero direct-row mutation, no sweep, and bounded redacted evidence.
  • First/middle/last aggregate create/update/unchanged paths are record-atomic. Injected child/relation/asset/audit/transaction failure leaves that aggregate unchanged while earlier committed aggregates remain; status is exactly partial iff a direct mutation committed.
  • A rejected existing aggregate remains unchanged and public-safe; a rejected new aggregate creates no row. Cross-source adoption/update/draft/delete, route/asset collision, fuzzy identity resolution, and hard delete fail closed.
  • Each partition's complete success runs exactly one source-scoped draft/soft-delete sweep; missing/duplicate/classification/parse/apply/post-state failure suppresses the whole affected sweep while independently complete partitions retain their truthful results.
  • queued/running/success/partial/failed/skipped, terminal immutability, attempted-versus-successful source fields, newest-run status, wrong/backward transitions, and unchanged-head versus coalesced skip semantics match #38/#273 exactly.
  • Exact request replay returns the original safe result; changed bytes under the same idempotency identity conflict; concurrent duplicate aggregate writes converge under database uniqueness; no duplicate current rows, child sets, logs, or audits are created.
  • New-log recovery of the exact partial/failed commit starts from the beginning, classifies prior writes unchanged, retries failed writes, executes previously suppressed sweeps only after completeness, and converges to the same final visible/draft/ownership digests as one clean run.
  • Fault injection covers crash before the first record, between record commit and outcome accounting, after several records, before/after a sweep, and before terminal success. Stale running evidence is never overwritten; a fenced simulated watchdog plus new recovery log converges without duplicates or false success.
  • SQLite service tests cover every invariant with backend-portable Django transactions and constraints; migration drift, content/source adapter suites, redaction/audit/database canaries, cross-source ownership, deterministic full-corpus replay, and graph-selected quality/Django/container verification pass.
  • Playwright and screenshots are not_applicable only if the final change graph confirms no template/public-query/render impact. If any render/read path enters the diff, return to PM and add core/full browser plus independent desktop/mobile screenshot scenarios before engineering continues.
  • Engineer evidence records exact base/head, plan/graph digests, commands/counts, canonical plan/inventory/evidence digests, injected fault matrix, and every rerun/reuse/skip/not-applicable disposition; independent tester recomputes the plan and verifies all criteria before PM acceptance.

Explicit non-goals

  • No GitHub webhook endpoint, HMAC/provider parsing, clone/fetch/checkout, credential resolution, remote-head observation, scheduler, durable-job registration, lock acquisition/coalescing, pending-follow-up emission, freshness alerting, watchdog daemon, or provider/network call.
  • No public reader, projection, search, graph, sitemap, feed, SEO, cache, or asset-delivery cutover; no request-time fallback or dual writer.
  • No Studio, Django admin, /api/v1/admin/, manual-sync UI/API, source-configuration mutation, preview, edit-on-GitHub, or operator force-SHA surface.
  • No ContentRelease candidate creation, activation, pointer swap, rollback, retained-release selection, staged child mutation, staged-table removal, or reinterpretation of historical release rows as writable direct rows.
  • No automatic rollback, arbitrary older commit, branch movement, hard delete, cross-source transfer/adoption, fuzzy person/account/member/course identity matching, or database override of source-owned editorial fields.
  • No source-repository/provider mutation, source pin approval, projection regeneration, public-data migration, production data, new package without separate justification, unrelated refactor, commit, push, merge, deploy, or Actions rerun during grooming.

Required final grooming handoff

Once #273 and #253 are accepted, PM must replace every prerequisite placeholder with exact accepted names/values, verify that no source-interface or partition ambiguity remains, remove needs grooming, and post ## Grooming Complete. Until then this issue is a precise blocked contract, not an engineer handoff.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Do not start implementation until #273 and #253 are accepted and the final model, service, adapter, and source identities are inserted during grooming. Begin with _docs/PROCESS.md and the architecture and verification specifications, then trace the accepted #273 transition seam and #103 verified-input contracts. Done requires deterministic direct upserts, truthful partial or failed terminal state, safe partition sweeps, and replay without duplicate owned rows.

Written by the indexing model from the issue text.

Assessment

Tech stack
django, python
Domain
backend, database
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.