Skip to content

THOTH-IMPORT-CONTRIB-RECOVERY-01: capability-bound orphan-safe Contributor recovery #963

Closed as not planned

Description

@ja573

THOTH-IMPORT-CONTRIB-RECOVERY-01 - capability-bound orphan-safe Contributor recovery

Status

SPECIFICATION RECORDED - IMPLEMENTATION NOT AUTHORIZED

Current gate: CRITICAL specification/setup recorded; no source/new-file change, migration, branch, commit, push, PR, CI dispatch, merge, deployment, production activation or data operation is authorized by this issue.

Identity

Candidate preparation baseline - not implementation authorization

Verified live at task creation on 2026-10-01:

  • develop @ 345a7a04131e7c0539f7518c6ea457fa5cb6a462
  • tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6

This is a preparation baseline only. A later implementation authorization must freshly reverify develop, CI, branch topology, migration namespace and the exact write envelope.

Objective

Provide a narrow backend recovery contract that allows thoth-app#187 to compensate a Contributor genuinely created by one bulk-import execution attempt after the failed new top-level Work has been deleted and verified, without ever turning ordinary Contributor deletion into a destructive race.

The contract must be sufficient for the app to distinguish:

  • cleanup completed safely;
  • cleanup was already completed safely;
  • cleanup refused because the Contributor is now in legitimate use;
  • cleanup/provenance/authorization failed or is unknown.

This task does not introduce file-level transactions or a general import-command backend.

Why backend work is required

The current authoritative contract has these properties:

  1. ordinary createContributor requires authentication but carries no import-recovery provenance;
  2. ordinary deleteContributor derives authorization from publishers reachable through the Contributor's current Contributions;
  3. a Contributor left orphaned after a failed Work has no such publisher linkage;
  4. the database FK from contribution.contributor_id to Contributor is ON DELETE CASCADE;
  5. therefore app-side "check zero Contributions, then call deleteContributor" is race-unsafe: a concurrent legitimate Contribution can appear between the check and delete and would then be cascaded.

The approved programme recovery invariant requires a failed Work's independently-owned reusable entities either to be deterministically reusable or to have safe compensation. A newly-created Contributor without ORCID identity satisfies neither condition under the current contract.

Approved observable contract

The backend implementation must satisfy all of the following.

1. Import-specific creation authority

The recovery-capable creation path must be distinct in semantics from ordinary Contributor creation.

When bulk-import execution genuinely creates a new Contributor, the backend must return both:

  • the created Contributor identity; and
  • server-verifiable recovery provenance / an opaque cleanup capability bound to that exact creation and the authenticated creation context.

A bare Contributor ID is not cleanup authority.

Existing/reused Contributors must never receive recovery authority.

2. Atomic creation + recovery provenance

The recovery-capable creation operation must be atomic with its recovery evidence.

There must be no committed state in which:

  • a new import-created Contributor exists; but
  • the backend cannot later verify the corresponding recovery authority.

If durable persistence is required to satisfy this, the implementation must use a migration and that migration becomes part of the later CRITICAL implementation package. If an alternative design satisfies the same atomicity without schema change, it must prove that explicitly.

3. Creation idempotency / uncertain-response safety

A network/client uncertainty after creation must not make retry create a second Contributor.

The import-specific creation contract must therefore have a stable client-supplied operation/idempotency key, or an equivalent server mechanism, such that:

  • first successful execution creates at most one Contributor and its recovery authority;
  • repeating the same creation request under the same authenticated context returns/reconciles the same creation result rather than creating another Contributor;
  • reusing that key with materially different Contributor creation payload is rejected, never silently treated as the same operation.

The operation key is an execution transport identity only. It must not become bibliographic Contributor identity and must not survive into normal name/ORCID matching semantics.

4. Atomic conditional cleanup

The recovery operation must consume server-verifiable recovery authority, not merely a Contributor ID.

At one transactional decision point it must establish that:

  • the recovery authority is valid for this exact Contributor creation;
  • the caller is authorized under the approved recovery context;
  • the Contributor currently has zero Contributions.

Only then may it delete the Contributor.

If any Contribution exists at that decision point, cleanup must refuse non-destructively. It must never cascade-delete a Contributor that has become referenced.

5. Cleanup idempotency / reconciliation

Repeating the same cleanup request after an uncertain response must be safe.

The backend result must make it possible to distinguish at least:

  • CLEANED - this operation deleted the capability-bound orphan;
  • ALREADY_CLEANED - the same recovery authority had already completed cleanup;
  • IN_USE - the Contributor exists and currently has one or more Contributions, so no deletion occurred;
  • invalid/unauthorized recovery authority;
  • genuine backend failure.

Exact GraphQL type/enum names may differ, but these states and their semantics must remain observable.

A missing Contributor must not be treated as ALREADY_CLEANED unless the backend can verify that the same recovery authority previously completed the cleanup.

6. Authorization / capability rules

The recovery path must not weaken ordinary Contributor deletion.

At minimum:

  • authentication is required;
  • recovery authority is unguessable or otherwise cryptographically/server-verifiably bound to the exact creation;
  • it is bound to the authenticated creation context defined by this task;
  • an arbitrary authenticated user who learns a Contributor UUID must not gain recovery delete authority;
  • existing/reused Contributors can never be enrolled retroactively in this recovery mechanism;
  • superuser behaviour, if different, must be explicit and tested rather than accidental.

The backend child implementation must choose and document the exact caller-binding policy before coding (for example creator user identity, or an equally strong server-verifiable creation context). It may not fall back to the current empty-publisher-set behaviour as implicit authorization.

7. No name-based identity change

This task must not:

  • make full name exact Contributor identity;
  • auto-merge Contributors;
  • change ORCID uniqueness/normalization semantics;
  • make contributor-name suggestions authoritative.

The downstream app continues to treat same-name Contributors as suggestions only.

Downstream #187 contract

After this backend contract is merged and generated into thoth-app, #187 may:

  1. use the recovery-capable create path only when the immutable plan requires creation of a new Contributor;
  2. track the returned recovery authority only for Contributors actually created during the current top-level Work's in-flight execution;
  3. delete and positively verify the exact failed new Work first;
  4. then invoke Contributor recovery for the capability-bound Contributors created for that failed Work;
  5. declare Rolled back - safe to retry the complete file only after every required cleanup result is terminal-safe;
  6. report Cleanup failed/unknown - manual reconciliation required for IN_USE, invalid/unauthorized, transport-unknown or backend-failure outcomes;
  7. never clean a Contributor created for an earlier successful Work merely because the failed Work also reused it.

Downstream app code must not guess mutation names or response shapes before this backend contract is merged.

Series boundary

No Series backend recovery change belongs in this task.

The approved programme decision records that a surviving Series is deterministically rediscovered by normal ONIX preflight using native Series identity where applicable, declared ISSN, then exact/normalised name within the Work's imprint. Ambiguity blocks rather than creating another Series.

Security and concurrency invariants

Tests must falsify at least:

  • arbitrary Contributor ID + valid login cannot invoke recovery;
  • recovery authority for Contributor A cannot clean Contributor B;
  • recovery authority for an existing/reused Contributor cannot exist through this path;
  • simultaneous cleanup vs new Contribution creation cannot cascade-delete the newly referenced Contributor;
  • simultaneous duplicate create requests with the same operation key produce one Contributor;
  • same operation key + different payload fails closed;
  • two different operation keys may create two Contributors only when the caller genuinely requested two creations;
  • replay after successful cleanup returns a reconciled safe state and performs no second destructive action;
  • invalid/tampered/expired authority, if expiry exists, fails non-destructively.

Concurrency evidence must exercise the database transaction/locking behaviour, not only mocked GraphQL calls.

Data / migration requirements

No migration is pre-authorized by this specification.

During implementation preparation:

  • determine whether durable recovery/idempotency state is required;
  • if yes, allocate exactly one reviewed migration slot and specify up/down semantics;
  • migration must be additive and backwards-compatible;
  • ordinary Contributor rows and existing clients must remain valid without backfill;
  • no production migration execution is part of source implementation authorization.

No historical Contributor is to be enrolled into recovery provenance.

Compatibility

The change must be additive:

  • existing createContributor, deleteContributor, Contributor queries and ordinary editor flows retain existing behaviour unless an independently justified security correction is separately approved;
  • existing GraphQL clients remain schema-compatible;
  • generated schema/contract changes are expected only for the new recovery surface;
  • thoth-app#187 remains blocked until the merged generated contract is available.

Expected implementation surface

The exact authorized write budget is intentionally deferred to a later read-only implementation-preparation gate.

Likely areas requiring inspection include:

  • Contributor model / recovery model if added;
  • GraphQL mutation/result types;
  • Contributor/recovery policy;
  • database CRUD/transaction logic;
  • schema/migration only if required;
  • GraphQL/model/concurrency tests;
  • generated GraphQL schema artifact(s).

This list is not source-write authorization and is not an approved path budget.

Non-goals

This task does not authorize or implement:

  • file-level import transactions;
  • Work cleanup itself;
  • Publication/Location/relation execution;
  • ONIX parsing/planning/UI;
  • Contributor merge/deduplication by name;
  • a general "delete contributor if orphaned" public mutation;
  • changes to ordinary Contributor identity semantics;
  • Series recovery;
  • production cleanup of any existing Contributor;
  • publisher-data inspection;
  • production migrations;
  • deployment/release/activation.

Acceptance

Source acceptance requires evidence that:

  1. recovery-capable creation is atomic with server-verifiable recovery authority;
  2. creation is idempotent under uncertain response/retry;
  3. cleanup is capability-bound and atomically conditional on zero Contributions;
  4. a concurrent legitimate Contribution prevents deletion;
  5. cleanup is idempotent and returns explicit reconciliation outcomes;
  6. ordinary Contributor create/delete behaviour remains compatible;
  7. no existing/reused Contributor can be recovery-deleted through the new path;
  8. no name-based identity semantic changes;
  9. generated GraphQL contract is consistent;
  10. all repository CI/test/build gates pass.

Integration acceptance remains separate: thoth-app#187 must later prove it consumes the merged contract correctly.

HOLD / STOP conditions

HOLD if:

  • develop moves before later exact-SHA implementation authorization;
  • safe authorization cannot be established for an orphan without broadening ordinary delete rights;
  • zero-Contribution cleanup cannot be made atomic against concurrent Contribution creation;
  • creation cannot be made idempotent under an uncertain response;
  • the implementation requires a wider shared async/job/import architecture;
  • more than one migration or a destructive/backfill migration appears necessary;
  • the exact source footprint cannot be bounded.

STOP if the proposed implementation:

  • exposes a bare Contributor-ID orphan delete;
  • relies on app-side check-then-delete;
  • cascade-deletes any Contributor with a current Contribution;
  • infers identity from a name;
  • retrofits recovery authority onto arbitrary existing Contributors;
  • turns this into a whole-file transaction/import-command redesign.

Required gates

specification/setup (this issue)
-> bounded read-only implementation-preparation
-> independent CRITICAL specification review
-> CTO final specification approval
-> exact-SHA bounded implementation authorization
-> DRAFT PR + natural CI
-> independent CRITICAL exact-head source review
-> explicit exact-head CTO merge authorization
-> merge + post-merge CI/schema reconciliation
-> thoth-app #187 re-preparation against merged generated contract

Implementation, review, merge, deployment and production activation remain separate permissions.

Current gate

THOTH-IMPORT-CONTRIB-RECOVERY-01
ISSUE / SPECIFICATION RECORD: CREATED
RISK: CRITICAL
IMPLEMENTATION: HOLD / NOT AUTHORIZED
NEXT: bounded read-only implementation-preparation + independent specification review

Activity

  1. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Read-only implementation preparation - COMPLETE / SPECIFICATION AMENDMENT REQUIRED

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Decision: PREPARATION COMPLETE - CHANGES REQUIRED BEFORE CRITICAL SPECIFICATION APPROVAL

    No source/new-file mutation, migration creation/execution, branch, commit, push, PR, CI dispatch, provider/runtime action, deployment, release or production action was performed.

    Live preparation baseline

    • repository: thoth-pub/thoth
    • base branch: develop
    • live base: 345a7a04131e7c0539f7518c6ea457fa5cb6a462
    • tree: 98e64f0a6a49975f52d3dd14e7855395de2f34e6
    • proposed branch feature/thoth-import-contrib-recovery-01: ABSENT
    • open PR from proposed branch: NONE

    The current base is merge commit #962. GitHub exposes no workflow run bound to exact merge SHA 345a7a....

    That is recorded as an evidence gap, not silently called GREEN.

    Its runtime parent 923545d5c9028bc04c40e38efeb7de674efed3fd has:

    • build-test-and-check run 35636185017: SUCCESS
    • run-migrations run 35636185185: SUCCESS

    The complete delta 923545d5... -> 345a7a... is six documentation/control paths only; no Rust, SQL, schema, GraphQL, dependency or runtime file changed. Therefore current runtime source is byte-equivalent to the tested runtime parent for the surfaces relevant to #963, while exact-base CI remains honestly absent.

    Migration conclusion

    Durable recovery/idempotency state is required.

    Without durable state the backend cannot simultaneously provide:

    • lost-response creation replay without duplicate creation;
    • a stable server-generated recovery capability;
    • verified ALREADY_CLEANED;
    • terminal IN_USE reconciliation;
    • actor-bound recovery after the Contributor has no publisher linkage.

    Repository make migration on current version 1.8.0 resolves today's path to:

    thoth-api/migrations/20261001_v1.9.0

    Collision checks found:

    • path absent from live develop;
    • no open issue records that exact path;
    • no current open PR contains that path.

    Candidate migration slot reserved for #963: 20261001_v1.9.0.

    This reservation is SHA/date-sensitive. Recheck immediately before implementation authorization; if occupied, HOLD and rebind rather than invent a second same-date path.

    Prepared database contract

    One additive table plus one closed enum is sufficient.

    Candidate table:

    contributor_import_recovery

    Required durable fields:

    • operation_id UUID PRIMARY KEY - client-stable creation idempotency key;
    • contributor_id UUID NOT NULL UNIQUE - exact Contributor created by this operation;
    • recovery_token UUID NOT NULL UNIQUE DEFAULT uuid_generate_v4() - server-generated recovery capability;
    • actor_user_id TEXT NOT NULL - exact authenticated creator principal;
    • request_sha256 TEXT NOT NULL - canonical fingerprint of the requested Contributor creation payload;
    • status contributor_import_recovery_status NOT NULL DEFAULT 'ACTIVE';
    • resolved_at TIMESTAMPTZ NULL;
    • created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP.

    Closed status values:

    • ACTIVE
    • CLEANED
    • IN_USE

    Required checks:

    • nonblank actor_user_id;
    • lowercase 64-hex SHA-256 fingerprint;
    • ACTIVE <=> resolved_at IS NULL;
    • terminal CLEANED|IN_USE <=> resolved_at IS NOT NULL.

    No foreign key from the recovery row to Contributor. This is intentional and required: the recovery row is the durable idempotency/reconciliation tombstone and must survive deletion of the Contributor it records. Creation transaction tests must prove an ACTIVE row cannot commit without its Contributor.

    Migration is additive, creates zero rows and needs no backfill. It does not alter a populated existing table. The down migration may drop the new table/enum for disposable/pre-activation validation; after activation and real recovery rows exist, rollback is forward-repair/compatibility work rather than destructive production down-migration execution.

    Prepared creation transaction

    Candidate GraphQL operation: additive import-specific Contributor creation; exact public names remain reviewable but semantics are frozen.

    Input must contain:

    • client operationId;
    • ordinary NewContributor payload.

    Coordinator semantics:

    1. require authentication through model-specific recovery policy;
    2. derive exact actor_user_id from request context - never caller input;
    3. compute deterministic SHA-256 over a canonical field-labelled representation of the Contributor payload;
    4. open one DB transaction;
    5. acquire transaction advisory lock using the established repository pattern, namespaced by IMPORT_CONTRIBUTOR_RECOVERY|<operationId>;
    6. if the operation exists:
      • different actor -> NO_ACCESS, disclosing no row details;
      • same actor + different request hash -> stable operation-conflict error;
      • CLEANED -> stable resolved-operation error; never recreate;
      • ACTIVE or IN_USE -> load and return the exact existing Contributor plus the same stored recovery token; missing Contributor here is an internal inconsistency, never recreated;
    7. if absent:
      • create the Contributor on the same DB connection/transaction;
      • create the recovery row with server-generated token;
      • commit both or neither.

    The operationId is transport idempotency only. It is never bibliographic identity and never enters normal Contributor lookup semantics.

    Prepared cleanup transaction

    Cleanup input is the server-generated recoveryToken, not Contributor ID and not operation ID.

    Coordinator semantics:

    1. require authentication;
    2. transactionally lock the recovery row by token;
    3. actor mismatch -> NO_ACCESS;
    4. terminal CLEANED -> return ALREADY_CLEANED;
    5. terminal IN_USE -> return IN_USE without re-evaluating or re-enabling deletion;
    6. for ACTIVE:
      • lock the exact Contributor row FOR UPDATE;
      • missing Contributor -> genuine inconsistent/unknown cleanup failure, never ALREADY_CLEANED;
      • test Contributions in the same transaction;
      • if any exists, atomically mark recovery status IN_USE + resolved_at, return IN_USE, delete nothing;
      • if zero, delete the exact Contributor and atomically mark CLEANED + resolved_at, return CLEANED.

    IN_USE is deliberately terminal. Once legitimate reuse wins, the old recovery capability is permanently revoked; it must never become destructive later merely because those legitimate Contributions are subsequently removed.

    Contributor row locking is required to race safely with FK-backed Contribution creation:

    • Contribution wins first -> cleanup later observes it and terminally returns IN_USE;
    • cleanup lock wins first -> cleanup deletes orphan and a concurrent Contribution insert cannot commit against the deleted FK target.

    No check-then-delete outside one transaction is permitted.

    Server capability pattern

    Use a server-generated random UUID recovery token, stored durably and never exposed through a query/list surface.

    This follows the repository's existing durable claim-token principle:

    • capability returned only by the mutation that grants it/replays that same operation;
    • durable row stores the token so lost-response creation replay can return the same capability;
    • cleanup requires the capability plus exact creator-user binding;
    • arbitrary Contributor UUID knowledge confers no new recovery authority.

    No new cryptography dependency is required.

    Required specification amendment - ordinary orphan delete policy

    Preparation found a pre-existing authorization edge in current source:

    ContributorPolicy::can_delete -> require_publishers_for(current)

    and

    require_role_for_publishers

    authenticates, iterates the derived publisher set, then returns success.

    For an orphan Contributor, Contributor::publisher_ids() returns an empty set because it derives publishers only through Contributions. The loop therefore performs no role check and any authenticated caller who knows the Contributor UUID passes ordinary deleteContributor authorization.

    Current tests cover:

    • referenced Contributor + correct publisher role -> allowed;
    • referenced Contributor + wrong publisher -> denied;

    but do not cover the orphan case.

    Known downstream consumer check:

    • thoth-app carries the schema field but has no Contributor-delete service/hook;
    • its contribution tests explicitly document that the app has no deleteContributor cleanup capability.

    This does not justify silently changing the API. It is nevertheless a material authorization condition for a CRITICAL recovery task.

    Recommended Specification Amendment 1:

    • ordinary deleteContributor on a Contributor with one or more publisher-linked Contributions retains existing publisher-scoped behaviour;
    • ordinary deleteContributor on a Contributor with zero Contributions is denied to ordinary authenticated users;
    • superuser orphan deletion remains allowed as the explicit administrative path;
    • import recovery deletion uses only the capability-bound THOTH-IMPORT-CONTRIB-RECOVERY-01: capability-bound orphan-safe Contributor recovery #963 coordinator and does not call ordinary deleteContributor;
    • add anonymous / ordinary authenticated / wrong-publisher / correct-publisher / superuser / orphan cases to authorization regression tests.

    This closes the existing vacuous-authorization path without changing Contributor identity or creating a general orphan-delete API.

    Because this is an authorization semantic change, it requires explicit CTO adoption before #963 can receive final CRITICAL specification approval.

    Candidate exact write envelope after Amendment 1 approval

    Maximum 12 paths, no others:

    Existing files:

    1. CHANGELOG.md
    2. thoth-api/src/schema.rs
    3. thoth-api/src/model/contributor/mod.rs
    4. thoth-api/src/model/contributor/policy.rs
    5. thoth-api/src/model/contributor/tests.rs
    6. thoth-api/src/graphql/mutation.rs
    7. thoth-api/src/graphql/tests.rs
    8. thoth-errors/src/lib.rs

    Authorized-new-file candidates:

    1. thoth-api/src/model/contributor/recovery.rs
    2. thoth-api/migrations/20261001_v1.9.0/up.sql
    3. thoth-api/migrations/20261001_v1.9.0/down.sql
    4. docs/engineering/ai-delivery/implementation-reports/THOTH-IMPORT-CONTRIB-RECOVERY-01-implementation-report.md

    No Cargo/dependency change is required. No thoth-client source/query change is required by the backend task. No GraphQL query surface is added for the recovery ledger.

    Live overlap check

    Open PRs targeting develop include:

    Neither is a dependency and neither contains the #963 migration path.

    Before implementation authorization, re-check whether either merged or materially changed. Do not incorporate unmerged PR bytes into #963 and do not silently resolve an overlap by broadening scope.

    Required implementation validation

    At minimum:

    • migration forward -> revert -> reapply on disposable PostgreSQL 17;
    • populated-database migration proving no existing row/backfill effect;
    • full workspace build/test/check/clippy/fmt;
    • focused GraphQL auth/error/SDL tests;
    • model/coordinator tests;
    • two-session concurrent same-operationId create proof -> one Contributor;
    • same operation + different payload -> fail closed;
    • lost-response replay -> same Contributor + same recovery token;
    • cleanup replay after CLEANED -> ALREADY_CLEANED;
    • cleanup replay after terminal IN_USE -> IN_USE;
    • two-session Contribution-create vs cleanup race in both orderings;
    • invalid token / different actor -> no access and no mutation;
    • ordinary orphan deleteContributor authorization regression if Amendment 1 is approved;
    • generated GraphQL SDL comparison and downstream-contract handoff evidence.

    No production migration execution belongs to source implementation.

    Current gate

    READ-ONLY PREPARATION COMPLETE
    MIGRATION REQUIRED: 20261001_v1.9.0 candidate reserved
    IMPLEMENTATION ENVELOPE: 12 paths
    ARCHITECTURE: IMPLEMENTABLE IN ONE thoth TASK
    BLOCKER: SPECIFICATION AMENDMENT 1 - ORDINARY ORPHAN deleteContributor AUTHORIZATION
    IMPLEMENTATION: NOT AUTHORIZED
    NEXT: CTO ADOPTION OF AMENDMENT 1 -> INDEPENDENT CRITICAL SPECIFICATION REVIEW
    
  2. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Specification Amendment 1 - ordinary orphan Contributor delete authorization

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Decision: CTO APPROVED SPECIFICATION AMENDMENT

    This amendment is adopted in response to the read-only implementation-preparation finding recorded in comment 5935744657.

    The preparation baseline remains unchanged at the time of adoption:

    • develop @ 345a7a04131e7c0539f7518c6ea457fa5cb6a462
    • tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6

    This amendment changes the approved specification only. It does not authorize implementation.

    A1. Ordinary deleteContributor authorization

    For a Contributor with one or more Contributions:

    • preserve the existing publisher-derived authorization model;
    • ordinary publisher-scoped deletion remains governed by the publishers reached through those Contributions;
    • wrong-publisher callers remain denied;
    • superuser remains allowed through the existing superuser policy path.

    For a Contributor with zero Contributions:

    • ordinary authenticated users must be denied;
    • an empty derived publisher set must not be treated as sufficient authorization;
    • superuser is the only approved ordinary administrative deletion path.

    This closes the current vacuous-authorization edge where require_publishers_for succeeds after authentication when the derived publisher set is empty.

    A2. Import-recovery deletion is separate

    The #963 recovery coordinator must not use ordinary deleteContributor authorization as its compensation authority.

    Import recovery remains governed exclusively by the capability-bound contract already specified:

    • server-verifiable recovery token/provenance for a Contributor genuinely created by that import operation;
    • exact creator-principal binding;
    • atomic zero-Contribution check and cleanup in one transaction;
    • terminal non-destructive IN_USE;
    • idempotent CLEANED / ALREADY_CLEANED reconciliation;
    • no bare Contributor-ID cleanup;
    • no name-based identity.

    Ordinary Contributor deletion and import recovery are distinct authorization surfaces and must remain distinct in source and tests.

    A3. Required authorization regression matrix

    The later implementation must prove at minimum:

    1. anonymous caller -> ordinary deleteContributor denied;
    2. ordinary authenticated caller + orphan Contributor -> denied;
    3. wrong-publisher caller + referenced Contributor -> denied;
    4. correctly scoped publisher caller + referenced Contributor -> allowed under existing rules;
    5. superuser + referenced Contributor -> allowed;
    6. superuser + orphan Contributor -> allowed;
    7. recovery token holder cannot use recovery authority to broaden ordinary deleteContributor;
    8. ordinary delete authority cannot substitute for or forge recovery authority.

    A4. Compatibility statement

    This is an intentional security/authorization correction.

    Known downstream inspection found:

    • thoth-app includes deleteContributor only in its checked-in schema contract;
    • no Contributor-delete service/hook/client path is present;
    • existing contribution tests explicitly document that the app does not expose Contributor cleanup.

    No downstream UI behaviour change is therefore currently known to be required by this amendment. Any later discovered consumer relying on ordinary non-superuser orphan deletion is a compatibility HOLD and must be assessed rather than silently preserved.

    A5. Prepared implementation envelope

    Subject to independent CRITICAL specification review and fresh pre-authorization verification, Amendment 1 fits inside the prepared 12-path envelope from comment 5935744657; no extra path is required.

    Current gate

    ORIGINAL SPECIFICATION: RECORDED
    READ-ONLY PREPARATION: COMPLETE
    SPECIFICATION AMENDMENT 1: CTO APPROVED
    INDEPENDENT CRITICAL SPECIFICATION REVIEW: REQUIRED
    IMPLEMENTATION: NOT AUTHORIZED
    

    No source/new-file mutation, migration creation/execution, branch, commit, push, PR, CI dispatch, merge, provider/runtime access, deployment, release, production activation or production-data operation is authorized by this amendment.

  3. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Independent CRITICAL specification review - CHANGES REQUIRED

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Reviewed specification: issue body + preparation comment 5935744657 + CTO-approved Amendment 1 comment 5935809908
    Reviewed repository baseline: develop @ 345a7a04131e7c0539f7518c6ea457fa5cb6a462 / tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6
    Decision: CHANGES REQUIRED

    This review re-read the actual specification and amendments against live repository doctrine and source. It does not rely on the prior preparation summary as authority.

    No source/new-file mutation, migration creation/execution, branch, commit, push, PR, CI dispatch, provider/runtime action, merge, deployment, release or production action was performed.

    Accepted design conclusions

    The following prepared decisions are sound and do not block specification approval:

    • durable recovery/idempotency state is required;
    • one additive recovery table plus a closed status enum can own that state;
    • recovery state must survive Contributor deletion, so omitting an FK from the recovery tombstone to Contributor is justified;
    • creation and recovery provenance must commit atomically;
    • a client operation ID is transport idempotency, never Contributor identity;
    • a server-generated random recovery token is an acceptable opaque capability;
    • actor identity must be server-derived, never caller-supplied;
    • cleanup must lock the exact Contributor row and decide zero-Contribution state in the same DB transaction;
    • PostgreSQL FK locking gives the required race property: a concurrent Contribution insert and FOR UPDATE/delete cannot both commit as though the other did not exist;
    • IN_USE must be terminal once legitimate reuse wins;
    • ordinary orphan deleteContributor must be corrected exactly as Amendment 1 specifies;
    • no name-based identity, general orphan-delete API, file-level transaction or shared async architecture is needed;
    • the prepared one-migration / 12-path scale remains plausible.

    CR-SPEC-1 - BLOCKING - recovery authority has no success-path revocation or bounded lifetime

    The prepared state machine is:

    ACTIVE -> CLEANED | IN_USE

    A recovery token is returned to the authenticated creator and remains ACTIVE until cleanup is attempted.

    For a successful top-level Work, #187 does not call cleanup. The recovery row therefore remains ACTIVE indefinitely.

    That creates authority beyond the stated compensation purpose:

    1. an authenticated user creates a Contributor through the recovery-capable import path and receives the token;
    2. the top-level Work succeeds;
    3. the Contributor is therefore referenced and cleanup would currently refuse;
    4. the token remains live;
    5. at any later time, if every Contribution is legitimately removed and the Contributor becomes orphaned, the old token can now satisfy the zero-Contribution condition and delete it.

    This contradicts the specification's own boundary that recovery authority exists to compensate an attempt-created Contributor for the in-flight failed Work, and it bypasses Amendment 1's intentional rule that ordinary orphan deletion is superuser-only.

    Client-side discarding of the token is not a security control.

    Required Amendment 2A

    The server-side recovery lifecycle must include both:

    1. successful-attempt revocation/finalization, and
    2. a finite server-controlled maximum capability lifetime so omission/crash of the success finalization cannot leave destructive authority live indefinitely.

    The exact lifecycle and expiry must be specified before approval.

    A bounded candidate shape is:

    ACTIVE
      -> CLEANED       failed Work, orphan removed
      -> IN_USE        failed Work cleanup refused because referenced
      -> RELEASED      successful top-level Work explicitly finalised
      -> EXPIRED       server lifetime elapsed
    

    All four terminal states are irreversible.

    Required semantics:

    • Update README #187 must release every recovery capability created for a top-level Work before it may report that Work as successfully complete;
    • release is idempotent and actor/capability-bound;
    • release never deletes a Contributor;
    • an expired capability cannot clean a Contributor and cannot become active again;
    • creation replay under a resolved/expired operation never creates another Contributor;
    • cleanup/release replay returns stable terminal reconciliation;
    • a server-side expiry check applies even if no client release was ever sent.

    The CTO must choose/approve the concrete maximum lifetime; it may not be left to the implementation agent.

    CR-SPEC-2 - BLOCKING - proposed task branch violates repository STANDARD branch doctrine

    The issue proposes:

    feature/thoth-import-contrib-recovery-01

    Live repository doctrine in root AGENTS.md requires STANDARD work to use:

    feature/<area>/<task>

    and requires a symmetric namespace preflight before branch creation.

    #963 targets develop directly and is not a PROGRAMME_INTEGRATION slice, so it is STANDARD work.

    The proposed flat branch is therefore not conformant.

    Live branch search found no existing feature/import... branch occupancy, so a conforming candidate is available.

    Required Amendment 2B

    Replace the proposed task branch with:

    feature/import/thoth-import-contrib-recovery-01

    Keep PR target:

    develop

    Before later branch creation, freshly verify:

    • exact ref absent;
    • flat parent feature/import does not occupy the namespace;
    • no incompatible descendant/flat occupancy exists;
    • authorized base SHA is unchanged.

    Do not create either branch form until exact-SHA implementation authorization.

    CR-SPEC-3 - BLOCKING - idempotency request fingerprint is not byte-defined

    Preparation requires:

    request_sha256

    over a "canonical field-labelled representation" of the Contributor payload.

    That is not sufficiently deterministic for a CRITICAL idempotency contract.

    Without an exact byte contract, two implementations or later refactors may disagree about:

    • field order;
    • null vs omitted optionals;
    • separators/escaping;
    • Unicode bytes;
    • canonical ORCID representation;
    • future field additions.

    The consequence is not merely cosmetic: the hash decides whether a repeated operationId is a safe replay or a conflicting mutation.

    Required Amendment 2C

    Freeze a versioned fingerprint contract.

    A bounded candidate requiring no dependency is:

    • define a dedicated ContributorImportCreateFingerprintV1 serialization shape;
    • fields exactly, in frozen order:
      1. version = 1
      2. firstName
      3. lastName
      4. fullName
      5. orcid
      6. website
    • encode optionals explicitly as JSON null, never omission;
    • ORCID uses the already-canonical Thoth Orcid value produced by GraphQL parsing;
    • other strings are hashed byte-for-byte as accepted by the GraphQL input; no trimming, case folding or Unicode normalization is introduced by THOTH-IMPORT-CONTRIB-RECOVERY-01: capability-bound orphan-safe Contributor recovery #963;
    • UTF-8 JSON with no insignificant whitespace;
    • a dedicated regression fixture must pin the exact serialized bytes and lowercase SHA-256;
    • changing this fingerprint contract later requires a new version, never silent reinterpretation of existing rows.

    Equivalent byte-exact encoding is acceptable only if it is written into the specification before approval.

    CR-SPEC-4 - NON-BLOCKING CONTROL NOTE - exact-base CI absent

    GitHub exposes no workflow run for exact merge SHA 345a7a....

    The runtime parent 923545d5c9028bc04c40e38efeb7de674efed3fd has green:

    • build/test/check run 35636185017;
    • migration run 35636185185.

    The complete delta to 345a7a... is documentation/control-only, so the runtime surfaces relevant to #963 are unchanged.

    This is sufficient for specification review but must remain explicit during implementation authorization. Do not describe exact base CI as GREEN unless a run actually exists.

    Required consistency correction

    The original acceptance item saying ordinary Contributor create/delete behaviour remains compatible must be read/amended to reflect Amendment 1:

    • ordinary referenced-Contributor delete compatibility is preserved;
    • ordinary orphan deletion is intentionally corrected to superuser-only;
    • capability-bound recovery is a separate surface.

    This is documentation consistency, but it should be folded into Amendment 2 so the final approved specification has no internally stale acceptance statement.

    Review decision

    CORE RECOVERY ARCHITECTURE           ACCEPTED
    AMENDMENT 1                         ACCEPTED
    CR-SPEC-1 capability lifecycle      CHANGES REQUIRED
    CR-SPEC-2 branch doctrine           CHANGES REQUIRED
    CR-SPEC-3 fingerprint bytes         CHANGES REQUIRED
    
    INDEPENDENT CRITICAL SPEC REVIEW    CHANGES REQUIRED
    FINAL SPECIFICATION APPROVAL        NOT AVAILABLE
    IMPLEMENTATION AUTHORIZATION        NOT AVAILABLE
    

    Next gate

    CTO adoption of Specification Amendment 2 resolving CR-SPEC-1 through CR-SPEC-3 and the acceptance consistency note.

    After Amendment 2 is durable, repeat the independent CRITICAL specification review against the full issue + Amendments 1 and 2.

    No implementation action is authorized by this review.

  4. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Specification Amendment 2 - recovery lifecycle, branch binding and fingerprint bytes

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Decision: CTO APPROVED SPECIFICATION AMENDMENT

    This amendment resolves all blocking findings from the independent CRITICAL specification review in comment 5935983779.

    The live preparation baseline remains unchanged at adoption:

    • develop @ 345a7a04131e7c0539f7518c6ea457fa5cb6a462
    • tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6

    This is a specification amendment only. It does not authorize implementation.


    A2.1 Recovery capability lifecycle and expiry

    The prepared three-state lifecycle is replaced by the following closed state machine:

    ACTIVE
      -> CLEANED
      -> IN_USE
      -> RELEASED
      -> EXPIRED
    

    Every state except ACTIVE is terminal and irreversible.

    The durable recovery row therefore additionally requires:

    • expires_at TIMESTAMPTZ NOT NULL;
    • status enum values ACTIVE | CLEANED | IN_USE | RELEASED | EXPIRED;
    • resolved_at IS NULL iff status is ACTIVE;
    • resolved_at IS NOT NULL for every terminal status.

    Maximum lifetime

    The server-enforced recovery capability lifetime is exactly:

    24 hours from the committed creation timestamp

    The authoritative expiry is derived by the backend from server/database time:

    expires_at = created_at + INTERVAL '24 hours'

    It is never caller-supplied and never extended or refreshed by replay.

    The 24-hour window is a recovery window, not a lease renewal mechanism.

    Expiry semantics

    Every recovery-capable mutation that loads an ACTIVE row must evaluate expiry inside its transaction.

    If:

    CURRENT_TIMESTAMP >= expires_at

    the backend must atomically transition the row:

    ACTIVE -> EXPIRED

    set resolved_at, perform no Contributor deletion, and return the terminal EXPIRED state.

    An EXPIRED capability:

    • can never clean a Contributor;
    • can never be reactivated;
    • can never have its expiry extended;
    • can never create another Contributor on operation replay.

    Successful-attempt release

    Add a capability-bound, actor-bound release/finalization operation.

    Input authority is the same server-generated recovery token used by cleanup.

    Release semantics:

    • ACTIVE + unexpired + correct actor -> atomically RELEASED, set resolved_at, delete nothing;
    • RELEASED replay -> return RELEASED;
    • CLEANED replay -> return CLEANED;
    • IN_USE replay -> return IN_USE;
    • EXPIRED replay -> return EXPIRED;
    • wrong actor / invalid token -> fail closed without disclosing capability state.

    Release is idempotent and never destructive.

    Downstream success rule

    For thoth-app#187, every recovery capability created while one top-level Work is in flight must reach a terminal non-destructive success-finalization state before that Work is reported as fully complete.

    Normal successful path:

    ACTIVE -> RELEASED

    If release transport is uncertain, the app must retry/reconcile the release result. It may not silently discard the token and report final success.

    If release cannot be reconciled, the Work may already exist successfully, but the import execution state must report a recovery-finalization problem requiring reconciliation; it must not claim clean terminal completion.

    A capability that reaches EXPIRED before successful release is terminal and non-destructive, but that is an abnormal reconciliation outcome, not the normal success path.


    A2.2 Creation replay after terminal recovery state

    For the same authenticated actor + same operation ID + same request fingerprint:

    • ACTIVE and unexpired -> return the same Contributor identity, same recovery token and ACTIVE state;
    • ACTIVE but expired by server time -> transition to EXPIRED, then return the same Contributor identity and EXPIRED state;
    • RELEASED -> return the same Contributor identity and RELEASED state;
    • IN_USE -> return the same Contributor identity and IN_USE state;
    • CLEANED -> return CLEANED reconciliation and never create another Contributor;
    • EXPIRED -> return the same Contributor identity if it still exists plus EXPIRED state; if the Contributor is unexpectedly absent, return an inconsistency error rather than reinterpret absence as cleanup.

    A terminal operation ID is never reusable for a second creation.

    Recovery-token disclosure on terminal replay is not required. If returned for reconciliation, terminal status is authoritative and the token confers no active recovery authority.


    A2.3 Correct STANDARD task branch

    The original proposed branch:

    feature/thoth-import-contrib-recovery-01

    is superseded.

    The governed STANDARD branch is:

    feature/import/thoth-import-contrib-recovery-01

    PR target remains:

    develop

    Before later branch creation, implementation preflight must freshly verify:

    • exact branch absent;
    • flat parent feature/import does not occupy that ref namespace;
    • no incompatible descendant/flat occupancy exists;
    • authorized develop base SHA is unchanged;
    • no intervening merge changes the prepared write envelope or migration namespace.

    No branch is authorized or created by this amendment.


    A2.4 Versioned byte-exact request fingerprint

    The phrase "canonical field-labelled representation" is replaced by this byte contract.

    The recovery row additionally stores:

    • request_fingerprint_version SMALLINT NOT NULL;
    • request_sha256 TEXT NOT NULL.

    Version 1 is the only permitted initial value:

    request_fingerprint_version = 1

    with a database check constraint enforcing that value for this task.

    Fingerprint V1 logical fields

    The fingerprint covers exactly these values, in this exact order:

    1. version literal 1
    2. firstName
    3. lastName
    4. fullName
    5. orcid
    6. website

    No operation ID, actor ID, recovery token, timestamps or database-generated values are included.

    Fingerprint V1 exact bytes

    Hash the UTF-8 bytes of this exact compact JSON object shape, with keys in this exact order and no insignificant whitespace:

    {"v":1,"firstName":...,"lastName":...,"fullName":...,"orcid":...,"website":...}

    Implementation must serialize a dedicated fixed-field Rust structure in that field order using the repository's pinned serde_json serializer and hash the resulting UTF-8 bytes with SHA-256.

    Rules:

    • optional values are encoded explicitly as JSON null, never omitted;
    • required strings are JSON strings;
    • firstName and website use their GraphQL-accepted string bytes exactly;
    • lastName and fullName use their GraphQL-accepted string bytes exactly;
    • orcid, when present, uses the canonical Thoth ORCID representation already produced by the GraphQL/domain parser, including the https://orcid.org/ prefix;
    • no trimming;
    • no case folding;
    • no Unicode normalization;
    • no HTML/entity normalization;
    • JSON escaping is exactly that produced by the pinned serde_json implementation for those input strings.

    The stored digest is lowercase hexadecimal SHA-256 over those exact bytes.

    Required fingerprint fixture

    Implementation tests must pin at least one complete V1 fixture containing:

    • a non-ASCII character;
    • a quote or backslash requiring JSON escaping;
    • one null optional;
    • a canonical ORCID.

    The test must assert:

    1. exact serialized UTF-8 byte sequence;
    2. exact lowercase 64-hex SHA-256.

    Changing field order, omission rules, normalization or encoding later requires a new fingerprint version and an explicit compatibility decision; existing V1 rows may never be silently reinterpreted.


    A2.5 Prepared durable table correction

    The prepared table now has the following minimum durable fields:

    • operation_id UUID PRIMARY KEY
    • contributor_id UUID NOT NULL UNIQUE
    • recovery_token UUID NOT NULL UNIQUE DEFAULT uuid_generate_v4()
    • actor_user_id TEXT NOT NULL
    • request_fingerprint_version SMALLINT NOT NULL
    • request_sha256 TEXT NOT NULL
    • status contributor_import_recovery_status NOT NULL DEFAULT 'ACTIVE'
    • expires_at TIMESTAMPTZ NOT NULL
    • resolved_at TIMESTAMPTZ NULL
    • created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP

    Status enum:

    ACTIVE | CLEANED | IN_USE | RELEASED | EXPIRED

    No FK to Contributor remains intentional so terminal reconciliation survives Contributor deletion.

    This remains one additive table + one enum and still fits the prepared single migration.


    A2.6 Acceptance consistency correction

    The original acceptance item saying ordinary Contributor create/delete behaviour remains compatible is superseded by:

    • ordinary Contributor creation behaviour remains compatible;
    • ordinary deletion of a referenced Contributor preserves existing publisher-scoped behaviour;
    • ordinary deletion of an orphan Contributor is intentionally corrected by Amendment 1 to superuser-only;
    • capability-bound import recovery is a separate authorization surface;
    • no existing/reused Contributor may be enrolled into recovery provenance.

    A2.7 Additional required lifecycle tests

    In addition to the existing #963 test matrix, implementation must prove:

    • successful release: ACTIVE -> RELEASED, no delete;
    • release replay is idempotent;
    • cleanup after RELEASED cannot delete;
    • 24-hour expiry is server-derived and cannot be extended by replay;
    • ACTIVE-at-expiry transitions atomically to EXPIRED without delete;
    • cleanup after EXPIRED cannot delete;
    • release after EXPIRED remains EXPIRED;
    • creation replay after RELEASED/IN_USE/CLEANED/EXPIRED never creates a second Contributor;
    • same operation ID does not refresh expires_at;
    • fingerprint V1 exact-byte fixture and exact SHA;
    • changing only one covered payload field under the same operation ID is rejected;
    • terminal status does not become ACTIVE again under any replay path.

    Current gate

    ORIGINAL SPECIFICATION: RECORDED
    READ-ONLY PREPARATION: COMPLETE
    AMENDMENT 1: CTO APPROVED
    FIRST CRITICAL SPEC REVIEW: CHANGES REQUIRED
    AMENDMENT 2: CTO APPROVED
    FRESH INDEPENDENT CRITICAL SPEC REVIEW: REQUIRED
    FINAL SPECIFICATION APPROVAL: NOT YET
    IMPLEMENTATION: NOT AUTHORIZED
    

    No source/new-file mutation, migration creation/execution, branch, commit, push, PR, CI dispatch, merge, provider/runtime access, deployment, release, production activation or production-data operation is authorized by this amendment.

  5. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Fresh independent CRITICAL specification review - CHANGES REQUIRED

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Reviewed specification: issue body + preparation 5935744657 + Amendment 1 5935809908 + first CRITICAL review 5935983779 + Amendment 2 5936089788
    Reviewed repository baseline: develop @ 345a7a04131e7c0539f7518c6ea457fa5cb6a462 / tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6
    Decision: CHANGES REQUIRED

    The live baseline remains unchanged. This review inspected the amended specification against current PostgreSQL/concurrency conventions and the cross-repository contract boundary.

    No source/new-file mutation, migration creation/execution, branch, commit, push, PR, CI dispatch, provider/runtime action, merge, deployment, release or production action was performed.

    Findings from the first review that are now closed

    Amendment 2 correctly closes:

    • CR-SPEC-1: unbounded success-path recovery authority, by adding RELEASED/EXPIRED and a finite lifetime;
    • CR-SPEC-2: invalid STANDARD branch spelling, by rebinding to feature/import/thoth-import-contrib-recovery-01;
    • CR-SPEC-3: ambiguous request fingerprint bytes, by defining versioned compact-JSON V1 bytes and a pinned fixture;
    • the stale compatibility statement from Amendment 1.

    Those findings are accepted.

    CR-SPEC-5 - BLOCKING - the specified 24-hour boundary uses a transaction timestamp, not decision-time wall clock

    Amendment 2 says both:

    • the capability lifetime is exactly 24 hours;
    • expiry is tested with CURRENT_TIMESTAMP >= expires_at;
    • created_at remains DEFAULT CURRENT_TIMESTAMP;
    • the stated lifetime is "24 hours from the committed creation timestamp".

    These statements are not mutually implementable as written.

    The repository itself explicitly documents that PostgreSQL CURRENT_TIMESTAMP is transaction-scoped, not statement/wall-clock scoped.

    Consequences:

    1. created_at DEFAULT CURRENT_TIMESTAMP records transaction start, not commit time;
    2. a cleanup transaction can start before expiry, block waiting for the Contributor row, cross the 24-hour wall-clock boundary, and still observe the old transaction-start CURRENT_TIMESTAMP;
    3. the destructive delete could therefore occur after the specification's supposedly exact capability expiry.

    For a CRITICAL destructive capability, the expiry decision must be made using server wall clock after all locks required for the destructive decision have been acquired.

    Required Amendment 3A

    Freeze these semantics:

    • lifetime is exactly 24 hours from a server-side creation timestamp sampled inside the creation transaction, not "commit timestamp";
    • creation samples one clock_timestamp() value and persists it as created_at;
    • expires_at = created_at + INTERVAL '24 hours' from that same sampled value;
    • replay does not change either timestamp;
    • cleanup locks the recovery row and exact Contributor row first, then samples clock_timestamp() immediately before the expiry/zero-Contribution decision;
    • release/replay paths lock the recovery row, then sample clock_timestamp() immediately before deciding whether ACTIVE must become EXPIRED;
    • resolved_at for the transition uses that same decision-time sample;
    • no destructive cleanup may occur if decision-time wall clock is >= expires_at.

    Using CURRENT_TIMESTAMP for this authority boundary is prohibited.

    CR-SPEC-6 - BLOCKING - shared GraphQL contract is still delegated to implementation

    The issue and preparation still say that exact mutation/type/enum names may differ or remain reviewable.

    That is too open for this task.

    #963 is an upstream shared API contract required by thoth-app#187. Project/repository doctrine requires the downstream repository not to guess an unmerged upstream contract, and an implementation agent must not become the architect of that shared contract.

    The specification already fixes behaviour tightly enough to freeze the public schema now.

    Required Amendment 3B

    Freeze an exact additive GraphQL contract before implementation.

    A bounded contract that fits the prepared envelope is:

    enum ContributorImportRecoveryStatus {
      ACTIVE
      CLEANED
      IN_USE
      RELEASED
      EXPIRED
    }
    
    input CreateContributorForImportInput {
      operationId: Uuid!
      contributor: NewContributor!
    }
    
    type ContributorImportRecovery {
      operationId: Uuid!
      contributorId: Uuid!
      recoveryToken: Uuid!
      status: ContributorImportRecoveryStatus!
      expiresAt: Timestamp!
      resolvedAt: Timestamp
    }
    
    createContributorForImport(
      data: CreateContributorForImportInput!
    ): ContributorImportRecovery!
    
    cleanupContributorImport(
      recoveryToken: Uuid!
    ): ContributorImportRecovery!
    
    releaseContributorImport(
      recoveryToken: Uuid!
    ): ContributorImportRecovery!

    Required semantics:

    • the payload is tombstone-safe: it returns contributorId, not a non-null Contributor object, so CLEANED and later legitimate deletion remain representable;
    • same-actor replay returns the same operation/contributor/token identifiers and current terminal/active status;
    • token is never queryable/listable through another field;
    • invalid token or actor mismatch -> ordinary NO_ACCESS, with no existence disclosure;
    • same operation ID + different V1 request fingerprint -> stable machine-readable error type CONTRIBUTOR_IMPORT_OPERATION_CONFLICT;
    • an impossible/inconsistent durable state -> stable machine-readable error type CONTRIBUTOR_IMPORT_RECOVERY_INCONSISTENT;
    • expiry and all normal lifecycle states are payload states, not transport errors.

    Equivalent naming is not left to implementation after this amendment: if the CTO wants different public names, those exact names must be written into Amendment 3 before approval.

    CR-SPEC-7 - BLOCKING - EXPIRED replay needs a fail-closed downstream retry rule

    Amendment 2 correctly makes EXPIRED terminal and says creation replay may return the stored Contributor identity.

    But the cross-repository retry consequence is not yet explicit.

    Consider:

    1. recovery-capable Contributor creation commits;
    2. response is lost;
    3. no Work is successfully created;
    4. more than 24 hours pass;
    5. replay returns the original contributorId with EXPIRED;
    6. the source Contributor has no ORCID and remains orphaned.

    If the app simply allocates a fresh operation ID and "tries again", the backend is allowed to create a second Contributor because name is deliberately not identity.

    That would recreate the exact duplicate ambiguity #963 exists to prevent.

    Required Amendment 3C

    Bind the downstream rule:

    • an EXPIRED creation/replay result is a manual-reconciliation terminal for that source Contributor attempt;
    • thoth-app#187 must not automatically substitute a fresh operation ID for the same planned source Contributor after EXPIRED;
    • it must not treat the returned contributorId as a fresh recovery-capable creation;
    • normal exact ORCID identity may still resolve an existing Contributor where applicable, but name alone remains non-binding;
    • the app must surface the existing Contributor identity and require reconciliation/explicit user resolution before a new creation attempt;
    • the same rule applies to any terminal/inconsistent state where automatic retry would otherwise mint a second no-ORCID Contributor.

    This does not turn operation ID into bibliographic identity. It is a fail-closed retry rule for one already-recorded transport attempt.

    Non-blocking controls retained

    • exact base 345a7a... still has no workflow run; runtime-equivalent parent CI remains evidence, not exact-base GREEN;
    • migration slot 20261001_v1.9.0 and 12-path envelope must be collision/rebase checked again immediately before implementation authorization;
    • open PR overlap with Support file upload for accessibility reports #744/THOTH-GQL-OPS-03: add effective-mode fleet verification #799 must be refreshed at implementation authorization;
    • recovery token is a capability/secret under existing repository doctrine and must never be logged or exposed through list/query/audit text.

    Review decision

    CORE RECOVERY ARCHITECTURE           ACCEPTED
    AMENDMENT 1                         ACCEPTED
    AMENDMENT 2 ORIGINAL FINDINGS       CLOSED
    
    CR-SPEC-5 WALL-CLOCK EXPIRY         CHANGES REQUIRED
    CR-SPEC-6 PUBLIC GRAPHQL CONTRACT   CHANGES REQUIRED
    CR-SPEC-7 EXPIRED RETRY RULE        CHANGES REQUIRED
    
    FRESH CRITICAL SPEC REVIEW          CHANGES REQUIRED
    FINAL SPECIFICATION APPROVAL        NOT AVAILABLE
    IMPLEMENTATION AUTHORIZATION        NOT AVAILABLE
    

    Next gate

    CTO adoption of Specification Amendment 3 resolving CR-SPEC-5 through CR-SPEC-7.

    After Amendment 3 is durable, perform one final independent CRITICAL specification review of the complete issue + Amendments 1-3.

    No implementation action is authorized by this review.

  6. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Specification Amendment 3 - decision-time expiry, frozen GraphQL contract and EXPIRED retry boundary

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Decision: CTO APPROVED SPECIFICATION AMENDMENT

    This amendment resolves CR-SPEC-5 through CR-SPEC-7 from independent review comment 5937234051.

    The live specification baseline remains unchanged at adoption:

    • develop @ 345a7a04131e7c0539f7518c6ea457fa5cb6a462
    • tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6

    This is specification-only. It does not authorize implementation.


    A3.1 Exact 24-hour wall-clock authority boundary

    Amendment 2's use of CURRENT_TIMESTAMP for the destructive capability boundary is superseded.

    PostgreSQL CURRENT_TIMESTAMP is transaction-scoped and must not decide recovery expiry.

    Creation timestamp

    The recovery-capable create coordinator must:

    1. begin its database transaction;
    2. acquire the operation-id advisory lock;
    3. sample server wall clock exactly once with clock_timestamp();
    4. use that sampled value as created_at;
    5. set expires_at = created_at + INTERVAL '24 hours';
    6. insert both Contributor and recovery row in the same transaction.

    created_at and expires_at are therefore coordinator-written values, not caller-supplied and not derived from a CURRENT_TIMESTAMP column default.

    The prepared table definition is amended accordingly:

    • created_at TIMESTAMPTZ NOT NULL - no default;
    • expires_at TIMESTAMPTZ NOT NULL - no default.

    Replay never changes either value.

    The 24-hour lifetime is exactly 24 hours from this server-side creation sample. It is not described as commit time.

    Cleanup decision time

    For ACTIVE cleanup:

    1. lock the recovery row;
    2. authenticate/bind the exact actor and capability;
    3. lock the exact Contributor row FOR UPDATE;
    4. sample clock_timestamp() after both required locks are held;
    5. if sampled wall clock is >= expires_at:
      • transition ACTIVE -> EXPIRED;
      • set resolved_at to that same sampled value;
      • delete nothing;
      • return EXPIRED;
    6. only if sampled wall clock is before expiry may the transaction inspect the locked Contributor's Contributions and decide CLEANED vs IN_USE.

    No destructive cleanup may occur after the decision-time wall clock has reached expiry, even if the transaction began earlier.

    Release and create-replay decision time

    Release and creation replay:

    1. lock the recovery row;
    2. authenticate/bind the actor;
    3. if status is ACTIVE, sample clock_timestamp() immediately before the lifecycle decision;
    4. expired ACTIVE -> atomically EXPIRED using that same value as resolved_at;
    5. otherwise continue with the specified ACTIVE replay/release behaviour.

    No lifecycle path may use transaction-start CURRENT_TIMESTAMP as the capability-expiry authority.


    A3.2 Frozen additive GraphQL contract

    The shared backend contract consumed later by thoth-app#187 is now frozen.

    Implementation may add descriptions/documentation but may not rename, remove, repurpose or change nullability of these fields/types without a new approved specification amendment.

    enum ContributorImportRecoveryStatus {
      ACTIVE
      CLEANED
      IN_USE
      RELEASED
      EXPIRED
    }
    
    enum ContributorImportRecoveryOutcome {
      CREATED
      REPLAYED
      CLEANED
      ALREADY_CLEANED
      IN_USE
      RELEASED
      EXPIRED
    }
    
    input CreateContributorForImportInput {
      operationId: Uuid!
      contributor: NewContributor!
    }
    
    type ContributorImportRecovery {
      operationId: Uuid!
      contributorId: Uuid!
      recoveryToken: Uuid!
      status: ContributorImportRecoveryStatus!
      expiresAt: Timestamp!
      resolvedAt: Timestamp
    }
    
    type ContributorImportRecoveryResult {
      recovery: ContributorImportRecovery!
      outcome: ContributorImportRecoveryOutcome!
    }
    
    createContributorForImport(
      data: CreateContributorForImportInput!
    ): ContributorImportRecoveryResult!
    
    cleanupContributorImport(
      recoveryToken: Uuid!
    ): ContributorImportRecoveryResult!
    
    releaseContributorImport(
      recoveryToken: Uuid!
    ): ContributorImportRecoveryResult!

    Payload semantics

    ContributorImportRecovery is deliberately tombstone-safe:

    • it returns contributorId, not a non-null Contributor object;
    • terminal reconciliation therefore remains representable after recovery cleanup or any later legitimate Contributor deletion;
    • terminal replay must not require the Contributor row still to exist merely to return the durable recovery record.

    recoveryToken is returned only through these mutation results. No query/list field for recovery records or tokens is added.

    The token is a capability/secret:

    • never log it;
    • never include it in audit text, generic error text or list/query output;
    • invalid token and wrong actor are indistinguishable to the caller and return ordinary NO_ACCESS.

    Exact result semantics

    createContributorForImport

    New operation:

    • status ACTIVE
    • outcome CREATED

    Same actor + same operation ID + same V1 fingerprint, ACTIVE and unexpired:

    • same operation/contributor/token/timestamps
    • status ACTIVE
    • outcome REPLAYED

    If an ACTIVE row has expired at decision time:

    • transition to EXPIRED
    • outcome EXPIRED

    Terminal replay:

    • CLEANED -> status CLEANED, outcome ALREADY_CLEANED
    • IN_USE -> status IN_USE, outcome IN_USE
    • RELEASED -> status RELEASED, outcome RELEASED
    • EXPIRED -> status EXPIRED, outcome EXPIRED

    No terminal replay creates another Contributor or reactivates authority.

    cleanupContributorImport

    ACTIVE + unexpired + zero Contributions:

    • transition to CLEANED;
    • delete exact Contributor;
    • status CLEANED;
    • outcome CLEANED.

    Already CLEANED:

    • status CLEANED;
    • outcome ALREADY_CLEANED;
    • perform no second delete.

    ACTIVE + unexpired + one or more Contributions:

    • transition to IN_USE;
    • delete nothing;
    • status IN_USE;
    • outcome IN_USE.

    IN_USE / RELEASED / EXPIRED replay:

    • preserve terminal status;
    • delete nothing;
    • outcome equals IN_USE / RELEASED / EXPIRED respectively.

    releaseContributorImport

    ACTIVE + unexpired:

    • transition to RELEASED;
    • delete nothing;
    • status RELEASED;
    • outcome RELEASED.

    Terminal replay:

    • RELEASED -> RELEASED
    • CLEANED -> ALREADY_CLEANED
    • IN_USE -> IN_USE
    • EXPIRED -> EXPIRED

    Release never deletes and never reactivates.

    Exact error classifications

    Add stable machine-readable GraphQL error types through ThothError::IntoFieldError:

    • same operation ID + different V1 request fingerprint:
      CONTRIBUTOR_IMPORT_OPERATION_CONFLICT
    • impossible/inconsistent recovery state that cannot be reconciled from the durable record:
      CONTRIBUTOR_IMPORT_RECOVERY_INCONSISTENT

    Invalid token or actor mismatch uses existing:

    NO_ACCESS

    These errors must not disclose stored token, actor identity, request fingerprint, SQL details or whether an invalid token ever existed.

    Normal lifecycle states ACTIVE/CLEANED/IN_USE/RELEASED/EXPIRED are payload states, not GraphQL errors.


    A3.3 Durable model consistency

    The durable recovery record defined by Amendments 2 and 3 is the sole source of lifecycle reconciliation.

    Minimum fields remain:

    • operation_id UUID PRIMARY KEY
    • contributor_id UUID NOT NULL UNIQUE
    • recovery_token UUID NOT NULL UNIQUE DEFAULT uuid_generate_v4()
    • actor_user_id TEXT NOT NULL
    • request_fingerprint_version SMALLINT NOT NULL
    • request_sha256 TEXT NOT NULL
    • status contributor_import_recovery_status NOT NULL DEFAULT 'ACTIVE'
    • expires_at TIMESTAMPTZ NOT NULL
    • resolved_at TIMESTAMPTZ NULL
    • created_at TIMESTAMPTZ NOT NULL

    No FK to Contributor.

    The server-generated recovery token remains stable for the durable operation record. Terminal tokens have no active destructive authority even if returned during authorized same-actor reconciliation.


    A3.4 EXPIRED and unreconciled-attempt downstream rule

    An EXPIRED operation is a fail-closed terminal state, not permission to mint a replacement Contributor automatically.

    For thoth-app#187:

    • the operationId must be materialised into the immutable confirmed execution plan before the corresponding Contributor creation mutation is attempted;
    • every retry/reconciliation of that plan must reuse that exact operation ID;
    • the app may not generate a replacement operation ID merely because the existing operation is EXPIRED or otherwise terminal/unreconciled.

    For an EXPIRED no-ORCID Contributor attempt:

    1. do not automatically call createContributorForImport with a fresh operation ID for the same planned source Contributor;
    2. do not treat the tombstone's contributorId as a fresh recovery-capable creation;
    3. surface the exact recorded Contributor identity as reconciliation evidence;
    4. require explicit user/operator resolution before a new creation attempt;
    5. if the recorded Contributor still exists, it may be explicitly selected/reused through the normal contributor-selection path;
    6. exact ORCID identity may continue to resolve automatically where the normal approved ORCID rules allow it;
    7. name/full-name alone remains non-binding and cannot auto-resolve the Contributor.

    If the recorded Contributor no longer exists for a terminal state that did not itself clean it, surface an inconsistency/manual-reconciliation condition; do not silently replace it.

    The same fail-closed rule applies to any terminal/inconsistent recovery state where automatic fresh creation could duplicate a no-ORCID Contributor.

    This does not make operation ID bibliographic identity. It is the durable transport/reconciliation identity of one already-recorded creation attempt.


    A3.5 Downstream success/failure reporting clarification

    For one top-level Work:

    Normal success requires every capability created while that Work was in flight to reach RELEASED before the app reports clean Work completion.

    Failed-Work retry-safe rollback requires:

    • failed new Work deletion positively verified; and
    • every required attempt-created Contributor recovery returns:
      • CLEANED on first cleanup, or
      • ALREADY_CLEANED on replay.

    IN_USE, EXPIRED, authorization failure, operation conflict, inconsistent state or transport-unknown result is not "safe to retry the complete file"; it requires reconciliation.

    A successfully created Work whose release/finalization cannot be reconciled may remain a successful data write, but the import session must report recovery-finalization incomplete rather than clean terminal success.


    A3.6 Prepared envelope impact

    Amendment 3 remains within the prepared maximum 12-path implementation envelope in comment 5935744657.

    No additional dependency/Cargo path is required:

    • clock_timestamp() is PostgreSQL built-in;
    • sha2, hex, serde, serde_json, uuid are already present;
    • GraphQL types/coordinator may live in the prepared Contributor recovery module plus mutation.rs;
    • stable error variants remain within thoth-errors/src/lib.rs.

    If implementation proves an additional path is materially required, HOLD and amend the write budget before editing it.


    A3.7 Additional mandatory review tests

    In addition to the existing test matrix, implementation must prove:

    • a cleanup transaction beginning before expiry but acquiring its final required lock after expiry returns EXPIRED and performs no delete;
    • created_at and expires_at are derived from one server clock_timestamp() sample;
    • replay does not move expiry;
    • GraphQL SDL matches the exact contract above;
    • no recovery query/list surface exists;
    • invalid-token and wrong-actor GraphQL failures are indistinguishable as NO_ACCESS;
    • first cleanup vs cleanup replay yields CLEANED vs ALREADY_CLEANED;
    • tombstone reconciliation works without requiring the Contributor row to exist;
    • EXPIRED operation IDs are not reusable for automatic new creation;
    • downstream contract evidence records that Update README #187 must carry/reuse operationId from its immutable plan.

    Current gate

    ORIGINAL SPECIFICATION: RECORDED
    READ-ONLY PREPARATION: COMPLETE
    AMENDMENT 1: CTO APPROVED
    FIRST CRITICAL REVIEW: CHANGES REQUIRED
    AMENDMENT 2: CTO APPROVED
    SECOND CRITICAL REVIEW: CHANGES REQUIRED
    AMENDMENT 3: CTO APPROVED
    FINAL INDEPENDENT CRITICAL SPECIFICATION REVIEW: REQUIRED
    FINAL CTO SPECIFICATION APPROVAL: NOT YET
    IMPLEMENTATION: NOT AUTHORIZED
    

    No source/new-file mutation, migration creation/execution, branch, commit, push, PR, CI dispatch, merge, provider/runtime access, deployment, release, production activation or production-data operation is authorized by this amendment.

  7. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Final independent CRITICAL specification review - APPROVED

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Decision: APPROVED

    Reviewed authority

    This approval is bound to the complete durable specification chain:

    Where text conflicts, later approved amendments supersede earlier preparation/body language exactly as stated.

    Reviewed repository baseline:

    • thoth/develop @ 345a7a04131e7c0539f7518c6ea457fa5cb6a462
    • tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6

    Live re-verification at final review:

    Those overlaps are not dependencies and do not block specification approval; they must be freshly reconciled before exact-SHA implementation authorization.

    Final reviewed architecture

    The specification now provides a complete, bounded and internally consistent recovery contract.

    Creation

    • import-specific Contributor creation only;
    • client operationId is transport idempotency, not bibliographic identity;
    • exact actor derived server-side;
    • same-actor/same-operation/same-V1-fingerprint replay returns the same durable operation;
    • different V1 fingerprint under the same operation ID fails closed;
    • Contributor creation and recovery provenance commit atomically;
    • recovery token is server-generated and bound to the exact operation;
    • no existing/reused Contributor may receive recovery authority.

    Durable state

    One additive recovery table and one closed enum are sufficient.

    Durable states:

    ACTIVE | CLEANED | IN_USE | RELEASED | EXPIRED

    Terminal states are irreversible.

    No FK to Contributor is correct because the row is a reconciliation tombstone that must survive cleanup.

    Capability lifetime

    • exactly 24 hours from one server clock_timestamp() creation sample;
    • created_at and expires_at are coordinator-written;
    • replay never refreshes them;
    • destructive cleanup samples clock_timestamp() after all required locks;
    • no delete may occur at or after expiry;
    • CURRENT_TIMESTAMP is prohibited as the destructive authority clock.

    This closes the lock-wait boundary correctly.

    Cleanup concurrency

    • lock recovery row;
    • bind token + actor;
    • lock exact Contributor row FOR UPDATE;
    • sample decision-time wall clock;
    • if expired -> EXPIRED, no delete;
    • if referenced -> terminal IN_USE, no delete;
    • if unreferenced -> delete exact Contributor + terminal CLEANED atomically.

    This is compatible with PostgreSQL FK locking: concurrent legitimate Contribution creation either wins first and blocks deletion, or loses to deletion and cannot commit a dangling FK.

    No application-side check-then-delete remains.

    Success-path authority revocation

    • successful Work execution finalises each created recovery capability through releaseContributorImport;
    • normal success is ACTIVE -> RELEASED;
    • release is capability-bound, actor-bound, idempotent and non-destructive;
    • unreconciled release prevents the import session from claiming clean terminal completion.

    This prevents stale success-path recovery authority.

    Expiry/retry safety

    An EXPIRED or otherwise unreconciled no-ORCID creation attempt is manual-reconciliation state.

    Downstream thoth-app#187 must carry the exact operation ID in the immutable plan, reuse it on retry, and may not automatically mint a new operation ID to recreate the same planned Contributor after EXPIRED/inconsistent terminal state.

    Name remains non-binding identity evidence.

    Ordinary delete authorization

    Amendment 1 correctly separates ordinary Contributor deletion from recovery:

    This closes the pre-existing empty-publisher-set authorization gap.

    Frozen GraphQL contract

    The Amendment 3 contract is approved exactly:

    • ContributorImportRecoveryStatus
    • ContributorImportRecoveryOutcome
    • CreateContributorForImportInput
    • ContributorImportRecovery
    • ContributorImportRecoveryResult
    • createContributorForImport
    • cleanupContributorImport
    • releaseContributorImport

    and stable error classifications:

    • CONTRIBUTOR_IMPORT_OPERATION_CONFLICT
    • CONTRIBUTOR_IMPORT_RECOVERY_INCONSISTENT
    • existing NO_ACCESS for invalid token / actor mismatch.

    The explicit outcome layer correctly preserves first CLEANED vs ALREADY_CLEANED replay while durable status remains tombstone-safe.

    No recovery query/list surface is approved.

    Fingerprint contract

    V1 is sufficiently byte-defined for a CRITICAL idempotency boundary:

    • fixed field order;
    • explicit nulls;
    • compact UTF-8 JSON;
    • canonical parsed ORCID;
    • no hidden normalization;
    • pinned exact-byte + exact-SHA fixture;
    • stored fingerprint version;
    • future semantic changes require a new version.

    No implementation discretion remains that could silently reinterpret existing idempotency rows.

    Migration / compatibility

    The reviewed migration design is additive:

    • one new table;
    • one new enum;
    • zero backfill;
    • no historical Contributor enrollment;
    • no existing row mutation;
    • ordinary GraphQL clients remain schema-compatible because all new recovery fields/operations are additive;
    • Amendment 1's orphan-delete authorization correction is the only intentional existing-operation semantic change.

    Production migration execution remains a separate gate.

    Reviewed implementation envelope

    Subject to fresh exact-SHA implementation authorization, the prepared maximum remains 12 paths:

    1. CHANGELOG.md
    2. thoth-api/src/schema.rs
    3. thoth-api/src/model/contributor/mod.rs
    4. thoth-api/src/model/contributor/policy.rs
    5. thoth-api/src/model/contributor/tests.rs
    6. thoth-api/src/graphql/mutation.rs
    7. thoth-api/src/graphql/tests.rs
    8. thoth-errors/src/lib.rs
    9. new thoth-api/src/model/contributor/recovery.rs
    10. new thoth-api/migrations/20261001_v1.9.0/up.sql
    11. new thoth-api/migrations/20261001_v1.9.0/down.sql
    12. new docs/engineering/ai-delivery/implementation-reports/THOTH-IMPORT-CONTRIB-RECOVERY-01-implementation-report.md

    No Cargo/dependency change is approved.

    If implementation needs any 13th path, differently named migration, extra dependency, wider async/import framework, or different public GraphQL contract, HOLD for specification amendment.

    Required validation accepted

    The complete test/validation matrix across the issue and Amendments 1-3 is sufficient, including:

    • migration forward/revert/reapply on disposable PostgreSQL 17;
    • populated-database migration;
    • full workspace gates;
    • authorization matrix;
    • exact SDL;
    • same-operation concurrent creation;
    • fingerprint conflict;
    • lost-response replay;
    • CLEANED/ALREADY_CLEANED;
    • terminal IN_USE;
    • RELEASED;
    • EXPIRED;
    • replay non-reactivation;
    • decision-time expiry after lock wait;
    • Contribution-create vs cleanup races in both orderings;
    • invalid token / wrong actor indistinguishability;
    • tombstone reconciliation without Contributor row;
    • exact V1 byte/SHA fixture.

    Non-blocking control note

    Exact base SHA 345a7a... still has no GitHub Actions run.

    The runtime-equivalent parent 923545d5c9028bc04c40e38efeb7de674efed3fd has successful build/test/check and migration runs, and the delta to the reviewed base is documentation/control-only.

    That is sufficient for specification approval only.

    A later implementation authorization must not describe 345a7a... as exact-base CI GREEN unless new evidence exists.

    Final review decision

    CORE ARCHITECTURE                    APPROVED
    AMENDMENT 1                         APPROVED
    AMENDMENT 2                         APPROVED
    AMENDMENT 3                         APPROVED
    
    AUTH / SECURITY                     APPROVED
    CONCURRENCY / IDEMPOTENCY           APPROVED
    MIGRATION DESIGN                    APPROVED
    GRAPHQL CONTRACT                    APPROVED
    DOWNSTREAM RETRY CONTRACT           APPROVED
    WRITE ENVELOPE                      APPROVED AS CANDIDATE
    
    FINAL INDEPENDENT CRITICAL
    SPECIFICATION REVIEW                APPROVED
    
    FINAL CTO SPECIFICATION APPROVAL    REQUIRED
    IMPLEMENTATION AUTHORIZATION        NOT GRANTED
    

    This approval is specification approval evidence only. It does not authorize source/new-file mutation, migration creation/execution, branch creation, commit, push, PR mutation, manual CI, provider/runtime access, merge, deployment, release, production activation or production-data operations.

    Next gate

    CTO final specification approval bound to this exact specification chain and current reviewed baseline.

    After that, perform a fresh implementation-authorization preflight for live base SHA, branch namespace, migration slot, PR overlap and exact 12-path action matrix before granting implementation authority.

  8. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Final CTO specification approval - APPROVED

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Decision: FINAL SPECIFICATION APPROVED

    CTO final specification approval is granted, bound exclusively to the following durable specification/review chain:

    Where earlier text conflicts with later approved amendments, the later approved amendment governs.

    Reviewed baseline at approval

    Live thoth/develop remains:

    • SHA 345a7a04131e7c0539f7518c6ea457fa5cb6a462
    • tree 98e64f0a6a49975f52d3dd14e7855395de2f34e6

    This approval is bound to that reviewed specification state and baseline evidence only.

    Final approved contract

    The approved implementation must preserve all of the following:

    • import-specific, actor-bound Contributor creation/recovery;
    • atomic Contributor creation + durable recovery provenance;
    • one client operationId per planned creation attempt, used only as transport/reconciliation identity;
    • V1 byte-exact request fingerprint;
    • server-generated recovery token;
    • irreversible lifecycle ACTIVE | CLEANED | IN_USE | RELEASED | EXPIRED;
    • exact 24-hour authority lifetime based on server clock_timestamp();
    • expiry decision after required locks, never transaction-scoped CURRENT_TIMESTAMP;
    • success-path finalization through RELEASED;
    • cleanup only under capability + actor binding + atomic zero-Contribution decision;
    • terminal IN_USE when legitimate reuse wins;
    • ordinary orphan deleteContributor denied to ordinary authenticated users and allowed only to superuser;
    • no bare Contributor-ID recovery delete;
    • no name-based identity or auto-merge;
    • no recovery authority for existing/reused Contributors;
    • exact frozen GraphQL contract from Amendment 3;
    • stable machine-readable conflict/inconsistency errors;
    • no recovery query/list surface;
    • downstream Update README #187 immutable-plan operationId reuse;
    • EXPIRED/unreconciled no-ORCID attempts require manual reconciliation and may not automatically mint a fresh operation ID;
    • one additive migration maximum;
    • prepared maximum implementation envelope of 12 paths;
    • no Cargo/dependency change;
    • no whole-file transaction/import-command redesign.

    Candidate implementation controls

    Subject to a fresh implementation-authorization preflight:

    • branch: feature/import/thoth-import-contrib-recovery-01
    • PR target: develop
    • candidate migration: thoth-api/migrations/20261001_v1.9.0
    • candidate path ceiling: 12 paths exactly as approved in final review comment 5937482656.

    These are not yet implementation authorization.

    Before implementation authority may be granted, reverify:

    Gate state

    SPECIFICATION                         FINAL CTO APPROVED
    INDEPENDENT CRITICAL SPEC REVIEW      APPROVED
    IMPLEMENTATION AUTHORIZATION          NOT GRANTED
    SOURCE/NEW-FILE MUTATION              NOT AUTHORIZED
    BRANCH CREATION                       NOT AUTHORIZED
    COMMIT/PUSH/PR                        NOT AUTHORIZED
    MIGRATION EXECUTION                   NOT AUTHORIZED
    MERGE/DEPLOY/PRODUCTION               NOT AUTHORIZED
    

    Next gate

    Perform a fresh exact-SHA implementation-authorization preflight and present the bounded implementation action matrix for explicit CTO authorization.

    This approval does not authorize source/new-file mutation, migration creation/execution, branch creation, commit, push, PR mutation, manual CI dispatch, provider/runtime access, merge, deployment, release, production activation or production-data operations.

  9. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    Exact-SHA implementation-authorization preflight - READY SUBJECT TO AGENT BINDING

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Specification authority: final CTO approval comment 5937502955, incorporating Amendments 1-3 and final CRITICAL review 5937482656

    Decision: IMPLEMENTATION PACKAGE READY - IMPLEMENTATION NOT YET AUTHORIZED

    No source/new-file mutation, migration creation/execution, branch, commit, push, PR mutation, manual CI, provider/runtime action, merge, deployment, release or production action was performed.

    1. Exact live base

    Fresh live verification:

    • repository: thoth-pub/thoth
    • base/PR target: develop
    • exact base SHA: 345a7a04131e7c0539f7518c6ea457fa5cb6a462
    • exact base tree: 98e64f0a6a49975f52d3dd14e7855395de2f34e6

    This exactly matches the final approved specification baseline.

    2. CI evidence

    GitHub exposes:

    • no workflow run bound to exact base 345a7a...;
    • no combined commit status on exact base.

    This is not described as GREEN.

    The previously reviewed runtime-equivalent parent 923545d5c9028bc04c40e38efeb7de674efed3fd had successful build/test/check and migration runs, and the delta to 345a7a... is documentation/control-only. That remains specification/preflight evidence only.

    Natural PR CI must provide the implementation-head evidence.

    3. Governed branch namespace

    Approved STANDARD branch:

    feature/import/thoth-import-contrib-recovery-01

    Fresh checks:

    • exact task branch: absent;
    • matching feature/import refs: none;
    • flat parent occupancy feature/import: none found;
    • no task PR exists.

    Namespace is currently clear.

    At implementation start, before branch creation, re-read live develop. If it is no longer exactly 345a7a..., HOLD before any mutation and return for reauthorization.

    4. Migration slot

    Approved candidate:

    thoth-api/migrations/20261001_v1.9.0

    Fresh checks:

    Slot is currently clear.

    If the slot becomes occupied before migration-file creation, HOLD; do not invent another migration path.

    5. Exact 12-path write envelope

    Existing authorized paths - all verified present on exact base:

    1. CHANGELOG.md
    2. thoth-api/src/schema.rs
    3. thoth-api/src/model/contributor/mod.rs
    4. thoth-api/src/model/contributor/policy.rs
    5. thoth-api/src/model/contributor/tests.rs
    6. thoth-api/src/graphql/mutation.rs
    7. thoth-api/src/graphql/tests.rs
    8. thoth-errors/src/lib.rs

    Authorized new files - all verified absent:

    1. thoth-api/src/model/contributor/recovery.rs
    2. thoth-api/migrations/20261001_v1.9.0/up.sql
    3. thoth-api/migrations/20261001_v1.9.0/down.sql
    4. docs/engineering/ai-delivery/implementation-reports/THOTH-IMPORT-CONTRIB-RECOVERY-01-implementation-report.md

    Maximum: 12 changed paths.

    No file deletion, move or rename is permitted.

    Any 13th path, Cargo/dependency change, different migration path, workflow change, client change or documentation path outside the implementation report requires HOLD and a new specification/write-budget amendment before editing.

    6. Fresh open-PR overlap

    Every currently open PR targeting develop was checked against the 12-path envelope.

    Overlaps:

    No open PR overlaps:

    These PRs are not dependencies and their unmerged bytes are not part of #963's base.

    Implementation must not copy, merge, cherry-pick or otherwise incorporate their unmerged changes.

    If any overlapping PR merges before the implementation branch is created, live develop necessarily changes and the exact-base HOLD rule above applies.

    7. Binding implementation content

    The implementation is limited to the final approved contract. In particular it must implement:

    • additive contributor_import_recovery table + enum;
    • exact ACTIVE/CLEANED/IN_USE/RELEASED/EXPIRED lifecycle;
    • exact 24-hour clock_timestamp() authority boundary;
    • exact V1 byte fingerprint;
    • actor-bound server-generated recovery token;
    • atomic create/idempotency/replay;
    • atomic zero-Contribution cleanup under required locks;
    • release/finalization;
    • Amendment 1 ordinary orphan-delete authorization correction;
    • exact Amendment 3 GraphQL schema names/nullability/outcome enum;
    • exact stable error classifications;
    • no recovery query/list surface;
    • complete approved concurrency/auth/lifecycle/fingerprint tests;
    • Unreleased changelog entry;
    • implementation report.

    No broader Contributor identity/deduplication, import-command framework, async architecture, Work cleanup, Series recovery or downstream app work belongs here.

    8. Required local validation

    Implementation authorization, when granted, should require at minimum:

    • disposable PostgreSQL 17 migration forward;
    • migration revert;
    • migration reapply;
    • populated disposable database migration proving no existing-row/backfill mutation;
    • focused Contributor recovery/model/auth tests;
    • database-level two-session concurrency tests required by the specification;
    • exact GraphQL SDL contract assertion;
    • exact V1 fingerprint byte/SHA fixture;
    • cargo test -p thoth-api --features backend;
    • cargo test --workspace;
    • cargo check --workspace;
    • cargo clippy --all --all-targets --all-features -- -D warnings;
    • cargo fmt --all -- --check;
    • git diff --check.

    No validation command may target production services/databases or require production credentials.

    9. Candidate action-authorization matrix

    The following actions are prepared for the next explicit CTO implementation authorization, not granted by this comment.

    Candidate authorized actions

    • repository/GitHub read inspection;
    • create feature/import/thoth-import-contrib-recovery-01 from exact SHA 345a7a04131e7c0539f7518c6ea457fa5cb6a462;
    • source/worktree modification only inside the 8 authorized existing paths;
    • creation only of the 4 authorized new files;
    • local disposable-database migration validation;
    • local tests/build/check/clippy/fmt/diff validation;
    • exactly one initial implementation commit containing the bounded task;
    • push only that task branch;
    • open one DRAFT PR targeting develop;
    • natural CI triggered automatically by push/PR creation;
    • update the draft PR body with the implementation handoff/evidence.

    Candidate explicitly denied actions

    10. Automatic side effects

    The later authorized branch push / draft PR may automatically trigger repository CI. That natural CI side effect is part of the candidate implementation package.

    No manual workflow dispatch is included.

    11. Agent separation control

    Repository doctrine requires the implementation agent/model and independent reviewer/model to be durably identified before source mutation.

    They are not yet bound in this ledger.

    Therefore the implementation package is technically complete but source mutation remains unavailable until the next authorization explicitly records:

    • implementing agent/model;
    • independent reviewer/model, which must not be the implementing agent and should be a different model/model family for this CRITICAL task.

    This identity binding may be included in the same CTO comment that grants the exact implementation action matrix; no additional specification amendment is required.

    12. Implementation HOLD/STOP conditions

    Before first mutation, HOLD if:

    • live develop != authorized SHA 345a7a...;
    • branch namespace is no longer clear;
    • migration slot is no longer free;
    • implementation/reviewer identities are not durably bound;
    • any required path falls outside the 12-path budget;
    • a dependency change is required;
    • the frozen GraphQL contract cannot be implemented exactly;
    • concurrency/authorization requirements cannot be tested locally.

    STOP if implementation would require any already-approved STOP condition, including bare-ID recovery delete, app-side check-then-delete, destructive referenced-Contributor cascade, name identity inference, retroactive recovery authority or whole-file import redesign.

    Preflight decision

    FINAL SPECIFICATION                 APPROVED
    FINAL CRITICAL SPEC REVIEW          APPROVED
    LIVE BASE                           MATCHES
    BRANCH NAMESPACE                    CLEAR
    MIGRATION SLOT                      CLEAR
    12-PATH ENVELOPE                    VERIFIED
    OPEN-PR OVERLAPS                    KNOWN / NON-BLOCKING
    EXACT-BASE CI                       ABSENT, HONESTLY RECORDED
    
    IMPLEMENTATION PACKAGE              READY
    AGENT/REVIEWER IDENTITY BINDING     REQUIRED AT AUTHORIZATION
    IMPLEMENTATION AUTHORIZATION        NOT YET GRANTED
    

    Next gate: explicit CTO implementation authorization binding the implementing agent/model, independent reviewer/model, exact base, branch, migration, 12-path envelope and action matrix above.

  10. ja573 commented on Oct 1, 2026

    @ja573
    MemberAuthor

    STOP - task premise superseded by programme architecture amendment

    Date: 2026-10-01
    Task: THOTH-IMPORT-CONTRIB-RECOVERY-01 / #963
    Risk: CRITICAL
    Programme authority: thoth-pub/thoth#885 comment 5937857546
    Downstream amendment: thoth-pub/thoth-app#187 comment 5937864828

    Decision: STOP - NO LONGER REQUIRED BY BULK-IMPORT-RELIABILITY

    The CTO has changed the bulk-import recovery boundary.

    Attempt-created Contributors that become orphaned after a failed new Work is deleted are now an accepted residual state. Their cleanup is no longer required before thoth-app#187 may treat that Work as rolled back and retryable.

    Accordingly, this task's core premise - that a new backend Contributor recovery contract is required to unblock #187 - no longer holds.

    Superseded implementation package

    The following previously approved/prepared #963 work must not be implemented under this task:

    • contributor_import_recovery durable table/enum;
    • recovery migration 20261001_v1.9.0;
    • createContributorForImport;
    • cleanupContributorImport;
    • releaseContributorImport;
    • recovery token/capability lifecycle;
    • operation-id recovery ledger;
    • associated 12-path implementation package;
    • branch feature/import/thoth-import-contrib-recovery-01;
    • the subsequently discussed hotfix rebinding.

    No branch, migration, source file, commit or PR was ever created for #963.

    Historical findings retained

    The prior specification, Amendments 1-3 and CRITICAL reviews remain durable historical evidence. They do not constitute implementation authority after this STOP.

    Preparation also identified a pre-existing ordinary deleteContributor authorization edge for orphan Contributors. That finding is not fixed by this STOP and is no longer part of the bulk-import blocker. If the CTO elects to address it, it requires its own independently specified task; it must not be smuggled into #187.

    Downstream effect

    thoth-app#187 no longer waits for any GraphQL contract from #963.

    #187 is separately amended so failed-Work compensation leaves attempt-created Contributors untouched and accepts orphan residue.

    Final task state

    SPECIFICATION / REVIEW WORK     HISTORICAL
    IMPLEMENTATION                  NEVER AUTHORIZED
    SOURCE / MIGRATION / BRANCH     NONE CREATED
    DOWNSTREAM DEPENDENCY           REMOVED
    TASK                            STOPPED / SUPERSEDED
    

    No backend mutation, migration, branch, commit, push, PR, CI, merge, release, deployment or production action is authorized or required by this task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions