Skip to content

fix title of rabbitMQ gateway nuget package - #9

Merged
iancooper merged 1 commit into
BrighterCommand:masterfrom
fpighi:master
Oct 8, 2014
Merged

iancooper merged 1 commit into
BrighterCommand:masterfrom
fpighi:master

Conversation

@fpighi

@fpighi fpighi commented Oct 8, 2014

Copy link
Copy Markdown

The title of the nuget package was wrong

iancooper added a commit that referenced this pull request Oct 8, 2014
fix title of rabbitMQ gateway nuget package
@iancooper
iancooper merged commit ed3d5e6 into BrighterCommand:master Oct 8, 2014
darrenschwarz added a commit to darrenschwarz/Brighter that referenced this pull request Dec 20, 2025
   Implements client certificate configuration for RabbitMQ connections
   to support mutual TLS authentication.

   Changes:
   - Add ClientCertificate, ClientCertificatePath, and
     ClientCertificatePassword properties to RmqMessagingGatewayConnection
   - Implement SSL configuration in RmqMessageGateway for both sync and
     async variants
   - Support X509Certificate2 objects and file paths (.pfx/PKCS#12 format)
   - Use X509CertificateLoader.LoadPkcs12FromFile (not obsolete constructors)
   - Certificate object takes precedence over file path
   - Maintain backwards compatibility (certificate configuration is optional)

   Testing:
   - Behavioral tests verify ConnectionFactory.Ssl configuration
   - Tests assert SSL enablement, certificate passing, and precedence
   - Error case tests (missing file, invalid certificate)
   - Self-signed certificates generated in test setup
   - Full test coverage for both sync and async variants
   - All 14 tests passing

   Compliance with Critical Guidelines:
   - Rule BrighterCommand#1: Implemented identically for both sync AND async variants
   - Rule BrighterCommand#3: Backwards compatible (certificate is optional, all nullable)
   - Rule BrighterCommand#7: Used typed X509Certificate2 API consistently
   - Rule BrighterCommand#9: Extracted LoadCertificate helper method
   - Rules BrighterCommand#10-12: No changes to trace propagation (observability preserved)
darrenschwarz added a commit to darrenschwarz/Brighter that referenced this pull request Dec 28, 2025
   Implements client certificate configuration for RabbitMQ connections
   to support mutual TLS authentication.

   Changes:
   - Add ClientCertificate, ClientCertificatePath, and
     ClientCertificatePassword properties to RmqMessagingGatewayConnection
   - Implement SSL configuration in RmqMessageGateway for both sync and
     async variants
   - Support X509Certificate2 objects and file paths (.pfx/PKCS#12 format)
   - Use X509CertificateLoader.LoadPkcs12FromFile (not obsolete constructors)
   - Certificate object takes precedence over file path
   - Maintain backwards compatibility (certificate configuration is optional)

   Testing:
   - Behavioral tests verify ConnectionFactory.Ssl configuration
   - Tests assert SSL enablement, certificate passing, and precedence
   - Error case tests (missing file, invalid certificate)
   - Self-signed certificates generated in test setup
   - Full test coverage for both sync and async variants
   - All 14 tests passing

   Compliance with Critical Guidelines:
   - Rule BrighterCommand#1: Implemented identically for both sync AND async variants
   - Rule BrighterCommand#3: Backwards compatible (certificate is optional, all nullable)
   - Rule BrighterCommand#7: Used typed X509Certificate2 API consistently
   - Rule BrighterCommand#9: Extracted LoadCertificate helper method
   - Rules BrighterCommand#10-12: No changes to trace propagation (observability preserved)
darrenschwarz added a commit to darrenschwarz/Brighter that referenced this pull request Dec 31, 2025
   Implements client certificate configuration for RabbitMQ connections
   to support mutual TLS authentication.

   Changes:
   - Add ClientCertificate, ClientCertificatePath, and
     ClientCertificatePassword properties to RmqMessagingGatewayConnection
   - Implement SSL configuration in RmqMessageGateway for both sync and
     async variants
   - Support X509Certificate2 objects and file paths (.pfx/PKCS#12 format)
   - Use X509CertificateLoader.LoadPkcs12FromFile (not obsolete constructors)
   - Certificate object takes precedence over file path
   - Maintain backwards compatibility (certificate configuration is optional)

   Testing:
   - Behavioral tests verify ConnectionFactory.Ssl configuration
   - Tests assert SSL enablement, certificate passing, and precedence
   - Error case tests (missing file, invalid certificate)
   - Self-signed certificates generated in test setup
   - Full test coverage for both sync and async variants
   - All 14 tests passing

   Compliance with Critical Guidelines:
   - Rule BrighterCommand#1: Implemented identically for both sync AND async variants
   - Rule BrighterCommand#3: Backwards compatible (certificate is optional, all nullable)
   - Rule BrighterCommand#7: Used typed X509Certificate2 API consistently
   - Rule BrighterCommand#9: Extracted LoadCertificate helper method
   - Rules BrighterCommand#10-12: No changes to trace propagation (observability preserved)
iancooper added a commit that referenced this pull request Feb 1, 2026
* Add mutual TLS support for RabbitMQ (#3902)

   Implements client certificate configuration for RabbitMQ connections
   to support mutual TLS authentication.

   Changes:
   - Add ClientCertificate, ClientCertificatePath, and
     ClientCertificatePassword properties to RmqMessagingGatewayConnection
   - Implement SSL configuration in RmqMessageGateway for both sync and
     async variants
   - Support X509Certificate2 objects and file paths (.pfx/PKCS#12 format)
   - Use X509CertificateLoader.LoadPkcs12FromFile (not obsolete constructors)
   - Certificate object takes precedence over file path
   - Maintain backwards compatibility (certificate configuration is optional)

   Testing:
   - Behavioral tests verify ConnectionFactory.Ssl configuration
   - Tests assert SSL enablement, certificate passing, and precedence
   - Error case tests (missing file, invalid certificate)
   - Self-signed certificates generated in test setup
   - Full test coverage for both sync and async variants
   - All 14 tests passing

   Compliance with Critical Guidelines:
   - Rule #1: Implemented identically for both sync AND async variants
   - Rule #3: Backwards compatible (certificate is optional, all nullable)
   - Rule #7: Used typed X509Certificate2 API consistently
   - Rule #9: Extracted LoadCertificate helper method
   - Rules #10-12: No changes to trace propagation (observability preserved)

* Add RabbitMQ mutual TLS support with self-signed certificate option

Implements mutual TLS (mTLS) authentication for RabbitMQ connections,
enabling secure communication where both client and server authenticate
using X.509 certificates.

Changes:
- Add ClientCertificate, ClientCertificatePath, and ClientCertificatePassword
  properties to RmqMessagingGatewayConnection (Sync & Async)
- Add TrustServerSelfSignedCertificate property for test/dev environments
- Configure SSL options with certificate chain validation
- Fix GetSanitizedUri() to handle mTLS URIs without username/password
- Use NET9_0_OR_GREATER conditional compilation for X509CertificateLoader
  (falls back to X509Certificate2 for .NET 8.0 and netstandard2.0)
- Add acceptance tests with [Trait("Requires", "Docker-mTLS")] for CI filtering

The implementation provides full parity between Sync (RMQ v6) and Async
(RMQ v7) gateways. Certificate loading supports both runtime objects and
file paths. Configuration is secure by default with opt-in flag for
accepting self-signed certificates in test environments only.

* Add observability tests for RabbitMQ mutual TLS

  Verifies that W3C Trace Context (TraceParent, TraceState, Baggage) and
  CloudEvents trace context survive mTLS connections, ensuring compliance
  with Critical Review Guidelines Rules #10-12.

  Tests verify:
  - TraceParent header preservation over mTLS
  - TraceState and Baggage propagation
  - BrighterTracer.WriteProducerEvent instrumentation
  - CloudEvents trace context serialization
  - Parity between Sync and Async gateways

  Tagged with [Trait("Category", "Observability")] for selective test execution.

* Add simple mTLS test harness for RabbitMQ

  Creates a minimal Web API that demonstrates publishing and consuming
  messages over RabbitMQ with mutual TLS authentication.

  Features:
  - Single application acts as both producer and consumer
  - TodoCreated event with TodoCreatedHandler
  - REST endpoints: POST /todos and GET /health
  - RabbitMQ connection configured with client certificate
  - Comprehensive README with setup and troubleshooting
  - Built for .NET 8.0 compatibility

  The test harness verifies end-to-end mTLS functionality including
  certificate loading, SSL connection establishment, message publishing
  with publisher confirms, and message consumption over secure connection.

* Exclude mTLS tests from regular CI

  The mTLS tests require:
  - Generated certificates (tests/certs/)
  - RabbitMQ with mTLS configuration
  - Docker compose setup with special config

  These tests are tagged with [Trait("Requires", "Docker-mTLS")] and
  should only run when explicitly requested with --filter "Requires=Docker-mTLS",
  not in the regular rabbitmq-ci workflow.

* Add Requires=Docker-mTLS trait to observability tests

  The observability test classes were missing the [Trait("Requires", "Docker-mTLS")]
  attribute, causing them to run in regular CI despite needing mTLS infrastructure.

  This trait ensures these tests are only executed when explicitly requested
  with --filter "Requires=Docker-mTLS", matching the acceptance tests.

* Fix sync mTLS test failures by enforcing sequential execution

Add Collection attributes to sync mTLS test classes to prevent parallel execution race conditions during TLS handshake.

Changes:
  - Add [Collection("RabbitMQ mTLS")] to mTLS test classes
  - Add RabbitMQMtlsTestCollection with DisableParallelization
  - Add quorum queue test coverage for both sync and async

  All 18 mTLS tests (9 sync + 9 async) now pass consistently.

* deleted local certs for testing

* Fix missing NOT operator in RMQ sync requeue logic

  The AddUserDefinedHeaders method in the SYNC version (RmqMessagePublisher.cs)
  was missing the negation operator that exists in the ASYNC version. This caused
  only system headers to be copied during message requeuing instead of user-defined
  headers, resulting in message body loss.

  Changed:
    if (_headersToReset.Contains(header.Key))
  To:
    if (!_headersToReset.Contains(header.Key))

  This brings the SYNC implementation in line with the ASYNC version from
  commit 76aeced.

* Fix mTLS certificate validation and race condition in sync consumer

  This commit includes fixes to complete the mTLS implementation for RabbitMQ
  messaging gateways, resolving test failures and a race condition.

  ## mTLS Implementation
  - Added RmqTlsConfigurator helper classes (Async and Sync variants) to
    centralize TLS/SSL configuration logic
  - Extracts certificate loading and SslOption configuration into reusable
    internal static class
  - Supports both X509Certificate2 objects and file paths with optional passwords
  - Integrates with RmqMessagingGatewayConnection configuration

  ## Bug Fixes

  1. Certificate validation failures (18 tests)
     - Server certificate is issued for hostname rabbitmq-mtls but tests
       connect to localhost, causing SSL name mismatch errors
     - Updated RmqTlsConfigurator to accept both RemoteCertificateChainErrors
       AND RemoteCertificateNameMismatch when TrustServerSelfSignedCertificate
       is enabled
     - This is appropriate for test/development environments with self-signed
       certificates

  2. Race condition in sync consumer (intermittent failures)
     - RmqMessageConsumer.CreateConsumer() was manually calling
       HandleBasicConsumeOk after BasicConsume
     - RabbitMQ client already calls this method automatically as a callback,
       causing concurrent modifications to non-thread-safe collections in
       DefaultBasicConsumer
     - Removed the redundant manual call and added explanatory comment

  All 32 mTLS tests now pass consistently (16 async + 16 sync).

  Files added:
  - src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqTlsConfigurator.cs
  - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqTlsConfigurator.cs

  Files modified:
  - src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqMessageGateway.cs
  - src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqMessagingGatewayConnection.cs
  - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageGateway.cs
  - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessagingGatewayConnection.cs
  - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageConsumer.cs

  Fixes #3902

* Address PR review comments

  - Update copyright attribution to Darren Schwarz
  - Remove excessive XML documentation from internal methods
  - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern)
  - Reorganize acceptance tests into separate folders
  - Add PowerShell certificate generation script for Windows
  - Revert race condition fix (will be addressed separately)
  - Update documentation with boolean logic bug fix explanation

* Address PR review comments

  - Update copyright attribution to Darren Schwarz per CLA
  - Remove ALL XML documentation from internal RmqTlsConfigurator classes
  - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern)
  - Reorganize acceptance tests into separate Acceptance/ folders
  - Add PowerShell certificate generation script for Windows developers
  - Revert race condition fix (will be addressed in separate issue)
  - Update namespaces for moved test files

  All review comments from @iancooper addressed.
  Unit tests: 14/14 passed
  Acceptance tests: 18/18 passed
  Total: 32/32 mTLS tests passing

* Address PR review comments

  - Update copyright attribution to Darren Schwarz per CLA
  - Remove ALL XML documentation from internal RmqTlsConfigurator classes
  - Remove license headers from test files per project convention
  - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern)
  - Reorganize acceptance tests into separate Acceptance/ folders
  - Add PowerShell certificate generation script for Windows developers
  - Revert race condition fix (will be addressed in separate issue)
  - Update namespaces for moved test files

  All review comments from @iancooper addressed.
  Unit tests: 14/14 passed
  Acceptance tests: 18/18 passed
  Total: 32/32 mTLS tests passing

---------

Co-authored-by: Ian Cooper <ian_hammond_cooper@yahoo.co.uk>
@claude claude Bot mentioned this pull request May 6, 2026
6 of 9 tasks
iancooper added a commit that referenced this pull request Jun 1, 2026
- ralph-implement: switch return contract to one-path-per-line, add
  main-agent normalisation + empty-list guard for git add/checkout (#2)
- design: add actionable ADR-number re-check in Step 6 validation (#4)
- README: correct Plan agent tool description (all tools except mutating) (#6)
- requirements: forbid sub-agent from prompting the user (#9)
- tasks: mark coverage spot-check as sampling, point to /spec:review (#3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
iancooper added a commit that referenced this pull request Jul 6, 2026
…xt break (PR #4067 review)

Documentation-only follow-ups from the review:

- #6: note on IRelationalDatabaseOutboxCausationQueries.CausationColumnExistsCommand
  that the backend probes (OBJECT_ID / to_regclass / information_schema) resolve
  against the connection default schema, so a box table in a non-default schema can
  read as "column absent" -- a shared limitation of the unqualified box SQL, not a
  regression.
- #8: remarks on DynamoDbInbox.GetCausationId(Async) that timeoutInMilliseconds is
  accepted for interface compatibility but ignored (DynamoDB handles timeout/retries),
  consistent with the other methods on the store.
- #9: remarks on IRequestContext.InstrumentationOptions + an ADR 0057 Negative-
  consequences note that adding the member is source/binary-breaking for external
  implementers (no default interface impl) and should ship in a minor bump.

#5 (ADR context_key tag) needed no change: the observability table already lists only
request.id / causation_id, matching the emitted tags.

No behaviour change. Core + DynamoDB inbox projects build clean net9.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
iancooper added a commit that referenced this pull request Jul 28, 2026
…tegory

Review finding #9: TransformLifetimeScopeAsync created its logger with
CreateLogger<TransformLifetimeScope> (the sync type), so async transform
release logged under the wrong category. Point it at its own type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AghTEi1U38TWCpw4ZTJkQ3
iancooper added a commit that referenced this pull request Aug 1, 2026
* docs: add ADR 0057 — replay outbox messages on inbox duplicate detection

Introduces the design for CausationId tracking and OnceOnlyAction.Replay,
enabling automatic replay of downstream outbox messages when the inbox
detects a duplicate command. Accepted after 7 rounds of adversarial review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add approved tasks for spec 0027 — replay outbox on inbox duplicate

Create and approve implementation task list (23 tasks) for the replay
outbox messages feature. Tasks cover structural prerequisites, core
behavior (CausationId propagation, UseInboxHandler replay logic,
pipeline validation), telemetry, DI registration, and all 18 persistent
store implementations. Minor ADR clarification for ReplayCausation
semantics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add PROMPT.md to .gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update progress files, switching branches

* docs(spec-0027): adopt BoxProvisioning for CausationId schema; split relational store tasks

Fold the inbox/outbox CausationId schema change into this spec via
BoxProvisioning instead of a deferred separate PR:

- requirements.md: schema evolution is in-scope via BoxProvisioning
  (data backfill stays out of scope); AC9 + Performance NFR now assert
  the column, the outbox CausationId index, and SupportsCausationTracking().
- ADR 0057: new "Schema Evolution via BoxProvisioning" section covering the
  three store classes (catalog-based relational, Spanner provisioner, NoSQL
  schemaless); per-backend catalog versions (outbox V8; inbox V3 for
  MsSql/MySql/Sqlite, V2 for the V1-only Postgres inbox); Spanner
  VLatestOutbox/VLatestInbox constant bumps + cross-backend drift test;
  Spanner SupportsCausationTracking() as a live column probe.
- tasks.md: split Task 19 -> 19a (atomic structural schema) + 19b-19f
  (per-backend IAmACausationTrackingInbox, test-first); Task 21 -> 21a
  (atomic structural schema + index) + 21b (Liquid generator) + 21c-21g
  (per-backend IAmACausationTrackingOutbox, test-first). The schema layer is
  atomic because SpannerVLatestDriftAgainstRelationalCatalogTests pins
  VLatest* to every relational catalog count.

Includes the 2026-06-17 focused re-review findings
(review-{requirements,design,tasks}.md); all threshold findings addressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(spec-0027): UseInboxHandler uses pipeline Context; expose InstrumentationOptions

Task 1 (structural/tidy-first):
- Part A: UseInboxHandler/UseInboxHandlerAsync use the pipeline's shared
  IRequestContext (this.Context) instead of a private InitRequestContext(),
  so RequestContext.Bag is shared across the pipeline (prereq for CausationId).
- Part B: expose instrumentationOptions as a protected InstrumentationOptions
  property on RequestHandler<T>/RequestHandlerAsync<T> so derived handlers can
  gate telemetry on InstrumentationOptions.Brighter.
- Tidy: remove duplicate base.InitializeFromAttributeParams() call in async handler.

Behaviour-preserving: OnceOnly tests 10/10 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(spec-0027): add Attribute property to PipelineStepDescription

Task 2 (structural/tidy-first): non-positional RequestHandlerAttribute? Attribute
init-property on PipelineStepDescription, populated at both Describe() construction
sites, so validation rules can inspect attribute properties (e.g. OnceOnlyAction).

Behaviour-preserving: Validation tests 90/90 green (net9.0 + net10.0).
Also ticks Task 1 in tasks.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(spec-0027): Describe() injects global inbox attributes for parity with Build()

Task 3 (structural/tidy-first):
- PipelineBuilder.Describe() now injects the global inbox UseInbox(Async)Attribute
  for handlers, using the same guards as Build()'s AddGlobalInboxAttributes
  (HasNoInboxAttributesInPipeline / HasExistingUseInboxAttributesInPipeline) via a
  reflection-only TryCreateGlobalInboxAttribute helper, so descriptions don't drift.
- BrighterPipelineValidationExtensions resolves InboxConfiguration from
  IAmConsumerOptions and passes it to the validate/describe PipelineBuilder
  (null when no consumer options, matching the runtime).

Behaviour-preserving: Validation + Pipeline tests 131/131 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(spec-0027): expose Outbox from IAmAnOutboxProducerMediator

Task 4 (structural/tidy-first): add read-only IAmAnOutbox? Outbox to the mediator
interface and implement it in OutboxProducerMediator as the sync or async outbox,
so pipeline validation can inspect the outbox (e.g. for causation-tracking support).

Behaviour-preserving: core + core-tests build clean (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	src/Paramore.Brighter/OutboxProducerMediator.cs

* feat(spec-0027): add OnceOnlyAction.Replay, CausationId constants, role interfaces

Task 5 (structural): type definitions only, no behaviour yet.
- OnceOnlyAction.Replay enum value
- RequestContextBagNames.CausationId ("Brighter-CausationId")
- BrighterSemanticConventions.CausationId ("paramore.brighter.causation_id")
- IAmACausationTrackingInbox role interface (SupportsCausationTracking[Async],
  GetCausationId[Async])
- IAmACausationTrackingOutbox role interface (SupportsCausationTracking[Async],
  ReplayCausation[Async])

Builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): InMemoryInbox stores and retrieves CausationId

Task 6 (test-first):
- Test: InMemoryInboxCausationTrackingTests — Add with CausationId in
  RequestContext.Bag then GetCausationId[Async] returns it; null when absent;
  SupportsCausationTracking() is true.
- InMemoryInbox implements IAmACausationTrackingInbox: InboxItem gains
  CausationId; Add[Async] reads it from the context bag; GetCausationId[Async]
  returns the stored value.

OnceOnly 14/14 + InMemory inbox 11/11 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): InMemoryOutbox stores CausationId and replays by clearing dispatch state

Task 7 (test-first):
- Test: InMemoryOutboxCausationTrackingTests — Add messages with CausationId in
  RequestContext.Bag, mark dispatched, then ReplayCausation[Async] resets the
  matching messages to outstanding while leaving other causations untouched;
  SupportsCausationTracking() is true.
- InMemoryOutbox implements IAmACausationTrackingOutbox: OutboxEntry gains
  CausationId; Add reads it from the context bag; ReplayCausation[Async] sets
  TimeFlushed to DateTimeOffset.MinValue for matching entries.

Core causation outbox 3/3 + InMemory outbox 22/22 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): sync UseInboxHandler generates CausationId on first handling

Task 8 (test-first):
- Test: UseInboxHandlerCausationTrackingTests — Send through a [UseInbox] handler
  sets CausationId (defaulting to the command id) in the shared RequestContext.Bag,
  and the inbox entry stores the same causation id.
- UseInboxHandler.Handle() defaults Context.Bag[CausationId] to request.Id before
  handling (preserving any already-present causation id).

OnceOnly 18/18 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): async UseInboxHandlerAsync generates CausationId on first handling

Task 9 (test-first):
- Test: UseInboxHandlerAsyncCausationTrackingTests — SendAsync through a
  [UseInboxAsync] handler sets CausationId (defaulting to the command id) in the
  shared RequestContext.Bag, and the inbox entry stores the same causation id.
- UseInboxHandlerAsync.HandleAsync() defaults Context.Bag[CausationId] to
  command.Id before handling, mirroring the sync handler.

OnceOnly 19/19 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): sync UseInboxHandler replays outbox on duplicate (Replay)

Task 10 (test-first) — the core feature:
- Test: UseInboxHandlerReplayTests — duplicate command with OnceOnlyAction.Replay
  does not re-execute the handler; the outbox messages sharing the causation id
  stored in the inbox are reset to outstanding; other causations untouched.
- UseInboxHandler gains optional IAmACausationTrackingOutbox? ctor param (DI-resolved)
  and a Replay branch: on a duplicate it reads the causation id from the tracking
  inbox and calls ReplayCausation on the outbox, then returns without re-handling.
- New Log.CommandHasAlreadyBeenSeenReplayingOutbox.

OnceOnly 20/20 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): async UseInboxHandlerAsync replays outbox on duplicate (Replay)

Task 11 (test-first): async mirror of Task 10.
- Test: UseInboxHandlerAsyncReplayTests — duplicate SendAsync with
  OnceOnlyAction.Replay does not re-execute the handler; matching causation's
  outbox messages reset to outstanding; other causations untouched.
- UseInboxHandlerAsync gains optional IAmACausationTrackingOutbox? ctor param and an
  async Replay branch using GetCausationIdAsync + ReplayCausationAsync.
- New Log.CommandHasBeenSeenReplayingOutbox.

OnceOnly 21/21 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): Replay with no outbox is a graceful terminal step (sync+async)

Tasks 12 & 13 (characterization, green on first run): when OnceOnlyAction.Replay is
configured on a duplicate but no causation-tracking outbox is registered, the
handler returns without throwing and does not re-execute. Locks in the _outbox
null-guard behaviour added in Tasks 10/11. No production code changed.

OnceOnly no-outbox 2/2 green (net9.0 + net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): pipeline validation rejects Replay without causation tracking

Task 14. Adds HandlerPipelineValidationRules.ReplayRequiresCausationTracking
(inbox, outbox): a collapsed spec that, for each pipeline configuring
OnceOnlyAction.Replay on a UseInbox(Async) step, reports:
- Error when the inbox/outbox does not implement the causation-tracking role
- Warning when the role is implemented but the live schema does not support it
- Warning when no outbox is configured (Replay = graceful terminal step)
Non-Replay pipelines yield no findings.

Wired into PipelineValidator.ValidateHandlerPipelines (new optional inbox/outbox
ctor params) and BrighterPipelineValidationExtensions.ValidatePipelines resolves
the inbox via InboxConfiguration.Inbox and the outbox via
IAmAnOutboxProducerMediator.Outbox.

Test: ReplayCausationTrackingValidationTests (7 facts). Core Validation 86/86
both TFMs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): UseInboxHandler adds Replay telemetry event to pipeline span

Task 15. On a duplicate seen with OnceOnlyAction.Replay, UseInboxHandler and
UseInboxHandlerAsync write an "UseInboxHandler Duplicate Replay" ActivityEvent
to the pipeline span (Context.Span), tagged with the request id and the replayed
causation id. The event is gated on a non-null span and on the configured
InstrumentationOptions including Brighter.

Rather than threading InstrumentationOptions through the handler constructor (the
handler's own options are always All and were the wrong source), expose the
pipeline's configured InstrumentationOptions on the RequestContext — peer to Span,
FeatureSwitches and ResiliencePipeline, all of which are already pipeline-scoped
config on IRequestContext. CommandProcessor.InitRequestContext sets it from the
same value used to create the span, and CreateCopy carries it. Middleware handlers
now read Context.InstrumentationOptions, the way they read Context.Span.

Test: UseInboxHandlerReplayTelemetryTests (sync + async event, Brighter-flag gate,
null-span no-op). Core.Tests 807/807 (7 skipped) net9.0; new test 4/4 net9+net10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(spec-0027): remove redundant protected InstrumentationOptions props

Task 15 superseded Task 1B's plan: telemetry gating now reads the
configured value via Context.InstrumentationOptions (first-class property
on IRequestContext/RequestContext), not the handler's own ctor value.

The protected InstrumentationOptions property added to RequestHandler<T>
and RequestHandlerAsync<T> in Task 1B (a7b743f85) has zero consumers and
reflected the wrong source (the handler's ctor value, not the configured
one). Remove both properties and their XML docs. The primary-ctor
instrumentationOptions parameter stays — RequestHandler still uses it
internally for BrighterTracer.WriteHandlerEvent.

Behaviour-preserving structural change. Core.Tests 807/807 (net10.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): UseInboxHandler adds Throw/Warn/Add telemetry events (Task 16)

Adds ActivityEvents to the pipeline span for the existing Throw, Warn, and
Add paths in UseInboxHandler[Async], matching the Replay-event pattern from
Task 15:
- Throw  -> "UseInboxHandler Duplicate Throw" (written before the throw)
- Warn   -> "UseInboxHandler Duplicate Warn"
- Add    -> "UseInboxHandler Add"

New WriteInboxEvent helper carries the RequestId tag and is gated on a
non-null Context.Span and Context.InstrumentationOptions.HasFlag(Brighter)
— same seam as WriteReplayEvent (no ctor param). Async Add captures the
span before the awaits because RequestContext.Span is thread-affine.

Test: UseInboxHandlerTelemetryTests (8 facts: Add/Throw/Warn sync+async,
Brighter-flag-off -> no event, null-span -> no event + handling still
happens). Core.Tests 815/815 (7 skipped) net9.0 + net10.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): register IAmACausationTrackingOutbox under role interface (Task 17)

When the configured outbox implements IAmACausationTrackingOutbox, register
it under that role interface (same singleton instance) in both AddProducers
registration paths: the eager path conditionally on outbox is-a check; the
deferred path via a factory that resolves IAmAnOutbox and casts (null for a
non-tracking outbox). Inbox needs no separate registration — UseInboxHandler
pattern-matches its inbox instance at runtime.

Test: CausationTrackingOutboxRegistrationTests (3 facts) — default
InMemoryOutbox resolvable as the role interface, same instance as IAmAnOutbox,
and a non-tracking SpyOutbox resolves to null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): update PROMPT.md resume state — Task 17 done, next Task 18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): base test classes for causation tracking (Task 18)

Add abstract CausationTrackingInboxBaseTests and
CausationTrackingOutboxBaseTests<TTransaction> in Paramore.Brighter.Base.Test
defining the causation-tracking scenarios persistent-store test projects will
inherit. Validated green against InMemoryInbox/InMemoryOutbox first via concrete
subclasses in Paramore.Brighter.InMemory.Tests (adds a Base.Test ProjectReference).

Outbox base assertions use OutstandingMessages only (not DispatchedMessages) so
they hold across both fake-clock and real-clock stores. Green on first run
(characterization) since InMemory already implements the interfaces (Tasks 6/7).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): relational inbox CausationId schema via BoxProvisioning (Task 19a)

Forced-atomic structural change adding a nullable CausationId column to every
relational inbox schema, so Tasks 19b-19f can implement IAmACausationTrackingInbox
against a column that actually exists.

Catalogs (idempotent ALTER TABLE ADD ... NULL, SourceReference "#2541"):
- MsSql/MySql/Sqlite inbox V2 -> V3
- PostgreSql inbox V1 -> V2 (gains an AddColumn helper + DefaultSchema; was V1-only)

Live builders emit the column: SqlInboxBuilder, MySqlInboxBuilder,
PostgreSqlInboxBuilder, SqliteInboxBuilder, SpannerInboxBuilder.

Spanner (no catalog): SpannerInboxProvisioner detection set gains CausationId;
SpannerBoxMigrationRunner.VLatestInbox 2 -> 3.

Tests updated to the new chain length and re-verified against real DB containers:
- ExpectedMigrationVersions InboxLatest bumped (PG 1->2, others 2->3)
- migrations-listed + bootstrap-upgrade tests renamed v2->v3 and assert the new column
- MsSql/Postgres drift parity + Spanner builder-drift comments refreshed
- cross-backend Spanner carve-out literal 1 -> 2 (PG stays one version behind)
- MsSql/Postgres legacy-race arms expect the extra applied migration

Verified green (real containers, both TFMs where multi-targeted): Sqlite 55,
MySQL 78, Postgres 79, MSSQL 85, Spanner 33.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): MsSql inbox implements IAmACausationTrackingInbox (Task 19b)

Shared RelationalDatabaseInbox base now implements IAmACausationTrackingInbox,
gated on an optional IRelationalDatabaseInboxCausationQueries the backend query
set may implement. MsSqlQueries implements it (causation-aware INSERT, GetCausationId
SELECT, sys.columns existence probe). Add/AddAsync append @CausationId read from
RequestContext.Bag when supported; SupportsCausationTracking runs a live column probe.
Backends whose queries do not implement the optional interface report no support and
return null (opt-in, non-breaking) until their own 19c-19f slices.

Test: MsSqlCausationTrackingInboxTest : CausationTrackingInboxBaseTests against the
SQL Server container. 4/4 net9+net10; MSSQL Inbox suite 47/47 regression-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): update PROMPT.md resume state — Task 19b done, next Task 19c

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): MySql inbox implements IAmACausationTrackingInbox (Task 19c)

MySqlQueries implements the optional IRelationalDatabaseInboxCausationQueries:
causation-aware INSERT, GetCausationId SELECT (LIMIT 1), and an
information_schema.columns existence probe scoped to DATABASE() (MySql has no
sys.columns). The shared RelationalDatabaseInbox base picks this up to expose
IAmACausationTrackingInbox for MySql; backends whose queries do not yet
implement the optional interface remain opt-out and non-breaking.

Test: MySqlCausationTrackingInboxTest : CausationTrackingInboxBaseTests against
the MySql container. 4/4 net9; MySql Inbox suite 50/50 regression-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): update PROMPT.md resume state — Task 19c done, next Task 19d

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: stop tracking PROMPT.md (AI session scratch, already in .gitignore)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Postgres inbox implements IAmACausationTrackingInbox (Task 19d)

PostgreSqlQueries implements the optional IRelationalDatabaseInboxCausationQueries:
causation-aware INSERT, GetCausationId SELECT (FETCH FIRST 1 ROW), and a column
existence probe. The probe uses to_regclass + pg_attribute rather than an
information_schema literal comparison because PostgreSqlInbox.GenerateSqlText
quotes the {0} table-name placeholder (PgIdentifier.Quote) — correct for the
identifier positions in the INSERT/SELECT, but the double-quote characters would
corrupt a string-literal comparison. to_regclass parses the quoted identifier,
returns NULL (not an error) for a missing relation, and pg_attribute.attname
matches the lowercase-folded causationid column.

Test: PostgresCausationTrackingInboxTest : CausationTrackingInboxBaseTests
against the Postgres container. 4/4 net9+net10; PG Inbox suite 69/69 both TFMs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Sqlite inbox implements IAmACausationTrackingInbox (Task 19e)

SqliteQueries implements IRelationalDatabaseInboxCausationQueries.
AddCausationCommand/GetCausationIdCommand mirror existing Sqlite commands
+ CausationId/LIMIT 1. Column probe via pragma_table_info('{0}'); SqliteInbox
does not override GenerateSqlText so the PG literal-quoting trap does not apply.

Test SqliteCausationTrackingInboxTest : CausationTrackingInboxBaseTests drives
a real Sqlite store. 4/4 net9+net10; Sqlite inbox suite 42/42 both TFMs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Spanner inbox implements IAmACausationTrackingInbox (Task 19f)

SpannerSqlQueries implements IRelationalDatabaseInboxCausationQueries.
AddCausationCommand/GetCausationIdCommand mirror existing Spanner commands
+ CausationId/LIMIT 1 (backtick identifiers). Column probe via
INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='{0}' AND COLUMN_NAME='CausationId'
(Spanner does not override GenerateSqlText, so {0} substitutes bare into the
string literal). SpannerInboxAsync.CreateSqlParameter now types @CausationId
as SpannerDbType.String — Spanner requires an explicit type when the value is
DBNull (null causation id), mirroring the existing @CommandBody Json case.

Test SpannerCausationTrackingInboxTest : CausationTrackingInboxBaseTests drives
a real Spanner emulator store. EmulatorDetection.EmulatorOrProduction is embedded
in the connection string (SPANNER_EMULATOR_HOST alone is not honoured by the
inbox's internal connections — same pattern the Spanner BoxProvisioning tests
use). 4/4 net9+net10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): NoSQL inbox stores implement IAmACausationTrackingInbox (Task 20)

DynamoDB, DynamoDB.V4, MongoDb and Firestore inboxes now read the causation id
from RequestContext.Bag on Add/AddAsync, persist it on the schemaless entity, and
implement IAmACausationTrackingInbox: SupportsCausationTracking[Async] => true (no
schema to probe), GetCausationId[Async] reads the stored value (null when absent).

- DynamoDB/V4: CommandItem<T> gains nullable CausationId; new non-generic CausationItem
  projection lets GetCausationId read the attribute without the command type.
- MongoDb: InboxMessage gains nullable CausationId; GetCausationId queries by Id/ContextKey.
- Firestore: ToDocument writes a CausationId field (null stored as NullValue); GetCausationId
  reuses a shared BuildIdContextQuery and reads the field.

Tests inherit the shared CausationTrackingInboxBaseTests (4 facts each). Verified GREEN
on net9.0+net10.0 for DynamoDB, DynamoDB.V4, MongoDb (16/16 inbox each, no regressions).
Firestore compiles; its tests run against real Firestore in CI (no local emulator).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): relational outbox CausationId schema + replay index via BoxProvisioning (Task 21a)

Forced-atomic structural change adding a nullable CausationId column AND a replay
index on it to every relational outbox schema, so Tasks 21c-21g can implement
IAmACausationTrackingOutbox against a column and index that actually exist. AC9
requires both a fresh install and a migrated upgrade to end with the column + index,
so each lands in both the live builder (fresh) and the V8 migration UpScript (upgrade).

Catalogs (idempotent ALTER TABLE ADD ... NULL + idempotent CREATE INDEX, SourceReference "#2541"):
- MsSql/MySql/PostgreSql/Sqlite outbox V7 -> V8 (uniform).
- Index idempotency per backend: MsSql sys.indexes guard + EXEC (decouples CREATE INDEX
  from the same-batch ADD COLUMN); MySql information_schema.statistics prepared-statement
  probe; PostgreSql/Sqlite native CREATE INDEX IF NOT EXISTS.

Live builders emit the column + index: SqlOutboxBuilder, MySqlOutboxBuilder (inline KEY),
PostgreSqlOutboxBuilder, SqliteOutboxBuilder, SpannerOutboxBuilder (separate index DDL).
MsSql inserts CausationId before SpecVersion to preserve the working final-column boundary.

Spanner (no catalog): SpannerOutboxProvisioner detection set gains CausationId;
SpannerBoxMigrationRunner.VLatestOutbox 7 -> 8; FreshInstall batches the table create
with the CausationId index via CreateDdlCommand (Spanner has no inline secondary index).

Index naming: idx_<table>_CausationId (schema/db-scoped backends); idx_CausationId for
MySql (index names are table-scoped there, matching the builder's inline KEY).

Tests updated to the new chain length and re-verified against real DB containers:
- ExpectedMigrationVersions OutboxLatest 7 -> 8 (all five test projects)
- migrations-listed + bootstrap-upgrade tests renamed v7->v8 and assert the new column
- per-backend builder-drift gains a separate CausationId-index assertion (AC9, columns-only
  drift does not cover the index); the Sqlite migrations-listed test asserts the V8 index inline
- MySql mid-chain resume count 5 -> 6 (V3 synthetic + V4..V8 applied)
- cross-backend Spanner VLatestOutbox parity + Spanner builder-drift refreshed
- DdlColumnExtractor reads only the first CREATE TABLE body, so appended CREATE INDEX
  statements and MySql's inline KEY are excluded from column parity automatically

Verified green (real containers, both TFMs where multi-targeted):
Sqlite BoxProvisioning 57/57, MySQL 80/80, Postgres 81/81, MSSQL 87/87, Spanner 34/34,
base BoxProvisioning.Tests 111/111. Pre-existing unrelated failures remain out of scope:
MSSQL outbox-store DateTimeOffset BST round-trip (#4161, confirmed pre-existing by revert)
and Spanner outbox-store EmulatorDetection (#4162).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): extend Liquid generator for causation-tracking outbox tests (Task 21b)

Add a new `Templates/Outbox/Causation/` Liquid template that emits a
`CausationTrackingOutboxTests` subclass of `CausationTrackingOutboxBaseTests<TTransaction>`
per outbox store, wiring the store via the existing per-store outbox provider
(CreateOutbox/CreateStore/DeleteStore). Wire the third generation pass into
OutboxGenerator (both single-outbox and multi-outbox branches), copy the new
template folder to the generator output, and regenerate all eight test projects.

Generated causation tests compile against every relational and NoSQL outbox
project (Sqlite, MSSQL, MySQL, PostgreSQL, Gcp/Spanner+Firestore, DynamoDB,
DynamoDB.V4, MongoDb) and reference the base test class. They stay RED until
each store implements IAmACausationTrackingOutbox in Tasks 21c-21g (relational)
and Task 22 (NoSQL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): MsSql outbox implements IAmACausationTrackingOutbox (Task 21c)

Mirror the inbox causation seam (Task 19b) for the relational outbox. The
shared RelationDatabaseOutbox base now implements IAmACausationTrackingOutbox,
gated on a new optional companion interface IRelationalDatabaseOutboxCausationQueries
that a backend's query set may implement. When present, single Add/AddAsync append
@CausationId (read from RequestContext.Bag) and use AddCausationCommand;
ReplayCausation[Async] runs an UPDATE clearing Dispatched for a causation so the
sweeper resends; SupportsCausationTracking[Async] runs a live column-existence probe.
When the companion is absent the outbox reports no causation support and Add stores
nothing extra (opt-in, non-breaking).

MsSqlQueries implements the companion: AddCausationCommand (INSERT incl. CausationId),
ReplayCausationCommand (Dispatched = NULL WHERE CausationId), and the sys.columns
probe. Drives the generated CausationTrackingOutboxTests (Text + Binary) green:
6/6 net9.0 + net10.0. The 12 pre-existing Outbox-store timestamp failures are #4161
(MSSQL DateTimeOffset BST round-trip), confirmed identical with the change stashed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): MySql outbox implements IAmACausationTrackingOutbox (Task 21d)

MySqlQueries now implements IRelationalDatabaseOutboxCausationQueries:
- AddCausationCommand: AddCommand + `CausationId` col + @CausationId
- ReplayCausationCommand: UPDATE {0} SET `Dispatched` = NULL WHERE `CausationId` = @CausationId
- CausationColumnExistsCommand: information_schema.columns probe scoped via DATABASE()
  (MySql has no sys.columns)

Generated CausationTrackingOutboxTests (Text + Binary) green 6/6 net9.0;
full MySql outbox suite 81/81, no regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): PostgreSql outbox implements IAmACausationTrackingOutbox (Task 21e)

PostgreSqlQueries now implements IRelationalDatabaseOutboxCausationQueries:
- AddCausationCommand: AddCommand + lowercase-quoted "causationid" col + @CausationId
- ReplayCausationCommand: UPDATE {0} SET "dispatched" = NULL WHERE "causationid" = @CausationId
- CausationColumnExistsCommand: pg_attribute probe via to_regclass('{0}') —
  to_regclass parses the quoted identifier that PostgreSqlOutbox.GenerateSqlText
  injects, avoiding the information_schema literal-quoting trap (cf. inbox Task 19d)

Generated CausationTrackingOutboxTests (Text + Binary) green 6/6 net9.0+net10.0;
full PG outbox suite 85/85 both TFMs, no regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Sqlite outbox implements IAmACausationTrackingOutbox (Task 21f)

SqliteQueries now implements IRelationalDatabaseOutboxCausationQueries:
- AddCausationCommand: AddCommand + CausationId col + @CausationId
- ReplayCausationCommand: UPDATE {0} SET [Dispatched] = NULL WHERE [CausationId] = @CausationId
- CausationColumnExistsCommand: pragma_table_info('{0}') probe (SqliteOutbox does
  not override GenerateSqlText, so {0} substitutes bare)

Generated CausationTrackingOutboxTests (Text + Binary) green 6/6 both TFMs;
full Sqlite outbox suite 79/79 both TFMs, no regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Spanner outbox implements IAmACausationTrackingOutbox (Task 21g)

SpannerQueries now implements IRelationalDatabaseOutboxCausationQueries:
- AddCausationCommand: AddCommand + `CausationId` backtick col + @CausationId
- ReplayCausationCommand: UPDATE `{0}` SET `Dispatched` = NULL WHERE `CausationId` = @CausationId
- CausationColumnExistsCommand: INFORMATION_SCHEMA.COLUMNS probe

SpannerOutbox.CreateSqlParameter types @CausationId as SpannerDbType.String so a
null causation id (DBNull) does not trip Spanner's "SpannerDbType must be set"
inference error (mirrors inbox Task 19f).

Test-infra (#4162): SpannerText/SpannerBinary outbox providers embed
EmulatorDetection.EmulatorOrProduction in the connection string so CreateStore
detects the emulator.

Generated CausationTrackingOutboxTests (Text + Binary) green 3/3 each, net9.0+net10.0
(run per-store; combined parallel runs flake on the pre-existing #4162 emulator
concurrency limit affecting all Spanner outbox store tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): MongoDb + DynamoDB(+V4) outbox implement IAmACausationTrackingOutbox (Task 22, part 1)

Schemaless NoSQL outbox stores read CausationId from RequestContext.Bag on Add
and replay a causation's messages by clearing their dispatched state.

MongoDb: OutboxMessage gains nullable CausationId; Add/AddAsync set it from the
bag; ReplayCausation[Async] UpdateMany sets Dispatched=null filtered by CausationId;
SupportsCausationTracking[Async]=>true.

DynamoDB + DynamoDB.V4: MessageItem gains CausationId as the hash key of a new
"Causation" GSI (DynamoDbConfiguration.CausationIndexName, default "Causation");
Add sets it from the bag; ReplayCausation[Async] queries the GSI for MessageIds
then UpdateItem "SET OutstandingCreatedTime = CreatedTime REMOVE DeliveryTime,
DeliveredAt" per item (restores the outstanding marker from the still-present
CreatedTime, robust to GSI projection); SupportsCausationTracking[Async]=>true.
Test table fixtures add the Causation GSI throughput entry.

Firestore (the 4th NoSQL outbox) deferred to part 2 — no local emulator.

Generated CausationTrackingOutboxTests green: MongoDb 3/3 (full outbox 23/23 both
TFMs), DynamoDB 3/3 (27/27), DynamoDB.V4 3/3 (27/27). Task 22 checkbox left
unticked pending Firestore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Firestore outbox implements IAmACausationTrackingOutbox (Task 22, part 2 — DONE)

FirestoreOutbox: ToDocument gains an optional causationId and writes a CausationId
StringValue field when present; the two single-Add paths (sync + async) pass
ReadCausationId(requestContext). ReplayCausation[Async] runs a StructuredQuery
filtering CausationId == causationId then commits writes resetting Dispatched=null
and IsDispatched=false (UpdateMask, Exists precondition) so the sweeper resends
them. SupportsCausationTracking[Async]=>true. Sync wraps async via
BrighterAsyncContext.Run (matching the store's existing pattern).

Verified by compile (Firestore project + Gcp.Tests build clean) — there is no local
Firestore emulator; GCP CI exercises the generated CausationTrackingOutboxTests
against real Firestore (same approach as inbox Task 20).

Completes Task 22 (all 4 NoSQL outbox stores) — checkbox ticked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(spec-0027): Task 23 build verification — Core.Tests 818/818 both TFMs

dotnet build src/Paramore.Brighter succeeds; Paramore.Brighter.Core.Tests
818 passed / 7 skipped on net9.0 + net10.0, no regressions (OnceOnly +
Validation green). Completes Task 23 — all 23 tasks of spec 0027 done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(spec-0027): set active spec to replay-matching-outbox-events

Point specs/.current-spec at the completed spec 0027 (it was empty) so
/spec:status resolves the active spec on a fresh session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): backward-compat hardening — write-path gate + DynamoDB GSI honesty (Tasks 24–27)

Review found Tasks 1–23 do not honour the opt-in / no-forced-upgrade contract:

1. Relational Add breaks un-migrated schemas: RelationDatabaseOutbox/
   RelationalDatabaseInbox gate the causation INSERT on `CausationQueries is
   not null`, which is always true now (every *Queries implements the
   interface), so every Add emits AddCausationCommand and fails on tables
   without the CausationId column — even for users who never use Replay.
2. DynamoDB outbox SupportsCausationTracking() => true is dishonest: replay
   queries the Causation GSI an existing table lacks, so validation passes
   then fails at runtime. MongoDb/Firestore/DynamoDB-inbox are genuinely safe.

Amend the spec (phases already approved — informational re-review):

- ADR 0057: new "Write-path gate" subsection (Add gated on memoized column
  existence, not the static interface); rewritten NoSQL paragraph (DynamoDB
  outbox SupportsCausationTracking() must DescribeTable the GSI; others stay
  true); mandatory upgrade reserved for V11.
- requirements.md: strengthened Non-breaking NFR; new AC10 (no forced
  upgrade) + AC11 (honest capability reporting).
- tasks.md: new Tasks 24–27. Task 24 split per-backend (24a Sqlite drives the
  shared base-class fix; 24b–24e characterize MsSql/MySql/Postgres/Spanner via
  *LegacySeeder helpers; 24e builds the missing Spanner legacy seeder).

Confirmatory /spec:review: design PASS (0 ≥60); tasks NEEDS WORK (2 ≥60,
Spanner seeder gap + monolithic gate) — both addressed in this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Task 24a — gate causation write on memoized column probe, not static interface (AC10)

Backward-compat fix. RelationDatabaseOutbox/RelationalDatabaseInbox previously
switched every Add/AddAsync to AddCausationCommand whenever the backend's *Queries
implemented IRelationalDatabase*CausationQueries — which is now ALWAYS true after
Tasks 19c–f/21c–g. That emitted an INSERT naming CausationId against tables that may
lack the column, breaking every deposit for users who upgrade Brighter without
migrating the schema.

The interface is a static driver capability and must not gate the write. Both base
classes now gate on a memoized live-schema column-existence probe (the same probe
behind SupportsCausationTracking[Async]): present → AddCausationCommand + @CausationId;
absent → plain AddCommand. Memoized per store instance in a private bool? field,
populated lazily by the first sync-or-async probe and read by both; concurrent first
probes are harmless; never invalidated (mid-process migration needs a restart).
Outbox AddAsync converted to async so it can await the async probe.

Test infra: SqliteInboxLegacySeeder gains SeedAtV2 (ContextKey present, no CausationId).

Test (Sqlite, no container): legacy-schema deposit/retrieve succeeds (was RED:
"table ... has no column named CausationId") · SupportsCausationTracking()==false ·
Replay-configured pipeline rejected by ValidatePipelines · column-present regression
guard still tracks. Sqlite 127/127 both TFMs; Core OnceOnly+Validation 135/135 both TFMs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): Task 24b — MsSql legacy-schema causation backward-compat characterization (AC10)

Mirrors the Sqlite Task 24a characterization against MSSQL legacy schemas to
confirm the shared memoized column-probe gate (b949256f9) on a real backend.
No production code — green on first run once 24a landed.

- MsSqlInboxLegacySeeder gains SeedAtV2 (ContextKey present, no CausationId) —
  the pre-Replay inbox a user runs after upgrading Brighter without the V3
  causation migration (mirror of the Sqlite seeder helper).
- New MsSqlLegacySchemaCausationCompatibilityTests (7 facts): legacy outbox/inbox
  deposit+retrieve OK, SupportsCausationTracking()==false on un-migrated schema,
  Replay validation rejected with Warning, plus column-present regression guards.

MSSQL 7/7 net9.0 + net10.0 (host port 11433 azure-sql-edge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): Task 24c — MySql legacy-schema causation backward-compat characterization (AC10)

Mirrors Task 24a/24b against MySQL legacy schemas to confirm the shared memoized
column-probe gate (b949256f9) on a real backend. No production code — green on
first run once 24a landed.

- MySqlInboxLegacySeeder gains SeedAtV2 (ContextKey present, no CausationId).
- New MySqlLegacySchemaCausationCompatibilityTests (7 facts): legacy outbox/inbox
  deposit+retrieve OK, SupportsCausationTracking()==false on un-migrated schema,
  Replay validation rejected with Warning, plus column-present regression guards.

MySql 7/7 net9.0 (net9-only project; container from main docker-compose, port 3306).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): Task 24d — Postgres legacy-schema causation backward-compat characterization (AC10)

Mirrors Task 24a/24b/24c against Postgres legacy schemas to confirm the shared
memoized column-probe gate (b949256f9). No production code — green on first run
once 24a landed.

- Reuses PostgreSqlInboxLegacySeeder.SeedAtV1 (PG inbox is V1-only — born with
  contextkey, no causationid) and PostgreSqlOutboxLegacySeeder.SeedAtV(7); no new
  seeder helper needed.
- New PostgresLegacySchemaCausationCompatibilityTests (7 facts): legacy outbox/inbox
  deposit+retrieve OK, SupportsCausationTracking()==false on un-migrated schema,
  Replay validation rejected with Warning, plus column-present regression guards.
- Lower-case table names: the PG outbox emits unquoted (case-folded) identifiers in
  its INSERT while the seeder's CREATE TABLE "{0}" preserves case — a mixed-case name
  would create "Table…" but be looked up as table…. Matches the existing PG outbox
  provider convention.

Postgres 7/7 net9.0 + net10.0 (container from main docker-compose, port 5432).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): Task 24e — Spanner legacy-schema causation backward-compat characterization (AC10)

Final per-backend characterization of the shared memoized column-probe gate
(b949256f9). No production code — green on first run once 24a landed.

- Builds the missing Spanner{Inbox,Outbox}LegacySeeder (raw DDL — Spanner has no
  migration catalog). Inbox SeedAtV2 = SpannerInboxBuilder minus CausationId
  (ContextKey is part of the PK so always present); outbox SeedAtV(7) =
  SpannerOutboxBuilder text DDL minus CausationId, no replay index.
- New SpannerLegacySchemaCausationCompatibilityTests (7 facts): legacy outbox/inbox
  deposit+retrieve OK, SupportsCausationTracking()==false on un-migrated schema,
  Replay validation rejected with Warning, plus column-present regression guards.
- Connection string embeds EmulatorDetection.EmulatorOrProduction so both DDL and
  store-internal connections detect the emulator (issue #4162). Single isolated
  class (filtered run) sidesteps the #4162 suite-parallelism flake.

Spanner 7/7 net9.0 + net10.0 (emulator; SPANNER_EMULATOR_HOST=localhost:9010).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(spec-0027): Task 25 — DynamoDB outbox honestly probes the Causation GSI (AC11)

DynamoDbOutbox.SupportsCausationTracking() was a static `=> true`, but
ReplayCausationAsync queries the `Causation` GSI which a pre-Replay table lacks —
validation passed and runtime replay threw "table does not have the specified
index". Now SupportsCausationTracking[Async]() does a memoized DescribeTableAsync
and checks _configuration.CausationIndexName is in the table's
GlobalSecondaryIndexes; sync wraps async per the store's existing pattern.
Applied to BOTH Paramore.Brighter.Outbox.DynamoDB and …DynamoDB.V4. NoSQL inboxes
and Mongo/Firestore are untouched (genuinely schemaless — stay `=> true`).

Test DynamoDbOutboxCausationIndexProbeTests (5 facts, both test projects):
provisions an outbox table without the Causation GSI (generates the request from
MessageItem, strips the GSI + orphaned CausationId attribute definition) →
SupportsCausationTracking()/…Async() == false and a Replay pipeline is rejected by
ValidatePipelines (Warning "outbox store schema does not support"); with the GSI →
true; Add works in both cases. RED → GREEN.

DynamoDB + DynamoDB.V4: 5/5 each net9.0 + net10.0; existing Causation generated
tests 3/3 each (no regression). DynamoDB-local container, port 8000.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): Task 26 — NoSQL backward-compat confidence satisfied by existing Task 20/22 coverage

T26 is confidence-only (no production change — schemaless NoSQL stores need no
migration). The Task 20 inbox + Task 22 outbox causation tests already lock this
contract against fresh, zero-setup schemaless stores: outbox Add→MarkDispatched→
ReplayCausation→outstanding-again (sync+async) + Supports()==true; inbox Add→
GetCausationId→value + Supports==true. Re-ran as evidence: MongoDb 7/7, DynamoDB
inbox 4/4 (net9.0); Firestore compile-verified + GCP CI. No duplicate tests added
(Change Scope). Ticked with a note documenting the evidence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(spec-0027): Task 27 — regression verification (backward-compat hardening complete)

Touched-suite scope (user-chosen): the only production code in Tasks 24–26 is the
DynamoDB outbox GSI probe (T25). Verified 2026-06-22:
- Core.Tests 818/818 (7 skipped) net9.0 + net10.0.
- DynamoDB full outbox 32/32 + DynamoDB.V4 full outbox 32/32 (net9) — covers T25;
  existing Causation generated tests 3/3 each; DynamoDB inbox causation 4/4.
- MongoDb causation 7/7 (net9); Firestore compile-verified.
- The 5 legacy-schema characterization classes each green at their commit.
No regressions. Pre-existing out-of-scope flakes untouched (#4161 MSSQL BST,
#4162 Spanner parallelism).

All Spec 0027 backward-compat tasks (24–27) complete. Remaining: push + PR (#2541),
awaiting user go-ahead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): degrade causation-tracking probe failure to a Warning in validation (review #1)

A SupportsCausationTracking() probe that throws at startup (unreachable store,
missing permission) no longer propagates out of ReplayRequiresCausationTracking
and crashes the host via BrighterValidationHostedService.StartAsync. The new
SupportsCausationTrackingSafely helper catches the failure and reports it as a
Warning ("capability unknown"), suppressing the redundant schema-unsupported
finding. Also closes the throwing-probe half of review finding #4.

ReplayCausationTracking 9/9 + full Validation 99/99, net9.0 + net10.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): cover After-step Replay placement and Warn action in validation (review #4)

Two characterization tests close the remaining coverage gaps from review finding #4:
- Replay configured on an After-timing UseInbox step is still detected (the rule
  inspects BeforeSteps.Concat(AfterSteps)) and flags a non-tracking inbox.
- OnceOnlyAction.Warn (like Throw) yields no causation-tracking findings.

Both green on first run — no production change. ReplayCausationTracking 11/11,
net9.0 + net10.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(spec-0027): clear low-severity review findings #2/#3/#5/#6

- #2: null-guard DynamoDB(+V4) GlobalSecondaryIndexes in the Causation GSI probe
  (empty under SDK default, but null if an app disables InitializeCollections) —
  treat null as "index absent" rather than risk an NRE.
- #3: ignore SQLite WAL/SHM sidecar files (*.db-shm, *.db-wal) so running the
  Dapper sample no longer leaves untracked files.
- #5: reconcile ADR 0057 observability table with the shipped telemetry tags
  (events emit request.id [+ causation_id on Replay], not context_key).
- #6: assert the V8 CREATE INDEX on CausationId in the MSSQL/MySQL/Postgres
  outbox migration-listing tests for parity with SQLite (AC9 index check on the
  upgrade path, per-dialect index DDL). All three green; DynamoDB projects build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): thread CausationId through relational bulk Add (PR #4067 review)

The relational bulk Add/AddAsync(IEnumerable<Message>) path built its insert
via BulkAddCommand, which never wrote @CausationId. A batch deposited under
[UseInbox] (e.g. DepositPost with a batchId) therefore persisted a NULL
CausationId on every relational store, so a later OnceOnlyAction.Replay
silently found nothing to resend — diverging from InMemory/DynamoDB, which
implement bulk Add by looping over single Add and so preserved causation.

- Add BulkAddCausationCommand to IRelationalDatabaseOutboxCausationQueries
  and implement it in MsSql, PostgreSql, MySql, Sqlite, and Spanner.
- Gate the bulk write on the same memoized CausationColumnExists[Async] probe
  as single Add; thread the causation id through GenerateBulkInsert as a
  per-row @p{i}_CausationId parameter. When the column is absent the generated
  SQL/params are byte-for-byte unchanged (AC10 backward-compat preserved).
- AddAsync(IEnumerable) becomes async to await the probe.
- Add bulk-deposit + Replay coverage to CausationTrackingOutboxBaseTests so
  the gap cannot reopen per backend.

Also (review minor #1): WriteReplayEvent now emits a distinct
"UseInboxHandler Duplicate Replay Skipped" telemetry event when no causation
id is found, so operators can tell a skipped replay from an actual one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* merge issue

* fix(spec-0027): thread CausationId through Mongo/Firestore bulk Add; guard non-RequestContext (PR #4067 review)

MongoDb and Firestore outbox bulk Add overloads dropped the CausationId,
so messages deposited via DepositPost(IEnumerable<>) landed with a null
causation and OnceOnlyAction.Replay silently resent nothing for them. Read
the causation id once and set it on every message, mirroring the single-
message paths. Covered by the existing CausationTrackingOutboxBaseTests bulk
cases the generated Mongo/Firestore tests inherit.

UseInboxHandler(Async) replaced InitRequestContext() with `Context as
RequestContext`, which is null when a custom IAmARequestContextFactory yields
a non-RequestContext — silently changing the Throw/Warn/Add inbox paths to
pass null instead of a context carrying Activity.Current. Fall back to a fresh
RequestContext { Span = Activity.Current } so those paths keep their pre-
feature behaviour, while the normal path still uses the shared context so the
causation id flows to the outbox.

Also align the sync Warn duplicate log level with the async handler
(Warning, was Debug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): clarify memoized-probe non-synchronization and deferred DI registration (PR #4067 review)

Document the deliberate non-synchronization of the memoized causation-probe
fields (_causationColumnExists ×2, _causationIndexExists ×2): volatile is not
applicable to a nullable value type, and a stale-null read only triggers one
extra idempotent probe that resolves to the same value.

Document why the deferred AddProducers overload cannot register
IAmACausationTrackingOutbox conditionally (the outbox type is unknown at
registration time), so its factory returns null for a non-tracking outbox and
GetRequiredService returns null rather than throwing — consumers use optional
GetService. Comment-only; no behavioural change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): end-to-end replay-on-seen over the InternalBus (PR #4067 review)

Closes the propagation test gap the PR #4067 review flagged: no test proved
the causation id flows inbox Add -> handler -> outbox Add within a real Send,
nor that a duplicate replays the original outgoing message end-to-end.

Routes everything over the real InternalBus with nothing injected through a
testing seam. A command is posted through the command processor, a Reactor
pump (hosted on a background thread via Performer) consumes it, the handler
records it in the inbox and forwards a downstream event back onto the bus, and
the inbox/outbox record receipt and the outgoing message naturally. Re-posting
the same command exercises the duplicate path: the handler does not re-run and
the outbox replays the original outgoing message.

- ProcessAndForwardHandler: [UseInbox(Replay)] handler that forwards a
  downstream event via Post threading the pipeline RequestContext (the
  propagation link), and signals the test thread through a .NET Channel.
- Replay re-dispatch uses ClearOutbox (the primitive Post is built on), not a
  background sweeper, so the test stays deterministic.
- The shared causation id is proven by the replay itself: ReplayCausation is
  keyed on the inbox's causation id, so the forwarded message only returns to
  outstanding if the outbox stored it under that same id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): address PR #4067 review (gitignore, pipeline reuse, single-read causation gate, replay span label)

- .gitignore: restore *.db-journal entry (kept the PROMPT.md de-dup)
- PipelineBuilder.Describe: materialize GetOtherHandlersInPipeline() once,
  filter for before/after instead of two reflection passes
- RelationDatabaseOutbox: thread an explicit includeCausation bool from the
  Add[Async] probe into InitAddDbCommand so the SQL choice and parameter list
  derive from a single read of the memoized causation-column flag (mirrors the
  bulk path) instead of an order-dependent double-read
- Observability: add BoxDbOperation.Replay ("replay.causation") and label the
  ReplayCausation spans with it across all stores (relational, DynamoDB v3/v4,
  Mongo, Firestore, InMemory) instead of mislabelling them MarkDispatched

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): note IRequestContext.InstrumentationOptions as a source-breaking change (PR #4067 review #1)

Adds a "Replay Outbox Messages on Inbox Duplicate" section to release_notes.md
under Master, with a source-breaking-change note for the new required
IRequestContext.InstrumentationOptions interface member — it affects any
external implementor of IRequestContext, not just Replay adopters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): regenerate causation outbox tests after merge (add Category trait)

Merging master brought in the generated-test template that emits an xUnit
[Trait("Category", "<provider>")] attribute. The committed Causation outbox
tests predated that template change, so regenerating adds the missing trait
to all 12 provider variants (Spanner already had it). Generated output only —
no logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): bind causation outbox param as typed String on all paths (PR #4067 review)

Spanner's single-arg CreateSqlParameter only forced an explicit SpannerDbType
for the literal "@CausationId", but the bulk insert path names its per-row
params "@p{i}_CausationId". Since Spanner ships the CausationId column in its
DDL, includeCausation is always true; in the normal (non-replay) case the
causation id is null, so the bulk path produced an untyped DBNull that Spanner
rejects — a bulk Add of >1 message threw.

Bind the causation parameter via the typed two-arg CreateSqlParameter(name,
DbType.String, value) on the single-Add, bulk-Add and replay paths (matching
how the bulk insert already types every other column), and drop Spanner's now
-redundant name-based special case. Other relational backends tolerate untyped
DBNull and are unaffected; verified via SQLite outbox 86/86 + causation 24/24.

Adds a Spanner bulk-deposit-with-null-causation regression test (Category
=Spanner, CI-excluded; runs against the emulator).

Also removes the accidental duplicate spec pointer specs/.current_spec
(underscore); specs/.current-spec (hyphen) is the canonical name the /spec
skills read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): gate inbox GetCausationId on live column probe (PR #4067 review)

The inbox write path degrades on an un-migrated table via the memoized
CausationColumnExists() probe, but GetCausationId/GetCausationIdAsync guarded
only on "CausationQueries is null". Every backend implements the causation
query interface, so on a backend whose table has not been migrated (the mixed
state the write-path gate tolerates) a Replay duplicate — UseInboxHandler calls
GetCausationId without first checking SupportsCausationTracking() — ran
SELECT ... [CausationId] ... and threw invalid-column instead of no-opping,
breaking the AC10 graceful-degradation promise. Pipeline validation only warns
here, so startup succeeds and the failure surfaces at runtime on first duplicate.

Gate both read methods on CausationColumnExists()/CausationColumnExistsAsync()
(which already return false when the query interface is absent), returning null
when the column is missing. This also protects ReplayCausation.

Regression test: un-migrated (V2) SQLite inbox returns null from GetCausationId
instead of throwing. SQLite legacy+causation 27/27, Core OnceOnly 40/40, full
SQLite suite 151/151.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): document Replay RequestContext footgun and DynamoDB index trap (PR #4067 review)

- release_notes: add a "Usage requirement" call-out that Replay only works when
  handlers thread their RequestContext through Post/DepositPost; a context-less
  Post creates a fresh context, drops the causation id, and makes Replay a silent
  no-op. Includes a ❌/✅ handler example.
- DynamoDbConfiguration.CausationIndexName: <remarks> warning that the value must
  stay "Causation" because the matching GSI hash key on MessageItem is a compile
  -time-constant annotation that cannot read the config; overriding it silently
  breaks replay (same latent trap as the Outstanding/Delivered index names).
- DynamoDbInbox.GetCausationId: correct the misleading comment that implied
  GetCausationIdAsync adds a span (neither method does today).

Docs/comments only — no behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): gate relational ReplayCausation on live column probe (PR #4067 review)

ReplayCausation/ReplayCausationAsync guarded only on "CausationQueries is
null", but every relational backend implements the causation query interface
even when its table is un-migrated. In the mixed-migration state the write-path
gate was built for -- inbox migrated (has CausationId), outbox NOT migrated
(interface present, column absent) -- UseInboxHandler gets a real causation id
from the inbox and calls ReplayCausation, which issued
UPDATE {0} SET Dispatched = NULL WHERE CausationId = @CausationId against a table
with no such column, throwing "invalid column CausationId" at runtime. Pipeline
validation only warns for the un-migrated outbox, so startup proceeds and the
failure surfaces on first duplicate.

Gate both replay paths on the live CausationColumnExists()/Async probe (which
already returns false when the query interface is absent), matching Add, bulk
Add and the inbox GetCausationId. Closes review findings #1 and #2 (the handler
callsite hole is closed at the base, so no per-callsite guard is needed).

Regression test: un-migrated (V7) SQLite outbox no-ops on ReplayCausation
instead of throwing. SQLite legacy 9/9, causation+outbox 97/97.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): DynamoDB replay catch-and-continue on vanished item (PR #4067 review)

ReplayCausationAsync issues a per-item conditional UpdateItem
(attribute_exists(MessageId)) for every MessageId the Causation GSI returns, but
the surrounding try had only a finally. Because the GSI is eventually
consistent, the query can return a MessageId for an item since swept / purged /
TTL-deleted / concurrently deleted; the condition then fails,
ConditionalCheckFailedException propagates, the pagination loop unwinds, and the
remaining messages in the causation are silently never re-dispatched.
MarkDispatchedAsync already catches this exact exception; replay did not.

Catch ConditionalCheckFailedException around the per-item UpdateItem and
continue, so a vanished item is skipped and the rest of the causation is still
replayed. Applied to both the V3 and V4 DynamoDbOutbox. Closes review finding #3.

Regression test drives replay through an AmazonDynamoDBClient subclass that
deletes the first GSI-returned item before its update runs, deterministically
reproducing the race; asserts no throw and the surviving message is outstanding
again. DynamoDB V3 outbox 35/35, V4 causation 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(spec-0027): cover causation-id preservation/chaining (PR #4067 review)

The UseInboxHandler guard "if (!Bag.ContainsKey(CausationId))" is the actual
linking semantic the feature is named for, but only its true branch (default the
id to the command's own id) was exercised. Add a characterization test that
seeds a parent causation id into the shared context bag before Send and asserts
the handler does NOT overwrite it -- both in the bag and in the stored inbox
entry -- so a child command is linked to its parent's causation. Closes the
highest-value test gap called out in the review. No prod code; green on first
run. Core UseInboxHandler causation 2/2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): make MsSql fresh-install causation index idempotent (PR #4067 review)

SqlOutboxBuilder.GetDDL emitted a bare CREATE INDEX [idx_..._CausationId] with
no guard, whereas the V8 migration catalog uses a sys.indexes IF NOT EXISTS +
EXEC('CREATE INDEX ...') form and the Sqlite/Postgres/Spanner builders use
CREATE INDEX IF NOT EXISTS. Safe today only because the MsSql CREATE TABLE is
itself unguarded (so GetDDL runs on a true fresh install), but it was the odd
one out. Wrap the index in the same sys.indexes guard for parity so re-running
the DDL is idempotent; EXEC-wrapped so it compiles at run time. Closes review
finding #7.

Existing builder-vs-migration assertions are substring (Contains idx_..._
CausationId) and stay green. MsSql outbox project builds clean net9.0; DB-backed
run deferred (no MSSQL container this session).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(spec-0027): document probe/timeout limitations and IRequestContext break (PR #4067 review)

Documentation-only follow-ups from the review:

- #6: note on IRelationalDatabaseOutboxCausationQueries.CausationColumnExistsCommand
  that the backend probes (OBJECT_ID / to_regclass / information_schema) resolve
  against the connection default schema, so a box table in a non-default schema can
  read as "column absent" -- a shared limitation of the unqualified box SQL, not a
  regression.
- #8: remarks on DynamoDbInbox.GetCausationId(Async) that timeoutInMilliseconds is
  accepted for interface compatibility but ignored (DynamoDB handles timeout/retries),
  consistent with the other methods on the store.
- #9: remarks on IRequestContext.InstrumentationOptions + an ADR 0057 Negative-
  consequences note that adding the member is source/binary-breaking for external
  implementers (no default interface impl) and should ship in a minor bump.

#5 (ADR context_key tag) needed no change: the observability table already lists only
request.id / causation_id, matching the emitted tags.

No behaviour change. Core + DynamoDB inbox projects build clean net9.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): capture inbox span before await, warn on custom-context replay (PR #4067 review)

Addresses three points from the PR #4067 code review:

1. Async telemetry events were silently dropped on the Throw/Warn/Replay
   duplicate paths: RequestContext.Span is thread-affine, and the branches
   re-read Context?.Span after the ExistsAsync await, so on a thread-pool
   continuation (ContinueOnCapturedContext == false) the span was null and
   the AddEvent never fired. Capture the span once before the first await and
   reuse it on every path (Throw/Warn/Replay/Add). Mirror the single capture
   in the sync handler for parity.

2. Default causation id is command.Id, so a message id handled by multiple
   [UseInbox] handlers shares it and a replay resends all of their outbox
   messages. This is intended (replay all downstream effects to recreate the
   original state); documented the caveat in ADR 0057, noting UseInbox is in
   practice applied to a single-handler Command, and the Bag escape hatch.

3. A custom IRequestContext (not a RequestContext) degrades Replay to a silent
   no-op. Emit a one-time Warning (CustomContextDisablesReplay) the first time
   the fallback is hit while OnceOnlyAction.Replay is configured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* add review notes

* fix(spec-0027): guard DynamoDB replay against a missing causation GSI (PR #4067 review)

Addresses the second PR #4067 review:

1. DynamoDB ReplayCausationAsync (V1 and V4) queried the Causation GSI
   unconditionally. In the "inbox migrated, outbox not yet" mixed state
   (AC10) the Dynamo inbox is schemaless so GetCausationId returns a non-null
   id, the handler reaches ReplayCausationAsync, and a table provisioned
   before the Replay feature has no Causation GSI — so the query threw a
   ValidationException that unwound the handler pipeline on every duplicate.
   Gate replay on the memoized SupportsCausationTrackingAsync probe and
   degrade to a no-op, matching the relational path. Adds a regression test
   in both V1 and V4 that drives replay against a GSI-less table (6/6 green
   against DynamoDB Local).

3. Documented the sync-over-async in the NoSQL replay entry points
   (DynamoDbOutbox.ReplayCausation x2, FirestoreOutbox.ReplayCausation) with a
   comment explaining why they block on the async-only SDKs and that this
   matches the existing convention in those classes.

(#2 no change: PROMPT.md is still ignored via the "AI agent session state"
.gitignore entry; the branch only dropped a duplicate copy.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec-0027): report replay no-op distinctly in telemetry (PR #4067 review #1)

WriteReplayEvent chose "Duplicate Replay" vs "Duplicate Replay Skipped"
solely on causationId being null. In the mixed-migration state (inbox…
iancooper added a commit that referenced this pull request Sep 10, 2026
Answers the fifth review. Its two headline findings were both real.

1. The documented default could not connect. SampleDatabase.DefaultConnectionString
   omitted TrustServerCertificate, and Microsoft.Data.SqlClient defaults Encrypt to
   true from 4.0 onward — this repository pins 7.0.2 — so against a default SQL
   Express instance with a self-signed certificate, login fails validation. Every
   other MSSQL connection string in the repository carries the opt-out. The review's
   diagnosis of WHY I missed it is the sharp part: I tested through
   ConnectionStrings__Brighter against a container, which is exactly the path that
   bypasses the constant. The fallback, which is what a reader hits first, was the
   one path never exercised. It also notes this default was previously unreachable
   because the receiver threw first, so this PR is the first time it is on the happy
   path.

2. EnsureQueueTable was check-then-act and raced in precisely the scenario it was
   added for. All four applications run it at startup against one shared database,
   and "either can be started alone" invites starting both — two instances both read
   0, both CREATE, and the loser dies with SqlException 2714. Box Provisioning is
   safe here because it takes MsSqlAdvisoryLock; the queue has no equivalent. Now one
   batch with IF NOT EXISTS guards, plus a catch for 2714/1913 so a genuine race
   resolves as success.

3. It also no longer create-once. The index is guarded independently, so a table
   that exists without its index gets one — including a table left by the old
   BrighterSqlQueue.sql, whose index had a different name. And both guards test
   SCHEMA_NAME() rather than a literal dbo, because GetDDL emits an unqualified
   CREATE TABLE that lands in the caller's default schema: a login whose default is
   not dbo would never match its own table and would re-attempt the create forever.

5. SampleDatabase.Name had no callers while the string it holds was still duplicated
   in DefaultConnectionString. The default is now built from it.

6. Events declares Microsoft.Data.SqlClient rather than inheriting it transitively.

8. Both receivers call ValidatePipelines(). The subscription comment cited
   ConsumerValidationRules.PumpHandlerMatch, which only runs behind that opt-in call
   and was therefore describing a check that was not running. Now it is.

Plus the README the review asked to pull in rather than defer, and two nits: the
host is disposed, and an unused using is gone.

Proven on a fresh SQL Server 2022 container, the .sql run once, no manual table
creation, committed source:

  two receivers started CONCURRENTLY on a database with zero tables:
    instance A: 0 race errors, instance B: 0 race errors
    tables: __BrighterMigrationHistory, InboxMessages, QueueData
    IX_QueueData_Topic present exactly once
  convergence: index dropped by hand, recreated on next start, 0 errors
  round trip: delivery 1 handled once, delivery 2 of the same id handled zero times
    with "has already been seen"; Inbox rows 1; 0 validation errors

Deferred on instruction: #9, a test pinning the ChannelFactory downcast. Adding
tests to samples is a policy change wider than this PR, and it is filed as #4339.

Not fixed: #7, Events now carries infrastructure as well as contracts. Noted as
non-blocking by the review and left rather than split, since all four applications
need the provisioner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146UueHL6H3zGBGTYwz7GtL
iancooper added a commit that referenced this pull request Sep 11, 2026
Answers the fifth review. Its two headline findings were both real.

1. The documented default could not connect. SampleDatabase.DefaultConnectionString
   omitted TrustServerCertificate, and Microsoft.Data.SqlClient defaults Encrypt to
   true from 4.0 onward — this repository pins 7.0.2 — so against a default SQL
   Express instance with a self-signed certificate, login fails validation. Every
   other MSSQL connection string in the repository carries the opt-out. The review's
   diagnosis of WHY I missed it is the sharp part: I tested through
   ConnectionStrings__Brighter against a container, which is exactly the path that
   bypasses the constant. The fallback, which is what a reader hits first, was the
   one path never exercised. It also notes this default was previously unreachable
   because the receiver threw first, so this PR is the first time it is on the happy
   path.

2. EnsureQueueTable was check-then-act and raced in precisely the scenario it was
   added for. All four applications run it at startup against one shared database,
   and "either can be started alone" invites starting both — two instances both read
   0, both CREATE, and the loser dies with SqlException 2714. Box Provisioning is
   safe here because it takes MsSqlAdvisoryLock; the queue has no equivalent. Now one
   batch with IF NOT EXISTS guards, plus a catch for 2714/1913 so a genuine race
   resolves as success.

3. It also no longer create-once. The index is guarded independently, so a table
   that exists without its index gets one — including a table left by the old
   BrighterSqlQueue.sql, whose index had a different name. And both guards test
   SCHEMA_NAME() rather than a literal dbo, because GetDDL emits an unqualified
   CREATE TABLE that lands in the caller's default schema: a login whose default is
   not dbo would never match its own table and would re-attempt the create forever.

5. SampleDatabase.Name had no callers while the string it holds was still duplicated
   in DefaultConnectionString. The default is now built from it.

6. Events declares Microsoft.Data.SqlClient rather than inheriting it transitively.

8. Both receivers call ValidatePipelines(). The subscription comment cited
   ConsumerValidationRules.PumpHandlerMatch, which only runs behind that opt-in call
   and was therefore describing a check that was not running. Now it is.

Plus the README the review asked to pull in rather than defer, and two nits: the
host is disposed, and an unused using is gone.

Proven on a fresh SQL Server 2022 container, the .sql run once, no manual table
creation, committed source:

  two receivers started CONCURRENTLY on a database with zero tables:
    instance A: 0 race errors, instance B: 0 race errors
    tables: __BrighterMigrationHistory, InboxMessages, QueueData
    IX_QueueData_Topic present exactly once
  convergence: index dropped by hand, recreated on next start, 0 errors
  round trip: delivery 1 handled once, delivery 2 of the same id handled zero times
    with "has already been seen"; Inbox rows 1; 0 validation errors

Deferred on instruction: #9, a test pinning the ChannelFactory downcast. Adding
tests to samples is a policy change wider than this PR, and it is filed as #4339.

Not fixed: #7, Events now carries infrastructure as well as contracts. Noted as
non-blocking by the review and left rather than split, since all four applications
need the provisioner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146UueHL6H3zGBGTYwz7GtL
iancooper added a commit that referenced this pull request Sep 12, 2026
Closes the eight open round-3 findings on 0071. No decision changes;
finding 1 was closed earlier by round-3 decision 1 (c2098a1).

- #2 states the external blast radius of breaking the handler factory
  family, and why 0070's "no known public implementations" argument does
  not transfer: IAmAHandlerFactorySync's own documentation tells clients
  to implement it (:32-34).
- #3 stops citing NFR-1(c) and AC-24 as covering breaks they do not.
  NFR-1's freeze names six interfaces, neither of these among them;
  AC-24 is written over factory interfaces and IAmALifetime is not one.
  Stated as the amendment it is, at eight interfaces of which three are
  not factories — matching 0070 step 7a's superset entry.
- #4 says plainly that ServiceProviderPipelineScope's wrapped lifetime
  scope is its creator's lifetime, not always Scoped as 0070 describes,
  and adds the missing touched-table row.
- #5 replaces the dangling Technology Choices cross-reference with the
  three inline declarations, and "four" with three.
- #6 reduces the nine-citation forces bullet to its claim; the citations
  now live once, in Technology Choices, with their files named.
- #7 states in step 4 that neither Release overload is modified, and why
  deleting the ReleaseLifetimeScope call would break the fallback path.
- #8 narrows "does nothing" to "does nothing to the handler".
- #9 reorders the mechanism diagram's lifelines to match the today
  diagram, so only the fourth changes identity.

Verified: 3/3 diagrams render (30/31/21 KB), escaped entities 0, every
new citation resolved against source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Closes the eight open round-3 findings on 0073. Findings 1 and 6 were
closed earlier by round-3 decision 3 (b7dc885). No decision changes.

- #2 stops asserting HttpRequestScope.Services is "Never null" and makes
  it true instead: HttpContext.RequestServices is null on a
  DefaultHttpContext with no IServiceProvidersFeature, so the provider
  now null-checks it as well as HttpContext, and the contract row states
  an obligation the implementation guards rather than an invariant it
  assumes. Without it a NullReferenceException reaches the Send caller
  through 0072's probe — the outcome FR-23 forbids.
- #3 adds step 4a: no test project in the repository can host a
  controller action (none of the 38 references Microsoft.AspNetCore.*,
  and Brighter.slnx has no entry), yet ten cited ACs need one. Names the
  new test project, its CPM entry for Mvc.Testing, and the slnx entry,
  and qualifies step 1's "no Directory.Packages.props entry" as a claim
  about the package and not the test host. Fixture layout is left to
  /spec:tasks, and says so.
- #4 halves FR-15 as FR-17 is halved: this ADR discharges the
  package-inertness half; the default-value clause is 0076's and the
  no-adoption behaviour is 0072's ladder.
- #5 writes ServiceCollectionExtensions.cs:119 explicitly — the bare
  form chained off BrighterOptions.cs, where those lines are unrelated.
- #7 attributes the provider's AlwaysNew obligation to FR-10, not
  FR-24.4, which is Brighter's half.
- #8 stops citing NFR-8 as if it established vocabulary; both claims now
  rest on ADR 0067's Terms block, and NFR-8 is cited where the
  documentation obligation is actually discharged.
- #9 adds <param>, <returns> and <exception> to the illustrative XML doc,
  which an implementor will copy verbatim.
- #10 "the Microsoft extension surface" -> "the framework or extension
  surface"; OpenTelemetry is not a Microsoft package.

Verified: 2/2 diagrams render, escaped entities 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Closes all twelve open round-3 findings on 0074, the round's biggest
batch. No decision changes.

- #1 narrows the Positive claim that an application never receives two
  errors to those two errors; FR-22.4 is also an Error and may accompany
  either, as the rules section already says.
- #2 "a fifth rule ... a wider fourth one" -> sixth/fifth, a residue of
  the five-rule set that predates FR-22.4.
- #3 five -> six for both the host-shape count and the FR-22.4 walk, and
  row 6 stops being silently omitted from it.
- #4 shows the guarded registration form. mapperRegistryFactory is
  deliberately nullable and PipelineValidator gates the wrap-transform
  rule on its null-ness, so the unconditional Lazy either threw or ran a
  rule in hosts where it does not run today — while the ADR claimed no
  behaviour change. Says what the exclusion set does with no registry,
  and keeps the forcing cost disclosed.
- #5 uses the slug at every mention of 0053/0064/0054 rather than
  declaring bare numbers avoided and then using them; C-16 assigns bare
  "ADR 0064" to a different file.
- #6 moves the eleven-row FR-25 clause map out of ## Context into its own
  "The documentation this set owes" section. Context is back to 12 lines,
  in line with every sibling.
- #7 gives FR-22.4 the same snapshot-ordering precondition the ADR states
  for FR-24.3; without it the natural fluent form loses the rule.
- #8 drops the claim that AC-42's [UsePolicyAsync] clause pins which
  mechanism excluded the handler — both paths yield no warning on an open
  generic. States the real reason the exclusion runs first.
- #9 partitions FR-22.3's inputs the same way in all four places: it
  reads both axes, where FR-22.1/22.2 read lifetimes and
  FR-22.4/FR-24.3/FR-17 read descriptors.
- #10 renames ScopeProviderRegistration to DescriptorRecord and says in
  Technology Choices why one record serves three service types.
- #11 renames the problem section to a behaviour, per the skeleton.
- #12 reconciles "the fifth" with "written last" — dependency order
  against numbering, with the six named.

Verified: 2/2 diagrams render, escaped entities 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Round 4's mechanical bucket for 0070 (§17.5). Nine findings, none needing a
decision; #1 and #2 were closed by round-4 decisions 1 and 8 already.

- #3 (70) `ServiceProviderPipelineScope` was "configured `Scoped`" in one
  paragraph and "not hard-wired to `Scoped`" two paragraphs later, and 0071
  had to correct it in prose. The type is now specified once, over its
  creator's configured lifetime and isolate-transient flag, with the note
  that on a transform pipeline that lifetime is always `Scoped`.
- #4 (66) AC-1 appeared nowhere in the ADR and AC-2/3/4/21/23 only in
  References. New step 9a maps each of the six to what it falsifies and to
  where the mechanism makes it observable; AC-1 added to References.
- #5 (64) Step 3's sample carried a `catch (AmbientScopeSourceException e)`
  for a type ADR 0072 introduces, and duplicated an edit 0072 step 1b
  claims. The sample now shows only the clause this ADR's commit contains,
  and a following paragraph hands the discriminating clause to 0072. The
  contract table marks the ambient half as 0072's widening.
- #6 (62) The mapper factories are not built inside `MessageMapperRegistry`,
  and `:807` is a mapper factory, not one of the transformer helpers the
  clause cited it for. Corrected to `:945`, `:957`.
- #7 (62) Step 4's two failed-build branches emit different messages and the
  ADR never said which AC-6 pins. Step 4a now states both branches and pins
  AC-6 to the pipeline-not-constructed one.
- #8 (62) "Nothing in this ADR changes when [0072] arrives" is contradicted
  by 0072, which relocates the cache and changes its fault behaviour. Now
  claims the outcome, not the mechanism.
- #9 (60) NFR-1's withdrawal and AC-24 name six interfaces that do not
  include the two mapper registries. The extension to them is now argued in
  0070's own voice, in the forces and in Negative, with step 7a stated as a
  deliberate superset of AC-24.
- #10 (45) s/step 7/step 7a/ for the release-note cross-reference.
- #11 (35) The `Log` row cites `:409`/`:318` as "beside
  `FailedToCleanUpAfterFailedBuild`" rather than as the class declaration.

Frontmatter untouched, so no index regeneration. Escaped entities 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Round 4's mechanical bucket for 0071 (§17.5). Eleven findings; #3 was closed
by round-4 decision 2 already.

- #1 (88, the round's highest) and set #1 (85) The ADR claimed a requirement
  in its Scope and denied it twice — the sibling-map positional sentence and
  a Negative bullet, both written before round-3 decision 1 gave it FR-13's
  handler clause. Both now say it discharges one clause and is otherwise
  structural.
- #2 (84) 0070 now specifies `ServiceProviderPipelineScope` over its
  creator's lifetime, so this ADR restates rather than widens; the touched
  table's "widened" row goes with it.
- #4 (72) Four threading methods were named where six exist —
  `AppendToAsyncPipeline` (`:451`) and `PushOntoAsyncPipeline` (`:525`) were
  absent from every list — and "four"/"six"/"eight" were used for
  overlapping enumerations. Now six threading methods plus two resolution
  helpers, eight in all, consistently.
- #5 (70) The amendment the ADR says it owes is owed to AC-14's "Explicitly
  NOT excluded" clause, not to AC-9; AC-9's own end-to-end `Send` still runs
  over the handle path, so "guarded only where it no longer applies" was
  false. AC-14 added to References.
- #6 (66) FR-7's "not re-implemented differently" clause is now quoted
  verbatim in the forces, with the reading this ADR takes stated and the
  consequence of the other reading named.
- #7 (66) 0070 and 0075 say the superset ledger satisfies AC-24; 0071 said
  AC-24 needs widening. Aligned on the superset position — no requirements
  amendment owed.
- #8 (64) A `Transient` handler pipeline holds a non-null `PipelineScope`
  while FR-27.1/AC-46 say it "takes no pipeline scope". The contract table
  now states that AC-46 is asserted over the ambient recorder and never over
  the property's nullness.
- #9 (62) `IAmALifetime.PipelineScope`'s XML doc said "owned", which 0072's
  borrowed ambient falsifies. Now matches the factory member's wording.
- #10 (60) Five `file:line` citations in one Negative bullet, against the
  one-per-bullet rule. Four moved into step 2, where an implementor needs
  them.
- #11 (40) `ControlBusHandlerFactory.cs:6`, not `ControlBusHandlerFactorySync
  (:6)` — the file does not carry the type's name.
- #12 (30) The third lifeline also changes label, so "only the fourth changes
  identity" now reads "role", with the relabelling explained.

Frontmatter untouched, so no index regeneration. Escaped entities 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Round 4's mechanical bucket for 0073 (§17.5), plus set #10 and set #11.
#3 (FR-15's normative clause) was closed by round-4 decision 6 already.

- #1 (72) The public/internal decision led with AC-18, which its own
  parenthetical falsifies: the recorder resolves `IEnumerable<IAmAScopeProvider>`
  and selects the entry that is not itself, so it never names this type and
  builds against an internal one. NFR-7 and AC-19/AC-29's implementation-type
  assertion now carry the decision; the AC-18 claim is corrected in place.
- #2 (70) Step 4a sent AC-14 to the new ASP.NET test project. AC-14 is a
  whole-suite regression criterion with no controller, no `HttpContext` and no
  provider, run over the existing projects. Nine criteria, not ten, and what
  AC-14 actually needs is stated separately.
- #4 (66) The bold Decision sentence claimed a public surface the next
  paragraph withdrew, and the frontmatter `summary` published the same claim
  into the generated index where the retraction does not travel. Both now say
  "opt-in surface": one method, no type named. Index regenerated in this
  commit — clean 1/1, counter unchanged at 98.
- #5 (65) `HttpRequestScope` was specified as holding an `HttpContext` and
  reading `RequestServices` on demand, so 0072's non-null obligation rested on
  a construction-time check of a settable property backed by a replaceable
  feature. It now captures the validated `IServiceProvider`, which makes the
  obligation structural. Role table, contract row, diagram node and step 1
  moved together.
- #6 (62) / set #10 (52) 37 test projects, not 38 — the 38th solution entry is
  `tests/README.md`. And `Brighter.slnx` does have an ASP.NET entry:
  `src/…ServiceActivator.Control.Api`, which this ADR itself cites two sections
  later. Both corrected; the conclusion is unaffected.
- #7 (61) The mechanism diagram had two null branches where the mechanism has
  three, and the missing one is the check the ADR twice calls load-bearing.
  Added; both diagrams re-rendered under mermaid-cli@11.
- #8 (55) FR-10 cited three times for D16's `AlwaysNew` rule, which FR-10 does
  not state. Now D16, with a note that the requirements make the same
  attribution twice.
- #9 (52) / set #11 (48) References gains AC-15, AC-16, AC-17 and AC-34, which
  step 4a relies on, and AC-22.2 is cited beside the NFR-2 claim it
  mechanically guards — AC-22.3, the only clause cited before, is the
  core-source guard and a different thing.
- #10 (48) FR-19 permits two log entries, not one; "the sole permitted
  difference" is now "the only difference in this case". Two places.

Escaped entities 0. Mermaid 2/2 render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Round 4's mechanical bucket for 0074 (§17.5). #4 was closed by round-4
decision 7, which dropped `SpecificationEvaluator` entirely.

- #1 (70) The exclusion set's request-type sourcing was stated twice and the
  two disagreed: three sources became two, and "both halves are walked over
  request types" is false — `Describe()` is parameterless and enumerates the
  subscriber registry itself. Step 5a now states it once, matching the
  captive-dependency section, and names the test that separates the two
  readings (AC-42's transform clause in a host with null publications).
- #2 (65) FR-17 borrowed FR-24.3's registration-position fallback, but its key
  is a value and two positions are always distinct — so the fallback turned
  the idempotent repeat FR-17 exempts into a Warning, which AC-49's third
  branch falsifies. Dropped: an unreadable descriptor contributes nothing, and
  the obligation to register an instance falls on the registrar.
- #3 (65) FR-22.4 was absent from the snapshot-staleness Negative bullet and
  from the Risks table, though it is the rule most exposed to it — the
  registration it hunts is written beside the other `services.Add*` calls,
  after a fluent `ValidatePipelines()` has snapshotted. Named in both, and
  what "called last" has to mean for AC-50's after-ordering branch to be
  reachable is now stated.
- #5 (62) "Six DI-package implementation types" — the constructor names two.
- #6 (62) Step 5a claimed AC-42's `[UsePolicyAsync]` clause is pinned on the
  handler half, which the captive-dependency section denies in terms. The
  claim goes; the nullable `registry` keeps the honest justification the ADR
  already had.
- #7 (60) The FR-25 clause map sat in `## Context` as a `###` section that
  decided nothing and said so, in implementation-plan terms the tone rules
  exclude. The map moves to Implementation Approach step 7 beside the
  documentation this ADR already owes; the ownership sentences go. 0074 now
  has the same `###` skeleton as its siblings.
- #8 (55) "Any host with `TransformerLifetime = Singleton`" overstates it —
  candidates come from the snapshot and `AutoFromAssemblies` filters out
  `Paramore.Brighter*`, so the transform must be registered explicitly. Noted
  for AC-42's prefix case too.
- #9 (50) The two-kind de-duplication rule had no owner. It is
  `ContainerRegistrationSnapshot`'s, applied where the candidate list is built.

Frontmatter and both mermaid blocks untouched. Escaped entities 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
The cross-ADR remainder of round 4's mechanical bucket (§17.5). Set #1, #4,
#7 and #10/#11 landed with their per-ADR commits; #5 and #6 were closed by
round-4 decision 6.

- set #2 (72) The ledger said "nothing in this repository implements
  `IBrighterOptions`"; one class does — `BrighterOptions` — and 0076 said
  both things in one sentence. The distinction is load-bearing: one
  implementation must be edited in the same commit, and nothing *breaks*.
- set #3 (70) ADR 0072 was the only break-bearing sibling with no ledger entry
  and no break statement, while changing shipped behaviour: #4260's eviction
  applies to the owned `Scoped` path too, so a host that never opts in sees a
  faulted resolution retried where today the remembered fault is rethrown.
  Step 7a gains a "Behavioural, ADR 0072" bullet — the ledger is now ELEVEN —
  and 0072 carries the reciprocal sentence (previous commit).
- set #8 (62) ADR 0071's "Nothing else in `PipelineBuilder` changes" made an
  exception for 0072's `catch`-filter edit and omitted 0075's constructor
  parameter and build-loop brackets. Both siblings are now named, in the
  touched table and in step 3, so the class reads the same from every end.
- set #9 (60) FR-13 was asserted to be "split three ways" by 0070, 0071 and
  0072 while its lead clause — Brighter releases every scope it created — went
  to nobody, and 0072's share was labelled with a clause FR-13 routes to FR-12
  in terms. It divides **by family, not by clause**: both clauses to 0070 for
  transform pipelines and to 0071 for handler pipelines, with FR-12 carrying
  the borrowed-scope carve-out in 0072. The two ADRs that used FR-13 as the
  model for FR-17's genuine three-way split now say what the model is.
- 0076 #7 (60) ADR 0073 called itself "one of ADR 0076's two callers"; there
  is one opt-in caller and 0076 says "first" everywhere. Corrected in 0073.

Verified after the whole bucket: seven ADRs, all `Proposed`, all in
`.adr-list`; sibling maps identical modulo the *(this one)* marker; the
unifying sentence in all seven; escaped entities 0 in all seven;
`docs/adr/index.md` byte-identical to a fresh regeneration at 98.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
#1 (92, Critical) the type-touch row saying PipelineValidator "calls
the extracted evaluator" is deleted — it was a relic of an abandoned
extract-to-core design and contradicted the row above it, step 1 and
the whole of Technology Choices. #2 (85) "plus one in core" -> "and
none in core". #3 (72) the failure-mode table gains the row C-20(i)
requires: a widest constructor MS DI would not select can warn WRONGLY,
so the count of report-wrongly rows is three, not two, and "all
reportable as misses" is corrected. #4 (70) NFR-9's exclusivity claim
restated as ownership, matching decision 6. #5 (68) the snapshot
collapses to one ArtefactRegistration per TYPE carrying its set of
kinds, so FR-22.3's de-duplication actually composes. #6 (65) the four
IBrighterOptions registration citations now describe the state after
ADR 0076 step 3 deletes them. #8 (58) AC-42 tests two of D15's three
cases, not three. #9 (56) BrighterOptionsRegistration named as the
carrier, and as a descriptor in the snapshot. #10 (55) the real reason
instance-registered artefacts are excluded. #11 (54) the duplicated
Specification/ValidationResultCollector commentary node and its solid
edge removed from the diagram; both diagrams re-rendered. #12 (52)
thread safety stated on the Validate() contract row. #13 (50) AC-24
moved onto the C-18 item. #14 (45) AC-44's obligation stated
accurately. #16 (40) reference-list drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
#1 (85, found independently by the 0073 and set-level reviewers) the
parenthetical asserting FR-10 does not contain the AlwaysNew rule is
replaced by the rule as FR-10 actually states it. requirements.md:215
carries it verbatim, and the two clauses this ADR called
misattributions — FR-27.1 and FR-24.4 — cite it correctly. #2 (78)
AC-29 dropped from the public-accessibility argument and from the
host-needing list: its provider CAPTURES a resolution source, in a
console host of AC-35's shape, and FR-23's condition does not arise for
this package at all — which this ADR already said twice elsewhere. The
count therefore goes to EIGHT. #4 (65) "the ten criteria" corrected —
round-4 residue, left behind when that round dropped AC-14 and changed
ten to nine. #3 (68) the "holds without exception across the
repository" claim narrowed to what reproduces, naming the two src
packages that are counterexamples to the broad form. #7 (55) states
that this ADR contributes no ledger entry, which every sibling states
for itself. #5 (65) and #6 (62) review-loop aside and /spec:tasks
reference removed. #8 (52) C-11 heading, #9 (48) D11 paraphrase, #10
(48) the NFR-8 sentence copied from a sibling where it applied.

Both diagrams re-rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
#2 (65) the two references to an earlier revision of this ADR and the
one to the authoring process are gone; the durable statements stand on
their own. #3 (62) Alternative 3a is no longer rejected on NFR-7, which
it satisfies completely — NFR-7 needs a public READ — but on the two
grounds that actually apply: no InternalsVisibleTo in this repository,
and the host case needs the write. #4 (60) step 5's heading and topic
sentence carry the conditional FR-9(i) uses, so a reader stopping there
does not conclude the shipped design leaks between subscribers. #5 (58)
work a subscriber's handler deliberately branches stays suppressed for
its whole life — by design under D6, failing toward isolation — stated
beside the unrelated-thread-pool case that does not. #6 (56) the "costs
nothing" claim is priced on the consumer path, where every MT_EVENT
message publishes. #8 (55) FR-9(b)'s per-task bracket shape is named
and rejected on its cost. #9 (54) the public mutator's named misuse is
turned into the documented recipe — take and dispose on the starting
flow. #10 (52) the break's comparator corrected to 0070's six
constructors, which is the same shape rather than the source-and-binary
interface break. #12 (45) AC-39's leak clauses, not its final clause.
#13 (38) 0067's Terms preamble now names 0070-0076.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
#1 (68) BrighterOptionsRegistration gains a declaration and a stated
readable surface. It was the only new type in the ADR with neither, and
it is the sole input to ADR 0074's FR-22.4 rule — the one rule whose
wrong answer is a false Error that fails startup, and the one 0074 says
it defines nothing about. Identity is reference equality against the
descriptor object services.Add received, which survives 0074's snapshot
because that snapshot copies the list, not the descriptors.

#2 (68) the risk table no longer claims AC-45 asserts the
before-ordering: AC-45 fixes no placement for the extension call, so
AC-48's second clause carries it on one path and the other three hold
structurally, through the single write-through definition reading the
override at resolution. #3 (62) "All four sites apply it" corrected to
four PATHS through one site, matching the Decision that deletes three of
them. #4 (60) the IBrighterOptions break attributed to step 7a's
superset rule rather than to AC-24. #5 (57) thread safety names both
routes by which a partially-applied options object is observable. #6
(55) the IAmConsumerOptions reader claim narrowed to what reproduces —
BuildDispatcher also reads InstrumentationOptions and ShutdownTimeout;
the load-bearing half, that none downcasts, is unaffected. #7 (52)
AC-45's clauses cited by content rather than by ordinal, which named
two different clauses in two places. #8 (50) citation density. #9 (45)
the third diagram's three edges are labelled and the read is dotted, so
a read is no longer drawn as a write.

All three diagrams re-rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
#2 (68) the finalizer path is addressed: the third drain step is
skipped there, because DisposeScope blocks on GetAwaiter().GetResult()
for an IAsyncDisposable scope — which every MS DI scope is — and
running it on the finalizer thread would stall all finalization. ADR
0068's best-effort-safety-net rule licenses the skip. #3 (65)
ServiceProviderPipelineScope's accessibility is stated: public with an
internal constructor, public because 0072 type-tests it across the
package boundary and NFR-7 makes a third-party container package a
first-class implementer, internal on the constructor because
ServiceProviderLifetimeScope is internal sealed and CS0051 would
otherwise bite — the rule 0074 works around for its own validator. This
also settles the disagreement 0072 showed between its own two
statements.

#4 (60) step 6's enumeration of where a blocking release is still taken
now names all three, including the two that release an ASYNC transform
pipeline's scope synchronously — CleanUpAfterFailedBuild's private void
and OutboxProducerMediator's Call reply path. #5 (60) step 9a gains a
row for the completed-transform-pipeline drain, the one behaviour here
no AC reaches, recorded as a design-owed test rather than left
unguarded. #8 (42) duplicated clause, #9 (38) the async row made
parallel to the sync row it mirrors.

set #9 / 0071 #10 (52): 0070 said AC-33 "is named in 0071 and not here"
and then named it twice; softened to "discharged in 0071 and only cited
here", which is what the References list and the step-4a paragraph do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
set #6 (70), and its per-ADR forms 0071 #6, 0072 #5, 0073 #5, 0075 #2 —
the five-way convergence. Every reference to what an earlier draft or
revision of an ADR said is gone; grep across the set now returns zero.
The durable statements were already present in each case, which is what
made these removals safe.

set #8 (62) every ADR's ## References now carries the same list of the
other six, with the description each sibling map already uses. Coverage
ranged from all six (0076) to none (0070), and the omissions dropped
precisely the load-bearing hand-offs — 0074 omitted 0075 while twice
naming it as a source, and 0075 omitted 0074 while naming it as the ADR
that declares the guidance page.

0071 #4 (65) FR-5 is no longer cited as authority for a handler
pipeline's execution-path teardown. FR-5 governs a failed TRANSFORM
pipeline build and is 0070's; FR-6 carries the argument on its own, and
the ADR now says so rather than citing a requirement it does not sit
under. 0071 #9 (45) the routing claim corrected — the two handler sites
resolve directly from Build/BuildAsync and pass through none of the six
threading methods. 0071 #12 (30) the TryRemove line.

Verified after the bucket, by running the checks: seven files, all
Proposed, all in .adr-list; sibling maps identical; unifying sentence in
all seven; escaped entities 0; docs/adr/index.md byte-identical to a
fresh regeneration at 99; all 15 mermaid diagrams render; AC-1…AC-51 all
cited, none out of range, none orphaned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
The 0070 slice of the round-6 mechanical bucket (§19.10). All three
rows under-stated their blast radius, seventeen for seventeen.

#6 (66) — step 4a's Warning enumeration is short by two. Recounted
against source: FailedToReleaseTransform exists on both transform
builders (TransformPipelineBuilder.cs:412, TransformPipelineBuilderAsync.cs:321)
and is emitted by ReleaseTransforms, whose guard step 4 already cites
at :215-223 for a different purpose. Eight messages, seven about a
mapper or transform release. The row named :331; the count is also
stated at :333, :335 and — never named by any row — at :202, the
IAmAScope error-conditions bullet, which said "raising the five that
exist". :263's touched-table row narrowed to "the two existing Warning
members" and had to widen once FailedToReleaseTransform is named in
the same file.

#7 (64) — Context's "every FR has exactly one owning ADR" is broken
two paragraphs later by FR-13 and is not the set's settled position:
0073:34 and 0074:32 both already say "FR-17 is split three ways — the
model FR-13 also follows, one requirement divided across the siblings
that each make part of it true". Weakened to that rule, in that
language, with FR-13, FR-15 and FR-17 named as the instances. The
invariant is stated in 0070 only, checked by grep rather than assumed.

#9 (60) — :207's "the four that create an artefact also take the
scope" is contradicted by the code block beneath it, the contract
table, the touched table and the diagram, all of which say six.
Counter-cases verified and left alone: :86, :392 and :450 say "four"
about NFR-1's four, and :236 about the four transform-builder catches.

Also corrected, found while verifying #6 and left wrong in only one
place would have re-created a cross-ADR split: FailedToDisposeScope is
declared at ServiceProviderLifetimeScope.cs:522, not :520, which is
the Log class's opening brace — fixed at 0070:333, 0070:395 and
0071:258. And FailedToReleasePipeline's three declarations are at
OutboxProducerMediator.cs:1449, Reactor.cs:638 and Proactor.cs:652;
0070 cited the attribute line while 0071 landed the declaration line
in 0f7c8fd, so 0070's three sites (:202, :270, :331) now match.

Index regeneration diff clean (no frontmatter touched); escaped-entity
grep 0; no mermaid block edited, so none re-rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Closes 0076 #1, #2, #3, #5, #7, set #1, set #2, 0072 #3, and the
factual half of gap C #4. One file, eleven edits, net -27 words.
No requirements revision, no index.md diff, no other ADR.

WHAT LANDED

- :39  FR-15's normative clause is guarded by AC-14, not AC-45 (set #2).
       AC-45's four hosts ALL call the extension, so none of them
       exercises the default; AC-18's two branches deliberately start
       from a non-default value too. AC-14 (FR-11, FR-15) is the only
       criterion whose Given leaves the affinity at its default.
- :47  FR-19's mechanism is ADR 0075's pump-flow bracket in
       Performer.Run(), and ADR 0072 discharges the requirement
       (0076 #1 / set #1 / 0072 #3). The wording is 0072:40's own -
       0072 already says "ADR 0076 supplies the property and its
       inheritance onto ConsumersOptions" - rather than a third
       statement of the division.
- :283 The stale FR-19 log bound ("at most two latched Warning
       entries") is DELETED rather than qualified (0076 #5). See the
       note on §19.9 row 5 below - this is what keeps the two
       independent.
- :287 "Step 7a enumerates the whole entry" -> "catalogues the whole
       entry and carries this break as a pointer to the Consequences
       bullet below" (0076 #2). The four items reached five of
       thirteen. The numeral is deliberately NOT moved to thirteen:
       0070:688 records that the count has gone 4->7->9->10->11->12->13,
       and S-a lesson 2 already established that a count in a pointer
       serves no argument.
- :340 "InternalsVisibleTo would serve the first caller and no other"
       -> the repository grants it nowhere (gap C #4).
- :402 The null-optionsFunc contract row no longer claims the guard
       "restores the failure shape - the same exception type, at the
       same point" (0076 #3). It states the two routes instead.
- :457 "(FR-19, D0b)" -> "(FR-19, ADR 0075)" - #1's derived statement.
- :514 The Negative bullet drops its five-of-thirteen enumeration.
- :562 Alternative 8 rejects on the repository rule, taking ADR 0075
       alternative 3's sentence VERBATIM, and drops the false "ADR 0075
       rejects InternalsVisibleTo for suppression on the same ground"
       (0076 #7 / gap C #4). 0075's ground is the rule; NFR-7 is
       explicitly secondary there.
- :581 References gains AC-14 (rule 21b) and loses C-2 and D0b, whose
       only body citation was :47's wrong mechanism claim. Both are
       still cited by 0072 and 0075, so nothing leaves the set.
- :588 The 0075 sibling gloss carries the pump-flow bracket as well as
       the InternalsVisibleTo rule - it is the line that summarises the
       relationship, and the relationship changed (S-d lesson 4).

WHAT THE ROW GOT WRONG

The row named eight sites; the batch edited eleven. The three it did
not name are :581 (References, both directions), :588 (the sibling
gloss) and :340's companion in the same finding.

THE RECOMMENDATION'S OWN EVIDENCE WAS WRONG TWICE (rule 19a)

1. 0076 #3's body says "each factory falls back to ServiceLifetime
   .Singleton". Recounted in source: FOUR do (mapper x2, transformer
   x2); ServiceProviderHandlerFactory.cs:50 falls back to Transient,
   and :51 falls back IsolateTransientHandlerScope to true. Its
   evidence block quotes the mapper factory alone and the headline
   generalises to five. Landed as "each of which then takes its own
   default lifetime", which is 0076:481's existing house phrasing.
2. The same finding says "the three GetRequiredService<IBrighterOptions>
   sites". There are five in src/, four of them reachable: :161, :169,
   :708 in the DI package and ServiceActivator :90; :79 is the circular
   lambda this ADR corrects and is never invoked. The numeral is
   dropped rather than corrected - 0076:395 already enumerates the
   three that its own argument is about, correctly scoped.

PROBED, ON THE PINNED M.E.DI 10.0.10 / net10.0

Reproduced the reviewer's result exactly: AddSingleton<T>(sp => null)
then GetService -> null, no throw; GetRequiredService -> Invalid
OperationException; with the ADR's ?? throw, both routes throw. One
fact the finding does not carry and the text deliberately does NOT
claim: today's GetRequiredService message is "No service for type ...
has been registered", which is false of a registered null-returning
factory. The replacement no longer asserts "the same exception type,
at the same point", so nothing rests on it.

§19.9 ROW 5 - THE ONE-COMMIT-OR-TWO CHECK §21.6 ASKED FOR: TWO

Checked, not assumed. #5's fix removes the numeral rather than
restating it, so 0076 now asserts NO consumer-side log count and NO
FR-19 mechanism of its own - row 5's amendment has nothing to re-tense
here. 0076's two AC-20 citations (:449, :529) are to AC-20's
mixed-host Given, not to its Warning-count Then, which is the clause
row 5 moves. The two are independent.

COUNTER-CASES VERIFIED AND LEFT ALONE

- gap C #4's second half - the same sentence repeated verbatim in
  0070:682, 0073:491, 0074:660, 0075:333 - is documentation.md:114
  HOUSE STYLE, bucket H, not a factual correction. Each is already
  true on the fact and each carries its own local argument. The
  unifying sentence is now byte-identical in the two ADRs that REJECT
  an option on that ground (0075:523, 0076:562); harmonising the
  appliers belongs in a bucket-H batch.
- 0076:43's "FR-16, FR-18, FR-19 and FR-23 by ADR 0072" stays true.
- 0076:96, :505, :527, :554 cite AC-45 for the four-path claim, which
  is what AC-45 asserts. Only :39's default-guard use was wrong.
- 0076:481's "falls back to a default" was already exact.
- 0076:449's "no criterion exercises a mixed host in both orderings,
  because AC-20 fixes one" survives row 5.
- No sibling recorded 0076's FR-19 error and none cites AC-45 as
  FR-15's guard - grepped across all six.
- 0076:455, :495, :532 point at step 7a without enumerating it.
- Below-threshold findings 0076 #9 (:96), #10 (:542), #11 (:285) and
  #12 (:389) are untouched and none is falsified by the new text
  (O-0075 lesson 1, run and clean).
- No mermaid block carries any identifier corrected here - all four
  grepped for FR-19, D0b, AC-45, AC-14, 7a, InternalsVisibleTo, null,
  thirteen and inert, zero hits (rule 12a).

BOOKKEEPING

requirements.md owes nothing and this was CHECKED: AC-14 and AC-45 are
correct as written, the InternalsVisibleTo rule is nowhere in the
document, and step 7a's catalogue is the ADRs' own. Seventh batch in a
row to owe nothing there; still not a rule. No frontmatter moved, so
no index.md diff was owed - the regeneration was run anyway and is
empty, still 99 indexed. Fences balance (16 in 0076); no mermaid block
touched and the set still holds 26. The 0076 row of "Where this ADR
sits" is still byte-identical across all seven and C-13's unifying
sentence is unmoved.

D8 MEASUREMENT - FLAT IN EVERY COLUMN

0076   156/2/0/123/32/4  ->  156/2/0/123/32/4   unchanged
set   1496/27/1/885/424/26 -> 1496/27/1/885/424/26  unchanged

Net -27 words. Every replacement is the same length or shorter except
:402 (+14, inside a table cell the instrument strips) and :588 (+7, a
References gloss that now carries two relationships). Bold was held
level deliberately: :47 bolds ADR 0075's as the mechanism, matching
:48's form, and UNBOLDS ADR 0072, which is now the discharger and
appears unbolded in :43's discharge list - a pointer carrying no
argument (S-d lesson 6). Alternative 8 keeps one bold run, as before.

Phase 4's instrument was reproduced FIRST and matched the RECORDED
BASELINE exactly on all seven rows at the review's own HEAD
(09856a6: 1468/27/1/864/400/26) before any edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Closes 0073 #1, #3, #4, #5, #7, #8, #9 and gap C #3, #5 — the whole
slice. One file, twelve sites, net -179 words. No requirements revision,
no index.md diff, no other ADR.

WHAT LANDED
- #1 the netstandard2.0 rejection rested on a false ecosystem fact.
  Both HTTP packages ship a serviced 2.3.x line with netstandard2.0
  assets — verified here from the local NuGet cache (2.3.12, lib/
  netstandard2.0, both packages), not taken from the finding. The
  bullet now says the line exists and is the ASP.NET Core 2.x branch,
  and the Negative bullet's "end-of-life 2.2.x" goes with it.
- #3 the It serves line's remaining half. S-d already named an owner
  per ID; what was left is that two siblings (0076 step 4, 0074's
  ladder row 11) attribute the three gestures to this ADR while this
  ADR attributed all of FR-25.11 to 0074. Stated in 0076:47's
  "what this ADR contributes" form.
- #4 "the only one an application author has to touch anything to use"
  is false of 0076's public property and 0072's provider.
- #5 + gap C #3 + gap C #5 are one paragraph — alternative 9 loses two
  grounds and its "decisive" claim.
- #7 FR-19 is consumer-side and bounds a difference between settings,
  not a host total; both citations now name FR-24's diagnostic model
  and D19, which is what actually bounds the count.
- #8 both length claims: AddBrighterHttpRequestScope and
  AddBrighterAspNetCoreScopes are both 27 characters (recounted).
- #9 the Decision's third paragraph deleted; Decision is now the house
  shape, one bold sentence and one paragraph.

WHAT THE ROW GOT WRONG
- Ten sites named, twelve edited. The two unnamed are both consequences
  rather than sites: "Three things make that the ONLY IMPLEMENTABLE
  choice" rests on #1's false fact and goes false when it is corrected
  (#1's prose flags it; its Evidence list does not), and deleting the
  Decision paragraph removed the ADR's ONLY pointer to alternative 9,
  which the "Why no criterion..." section does not carry. The pointer
  is restored in this file's own house form ("Alternative N records
  ..."), which it already uses six times.
- TWO FINDINGS IN ONE SLICE RECOMMENDED OPPOSITE TREATMENTS OF THE SAME
  NUMERAL. gap C #3's fix text writes "seventeen of the eighteen classes
  in the DI package are public" into ground 2; gap C #5 rejects ground 3
  precisely because the set adds nine more internals to that package and
  the ratio inverts. Both numerals were dropped and ground 2 points at
  ADR 0070, which owns the convention (0076 #2's lesson, and S-a's).
- gap C #3 also asserts in passing that "0073's own first ground is
  decisive by its own words" — that is the sentence #5 exists to
  falsify, and #5's evidence is the ADR's own section. Read the
  evidence, not the aside: demoted, not preserved.

COUNTER-CASES VERIFIED AND LEFT ALONE
- 0073:298's "an FR-19 violation in resolution and identity rather than
  in logging" cites FR-19's core clause, which 19.9 row 5 does not
  touch. FR-19 keeps one body citation, so its References entry stays
  (grep -no, counted in the body not the file).
- Dropping FR-19's log bound here makes this ADR independent of row 5,
  the O-0076 lesson-5 shape: a fix that removes a number cannot be
  falsified by a batch that changes it.
- The set-wide "Where this ADR sits" table row is byte-identical in all
  seven and is NOT what #4 touches — the per-ADR lead sentence is.
- 0074 ships no gesture: it extends the pre-existing ValidatePipelines()
  machinery of 0053/0064 (0074:61), so "the only one that ships an
  application-facing registration gesture" holds across the set.
- The InternalsVisibleTo sentence in ground 1 is kept verbatim: gap C
  #4's repeat-the-sentence half is bucket H (documentation.md:114) and
  20.2 bars it from this batch.
- 0073 #10, #11, #12, #13 (all below threshold) were each re-read
  against the new text; none is falsified by it. Rule 12a run: no
  mermaid block carries FR-19, a candidate name length, a ground count
  or the 2.2.x fact — the diff touches no diagram line.

BOOKKEEPING
requirements.md owes nothing and this was CHECKED against the document:
it attributes FR-25.11 to no ADR, names neither type nor any
accessibility of them, carries no candidate-name length claim, and its
one netstandard2.0 clause is AC-22's whole-solution build. Eighth batch
in a row to owe nothing there; still not a rule. No frontmatter moved,
so no index.md diff was owed — the regeneration was run anyway and is
empty, still 99 indexed. Fences balance (8 in 0073); no mermaid touched
and the set still holds 26.

D8 MEASUREMENT — no column rose, three fell.
0073 166/2/0/130/55/3 -> 165/1/0/124/55/3
set  1496/27/1/885/424/26 -> 1495/26/1/879/424/26
Phase 4's instrument was reproduced FIRST and matched the recorded
baseline exactly on all seven rows at the review's own HEAD
(09856a6: 1468/27/1/864/400/26), and today's set matched O-0076's
recorded post-landing figures before any edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
…atch O-0074)

Round 7's fourth ordinary slice, and the first ordinary slice to carry a
requirements revision. Closes 0074 #3, #4, #5, #7, gap B #3, gap B #5,
gap C #8 and set #10 - the whole slice, nothing left open.

WHAT LANDED - eleven sites in 0074, one clause in requirements.md:

 - :85  (set #10) the dependency-order list gains 0075, and says what it owes
        it: the guidance page this ADR schedules takes two clause families
        from 0075, which its own Scope (:49) already states.
 - :203 (0074 #5) the ents node's "the attribute half of FR-22.3's
        conjunction" -> "FR-22.3's exclusion conjunction".
 - :292 (0074 #3) new class-diagram edge ScopeConfigurationRules ..>
        ContainerRegistrationSnapshot. Rendered and read: the snapshot now has
        two incoming edges where the review found one.
 - :306 (0074 #5) the ArtefactExclusionSet roles cell states the whole
        conjunction it answers, not the attribute half.
 - :307 (0074 #3) ScopeConfigurationRules gains the snapshot as a collaborator.
 - :497 (gap B #3) the funnel gains the branch it was missing: ctor --"none - a
        tie, or no public constructor"--> skip2, and ctor --"one"--> parm.
 - :512 (gap B #5) the false "no single triple can serve a Singleton mapper and
        a Singleton transform at once" goes; the sentence now says what is true
        and shorter - AC-42 moves the Singleton between MapperLifetime and
        TransformerLifetime as it moves between kinds.
 - :534 (0074 #4 + gap B #3) the open-generic paragraph rewritten, -30 words.
 - :550 (blast radius of the row below) the "the rest are silent misses" lead
        gains the open-generic row's exception.
 - :553 (gap B #3) the open-generic failure-mode row says what happens.
 - :623 (0074 #7) "Every T in the repository today is a core type" ->
        "Every T a core signature names".
 - alternative 16 (gap C #8) - "leave it to the container's own ValidateScopes",
        with its ground measured rather than asserted.
 - requirements.md AC-42, revision 25 - gap B #5's source.

TWO FINDINGS IN THIS SLICE PRESCRIBED OPPOSITE PATCHES FOR ONE DEFECT, AND ONLY
READING BOTH CAUGHT IT (O-0073 lesson 1, second occurrence). 0074 #4 says the
funnel has no open-generic stage and the prose orders the exclusion against one,
and recommends ADDING the stage. gap B #3 says the funnel contradicts the
failure-mode row, and recommends FIXING THE SOURCE - no such rule exists.
Probed (net10.0): a generic type definition IS assignable to the non-generic
marker interface, and its widest constructor's concrete parameters are ordinary
types with descriptors - only a parameter typed by a type parameter has none.
So the funnel as drawn inspects open generics, correctly: an application's open
generic handler with a Scoped concrete dependency is a real captive dependency.
Adding the skip node would have invented normative behaviour no requirement
carries and would have made the rule miss it. The row is what was wrong.

AND THE PARAGRAPH IT SAT IN WAS WRONG ABOUT ITS OWN SUBJECT. :534 said
ExceptionPolicyHandlerAsync<> "would also be skipped by the open-generic rule
below, so on that type the two paths are indistinguishable". There is one path:
EnsureHandlerIsRegistered registers the OPEN definition as its own service type
(ServiceCollectionSubscriberRegistry.cs:74-77) and UsePolicyAsyncAttribute
.GetHandlerType() returns that same open definition (:80-83), so the candidate
and the excluded type are one type and the conjunction reaches it. Removing the
phantom second mechanism makes AC-42's [UsePolicyAsync] clause pin the exclusion
rather than fail to discriminate - the criterion got stronger, not weaker.

THE ROW NAMED TEN SITES AND THE BATCH EDITED ELEVEN. The unnamed one is :550,
the lead-in that classifies every failure-mode row as "wrong", "moot" or "a
silent miss": once the open-generic row records what the funnel DOES read, it is
none of the three. A row's own summary sentence is a site (rule 11).

ALTERNATIVE 16's GROUND WAS MEASURED, AND IT IS NOT THE ONE THE FINDING GAVE.
gap C #8 offers "ValidateScopes cannot see a Singleton-governed artefact never
registered as a service". Probed (MEDI 10.0.10, net10.0): every artefact IS
registered - at Transient, by Brighter's own builders - and it is Brighter that
holds a Singleton-configured one for the process. So ValidateOnBuild:true
reports NOTHING at start-up, while the same Scoped dependency under a genuine
Singleton DESCRIPTOR is caught. What is left is a resolution-time throw
("Cannot resolve 'X' from root provider because it requires scoped service
'Y'") on the first message, and silence where the container was built without
ValidateScopes. Lateness and reach are the grounds; completeness is not, and
the entry says so, because *Negative* records ValidateScopes as the complete
check and that sentence stands.

COUNTER-CASES VERIFIED AND LEFT ALONE. :781's Risks row keeps "an implementation
that drops the attribute half" - with the roles cell now stating the conjunction
that is the right half, which is finding #5's own point. :305's "the only role
in this table that reads the service collection" survives: the rules ask the
snapshot, the snapshot reads the collection. :544's constructed-generic
parameter reading is a different case from a type-parameter-typed one and is
untouched. gap B #3's third bullet - "no statically known implementation type is
only implicit in node k" - is left implicit deliberately: node k cannot test a
type that is not there, and :504 already says which of the two implementation
properties is read. The four below-threshold 0074 findings (#8 keyed rows -
already closed by S-a; #9 five bolds for three consequences; #10 the two-of-ten
Contract table; #11 the ents inventory; #12 the AC-40/41 guard list) were each
re-read against the new text and none is falsified (O-0075 lesson 1). Rule 12a
run: the only diagram hit for any corrected identifier is skip2["not inspected"],
which is the gov branch and is correct.

BOOKKEEPING. requirements.md is a revision and carries all three parts: header
24 -> 25 (checked against .requirements-approved, which recorded 24, before the
increment), the history row, and the approval note. Counts UNCHANGED at
27/10/54/124 - this removes a false reason clause and adds no rule, criterion or
scope, so it is alignment. The pair's construction is deliberately NOT rewritten:
a one-host {Transient, Singleton, Singleton} version is now visibly available and
was not taken, because the criterion's content was never at stake. No frontmatter
moved, so no index.md diff was owed - the regeneration was RUN anyway and is
empty, still 99 indexed. Fences balance (12 in 0074); all four mermaid blocks
render with mmdc and the two edited ones were rendered to PNG at 1600px and READ;
the set still holds 26.

MEASURED (D8). 0074 283/4/1/142/74/4 -> 284/4/1/144/74/4; set 1495/26/1/879/424/26
-> 1496/26/1/881/424/26. TWO COLUMNS ROSE AND BOTH ARE ONE NEW ALTERNATIVE: +1
block and +2 prose bold are the **16. ...** lead and its **Rejected ...**, the
form all fifteen existing entries take - exactly S-b's alternative-15 figure. It
was drafted at 160 words, over the >150 threshold, and TRIMMED to 145 rather than
defended; no other column moved and nothing else gained bold. Phase 4's
instrument was reproduced FIRST and matched the recorded baseline exactly on all
seven rows at the review's own HEAD (09856a6: 1468/27/1/864/400/26), and
today's set matched O-0073's recorded post-landing figures, before any edit.
The script and the three probes are this session's scratchpad count_readability.py,
genprobe/ and scopeprobe/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Round 7's fifth ordinary slice. Closes 0070 #1, #2, #3, #4, #6, gap C #1,
gap C #9 and gap C #10 - the whole slice, nothing left open.

WHAT LANDED - 0070 (sixteen sites) and 0075:476 (one), 17 insertions /
16 deletions, net +53 words.

- #1 The Warning ledger was eight/seven and is ten/nine.
  FailedToReleaseTransformerAfterInitFailure (TransformerFactory.cs:69,
  TransformerFactoryAsync.cs:69) is a mapper-or-transform release Warning
  on this build path, in two types :358 already lists as touched.
- #2 TransformPipelineDrain is internal static and holds no state, so the
  third step arrives as a third delegate. Step 5 now gives both new
  signatures; the touched table's "Both parameter lists are unchanged" is
  now "Each list gains one delegate and nothing else".
- #3 MessageMapperRegistry forwards (step 6); it is not one of the
  two-line no-op implementations. Step 2 and the Risks cell both narrowed.
- #4 The Create contract row promised "exactly its current behaviour",
  which step 9 abolishes; it now points at step 6's rule for that lifetime.
- #6 The roles table named the builders as the six interfaces' only
  caller. False of four: the registry forwards to the mapper factories and
  TransformerFactory[Async] calls the transformer factories' Create.
- gap C #1 The "No hidden state" Positive is falsified by 0075's three
  AsyncLocal brackets. Rewritten to the narrower true thing, and its
  generator - alternative 1's first ground - with it.
- gap C #9 Alternative 8 was "Rejected by the settled decision on C-8";
  C-8 (requirements.md:375) delegates that decision TO the ADR. Circular
  appeal deleted, the concrete reason leads.
- gap C #10 Alternative 1's third count objected to "a second public core
  type"; the set adds five. Dropped, "three counts" -> "two counts".

WHAT THE ROW GOT WRONG - it named ten sites and the batch edited seventeen.

- #1's row named three sites and said :456 "stands as written". It does
  not: its numeral IS the release-group count. Two more were unnamed -
  :300 ("leaves the seven that exist alone") and :458 ("The seven keep
  their level") - and alternative 11 carries the numeral four times, not
  once. Five sites, seven occurrences.
- #2's row named the table clause and step 5's lead but not the finalizer
  paragraph, whose bold lead asserted "Drain keeps its signature" and
  whose "a third parameter" is a FOURTH one once the fix lands. Rule 11:
  a paragraph's bold lead is a site.
- gap C #1 named 0070's bullet but not 0075:476, which quotes that
  bullet's title verbatim. Renaming the bullet falsified the sibling, so
  both ends are in this commit (rule 3).

TWO FINDINGS IN ONE PARAGRAPH, AND THE RECOMMENDATIONS WERE DRAFTS

- gap C #1 and gap C #10 both edit alternative 1. Composed from their
  evidence, not their patches (O-0073 lesson 1).
- gap C #1's own draft ground was wrong: I first wrote that a per-flow
  slot has no end tied to the pipeline's life, then found alternative 1's
  own text says the builder brackets it, so FR-5's release is reachable.
  The true ground is 0075:476's own distinction - a bit, not a resource:
  a leaked bit costs a missed adoption, a leaked scope is shared by two
  pipelines and disposed by whichever finishes first (rule 21, the house
  form already existed in the sibling).
- #6's recommendation was "split the row - one phrase cannot carry two
  families". DECLINED: there are three call patterns, not two (registries
  <- builders; mapper factories <- the registry; transformer factories <-
  the builder for CreatePipelineScope and TransformerFactory[Async] for
  Create), so a split still needs a compound phrase in the factory row and
  would duplicate a correct Responsibilities cell. One row, corrected.
- #1's recommendation put the new message in the third bullet
  (FailedToReleasePipeline). It is a different member on different types;
  it got its own bullet, which is what the +1 block below is.

COUNTER-CASES VERIFIED AND LEFT ALONE

- :396 "No ambient state anywhere ... nothing per-flow" - its subject is
  the scope-carrying mechanism, of which it is true (rule 14: narrower
  than the design, not false). :609 already states the correct set-wide
  form and NAMES 0075's flag as the only such mechanism in play; the
  Positive bullet was made consistent with :609 rather than restated.
- C-8 keeps eight body citations (:37, :41, :47, :297, :298, :299, :530,
  :656) after the appeal was deleted, so ## References owes no deletion
  (rule 21c, counted in the body).
- Residual "seven"/"eight" in 0070 are four different subjects: seven
  ADRs (:74), seventeen-of-eighteen classes (:385), the siblings' eight
  release-note items (:577) and eight broken interfaces (:579).
- Rule 12a run: no mermaid block carries a Warning count, a Drain
  parameter list or the renamed bullet. The sequence diagram already
  draws Pipeline->>Registry, which corroborates #6's fix. The class
  diagram's builder->transformer-factory edge glosses the
  TransformerFactory hop at the same level :593 already coexists with -
  unchanged before this batch and unchanged by it.
- O-0075 lesson 1 run: 0070 #8, #9, #10, #11, #12, #13, #14 and gap C
  #12, #14 re-read against the new text; none falsified. Clean for the
  third batch running.
- 0075:565's References gloss ("it removed all per-flow state from the
  design") stays true via :396 and :609 and was not edited.

BOOKKEEPING

requirements.md owes nothing and this was CHECKED against the document:
no log-message name or count appears in it at all, TransformPipelineDrain
/ Drain / releaseScope appear nowhere, "per-flow" and "hidden state"
appear nowhere, the fallback-contract wording appears nowhere, and its one
MessageMapperRegistry hit is C-17's registry-BUILDER file citation. Ninth
batch in a row to owe nothing there; that is now nine and it is still not
a rule. No frontmatter moved, so no index.md diff was owed - the
regeneration was run anyway and is empty, still 99 indexed. Fences balance
(14 in 0070, 12 in 0075); no mermaid block touched and the set still holds
26. HTML entities 0.

D8 MEASUREMENT - ONE COLUMN ROSE BY ONE

0070 259/3/0/112/82/4 -> 260/3/0/112/82/4; set 1496/26/1/881/424/26 ->
1497/26/1/881/424/26. The +1 block is the new ledger bullet: the family
gained a member, so the list did. No bold added in either column. Two
sentences I drafted crossed 40 words and were SPLIT rather than kept -
0070's 40+ sentence count falls 56 -> 55, so 0070 #7 (56, promoted into
Phase 5) is left slightly better, not worse (O-0075 lesson 6).
Phase 4's instrument was reproduced FIRST and matched the recorded
baseline exactly on all seven rows at the review's own HEAD (09856a6:
1468/27/1/864/400/26), and today's set matched O-0074's recorded
post-landing figures, before any edit. The 0075 row's apparent drift from
O-0075's note is 532b3ba, the owner-directed Terms block, and the
instrument reproduces that note's figure exactly at 18e2971.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Closes 0071 #1, #2, #3 and #4 - the whole slice, nothing left open. One
file, four sites. No requirements revision, no index.md diff, no other ADR.

WHAT LANDED

- #1 (63), :402. "What the ordering protects here is a factory this
  repository does not ship: a third-party Release that ... touches the
  handler instance" is false as a disjunction - SimpleHandlerFactory,
  SimpleHandlerFactorySync and SimpleHandlerFactoryAsync all dispose the
  handler inside Release (verified in source: SimpleHandlerFactory.cs:18-24
  and :27-33, SimpleHandlerFactorySync.cs:39-43, SimpleHandlerFactoryAsync
  .cs:51-58), and the same ADR says so at :592. The protected case is a
  CONJUNCTION - a factory that offers a handle AND leaves its Release work
  to do afterwards - and the reason the shipped ones show no order was
  never given: they return null from CreatePipelineScope() (:415, :416).
  Both halves now stated.
- #2 (60), :114. The "today" walkthrough named four of the six threading
  methods and both named ones are sync-only, while the same sentence
  covers the async handler at :236. Now names all six, paired. Every line
  number re-verified against PipelineBuilder.cs: :272, :316, :499, :430,
  :525, :451.
- #3 (60), :542. Step 6 restated AC-33's required test as "over a handle
  whose Dispose() throws", which a stub IAmAScope satisfies - bypassing
  ServiceProviderPipelineScope and therefore ADR 0070 step 4b, the
  dependency :413 names. Now states AC-33's own injection point
  (requirements.md:537), so the test cannot go green without it.
- #4 (60), :387. The counterfactual priced the new behaviour against a
  state in which the compared object cannot exist: IAmALifetime has no
  PipelineScope until this ADR adds it (IAmALifetime.cs:34-47 is two Add
  overloads), so there was no unrecognised handle to pass and no "two DI
  scopes" to count. Replaced with the narrower true statement; the bold
  numeral goes with it.

WHAT THE ROW GOT WRONG, AND WHAT IT GOT RIGHT

The row's one note was a dependency: "0071 #3 names AC-33's instrument,
which S3's amendment moves - check against 19.9 row 1 first". CHECKED AND
DISCHARGED, AND THE DEPENDENCY DOES NOT EXIST. Row 1 amends AC-14 (its
whole-suite regression half, its spy clause, and S3's migrated-not-
duplicated redesignation of the FactoryLifetimeTests pair); AC-33's
instrument is a container-Scoped dependency of the handler whose Dispose()
throws, which row 1 does not touch. Both criteria were read in full. What
IS true is that row 1's AC-14 amendment paragraph (:535) and #3's bullet
(:542) sit in the SAME SECTION - step 6 - which is presumably what the
note was reaching for. They are different sentences and this batch left
:535 alone, so row 1 lands whole and unchanged.

The row named four sites and the batch edited four - the third accurate
row in the corpus after S-b and S-e, and the first in the ordinary stream.
The reason is the same one those two had: this is the round's narrowest
ADR slice.

THE COUNTER-CASES VERIFIED AND LEFT ALONE

- :547 ("on the handle path Brighter's handler factory releases nothing at
  all, so the order is invisible to any host built from what this
  repository ships") is the second site for #1's subject and is NOT an
  edit. Its ground is narrower than its conclusion - it covers the
  container-backed factory only - but the conclusion is TRUE, because the
  factories the ground omits offer no handle. Rule 14: narrower than its
  own subject, not false. #1's fix supplies the missing half where the
  mechanism is explained.
- :41 (0071 #8, 57, below threshold) still says "today a throwing Release
  can skip the reclamation entirely". It is about TODAY's behaviour; this
  batch asserts nothing about today's reclamation, so nothing falsified
  it. S-e lesson 5.
- :592's "SimpleHandlerFactorySync.Release calls disposable?.Dispose()" is
  the accurate statement of the fact #1 corrects, and stays (rule 15).
- :425 and :437-443 already list all six threading methods and eight
  resolution-path methods; #2's fix makes :114 agree with them rather than
  the reverse.
- :413's AC-33/AC-51 pairing and its ADR 0070 step 4b dependency, :490,
  :39, :559 and :389 were each re-read against the new text; none is
  falsified.
- 0071 #6, #7, #9, #10, #11 and #12 (all below threshold) re-read against
  the new text, none falsified - clean for the fourth batch running.
- Rule 12a run: all five mermaid blocks carry no threading-method name, no
  AC-33 instrument, no scope count and no claim about which factories
  ship. The diff touches no diagram line.

BOOKKEEPING

requirements.md owes nothing and this was CHECKED against the document -
AC-33 is the SOURCE this batch brings the ADR into line with and needs no
change; the threading methods, SimpleHandlerFactory, ControlBusHandler
FactorySync and CreatePipelineScope appear in it nowhere, and its single
hit for any of those strings is revision 22's history row about NFR-1's
interfaces, a different subject. Tenth batch in a row to owe nothing
there; that is now ten and it is still not a rule. No frontmatter moved,
so no index.md diff was owed - the regeneration was run anyway and is
empty, still 99 indexed. Fences balance (16 in 0071); no mermaid block
touched and the set still holds 26; HTML entities 0.

D8 MEASUREMENT - NO COLUMN ROSE AND ONE FELL

0071 210/5/0/134/64/5 -> 210/5/0/133/64/5, set 1497/26/1/881/424/26 ->
1497/26/1/880/424/26. Net +29 words across four sites. The prose bold that
went is #4's bare numeral "two", which is one of the ~30 0071 #5 (bucket
R, Phase 5) exists to remove - rule 18b, a promoted finding behaves like
an at-threshold one, and this batch moves it the right way. No bold added
anywhere.

Phase 4's instrument was REBUILT FROM ITS STATED DEFINITIONS and
reproduced the recorded baseline exactly on all seven rows at the review's
own HEAD (09856a6: 1468/27/1/864/400/26), and today's set matched
O-0070's recorded post-landing figures, before any edit. One definitional
find worth keeping: an alpha-suffixed ordinal ("5a.") is NOT a list lead
under the reference script - admitting it over-counts 0074's bullet leads
by two and is the only thing that stops the table reproducing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
Closes the whole of 0072's slice: 0072 #1, #2, #4, #5, #6, #7, gap B #6,
gap B #7, gap C #2, set #11 and set #13. Eleven findings, one ADR, plus the
index.md row the frontmatter edit owes. #3 landed with O-0076 and #8 is
bucket R and rides Phase 5; neither is here.

WHAT LANDED (fourteen sites)

- #1  :645 - the translated ConfigurationException IS wrapped a second time.
      PipelineBuilder's two catches filter it out (:202 `e is not
      ConfigurationException`, :248 `!(e is ConfigurationException)`); the four
      transform-builder catches are bare `catch (Exception e)` and re-wrap
      (TransformPipelineBuilder.cs:116/:124, :157/:165 and the identical lines
      in the async twin - all four opened). A Post caller now sees it as the
      inner exception, stated rather than claimed away. :488 already said the
      transform builders wrap "without even a filter" and stands (rule 15).
- #2  :159 + gap C #2 :658 - the same over-claim in two bold leads. Rows 1 and
      2 take no pipeline scope and row 4 rethrows, so "every path that is not
      borrowed" is false of three of the ten rows the sentence is read off.
      Both leads composed from the two findings' EVIDENCE, not from either
      patch (O-0073 lesson 1): :159 is now bounded to a declined ambient or
      none, :658 to the count its own list supports.
- #4  :591 - AddProducers (two overloads) and UsePublicationFinder<T> take a
      caller-supplied ServiceLifetime and use it verbatim, so the package CAN
      make a container-Scoped registration. Premise corrected, conclusion kept.
- #5  :596 - the transaction consequence was argued at full length in step 2
      and again in Positive. Step 2 is now three sentences; the Positive bullet
      is untouched, and so is the outbox-boundary paragraph (see below).
- #6  :411 - TryAddScoped's stated reason does not hold: AC-37 clause 2 fixes
      no ordering (requirements.md:762) and a later Singleton wins as the last
      descriptor either way. The reason that does hold is idempotence -
      BrighterHandlerBuilder runs once per entry point and twice in a mixed
      host (ServiceActivator .../ServiceCollectionExtensions.cs:64 and :131),
      and every registration in its body is a TryAdd (:149-215, all read).
      The recommendation's *why* ("which is why every other registration is a
      TryAdd") was NOT adopted: the source's own comment at :195 gives a
      different reason for the scheduler ones. The fact is stated, not the why.
- #7 + gap B #6  :534 - the pseudo-code no longer writes OWNED for the one
      return the prose at :163 reserves it against. Composed from the ladder
      row's own wording ("ADR 0067's per-resolution handle"), which is what
      both recommendations were reaching for.
- gap B #7  :616 + :621 - see below; this one did not go the way the row said.
- set #11  :68 - "the first two having only closed defects" is false of both.
      0070 also introduces IAmAScope (:32, its own Scope says so) and 0071 also
      breaks six interfaces and carries two behavioural entries in 0070 step 7a
      (:580, :581). The evidence named only 0071 - rule 19b again.
- set #13  :42 + :43 - the Scope gave AmbientScopeDiagnostics two of its three
      latches. FR-23's is the third and now sits on FR-23's own bullet. The
      body (:179, :199, :267, :437-:447), 0074:57 and 0072's OWN flowchart node
      ("three independent latches") all said three; only the Scope did not.

THREE SITES NO ROW NAMED

- :8 frontmatter summary carried #2's over-claim verbatim ("Every path that is
      not borrowed converges on create-and-own"). DELETED rather than
      corrected: the clause immediately before it already says the non-borrow
      outcome is "creates and owns a scope exactly as it does today", so the
      sentence added only the false universal (rule 19b). This is the S half of
      a field O-front still owes the R half of (rule 2a); the length breach
      set #7 convicts is untouched, and this makes it smaller, not larger.
      index.md republishes the field verbatim, so the regeneration is in this
      commit (rule 4) - exactly one row changed, still 99 indexed.
- :676 the *Negative* migration bullet said "every path degrades to *create
      your own*" - the same false universal in a third place, and rule 14
      convicts it: a {Transient,Transient,Transient} host's pipelines create
      nothing. Now "every path behaves exactly as it does today", one word
      shorter and true of rows 1, 2 and 3 alike.
- :621 is the site gap B #7's recommendation EXPLICITLY CLEARED - "Then
      0072:615's iff-rule is true as written and needs no change" - and it is
      the site the fix had to land on. O-0070 lesson 2, second time.

WHAT THE ROW GOT WRONG, AND THE ONE FINDING THAT WENT THE OTHER WAY

Row named eleven sites; the batch edited fourteen. The under-statement is the
corpus ratio, not a surprise.

gap B #7 is the one to carry forward. Its recommendation is to NARROW the
participating set - "the transformer counts as a participant whenever a
transformer factory exists" - and it is refuted by requirements.md:50, which
defines *participating factory* structurally, "not by whether a given factory
happens to resolve anything for it (D12)". Adopting it would also change an
affinity outcome: {Scoped mapper, Transient transformer, no transformer
factory} computes AlwaysNew today and would compute JoinAmbient after, and
ScopeAffinityPolicy cannot see whether a factory instance exists. That is a
patch that changes what the design DOES, so it is an owner call (O-0074
lesson 1). What landed instead corrects the document: the set stays structural
and the ask-rule is tied to a participant that exists to be asked, which is
FR-27.1's own wording ("at least one of its participating factories").
Verified: BOTH DI wiring paths supply a transformer factory unconditionally
(ServiceCollectionExtensions.cs:765 producer, ServiceActivator ...:154
consumer, both through :943-945), so the shape needs deliberate hand-wiring
that mixes DI factories with a null one. FLAGGED, not acted on: FR-27.1 has no
bound for that shape. A criterion for a configuration no conformant host can
produce is the AC-29/AC-20 defect shape, declined twice already.

COUNTER-CASES VERIFIED AND LEFT ALONE, WITH THE DISCRIMINATOR

- :600 the outbox-boundary paragraph was NOT deleted, though #5 said it "goes
      with" the cut. It is not a duplicate of the Positive bullet - that bullet
      states no boundary - and it is 0072's only citation of
      ServiceCollectionExtensions.cs:431/:487/:648 and CommandProcessor.cs:795.
      An ADR restating a constraint's own bounds is rule 17, not a defect
      (O-0073 lesson 2: a deletion owes a grep of what the paragraph POINTED
      AT).
- step 1b was NOT edited, though #1 says "either way correct step 1b, which is
      the source that generates the claim". Step 1b is about the
      AmbientScopeSourceException clause and says nothing about
      ConfigurationException re-wrapping; the claim's only source was :645.
      Checked, not assumed.
- #1's alternative - give the transform catches a ConfigurationException
      exclusion - adds normative behaviour and is an owner call, not taken.
- :694 alternative 1 ("ADRs 0070 and 0071 already close Defect 1 and Defect
      1b") reads collectively and its conclusion holds; set #11 convicts the
      "only" at :68, not this. Rule 14.
- :675 still says step 4 keeps FR-23's forbidden exception from the caller -
      still true after #1: the caller gets a ConfigurationException either way,
      and what changed is how deep the message sits.
- :683's Risks row and :303 are about AmbientScopeSourceException's unwrapped
      path, a different mechanism from #1's; neither falsified.
- :669's Negative bullet keeps the AC-37 re-registration point #6's fix drops
      from :411, so nothing was orphaned (rule 21c: AC-37 survives at :411,
      :669 and :685; FR-16(c), C-21 and AC-52 at :38, :660 and ## References).
- 0070:181 and :440 are the accurate statements of the participation rule and
      the null-conditional second ask; they are citations, not edits (rule 15).
- The five below-threshold 0072 findings (#9, #10, #11, #12, #13) were re-read
      against the new text; none is falsified - O-0075 lesson 1, clean for the
      fifth batch running.

BOOKKEEPING

requirements.md owes NOTHING and this was CHECKED against the document:
AddProducers, UsePublicationFinder, TryAddScoped, ScopedArtefactCache,
AmbientScopeDiagnostics, OWNED and the "closed defects" framing appear in it
nowhere; its two BrighterHandlerBuilder hits are FR-17's four-path paragraph,
which says the same thing :411 now says. On set #13 the requirement
(requirements.md:241, "three rules require a latched Warning") is the SOURCE
the ADR is brought into line with - discharged entirely at the ADR end, as
O-0071's was. Eleventh batch in a row to owe nothing there; still not a rule.
Frontmatter DID move, so the index.md diff is in this commit - one row, still
99 indexed (101 pipe lines = header + separator + 99). Fences balance (16);
HTML entities 0; the participating-set table keeps four fields row-to-row.
Rule 12a run: no mermaid block carries a ladder outcome, the degradation claim
or a registration lifetime; the one diagnostics node already reads "three
independent latches" and corroborates set #13. No diagram touched; set holds 26.

D8 MEASUREMENT - NO COLUMN ROSE AND TWO FELL

0072  233/9/0/102/62/3 -> 232/9/0/101/62/3
set  1497/25/1/882/424/26 -> 1496/25/1/881/424/26

The block and the prose bold that went are the same thing: "**That is the
load-bearing consequence...**", the lead of the paragraph #5 compresses. Three
sentences I drafted came out at 42, 42 and 55 words and were split before this
message was written (O-0070 lesson 7); 0072's 40+ sentence count is 92 before
and 92 after, so the batch does not make 0072 #8's Phase 5 work harder.
Phase 4's instrument was rebuilt from PROMPT-reference.md section 8's
definitions and reproduced the recorded baseline EXACTLY on all seven rows at
09856a6 (1468/27/1/864/400/26) on the first run, then matched REQ-row4's
recorded post-landing figures before any edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
… 7 (batch O-set)

Closes set #4 (68), set #8 (62), set #9 (62), set #12 (62) and set #14 (60) -
the whole batch, nothing left open. Bucket S/factual. No requirements revision.

WHAT LANDED - sixteen ADR sites in seven files, plus one index.md row.

  set #9  the shared "Where this ADR sits" preamble in ALL SEVEN drops "one
          decision each". It is false of two: 0072:31 and 0076:34 each open
          "This ADR decides two things". Each ADR's own opening sentence already
          states its own decision count correctly, so this was the second,
          drifted copy - 0070:385's house rule, an argument stated in two places
          is an argument that drifts apart, and S-a lesson 2's "drop the numeral
          from the sibling pointers" in a second guise.

  set #8  0071's `handler` tag removed; it is outside adr_frontmatter.md's
          controlled vocabulary and 0071 was the only ADR in 99 using it.
          index.md republishes tags, so the regeneration rides this commit
          (rule 4) - exactly one row moved, still 99 indexed / 101 pipe lines,
          and the regeneration is idempotent.

  set #14 FR-6 was split in fact and described as split at ONE end only.
          0070:41 claimed it unqualified under "Each requirement below is
          discharged here"; 0071:41 qualified it "for the handler family" but
          named no other end. Both now carry the house form, taken verbatim
          from 0071's own NFR-5/NFR-6 bullet: "ADR 0071 carries the handler
          half" / "ADR 0070 carries the transform half".

  set #4  FR-10 names three types (D4) and was described as split at NEITHER
          end. 0072:35 claimed it whole while declaring `IAmAScopeProvider` and
          `ScopeAffinity`; `IAmAScope` is 0070's, and the string FR-10 appeared
          nowhere in 0070. Both ends now state the division, on the FR-13 model
          0073:50 and 0074:48 already name, and 0070's ## References gains
          FR-10 (its `IAmAScope` half).

  set #12 C-8 covers the same three seam types. 0070:37 reported it "confirmed
          that the seam types live in Paramore.Brighter" while introducing one
          of them; 0072, which declares the other two in core, never cited C-8.
          0070:37 now confirms the home C-8 assumes for `IAmAScope` and names
          0072 for the other two; 0072:510 states the reciprocal and 0072's
          ## References gains C-8 (rule 21b).

WHAT THE ROW GOT WRONG

  The row named twelve sites and the batch edited SIXTEEN, plus the index.

  TWO UNNAMED, and one is in a file no set finding mentions for this subject:
  - 0072:720, ## References, which owed C-8 the moment 0072:510 confirmed a
    home (rule 21b: where a bullet names a guard, ## References owes it).
  - 0073:52, "It serves FR-10, FR-12, FR-16, FR-18 and FR-23, EACH DISCHARGED
    BY ADR 0072" - a third ADR asserting the ownership this batch divides.
    Rule 21b exactly: assigning ownership falsifies every sibling that
    previously claimed the thing, and the sweep that found it was over the
    ownership VERBS (discharg|whole of|split), not over FR-10.

  AND THE REVIEW FILE'S OWN RANKED INDEX MISNUMBERS THIS BATCH. review-design.md
  line 141's index OMITS the C-8 finding and renumbers the three below it, so
  its "set #12" and "set #14" are the set-level section's #13 and #15. The
  set-level section (line 1275) is the source and is internally consistent -
  18 findings, 14 at or above 60, rows 1-14 - and O-0072's landing note
  confirms the convention by closing "set #13" = the latches finding. A session
  trusting the ranked index would land two wrong findings.

  A RECOMMENDATION'S SITE LIST OVER-REACHED FOR ONCE, RATHER THAN UNDER-REACHING.
  set #4 says the string FR-10 appears "in neither 0070 nor 0071 - not in Scope,
  not in References", implying both owe a citation. 0071 owes NOTHING: it
  declares no FR-10 type and exposes no provider, and the property the finding
  is reaching for - that the added core member names `IAmAScope` rather than a
  container type - is already owned by NFR-1(a), which 0070 claims at 0070:49
  and 0071 explicitly routes to 0070 at 0071:48. Reading FR-10 rather than the
  finding is what settled it (rule 17).

COUNTER-CASES VERIFIED AND LEFT ALONE

  - 0070:297 "This confirms C-8's assumption" - the Home row of `IAmAScope`'s
    own contract spec, so its subject IS that one type. Narrower than its
    subject is not false (rule 14), and a qualifier added there would be a
    claim the section does not need. 0070:298 "(C-8, settled)" is the disposal
    half and is exactly right.
  - 0070:511, :521, :530 state FR-6 inside step 5, whose subject is
    `TransformPipeline<TRequest>` throughout; 0070:47 and 0071:44 cite FR-6 only
    as AC-8's tag. Citations, not claims (rule 15).
  - 0072:101 "FR-10 pairs that obligation with OOS-3" and 0073:97 "FR-10 states
    the rule on the provider" are content citations, and 0073:506 is a bare
    ## References entry - none asserts ownership.
  - The "Where this ADR sits" TABLE was not touched. Its 0072 row names one of
    that ADR's two decisions, and its 0076 row already names two - it is the
    PREAMBLE that made the rows read as exhaustive (rule 12b, a classifier
    sentence is the lead-in of every row at once). Dropping the clause leaves
    the table byte-identical in all seven, and every ## References sibling
    one-liner still matches its cell.
  - The phrase was INHERITED, and inheriting a sentence copies its truth
    conditions and not its truth: "Four ADRs came out of the #4252 lifetime
    work, one decision each" is the 0066-0069 preamble, and it is TRUE there -
    none of those four opens "decides two things" (checked). Those are Accepted
    ADRs outside the review's remit and were not touched (S-g's rule).
  - No taxonomy addition was proposed for `handler`. The house tags the
    CONCERN, not the pipeline element: 0037-reject-message-on-error-handler and
    0063-request-validation-handler both name a handler in the title and
    neither reaches for such a tag, and 0067 - the nearest sibling in subject -
    carries three tags. `lifetime, di, pipeline` already say what 0071 is
    about. Rule 21a run and it came back "delete", not "keep it and mark it".
  - Below-threshold set findings re-read against the new text: #15 (C-3/AC-21),
    #17 (OOS-6/OOS-11) and #18 (0075's title) are untouched and none is
    falsified. Note #15 is scored 55 in its own section and 60 in the ranked
    index; the section governs, so it stays closed by being below threshold.
  - Rule 12a run: no mermaid block in the set carries FR-6, FR-10, C-8 or a
    decision count.

BOOKKEEPING

  requirements.md owes nothing and this was CHECKED against the document, not
  assumed. It contains "frontmatter", "tag" and "Where this ADR sits" zero
  times; its three "one decision" hits are all "adoption decision" in AC-13 and
  AC-52; and it never assigns an FR to an ADR - the ADRs are what is brought
  into line with FR-10 and C-8, exactly as O-0071's #3 and O-0072's set #13
  were. C-8's own "The ADR should confirm this" is the document's collective
  use of "the ADR" for design (C-13), not a claim that one ADR does it.

  Fences balance (14/16/16/8/12/12/16); HTML entities 0; the set still holds 26
  mermaid diagrams and no diagram line is touched.

D8 MEASUREMENT

  NO COLUMN MOVED IN EITHER DIRECTION: set 1496/25/1/881/424/26 before and
  after, and every per-file row identical. Phase 4's instrument was rebuilt
  from PROMPT-reference.md section 8's definitions and reproduced the recorded
  baseline EXACTLY on all seven rows at 09856a6 (1468/27/1/864/400/26) on the
  first run, then matched O-front's post-landing figures before any edit.

  One column DID rise on the first pass - 0072 prose bold 101 -> 102, from a
  bolded **C-8** in step 1 - and it was removed rather than explained: D8's own
  "a fix that needs bold is in the wrong place", and 0072 already cites
  constraints unbolded (0072:646, C-12a). The reported figure is after that fix.

  0070 #7 is PROMOTED into Phase 5 (rule 18b), so its dimension was measured
  too: 0070's 40+ word sentences are 55 before and 55 after, against the review
  HEAD's 59. The batch adds one sentence to 0070 and it is not a long one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
…tch Ph5-0072L)

Phase 5's first LANGUAGE batch, and 0072's second and last: its structure
landed as Ph5-0072 (4ebb49d), so this is the language-only half the
2026-08-27 ruling left it owing. One ADR, +45/-36 lines. No requirements
revision, no frontmatter, no index.md, no ADR but this one.

WHAT LANDED, by the owner's four criteria

1. Terminology we don't explain. A `### Terms` block on 0067:40's house form
   -- `## Context`, ahead of `### Scope` -- defining `ambient`, `artefact`,
   `seam` and `residue`. `ambient` had 125 bare uses and no definition
   anywhere in the set. The block carries 0067's pointer, so `Where this ADR
   sits` drops the sentence that repeated it.
2. Long, run-on sentences. All 24 sentences of 40+ words are split: 40+
   24 -> 0, 50+ 4 -> 0, 60+ 1 -> 0, worst 80w -> 39w. Sentences carrying two
   or more em-dash asides fall 48 -> 38.
3. Conciseness. Nine meta-commentary sites removed (17 -> 11 on the phrase
   instrument), including BOTH rhetoric phrases the structural pass had
   promoted into headings -- `2c ... stated as an answer rather than left to
   be inferred` and `2d. The residue is stated rather than claimed away, and
   there are three parts to it` -- plus `What AC-30 reaches, stated rather
   than claimed away`, `named so the omission is not read as an oversight`,
   `the design point in each is worth stating in prose`, `Three details the
   ladder is making a point of`, `must be said plainly`, `it is worth saying
   so` and `which is the whole point of it`. The contrastive tic `rather
   than` 74 -> 66; the rest are load-bearing contrasts and were left.
4. Decision + Why, not a history of argument. All three revision-history
   passages are STRUCK, not deferred, each collapsed to the rule that now
   holds. The 96-word FR-27.1/AC-46 pair becomes one 32-word sentence plus a
   cross-reference; the FR-19/AC-20/C-14 passage loses `used to assert ...
   which was right while ...`; the FR-19 Scope bullet loses `assumed that
   case away until revision 26`. The history-phrase instrument reads 6 -> 0.

WHAT THE ROW GOT WRONG, AND WHAT THIS BATCH LEARNED

* THE LANGUAGE INSTRUMENT'S DEFINITION IS THE FIRST OF THE THREE THAT DOES
  NOT REPRODUCE. Sec 9's and sec 11's rebuilt from their written definitions and
  reproduced EXACTLY, first run, at both commits (D8 rule 25 discharged
  twice: 1468/27/1/864/400/26 at 09856a6, 1495/25/1/863/424/26 at HEAD~,
  and sec 11's entire baseline table AND control row). PROMPT-phase5.md's
  criteria table gives the language splitter ONE LINE, and a search over the
  splitter's parameter space gets no closer than 59/17/8/81 against the
  recorded 65/20/6/80. The `rather than` count (74) and the identifier
  per-ADR column reproduce; the sentence columns do not. The definition is
  now written out in full in PROMPT-phase5.md so the six remaining batches
  share one instrument, and this batch's before/after is measured with it
  throughout -- the DELTA is sound even where the absolute is not.
* A RETITLE IS A CROSS-REFERENCE EDIT (Ph5-0072 lesson 3), and this time the
  sweep ACQUITTED. `grep -no "step [0-9][a-z]*"` over the whole file returns
  1b, 2a, 2b, 3, 3a, 4, 4a, 6 and 7a and NEVER 2c or 2d, so neither retitled
  heading is pointed at by number; and no sibling names either title. The
  cheap command is worth running for the acquittal as well as the conviction.
* MY OWN NEW TEXT CREATED A DUPLICATE (rule 18a). The Terms block's opening
  pointer to ADR 0067 restated `Where this ADR sits`'s own first sentence
  almost word for word. Found by grepping the file for the sentence I had
  just written. Fixed by deleting the older copy, which is where the
  duplication left the document longer than it needed to be.
* CRITERION 1's IDENTIFIER HALF IS NOT CLOSABLE BY A LANGUAGE PASS, and that
  is an OWNER CALL rather than an omission (D8 rule 23). 0072 threads 3+ IDs
  through 13 prose sentences, which documentation.md:110 bars -- but every
  one of them is a citation list in a `Scope` bullet or a `Consequences`
  bullet, and the Scope section is organised requirement by requirement by
  the house form. Cutting them drops information rather than words. Recorded
  in PROMPT-phase5.md's collation table as evidence for the deferred
  house-style commit: the existing rule collides with the Scope section's
  own form, and the collation has to say which wins.

COUNTER-CASES VERIFIED AND DELIBERATELY LEFT ALONE

* `0070:526`'s `The residual cost is real and is stated rather than claimed
  away` and `0076:424`'s `The residue is smaller than it first looks` carry
  the same tic this batch removed from 0072. Rule 21 checked: it is a
  two-ADR rhetorical habit, not a house FORM, and rule 1 gives each ADR its
  own batch. Left for 0070's and 0076's.
* `## Alternatives Considered`'s eight `**N. Title.**` entries: untouched
  again, for Ph5-0072 lesson 4's reason -- the form is unanimous across all
  seven.
* The sentences in the 33-39 word band (60 of them) are ABOVE
  documentation.md:124's ~25 words and BELOW criterion 2's threshold of 40.
  Not touched, and named here so a later reader does not read the 40+ count
  of zero as "every sentence is short".
* `load-bearing` and `seam` as vocabulary: the owner named both and then set
  them aside (`do not spend a session on vocabulary`). `seam` gains a Terms
  entry because criterion 1 asks for definitions, not for replacements; no
  occurrence was reworded.
* Row 9's AC-54 coverage sentence was rewritten out of the past tense
  (`had the same gap until AC-54`) WITHOUT changing what it asserts: one of
  five outcomes covered, four uncovered, before and after.

BOOKKEEPING

No requirements.md edit, so NO REVISION IS OWED and none was taken; it
stands at 30. Frontmatter untouched -- CHECKED, not assumed (rule 4), by
diffing the frontmatter keys and confirming index.md's 0072 row still
republishes `title` and `summary` verbatim -- so no index.md diff is owed.
HTML entities 0. No fenced block and no diagram line touched, verified by
diffing fence membership line by line (diagrams 3, unchanged), so rule 12a
is trivially clean. No double blank lines.

D8 MEASUREMENT, BOTH TABLES, AS SEC 11 REQUIRES

sec 11 (region) 0072 row -- ####|blocks|prose|list|list%|bold lead|run|span:
  18|86|59|27|31%|22|8|50 -> UNCHANGED in every column, and the set stays at
  22 / 606 / 367 / 239 / 39% / 218, run-max 19, span-max 203. That is the
  right result for a language-only batch: no structure was added and none
  was spent. The control row is unchanged too (#### 8, run 12, span 67) --
  the Terms block is a `###`, not a `####`, and does not sit in the longest
  control span.

sec 9 0072 row: 231|9|0|83|62|3 -> 236|9|0|83|66|3; set 1495/25/1/863/424/26
  -> 1500/25/1/863/428/26. Blocks +5 and bullet leads +4 are the Terms
  block, one paragraph and four definitions, which criterion 1 requires. A
  BATCH THAT RAISES A COLUMN OWES AN EXPLANATION AND THAT IS IT. Everything
  else holds or improves: BOLD IN PROSE IS UNCHANGED AT 83, so no bold was
  added; blocks over 150 and over 200 do not move; diagrams do not move.
  The prose itself came out SHORTER -- 17,373 words -> 17,240, a fall of 133
  despite the Terms block adding about 180, so roughly 310 words of prose
  were spent.

Rule 18b, the promoted 0070 #7 dimension: 0072's 40+ sentence count is the
criterion this batch exists to move, and it is 24 -> 0.

Closes nothing in R->Ph5; 0072 #8 was closed by Ph5-0072. Six ADRs remain,
one batch each, structure and language together: 0073, 0074, 0075 (+0075 #9),
0076, 0071 (+0071 #5), and 0070 last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 12, 2026
… Ph5-0075)

Phase 5's THIRD combined batch, `0075` alone, closing `0075 #9` (60). No
requirements revision, no frontmatter, no `index.md`, no ADR but this one.

WHAT LANDED

Structure — thirteen bolded lead-ins become `####` sub-headings and stop being
emphasis: four `**Why …**` labels in `### Technology Choices`, and one heading
per step (1, 2, 3, 4, 4a, 5, 5a, 6, 7) in `### Implementation Approach`. NO STEP
IS RENUMBERED. `0073`'s treatment, transferred as the resume block predicted.

Language — five `### Terms` entries added to the block `532b3ba02` created
(`flow`, `ambient scope`/`adoption`, `affinity`/`AlwaysNew`, `the ladder`/`the
ask`, `the twins`); all ten sentences of 40+ words split (worst 60w -> 39w); all
FIVE revision-history passages STRUCK; ten meta sites removed; the two doubled
`rather than` tics unpicked.

`0075 #9` — FOUR ⚠-plus-bold caveats, not the five its evidence names: `O-0075`
had already closed the Risks-cell one. All four relocated as prescribed, with no
new text: the FR-19 caveat folded into step 4a's *What this makes true*, the
allocation caveat made the lead of *What suppression costs*, the second-assembly
caveat made the lead of *Core gains a public static with a public mutator*, and
Alternative 5's de-glyphed. ⚠ glyphs 4 -> 0.

WHAT THE ROW GOT WRONG

- The finding's evidence lists FIVE sites and only FOUR survive; `0075:493`'s
  Risks cell was fixed by `O-0075`. Re-derive a glyph count, do not trust one.
- `git show 09856a6:<path>` over today's filenames silently returns an EMPTY
  FILE for THREE of the seven, not the one `PROMPT.md` names: `0070` and `0071`
  were also renamed after the review commit. Every row was checked non-zero
  before any total was trusted.

COUNTER-CASES VERIFIED AND LEFT

- The ten `**N. Title.**` Alternatives leads (rule 21, unanimous across seven).
- The two `**And …**` continuations in `### Technology Choices` and the ten
  sub-topics inside the steps: sub-topics, not peers (§21.30 lesson 6). `#####`
  was considered and DECLINED — only `0074` uses that level in the whole set,
  and `0075`'s own control region has none, so it would be an import.
- Bucket H: `:523`'s designated `InternalsVisibleTo` sentence verified BYTE-
  IDENTICAL before and after, and still byte-identical with `0076:562`. Its
  three siblings (`:333`, `:353`, `:527`) untouched.
- Conversion trap (§21.32 lesson 1) CHECKED and ACQUITTED — `0075` does not
  carry the `**N.** **Bold**` habit; no block newly begins with bold.
- Cross-reference sweep ACQUITTED: every sibling pointer into `0075` names step
  4a, which keeps its number; no sibling names a lead-in by title.
- Terms duplication sweep (rule 1, all four halves, `## References` glosses
  included) found ONE: the inline `AlwaysNew` gloss at `:216`, deleted from the
  body. `:42`'s "not one thread" also removed, the Terms `Flow` entry carrying it.

D8 MEASUREMENT, BOTH TABLES

§11 `0075`: `4|94|66|28|29%|37|10|102` -> `17|94|66|28|29%|24|10|38`. THE
STRUCTURAL TEST PASSES EXACTLY — bold lead -13 against `####` +13 — and blocks,
prose, list and list% DO NOT MOVE, so no structure was bought with length. Span
102 -> 38; the residue is step 4a, whose sub-topics are deliberately left bold.
Run UNMOVED at 10, which is its `## Alternatives Considered` FLOOR, as predicted.
§11 CONTROL: `5|76|43|33|43%|14|4|49` -> `5|82|44|38|46%|14|4|49` — blocks +6 and
list +5 are the `Terms` block and nothing else; `####`, bold lead, run and span
do not move. SET: `####` 45 -> 58, bold lead 194 -> 181, blocks/prose/list/list%
unchanged at 598/359/239/39%; run-max 17 and span-max 203 unchanged.

§9 `0075`: `189|1|0|144|55|3` -> `195|1|0|129|60|3`; set `1501/25/1/838/436/26`
-> `1507/25/1/823/441/26`. BOLD IN PROSE FALLS 15 AND RECONCILES EXACTLY: 13
conversions plus 2 de-boldings. Blocks +6 and bullet leads +5 are the `Terms`
block, the whole of the explanation rule 24 asks for. Long blocks and diagrams
do not move.

Language: 40+ 10 -> 0, 50+ 1 -> 0, 60+ 1 -> 0, worst 60w -> 39w, 2+ em-dash
20 -> 19, `rather than` 78 -> 69 (the heaviest tic in the set), history 5 -> 0.
Identifier load 51 distinct, unchanged, 2.2 -> 2.1 per 100w. 3+-id prose
sentences 8 -> 6, EXCLUDING the `## References` inventory line — the first time
a Phase 5 batch has moved that column, and it moved as a side-effect of the
sentence splits, not by cutting citations; rule 5's collision stands. Prose
11,380 -> 11,419 words, a rise of 39 against a `Terms` block of about 247, so
roughly 210 words of prose were spent. ⚠ The `meta` column is NOT recoverable
from the written definition; ten sites is my own list, reported as such.

D8 RULE 25 DISCHARGED ON ALL FIVE INSTRUMENTS, EVERY ONE FIRST-RUN EXACT: §9 at
`09856a630` (1468/27/1/864/400/26) AND at HEAD (1501/25/1/838/436/26), all seven
rows each; §11's ENTIRE baseline table AND its control row at `a97c0ebdc`;
`lang.py` on all seven rows and all seven recoverable columns at `4a4c34f9c`, a
SECOND independent session reproducing it; `ids.py` at 55 for `0073` and `0074`.

Bookkeeping: no `requirements.md` edit, so no revision is owed and none was taken
— it stands at 30. Frontmatter untouched, CHECKED not assumed, so no `index.md`
diff is owed. HTML entities 0; fence lines byte-identical (12 fences, 3 mermaid
diagrams, unchanged), so rule 12a is trivially clean; no double blank lines, and
every `####` carries a blank line on each side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJjvJaUupNvDeTpEPp73as
iancooper added a commit that referenced this pull request Sep 26, 2026
Issues worked, each at every site in the file:
- the disposable clone's refs: origin/ forms, origin/master reset to the
  real one, clone figures are not calibration values (#1);
- complete K-fixture file sets, K13 and K14 added (#4, #18);
- which fixture carries which AC: AC-27 on K2, K1's complete two-path
  list under the amended FR-14, AC-40, AC-42 (#2, #3, #7, #9);
- a visible red for every script task: plain write in T3.1, atomic write
  by inspection in T7.1, in_range in T8.2, adr-unresolved fixture (#5,
  #8, #11);
- T11.1 split in three, Phase 5 dependency, baselines, T1.3 placement
  (#10, #12, #13, #14, #17);
- judged-path ACs recorded as unexercised rather than passed, and the
  coverage tables corrected (#6, #15, #16, #19);
- T17.5's AC-25 and stand-in reruns held inside the (C-8) window.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2WY2ELStCVRM2SW3vajnr
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.

2 participants