Skip to content

Forward-integrate v1.6.3 hotfix from master into develop - #811

Merged
ja573 merged 11 commits into
developfrom
hotfix/803-forward-integrate-v1.6.3
Aug 13, 2026
Merged

Forward-integrate v1.6.3 hotfix from master into develop#811
ja573 merged 11 commits into
developfrom
hotfix/803-forward-integrate-v1.6.3

Conversation

@ja573

@ja573 ja573 commented Aug 13, 2026

Copy link
Copy Markdown
Member

Forward-integrate v1.6.3 hotfix from master into develop

Forward-integration of the released THOTH-CHAPTER-01 (#803) single-parent BookChapter enforcement (v1.6.3) into develop. Not new development — this PR only resolves the two merge conflicts and preserves everything else.

  • Task: THOTH-CHAPTER-01 / THOTH-CHAPTER-01: Enforce a single parent for chapter works #803
  • Released master commit merged (MERGE_HEAD): d6c7effbaf63fa4a591a3859e61cb7714984f27f
  • Merge direction: master → develop only (never develop → master).
  • Merge commit: 1a896ac40c0eb5cd640d8b928b1bc126aacb88dd (first parent = develop lineage 3b6b3a31…; second parent = master d6c7eff…).

Conflicts resolved (only these two files were conflicted)

  1. CHANGELOG.md — kept all develop ## [Unreleased] content (Added/Changed/Removed/Fixed, incl. BE-03-SPEC, PRs 800/797/778/…) and merged the released THOTH-CHAPTER-01: Enforce a single parent for chapter works #803/THOTH-CHAPTER-01 (#803): enforce single parent for BookChapter works [HIGH-RISK HOTFIX] #806 entry as the first item under develop's existing ### Changed. The THOTH-CHAPTER-01: Enforce a single parent for chapter works #803 entry retains its narrow meaning (a BookChapter Work may have at most one DISTINCT parent) and does not claim to solve the historical wrong-parent incident. Present exactly once.
  2. thoth-api/src/graphql/tests.rs — kept all develop tests and its newer async test infrastructure (block_on_graphql(juniper::execute(…))), and added master's regression test create_work_relation_rejects_second_book_chapter_parent_through_graphql, adapted minimally to develop's async convention (its two juniper::execute_sync(…) calls became block_on_graphql(juniper::execute(…))). The test's acceptance semantics are unchanged: authorized first parent succeeds; authorized second distinct parent is rejected with the client message "A book chapter may belong to only one parent work"; the unauthorized caller is still rejected by authorization.

Already-merged hotfix files (auto-merged cleanly; not modified during resolution)

thoth-api/migrations/20260813_v1.6.3/{up,down}.sql, thoth-api/src/model/work_relation/tests.rs, thoth-errors/src/database_errors.rs. The released #803 enforcement (triggers, guard, error mapping) is carried forward unchanged. My migration 20260813_v1.6.3 sorts after develop's newest (20260812_v1.7.0) — correct ordering; it applies cleanly on develop's schema.

Net effect

git diff --cached --numstat shows 1664 insertions, 0 deletions across 6 files — the merge only adds the #803 hotfix; no develop work is removed or altered.

No production implementation changes

Conflict resolution changed only test/CHANGELOG text. The approved #803 enforcement architecture, migration behaviour, and error semantics are unchanged.

Verification (on the merged tree)

  • git diff --check / git diff --cached --check: clean · cargo fmt --all -- --check: clean.
  • cargo clippy -p thoth-api --all-targets --all-features -- -D warnings: clean.
  • Targeted: the THOTH-CHAPTER-01: Enforce a single parent for chapter works #803 GraphQL regression, my WorkRelation single-parent / enforcement / migration / concurrency suite (20 tests), and the affected develop GraphQL tests (block_on_graphql_*, generated_schema_exposes_no_package_or_capability_surface, ordinary_publisher_mutations_leave_package_unchanged) — all pass (25/25).
  • make check-all: passed (rc=0) on the merged tree.

Requires independent review before merge. Do not merge into develop without review. Direction is master → develop only.

Refs #803.

ja573 added 7 commits August 12, 2026 16:57
…Phase A)

THOTH-CHAPTER-01 / #803 - HIGH-RISK HOTFIX, Phase A only (audit + design).

Add a read-only existing-data audit for chapter Works with more than one
parent Work, validated against controlled data using the DB-backed test
harness. The qualifying predicate targets work_relation rows with
relation_type = 'is-child-of' (a chapter that is-child-of more than one
DISTINCT parent). Tests cover 0/1/>1 parents in both relation orientations,
exact-duplicate rejection, unrelated relation types, is-part-of, and
opposite-orientation (parent-book) rows.

Also validates, ephemerally and inside a rolled-back transaction, that the
candidate Phase B enforcement - a partial unique index on work_relation -
is representable, rejects a second distinct parent, leaves has-child and
is-part-of relations unaffected, and fails to build over corrupt data.

No production write behaviour change, no migration, no enforcement activated.

Refs #803.
…esign

Independent review correction. The approved #803 invariant is about chapter
Works (work_type = 'book-chapter'), and the issue states "do not broaden this
into general WorkRelation semantic validation". The prior revision scoped the
audit and proposed enforcement to any is-child-of relator (a global partial
unique index), which was broader than approved.

Corrections:
- Authoritative blocking audit now joins work and filters
  work_type = 'book-chapter'; only book-chapter Works with >1 DISTINCT
  is-child-of parent are #803 blocking evidence.
- Add a SEPARATE non-BookChapter is-child-of diagnostic, explicitly NOT #803
  blocking evidence.
- Replace the global partial-unique-index design validation with a
  BookChapter-scoped trigger (serialises on the chapter work row via
  FOR NO KEY UPDATE; RAISE ... USING CONSTRAINT for deterministic client
  error mapping), validated to reject a second distinct parent, to leave
  Monographs / is-part-of / has-child unaffected, and to protect the
  work_type -> book-chapter transition.

Still Phase A only: no migration, no enforcement activated, no production
write behaviour change.

Refs #803.
…nce (#803)

Second independent review corrections (design/tests only; still Phase A,
non-enforcing).

1. Pre-lock work_type read race: the candidate relation trigger now acquires
   the per-Work serialization lock (SELECT ... FROM work FOR NO KEY UPDATE on
   the relator's work row) BEFORE reading work_type, for ANY is-child-of
   mutation. The work_type-transition trigger takes the SAME lock before
   counting parents. This closes the race where a Monograph is-child-of insert
   skipped locking while a concurrent Monograph->BookChapter transition could
   not see the uncommitted second parent.

2. Deterministic two-connection concurrency evidence: two tests exercise both
   lock orderings (transition-first and relation-first) using independent
   connections, channels and a bounded pg_stat_activity wait-for-lock gate.
   Each asserts the loser actually blocked and that the final persisted state
   is never book-chapter with >1 parent.

3. Production write-path test: WorkRelation::create rejects a second distinct
   parent and rolls back the paired inverse row.

4. Single-parent replacement (A -> B on the same relation) remains allowed.

Design docs (PR/#803) also corrected: the Phase B migration now validates and
installs enforcement inside one write-excluded transaction (LOCK TABLE work,
work_relation IN SHARE ROW EXCLUSIVE MODE) to close the audit->activation
TOCTOU window; CREATE TRIGGER lock mode documented as SHARE ROW EXCLUSIVE (not
ACCESS EXCLUSIVE); production lock-wait/duration marked unverified.

No migration, no enforcement activated, no production write behaviour change.

Refs #803.
… Phase B)

THOTH-CHAPTER-01 / #803 — HIGH-RISK HOTFIX, Phase B (production enforcement),
continued on the existing branch/PR #806 per the recorded CTO decision.

A Work with work_type = 'book-chapter' may have at most one DISTINCT parent
(a work_relation of type 'is-child-of'). Zero-parent chapters remain valid;
non-book-chapter works are unaffected.

Migration 20260813_v1.6.3 (Diesel runs each migration in one transaction):
- LOCK TABLE work, work_relation IN SHARE ROW EXCLUSIVE MODE, then a
  corrupt-data guard that aborts if any book-chapter already has >1 distinct
  is-child-of parent, then two triggers — all in the single migration
  transaction, so there is no audit->install TOCTOU window.
- work_relation trigger (BEFORE INSERT OR UPDATE): for any is-child-of row,
  SELECT work_type FROM work ... FOR NO KEY UPDATE (lock + read in one query,
  before deciding the rule applies) and reject a second distinct parent for a
  book-chapter relator, excluding the row being updated.
- work trigger (BEFORE UPDATE OF work_type, only for a transition into
  book-chapter): same FOR NO KEY UPDATE lock, then reject if the work already
  has >1 distinct is-child-of parent.
Both raise a unique_violation with constraint
work_relation_single_book_chapter_parent, mapped to a deterministic client
message via DATABASE_CONSTRAINT_ERRORS. down.sql drops the triggers/functions
only; no data is deleted, reparented, or rewritten.

Tests: production WorkRelation::create rejection (both orientations, inverse
rolled back); relation-update semantics (replace single parent allowed;
second parent by related/relator/type change rejected; away-from-is-child-of
allowed; exact duplicate unchanged); work_type transition guard; non-book
chapter regressions; deterministic two-connection concurrency (parent-vs-
parent, relation-vs-transition both orderings) asserting the loser blocks on
the shared lock and verifying final state; migration matrix on isolated
databases (empty/valid/invalid-abort/down/up-down-up) and an activation-race
test; a read-only audit-detection test; and a GraphQL create_work_relation
boundary + authorization test.

Changelog updated under Unreleased.

Refs #803.
…race test (#803)

The activation-race test's concurrent writer attempted its insert without
waiting for the migration to hold the SHARE ROW EXCLUSIVE table lock. Under
CI timing the writer could run first (enforcement reverted, no contention),
so it neither blocked nor was rejected, failing the `blocked` assertion.

Add a go-signal so the writer only issues its insert after the migration is
known to hold the lock, matching the other two-connection concurrency tests.
Verified stable locally (activation-race 5x, full audit module 3x under
default parallelism).

Refs #803.
…nt-audit

THOTH-CHAPTER-01 (#803): enforce single parent for BookChapter works [HIGH-RISK HOTFIX]
Forward-integrate THOTH-CHAPTER-01 (#803) after the v1.6.3 release.
Preserve all unreleased develop work while carrying the released
single-parent BookChapter enforcement forward.

Conflicts resolved (union of develop content + hotfix addition):
- CHANGELOG.md: keep all develop Unreleased entries; add the released
  #803/#806 entry once under develop's existing "### Changed".
- thoth-api/src/graphql/tests.rs: keep all develop tests and the newer
  async test infrastructure; add the #803 GraphQL regression test adapted
  to develop's block_on_graphql(juniper::execute(...)) convention, with
  unchanged acceptance semantics.

No production implementation, migration behaviour, or error semantics were
changed during conflict resolution.

Direction: master -> develop only.

Refs #803.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@ja573

ja573 commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

CI failure at head 99f2302e — root cause is a Diesel migration version collision, not a rustdoc issue

Investigated the failing test job at the exact PR head (99f2302e9540a060ed51d8c0379c24746de3299e, run 31701275404). No change has been pushed — this comment records the finding, because the correct fix is outside the scope of a CI-only correction and needs an owner decision.

Reported vs. actual. The failure was described as a rustdoc E0603: module 'audit' is private doctest error around a helper named subprocess_helper_lock_wait_active_migration / run_lock_wait_active_migration_helper_from_env. That is not what is happening:

  • Those symbols do not exist anywhere in the repository (grep -rn over *.rs returns nothing).
  • cargo test -p thoth-api --features backend --doc passes: 0 passed; 0 failed; 8 ignored. There is no private-module doctest error.

Actual failure. 12 tests fail in CI (16 locally, ordering-dependent) — the whole #803 enforcement suite, including migration_up_on_empty_database_succeeds asserting "triggers installed". The released v1.6.3 single-parent enforcement is not installed at all after forward integration.

Root cause. After develop advanced (BE-03, #809), the repository now contains two migration directories sharing the same Diesel version prefix 20260813:

directory origin status
20260813_v1.6.3 #803 Phase B (2590b4ce) released in v1.6.3 (tag on master)
20260813_v1.7.0 BE-03 (3facc3f9, #809) develop-only, unreleased

Diesel keys migrations by the numeric prefix before the first _, so both are version 20260813. Only one can ever run; the other is silently skipped.

Evidence on a fresh database with all migrations applied via the repo runner (thoth migrate):

__diesel_schema_migrations versions: 20250000,20260417,20260429,20260504,20260805,20260812,20260813
rows with version 20260813          : 1          <- two directories, one recorded version
BE-03 table publisher_service_configuration_history : present   <- 20260813_v1.7.0 ran
#803 triggers (%single_book_chapter%)              : NONE      <- 20260813_v1.6.3 SILENTLY SKIPPED

Re-running thoth migrate does not repair it (STILL NONE) — once 20260813 is recorded, the other directory is never applied.

Why this matters in both directions

  1. On develop (observed): the released THOTH-CHAPTER-01: Enforce a single parent for chapter works #803 BookChapter single-parent enforcement silently does not exist.
  2. On production when develop is later released (mirror case, by the same mechanism): production already recorded 20260813 from the v1.6.3 hotfix, so BE-03's migration would be silently skipped — its column/enum/history table would never be created while BE-03 code expects them.

Why I did not just renumber it. Renumbering the released 20260813_v1.6.3 is unsafe: production has already recorded 20260813, so a new version would make it re-run, and its DDL is intentionally non-idempotent (plain CREATE FUNCTION / CREATE TRIGGER, up.sql lines 46/78/87/107) → the deploy would fail with "already exists". It would also rewrite released history. The safe technical fix is to renumber the unreleased BE-03 migration (e.g. 20260813_v1.7.020260814_v1.7.0), but that is unrelated develop work owned by #809/BE-03, and any dev/staging database that already applied BE-03 would need reconciliation. That is a programme decision, not a CI-only correction.

Scope confirmation: no files were changed; no #803 enforcement, migration SQL, trigger logic, concurrency behaviour, error mapping, GraphQL semantics, authorization, or conflict resolution was modified. Working tree clean; nothing pushed; PR not merged.

Status: BLOCKED pending a decision on which migration version is renumbered.

@ja573
ja573 merged commit a4585a8 into develop Aug 13, 2026
10 checks passed
@ja573
ja573 deleted the hotfix/803-forward-integrate-v1.6.3 branch August 13, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant