Skip to content

docs(publisher-services): specify BE-03 protected service configuration - #808

Merged
ja573 merged 5 commits into
developfrom
feature/publisher-services/be-03-spec
Aug 12, 2026
Merged

docs(publisher-services): specify BE-03 protected service configuration#808
ja573 merged 5 commits into
developfrom
feature/publisher-services/be-03-spec

Conversation

@ja573

@ja573 ja573 commented Aug 12, 2026

Copy link
Copy Markdown
Member

Task identity

Programme:       Publisher Services and Distribution Configuration
Task:            BE-03-SPEC
Implementation:  BE-03 - Protected service configuration
Risk:            HIGH
Base:            develop
Exact head:      a3fc706467d02656705e2d58d12c89493612b0db

This PR is a specification and control-record change only. It does not authorize or implement BE-03 runtime behaviour.

Scope

Creates the bounded implementation specification for BE-03 protected publisher service configuration and records the associated programme decision candidate. The specification defines:

  • protected owner/superuser configuration reads;
  • superuser-only replacement mutation;
  • least-privilege publisher authorization using the existing PUBLISHER_USER policy model;
  • one canonical service-configuration write coordinator;
  • additive configuration versioning and optimistic concurrency;
  • bounded configuration audit history with source/actor provenance;
  • reuse of BE-02 distribution-platform lifecycle and ADR-0007 DataLoader foundations;
  • OAPEN/DOAB linked normalization, including split-state repair even when membership is unchanged;
  • the BE-03/BE-04 transaction seam and rollout/rollback requirements;
  • GraphQL/schema/client compatibility obligations and required implementation evidence.

Proposed programme decision

docs/publisher-services/decisions.md records a phase-boundary decision candidate, PROPOSED IN THIS SPECIFICATION CANDIDATE, under a durable ADR-0005 authority condition rather than a mutable status word:

  • BE-03 owns desired publisher service configuration only;
  • BE-04 owns durable distribution jobs, attempts, worker lifecycle and job-aware status;
  • APP-01's BE-03-dependent scope is configuration read/edit, linked-platform UI, concurrency handling and server-normalized state;
  • job/attempt/failure/pending-onboarding UI requires BE-04;
  • APP-02 remains dependent on BE-03 + BE-04.

This explicitly refines the narrow earlier APP-01 wording that implied back-catalogue job status was available from BE-03 alone.

Remediation already incorporated

The exact head includes three ordinary remediation commits, each on top of the then-current head, with no amend, rebase, squash or force-push at any point.

One bounded commit addressing four control-side pre-review findings:

  1. protected reads now require PUBLISHER_USER scoped to the target publisher (or SUPERUSER); unrelated publisher-scoped roles do not inherit access;
  2. desired-state replacement invokes BE-02 linked normalization even when OAPEN/DOAB membership is already equal, so one-sided/split activation/split timestamp states are repairable;
  3. the APP-01/BE-04 programme boundary is explicit rather than silently contradictory;
  4. one authoritative service-configuration write coordinator owns package, desired platform state, canonical version and audit history; lower-level BE-02 lifecycle functions are subordinate primitives, not alternative production configuration entry points.

One bounded commit answering the first fresh independent specification review of head 5dc2b1dd (one P1, six P2):

  1. the protected surface exposes effectiveCapabilities derived from ThothPackage::capabilities(), reconciling BE-03 with ADR-0001 section 4.4;
  2. the publisher.updated_at consequence of the token's location is recorded explicitly;
  3. the connection-scoped enable primitive must itself check assignability before any write;
  4. staff-report list arguments follow the merged [T!] = [] convention;
  5. the write-bypass search scope is corrected to the crates this repository actually contains;
  6. transient PR/CI lifecycle claims are removed from committed files under ADR-0005;
  7. the programme decision's self-blocking status word is replaced by a durable authority condition.

One bounded commit answering the second fresh independent specification review of head a1acd3f6 (one P1, one P2), which independently confirmed all seven findings above resolved and re-confirmed the settled architecture:

  1. public.publisher carries a second material UPDATE trigger, set_work_updated_at_with_relations, so a committed configuration change also refreshes work.updated_at_with_relations across that publisher's catalogue through one set-based statement. Both triggers are now recorded; the three-value observable semantics, the newly-caused cascade for platform-only changes and linked-state repairs relative to merged BE-02, the real transaction write footprint (one publisher row + bounded configuration/audit rows + N related work rows) and the catalogue-size lock/duration growth replace the earlier single-publisher-row framing. The canonical token stays on publisher; the separate-table alternative's genuine benefit and its failure to avoid the same cascade for canonical package changes are both stated. Six-case database evidence with target and control works, plus a catalogue-scale write-amplification measurement recorded without production extrapolation, are required, and a new stop condition mandates escalation rather than any silent change to shared publisher trigger or storage semantics if that evidence is bad;
  2. the write-bypass evidence is restated as an exact reproducible command with its exact output — zero non-test production hits, all matches confined to two #[cfg(test)]-gated files — replacing an inaccurate approximate count, and framed as authoring-time evidence that does not waive the future implementation's own search requirement.

Audit / migration seam

The candidate keeps SUPERUSER_API and MIGRATION_BACKFILL source values. The proposed audit actor is a non-blank text identity with source-scoped provenance: authenticated ZITADEL user ID for superuser API changes and a stable non-secret control identity for a future separately authorized MIG-01 path. MIG-01 remains unimplemented and must use the same configuration persistence invariants while creating no job or dissemination side effect.

Branch history

The branch was originally created from BE-02-CLOSEOUT-01 head 1f2cb585b25336ab9806adaeff9538b1ac3fa8ea. That closeout is now merged into develop as 7aeb715f9815a41b6357d8b6a7037ac62ebb25bb. The current merge base remains the closeout head; no rebase, merge-from-develop, squash or history rewrite was performed.

The PR diff against current develop is exactly five documentation/control files:

  • CHANGELOG.md
  • docs/engineering/ai-delivery/tasks/BE-03.md
  • docs/engineering/ai-delivery/implementation-reports/BE-03-SPEC-implementation-report.md
  • docs/publisher-services/decisions.md
  • docs/publisher-services/task-status.md

Validation

Authoring evidence records:

git diff --check     clean
relative links       all resolve
path containment     docs/** + CHANGELOG.md only
runtime paths        unchanged
migration/schema     unchanged
Cargo/workflows      unchanged
BE-03 impl branch    absent
manual CI dispatch   none

CI triggered normally by this draft PR is repository-controlled; no workflow is manually dispatched by this task.

Authorization boundary

BE-03 implementation:       NOT AUTHORIZED
BE-03 runtime:              NOT STARTED
BE-04:                      NOT IMPLEMENTED
MIG-01:                     NOT STARTED
deployment:                 NONE
environment migration:      NONE
production migration:       NONE
production access:          NONE
assignment/backfill:        NONE
distribution activation:    NONE
OBSERVE/ENFORCE:            NONE
PR #799:                    UNTOUCHED

This PR requires fresh independent exact-head specification review. The authoring/remediation agent has not approved it. Specification approval and merge are separate CTO-controlled gates; implementation requires a later fresh-base authorization after the approved specification is repository-authoritative on develop.

ja573 and others added 5 commits August 12, 2026 17:52
Add the complete bounded implementation specification for BE-03, the protected
publisher service-configuration surface, together with the programme decision
its phase boundary requires.

The specification settles, against the merged BE-01 and BE-02 code rather than
against the design document alone:

- the protected PublisherServiceConfiguration type, reusing BE-02's existing
  PublisherDistributionPlatformAssignment rather than duplicating it;
- the owner-and-superuser query and its complete deny/allow matrix, resolved
  through the existing ZITADEL publisher-role model. An account may hold scoped
  roles for several organisations, so ownership is a per-publisher role check.
  No new ownership table, role or authorization framework is introduced;
- the superuser-only staff report: paginated, deterministically ordered with
  the mandatory publisher_id tie-breaker, and filterable by publisher, package
  and enabled platform;
- the superuser-only replace mutation in exactly one transaction, with the
  required logical sequence and a deterministic lock order.

Two design questions the design document left open are resolved explicitly.

The concurrency token is a dedicated publisher.service_configuration_updated_at
column with a strict per-publisher monotonicity rule. publisher.updated_at is
rejected because the set_updated_at trigger moves it on any publisher metadata
edit while an assignment-only configuration change would not move it at all; a
computed maximum across publisher and assignment timestamps is rejected because
it is not one stored token comparable in one atomic predicate.

The audit source is a closed PostgreSQL enum with SUPERUSER_API and the
reserved MIGRATION_BACKFILL, with the ALTER TYPE ADD VALUE consequences
recorded. Actor identity is the existing ZITADEL user id stored as user_id
text, exactly the convention publisher_history already uses, so no actor format
is invented and no local account table is required.

Two reuse constraints are specified rather than left to implementation. BE-02's
enable/disable take a pool and open their own transaction on their own
connection, so calling them from inside the BE-03 transaction would block on
the publisher row lock the transaction itself holds; the specification requires
an additive, behaviour-preserving connection-scoped refactor instead of a
second lifecycle implementation. The report must reuse BE-02's existing
request-local ADR-0007 assignment loader rather than creating a second
equivalent loader.

The BE-03/BE-04 phase boundary is surfaced as a PROPOSED programme decision
rather than guessed: BE-03 owns desired configuration only and creates no job
row, no placeholder job and no fabricated job status, while BE-04 later extends
the same transaction boundary.

Specification and control records only. No runtime, schema, migration,
GraphQL, workflow or infrastructure change; no implementation; and the
reserved branch feature/publisher-services/be-03 is not created.
Add the BE-03-SPEC implementation report and an explicit template-conformance
map in BE-03.md.

The specification uses the house structure established by BE-01 and BE-02,
which is a superset of task-specification-template.md rather than a literal
section-for-section copy. The map makes template completeness auditable by
naming the section that satisfies each required template field.

The report records the preflight, the branch sequencing rationale, the merged
code actually inspected before each design decision, the two rejected
concurrency-token alternatives with the evidence against them, the BE-02
pool-connection composition hazard and its additive resolution, and the one
programme decision left PROPOSED for the CTO rather than guessed.

Specification and control records only. No runtime, schema, migration,
GraphQL or workflow change; feature/publisher-services/be-03 remains absent.
Answer four material defects found by the control-side pre-review of the
BE-03-SPEC content, consistently across the specification, its implementation
report, the programme decision record, the tracker and the existing changelog
entry. Documentation only: no runtime code, migration, schema.rs, generated SDL,
client artifact, Cargo file or workflow is touched, no implementation branch is
created, and no authorization is granted or implied.

Finding 1 - protected-read authorization contradiction. Section 11.1 previously
allowed PUBLISHER_ADMIN or "any publisher-scoped role satisfying the check",
which contradicted the prescribed PolicyContext::require_publisher_for call.
That helper requires exactly Role::PublisherUser for non-superusers, and
policy.rs contains no role inheritance. Adopt the least-privilege matrix:
superuser allow for any publisher; PUBLISHER_USER scoped to the target publisher
allow; PUBLISHER_USER scoped elsewhere, PUBLISHER_ADMIN, WORK_LIFECYCLE or
CDN_WRITE without PUBLISHER_USER, no applicable role, and anonymous all deny;
writes superuser-only. Commercial package visibility is not widened by an
unrelated scoped capability, and any future widening is routed to an explicit
CTO decision through a new stop condition rather than a looser matrix.

Finding 2 - OAPEN/DOAB split-state repair was unreachable. A membership-only
diff is empty for a pair that is already both-enabled but split by activation or
timestamp, so BE-02's normalization would never run. The mutation now calls the
connection-scoped enable primitive once per desired linked group
unconditionally, and the primitive alone decides no-op versus repair, so no
second linked-platform algorithm is introduced. Redefine the true no-op as the
conjunction of unchanged package, equal normalized enabled membership, and every
requested enabled group already fully normalized. A repair is a committed
change: it bumps the configuration token and writes exactly one audit row inside
the same transaction, distinguishable through configurationVersion alone, with
no activation identifier added to the audit JSON. Require the primitives to
return an Unchanged/Changed outcome so the caller can tell the cases apart.

Finding 3 - reconcile APP-01. Keep BE-03 owning desired configuration and BE-04
owning durable job state, and add an explicit APP-01 reconciliation naming the
five capabilities BE-03 alone supports and the four that require BE-04. State
that the candidate refines, and in that narrow respect supersedes, the earlier
wording assigning back-catalogue-status inspection to a BE-03-only dependency;
APP-02 remains dependent on BE-03 and BE-04. The decision remains PROPOSED and
programme-local, requiring no ADR.

Finding 4 - canonical token/audit semantics could be bypassed. Fix one
authoritative service-configuration write coordinator owning package,
enabled-platform desired state, canonical version token and audit history in one
caller-owned transaction under an explicit source/actor context, and require the
GraphQL mutation to call it. BE-02's pool-level enable/disable are retained
unchanged but reclassified as lower-level domain/compatibility functions barred
from new production configuration call sites, with required call-site
enumeration and bypass-search evidence; they have zero production call sites
today. For the MIG-01 seam, keep SUPERUSER_API and MIGRATION_BACKFILL and
redefine the audit actor column as actor text NOT NULL with a non-blank check
constraint and source-scoped provenance, so an authenticated superuser id and a
controlled non-secret migration control identity both satisfy it without
inventing credentials; MIG-01 is bound to the same coordinator, token update and
audit invariants. The rejected alternative, deferring MIGRATION_BACKFILL, is
recorded with its reasons.

BE-03 implementation remains NOT AUTHORIZED, feature/publisher-services/be-03
remains absent, and the BE-03/BE-04/APP-01 phase boundary remains PROPOSED
pending an explicit CTO decision.
…dings

Answer the fresh independent specification review of PR #808 at exact head
5dc2b1d (CHANGES REQUIRED; one P1, six P2, no BLOCKED-level finding) in one
bounded documentation pass. No already-settled architecture is reopened.

P1 - reconcile the protected surface with ADR-0001 section 4.4 by exposing
effective capability codes. Add
`effectiveCapabilities: [PublisherCapability!]!` to
`PublisherServiceConfiguration`, derived on read from the canonical
subscription package through BE-01's merged `ThothPackage::capabilities()` in
that mapping's canonical order, persisted nowhere, not an input, not in the
audit JSON, and protected by the same single section 11.1 read decision.
`PublisherCapability` joins `ThothPackage` as an enum becoming SDL-reachable.
Recorded explicitly as reconciliation with approved ADR-0001/BE-01 architecture,
not a new decision and not another deferral.

P2 - record that storing the token on `publisher` under the existing
`set_updated_at` trigger also moves `Publisher.updatedAt` on every committed
change, including platform-only changes and linked-state repairs, while a stale
request and a true no-op move neither timestamp; that `Publisher.updatedAt` is
not the configuration concurrency token; and that the new field may appear as an
additional key in future `publisher_history.data` snapshots. Keep the generic
publisher history and the configuration audit distinct.

P2 - require the connection-scoped enable primitive to check `is_assignable()`
itself before any write, alongside the retained pool-level early check and the
coordinator's whole-set pre-validation, with a direct `enable_on(JISC_NBK)`
regression.

P2 - align the staff report's list arguments with the merged
`Option<Vec<T>>`/`[T!] = []` convention.

P2 - correct the write-bypass search scope to the crates this repository
actually contains; `thoth-app` is a separate repository.

P2 - remove transient pull-request and CI lifecycle claims from the committed
report under ADR-0005, and fill the empty observations heading.

P2 - replace the self-blocking `PROPOSED - AWAITING CTO DECISION` status and its
literal-`APPROVED` stop condition with a durable authority condition stated
identically in decisions.md and BE-03.md.

Documentation and control records only: no runtime code, migration, schema.rs,
generated SDL, client artifact, Cargo file or workflow is touched. BE-03
implementation remains NOT AUTHORIZED, `feature/publisher-services/be-03`
remains absent, and the programme decision remains a candidate subject to CTO
specification approval of the exact reviewed content.
Answer the second fresh independent specification review of PR #808 at exact head
a1acd3f (CHANGES REQUIRED; one P1, one P2) in one bounded documentation pass.
That review independently confirmed all seven findings of the previous review
RESOLVED and re-confirmed the settled architecture - protected effective
capabilities, PUBLISHER_USER/SUPERUSER authorization, the canonical write
coordinator, the connection-scoped BE-02 lifecycle composition, OAPEN/DOAB
normalization and repair reachability, true-no-op semantics, optimistic
concurrency, the audit/source model, the MIG-01 seam, the BE-03/BE-04 ownership
boundary, the APP-01/APP-02 reconciliation, the migration shape, the GraphQL
protected exposure, the DataLoader approach and rollout/rollback. None of that is
reopened here.

P1 - a second existing trigger on public.publisher was unaccounted for. Verified
in thoth-api/migrations/20250000_v1.0.0/up.sql: alongside set_updated_at (BEFORE
UPDATE, diesel_set_updated_at()), publisher carries
set_work_updated_at_with_relations (AFTER UPDATE,
publisher_work_updated_at_with_relations()), which on NEW IS DISTINCT FROM OLD
executes one set-based UPDATE work SET updated_at_with_relations =
current_timestamp FROM imprint WHERE work.imprint_id = imprint.imprint_id AND
imprint.publisher_id = NEW.publisher_id. A committed configuration operation that
UPDATEs the publisher row therefore has three observable persisted consequences,
not one, and work.updated_at_with_relations is a public downstream freshness
signal - resolved on the public Work type, a filter argument on six anonymous
queries, orderable, selected by the generated WorkLastUpdatedQuery /
WorksLastUpdatedQuery, and the value thoth-export-server's MetadataRecord compares
against its Redis cache timestamp.

Record both triggers in section 2.1 item 8. Rewrite section 6.4 to state all
three consequences, that a platform-only change and an OAPEN/DOAB linked-state
repair newly cause the cascade relative to merged BE-02 (which touches only
publisher_distribution_platform and does not UPDATE publisher), that other
publishers' works are never touched, that stale, true-no-op and rolled-back cases
fire neither trigger, and that the public signal reveals refreshed catalogue state
without directly exposing package, capabilities, enabled platforms, actor, source
or audit content - while acknowledging that timing correlation remains possible.
Extend the section 18.4 evidence to a three-value six-case table proved against a
real disposable PostgreSQL database with at least two imprints, at least two
target works and a control work of another publisher, and add a catalogue-scale
write-amplification and lock-footprint measurement recorded without production
extrapolation. Correct sections 7.3 step 9 and 7.8, whose "touches exactly one
publisher", "no multi-object ordering" and 17-value-bound claims now apply only to
BE-03's explicit application-level ordering and statement count: the transaction
writes one publisher row plus bounded configuration/audit rows plus N related work
rows through one set-based trigger statement, never an application loop, so
row-lock footprint and transaction duration grow with catalogue size while the
publisher FOR UPDATE lock is held. No production duration and no safe catalogue
size is invented.

Keep the canonical token at publisher.service_configuration_updated_at and
introduce no second configuration/version table. Section 6.3 now states the
separate-table alternative's genuine benefit honestly - it would avoid both
triggers for platform-only or version-only persistence - and records that it would
not avoid the same cascade for canonical package changes while BE-01's
subscription_package remains on publisher, and that eliminating the cascade fully
would require wider changes to approved BE-01 storage or shared publisher trigger
semantics that this finding does not justify. Acceptance is therefore
evidence-conditional: section 7.9 states that BE-04 inherits the cascade with the
transaction boundary, section 17.1 no longer permits a "no observable downstream
data change" reading while making clear the cascade is not distribution
activation, and new stop condition 19 requires BLOCKED and a separate architecture
escalation - never a silent trigger, storage or token change - if the measurements
are bad. The former stop condition 19 is renumbered 20.

P2 - correct the inaccurate write-bypass search count. The conclusion holds: zero
production call sites of BE-02's pool-level enable/disable. The report now quotes
the exact command, its exact per-file output (14 in
graphql/distribution_platform_tests.rs, 44 in
model/publisher_distribution_platform/tests.rs, 58 total), records zero non-test
production hits, states that all matches are confined to those two
cfg(test)-gated files, and drops the approximate count. It is framed as
authoring-time evidence that does not waive section 18.9's requirement for the
future implementation to rerun the search at its own head.

Documentation and control records only: no runtime code, migration, schema.rs,
generated SDL, client artifact, Cargo file or workflow is touched. BE-03
implementation remains NOT AUTHORIZED, feature/publisher-services/be-03 remains
absent, and the programme decision remains a candidate subject to CTO
specification approval of the exact reviewed content.

ja573 commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

CTO control record for BE-03-SPEC at exact head a3fc706467d02656705e2d58d12c89493612b0db.

Independent review: a fresh independent exact-head specification review returned APPROVED with no findings for this exact head. The review was read-only and was not submitted as a GitHub review under the CTO/user identity.

CTO approval and merge authorization (2026-08-12): the CTO explicitly approves this exact BE-03-SPEC content as the authoritative implementation specification for BE-03 and approves the contained Publisher Services programme decision establishing the BE-03/BE-04/APP-01/APP-02 boundary, subject to this exact approved content becoming reachable from develop. The CTO also explicitly authorizes merge of PR #808 at this exact head into develop.

Authorization boundary: specification/control repository integration only. This does not authorize BE-03 implementation, creation of the BE-03 implementation branch, deployment, environment or production migration execution, MIG-01, backfill, assignment creation, distribution activation, OBSERVE/ENFORCE, production access, BE-04 implementation, or action on PR #799.

Any head change before merge invalidates this exact-head approval and merge authorization.

@ja573
ja573 marked this pull request as ready for review August 12, 2026 19:53
@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 merged commit 3b6b3a3 into develop Aug 12, 2026
10 checks passed
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