Repository navigation
fix title of rabbitMQ gateway nuget package - #9
Merged
Merged
Conversation
iancooper
added a commit
that referenced
this pull request
Oct 8, 2014
fix title of rabbitMQ gateway nuget package
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>
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
5 tasks done
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The title of the nuget package was wrong