Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
f857437
docs(roadmap): close merged P02d-6 and record next packet
cemililik Oct 10, 2026
3160714
docs(decisions): accept P02d-7 demo and audit contracts
cemililik Oct 10, 2026
82459cb
feat(dev): add the shared seeded demo lifecycle
cemililik Oct 11, 2026
4966dca
fix(dev): close demo lifecycle review findings
cemililik Oct 11, 2026
6395d85
fix(dev): isolate demo profiles and Compose controls
cemililik Oct 11, 2026
eb3f2da
test(dev): prove demo profile refusal is causal
cemililik Oct 11, 2026
69363fe
test(architecture): enforce the P7 demo identity boundary
cemililik Oct 11, 2026
f54c2f0
fix(architecture): close demo identity scan omissions
cemililik Oct 11, 2026
e70e204
fix(architecture): preserve conditional and runtime census semantics
cemililik Oct 11, 2026
69ce613
fix(architecture): refuse unmodeled resolver options
cemililik Oct 11, 2026
d99d10b
feat(dev): enforce seeded browser and Lighthouse demo proofs
cemililik Oct 11, 2026
38c15d9
fix(dev): refuse incomplete or invisible demo proof results
cemililik Oct 11, 2026
39cee01
fix(dev): join audit output and reject hidden product markers
cemililik Oct 11, 2026
e1486de
fix(dev): preserve the total owned shutdown deadline
cemililik Oct 11, 2026
06d46ad
docs(roadmap): close P7 browser proof review rounds
cemililik Oct 11, 2026
bf5bc9c
feat(ci): enforce P7 public demo and reconcile phase exit
cemililik Oct 11, 2026
100b022
fix(ci): complete P7 exit proofs and reserve owned cleanup
cemililik Oct 11, 2026
8605fc9
docs(roadmap): close both P7 phase-exit review rounds
cemililik Oct 11, 2026
36cd6e0
docs: record P02d-7 CI and phase-exit verification
cemililik Oct 11, 2026
e00395a
docs(roadmap): align locale exit rows with verified CI
cemililik Oct 11, 2026
d47ef17
test(architecture): handle optional demo curricula consistently
cemililik Oct 11, 2026
dca3a7b
fix(dev): align demo tool versions and review evidence
cemililik Oct 11, 2026
aebefee
docs(roadmap): record P7 review remediation verification
cemililik Oct 11, 2026
cd6affd
fix(dev): preserve independent demo cleanup and seed launch
cemililik Oct 11, 2026
db6bcdf
test(architecture): close demo source census admission gaps
cemililik Oct 11, 2026
90114f0
test(architecture): refuse ambiguous demo census resolution
cemililik Oct 11, 2026
c6cd69f
test(web): inspect every inherited demo census config candidate
cemililik Oct 11, 2026
9a0d285
test(dev): verify demo reconciliation and align delivery guidance
cemililik Oct 11, 2026
e3a32d2
docs(roadmap): close systematic P7 remediation review rounds
cemililik Oct 11, 2026
4541d63
docs(roadmap): clarify proof helper closeout wording
cemililik Oct 11, 2026
d16d2d7
test(architecture): admit ignored local machine artifacts
cemililik Oct 11, 2026
9343e99
test(architecture): refuse ignored MSBuild user hooks
cemililik Oct 11, 2026
ec86884
docs(roadmap): close ignored-machine-file review follow-up
cemililik Oct 11, 2026
32a7951
test(cache): coordinate the noncooperative factory timeout proof
cemililik Oct 11, 2026
8d9d219
docs(roadmap): record cache timeout proof reviews
cemililik Oct 11, 2026
3e86041
docs(roadmap): close Phase 02d after PR 28 merge
cemililik Oct 11, 2026
6baeffb
docs(roadmap): complete P02b-0 entry verification
cemililik Oct 12, 2026
fffbd4d
docs(decisions): propose P02b-1 event producer contracts
cemililik Oct 12, 2026
907ca35
docs(decisions): clarify P02b-1 producer decision boundaries
cemililik Oct 12, 2026
b157650
docs(decisions): accept P02b-1 producer contracts
cemililik Oct 12, 2026
cbab31a
feat(kernel): share producer and audit execution lifecycle
cemililik Oct 12, 2026
ab2c37d
fix(kernel): fence producer ownership and prove all ancestors
cemililik Oct 12, 2026
3fd8e8a
docs(events): close shared lifecycle review rounds
cemililik Oct 12, 2026
7541b73
docs(events): align producer packet review status
cemililik Oct 12, 2026
db882e0
feat(kernel): persist outbox events in the admitted transaction
cemililik Oct 12, 2026
37d95bd
docs(events): reconcile the delivered outbox entry baseline
cemililik Oct 12, 2026
937e36a
test(kernel): prove enqueue and completion never publish
cemililik Oct 12, 2026
d6fbd85
feat(kernel): complete saved domain facts before commit
cemililik Oct 12, 2026
3473843
docs(events): close domain completion review rounds
cemililik Oct 12, 2026
b6e3932
test(events): enforce producer contracts across production code
cemililik Oct 12, 2026
98c22a8
test(events): cover abstract and virtual helper dispatch
cemililik Oct 12, 2026
1e799a8
docs(roadmap): close P02b-1 implementation and review rounds
cemililik Oct 12, 2026
11871a2
test(web): expose bounded browser admission failure phases
cemililik Oct 12, 2026
471d0a0
docs(events): reconcile producer and pending delivery guarantees
cemililik Oct 12, 2026
d5afc66
docs(architecture): distinguish local event invocation ordering
cemililik Oct 12, 2026
d38b0c5
docs(events): align transport comments with ordering ownership
cemililik Oct 12, 2026
a903b53
test(web): preserve the first admission proof failure
cemililik Oct 12, 2026
cb2bb6d
docs(roadmap): record P02b-1 CI follow-up verification
cemililik Oct 12, 2026
d8fd919
test(kernel): strengthen producer and audit proof controls
cemililik Oct 12, 2026
75fc79b
docs(roadmap): record external producer proof review
cemililik Oct 12, 2026
c7dae0c
fix(kernel): distinguish frame refusal from indeterminate commit
cemililik Oct 12, 2026
3f4c121
fix(kernel): bound shared execution proofs and reconcile review records
cemililik Oct 12, 2026
9b2fcf6
fix(web): authenticate sanitized browser proof failures
cemililik Oct 12, 2026
b561c8f
docs(roadmap): record verified PR29 remediation and reviews
cemililik Oct 12, 2026
96e6f4d
fix(kernel): poison the unit after a failed outbox insert
cemililik Oct 12, 2026
8a97cd2
docs(roadmap): record outbox failure and ownership verification
cemililik Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .claude/skills/add-architecture-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ Patterns to follow:

### Step 4: Common architecture-test families

**The shipped set is eighteen files, not a family per topic.** Add yours to the one whose
subject it shares:
**Choose by subject, not by ADR number.** These common families are starting
points; inspect the test directory and catalogue for the complete shipped set.
Add yours to the file whose subject it shares:

| File | What it covers |
|------|----------------|
Expand All @@ -130,6 +131,9 @@ subject it shares:
| `RequestSurfaceTests.cs` | What the step-4 authority ceiling admits: the two request markers, their permitted sets, the shape of the attributes themselves, and the ban on request shapes MediatR runs with no pipeline. |
| `PlatformAdminScopeConventionTests.cs` | The single sanctioned `BYPASSRLS` path: who may resolve the keyed platform data source, where connection strings are read, the entry gate, and what the scope must not touch. |
| `RepositoryLayoutTests.cs` | `No_Source_Folder_Named_Verticals`, the single-frontend-app rule, and the commit-subject grammar — checked by running the `commit-msg` hook. |
| `DomainEventHandlerTests.cs` | Complete domain-handler/helper/interface census; outbox-only reactions with planted forbidden-capability controls. |
| `IntegrationEventContractTests.cs` | Production event inheritance, explicit identifier partition derivation and identifier-only payload/serializer shape. |
| `OutboxProducerConventionTests.cs` | Producer/helper fence against extra connections, commit ownership and direct publication. |
| `AggregateWriteTests.cs` | The one sanctioned cross-aggregate write, and the count of aggregate roots a handler's ports can write that keeps it at one. |
| `CustomizationRegistryTests.cs` | The closed renderer registries that exist twice — in C# and in TypeScript — and stop the two drifting. |
| `AuditConventionTests.cs` | The audit aggregate's shape, the closed-set columns against their `CHECK`s, append-only enforcement at the source level, the closed set of `audit_log` writers, and the `OperationType` table. |
Expand All @@ -144,7 +148,7 @@ Rules for surfaces no file covers yet — permissions, the Hub adapters and endp
[the catalogue](../../../docs/standards/21-architecture-tests-catalogue.md) against
the phase that ships the code they inspect. Check its Status line before assuming a
net is under you, and create a new file only when your rule's subject is not one of
the eighteen above.
the listed families.

> **The tenancy rules live in three files, and the split is by subject, not by ADR.**
> All three cite ADR-0036, so "put it with the other ADR-0036 rules" is not a usable
Expand Down
110 changes: 80 additions & 30 deletions .claude/skills/add-integration-event/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ Amendment 1 (Dapr pub/sub dispatch),
and [15-event-and-outbox.md](../../../docs/architecture/15-event-and-outbox.md).
The binding port and envelope contract is
[ADR-0038](../../../docs/decisions/0038-cross-cutting-port-and-event-contracts.md).
Accepted [ADR-0057](../../../docs/decisions/0057-transactional-event-production-and-completion.md)
owns immediate producer admission, completion and payload policy. P02b-1
Steps 2/3 implement immediate enqueue, payload admission, successful-save
collection and the completed-provisioning publisher. Step 4 implements the
production censuses and catalogue enforcement; the
[delivery record](../../../docs/roadmap/phase-02b-events-auth.md#p02b-1-step-4--producer-enforcement-and-closeout-2026-10-12)
owns final verification and review status.
P02b-2/P02b-3 retain dispatcher/inbox decisions.

## When to use

Expand All @@ -33,8 +41,10 @@ The binding port and envelope contract is

## When not to use

- Two aggregates in the **same** module need to coordinate — use MediatR
`INotification` (a domain event) and handle it in-process inside that module.
- A domain fact needs only in-module translation to outbox — use MediatR
`INotification` inside the owner transaction. Domain reactions cannot mutate
roots or coordinate additional writes; substantive reactions belong in an
integration consumer under ADR-0057.
- The consumer can fetch the data on-demand from the producer's read API.
- Provider webhooks (Stripe, LiveKit) — they enter via a webhook receiver that
writes to the outbox itself, not via this skill.
Expand All @@ -43,11 +53,11 @@ The binding port and envelope contract is

| Input | Required | Description |
|-------|----------|-------------|
| Event name | Yes | `<Verb><Aggregate>IntegrationEventV<N>`, PascalCase + version suffix. |
| Event name | Yes | `<Aggregate><PastTenseFact>IntegrationEventV<N>`, PascalCase + version suffix. |
| Producing module | Yes | Owns the aggregate the event describes. |
| Consuming module(s) | Yes | At least one; can be many. |
| Topic | Declared | The event's `Topic` override, normally `learnstack.{module}.{aggregate}`. |
| Schema fields | Yes | `IntegrationEventBase` supplies `EventId`, `OccurredAt`, `TenantId` (all `required`) and demands `Topic` and `PartitionKey` overrides. Everything else is yours to declare. |
| Schema fields | Yes | `IntegrationEventBase` supplies `EventId`, `OccurredAt`, `TenantId` (all `required`) and demands `Topic` and `PartitionKey` overrides. Additional fields are GUIDs or approved strongly-typed identifiers, optionally nullable; no text, DTOs, collections, binary/raw JSON or serializer overrides. |

## Workflow

Expand All @@ -56,25 +66,22 @@ The binding port and envelope contract is
In `<Producer>.Application.Contracts/IntegrationEvents/<EventName>.cs`:

```csharp
[IntegrationEventPartition(nameof(EnrollmentId))]
public sealed record EnrollmentCreatedIntegrationEventV1 : IntegrationEventBase
{
public required Guid EnrollmentId { get; init; }
public required Guid LearnerId { get; init; }
public required Guid CourseVersionId { get; init; }
public Guid? CohortId { get; init; }
public required string Source { get; init; } // "manual" | "billing" | "invitation"

// Both abstract on IntegrationEventBase, so this record does not compile
// without them — deliberately, because each is a property of the event TYPE
// and a value with two sources is a value that can disagree with itself.
public override string Topic => "learnstack.enrollment.enrollment";

// Ordering is guaranteed per partition key and nowhere else, and the
// aggregate this event is about is the ordering domain. Keying on TenantId
// instead would serialise the tenant's whole stream onto one partition — a
// real throughput cost, and one worth taking deliberately rather than by
// inheriting a default.
public override string PartitionKey => EnrollmentId.ToString();
// Declare EnrollmentId as the partition source. ADR-0057 accepts canonical
// identifier derivation; G3 durable publish ordering remains P02b-2.
public override string PartitionKey => EnrollmentId.ToString("D");
}
```

Expand All @@ -87,6 +94,13 @@ five members, and every one of them is mandatory:
`Integration_Event_TopicNames_FollowConvention`
- `PartitionKey` — `abstract`; the ordering domain, declared by each event

The concrete record declares `[IntegrationEventPartition(nameof(EnrollmentId))]`.
Use `[IntegrationEventPartition(nameof(TenantId), tenantWide: true)]` only for an
explicitly tenant-wide fact. The named non-nullable source must match the actual
canonical lowercase `D` GUID key; inheriting a declaration is insufficient.
Approved typed payload identifiers are currently `TenantId`, `OrganizationId`
and `UserId`; module-local identifiers cross the contract as `Guid`.

It supplies **no** `OrganizationId`, `CorrelationId`, `CausationId` or
`ActorUserId`: those describe the delivery and travel on
`IntegrationEventEnvelope`, copied from the outbox row. An organization-owned
Expand Down Expand Up @@ -117,17 +131,36 @@ await outbox.EnqueueAsync(new EnrollmentCreatedIntegrationEventV1
// one layer in, so that spelling needs `courseVersionId.Value`.
CourseVersionId = request.CourseVersionId,
CohortId = request.CohortId,
Source = request.Source.ToString().ToLowerInvariant(),
}, cancellationToken);

await db.SaveChangesAsync(cancellationToken); // atomic
await db.SaveChangesAsync(cancellationToken); // flush; owner commits state + outbox + audit
```

Rules:

- `IOutbox.EnqueueAsync` enlists in the **ambient unit of work** — the one connection and
transaction `IUnitOfWork` owns ([ADR-0040](../../../docs/decisions/0040-ambient-unit-of-work.md)),
not a module's `DbContext`. Do not open a second transaction.
- `IOutbox.EnqueueAsync` validates and INSERTs immediately through the enrolled,
announced, writable ambient `IUnitOfWork.Connection`/`Transaction`. It opens
no connection, commits nothing and never calls `IEventBus`. The frame compares
the live announcement generation and event tenant, including unresolved
provisioning; caller snapshots are not authority. Raw transactions cannot
bypass producer enrollment. `id = EventId`; duplicates fail.
- Serialize the concrete runtime event through the existing frozen
`IntegrationEventBase.PayloadJsonOptions`. The complete canonical UTF-8 input
fits `OutboxPayloadLimits.MaxBytes` (262,144 bytes inclusive) before INSERT;
multibyte accounting is bytes, not characters. JSON `metadata` is SQL NULL;
`IOutbox` accepts no arbitrary metadata. No DDL size CHECK is claimed.
- Tenant/organization come from the transaction announcement; correlation,
causation and optional causal actor come from trusted execution. That boundary
guarantees W3C Activity without a listener. Domain translation mints a new
integration EventId and uses its domain EventId as causation. A non-marked
event does not erase an announced organization; G1 delivery scope stays open.
- The owner drains successful-save events FIFO before invariants/MUST audit and
commit, at most 256 per transaction. Domain handlers only translate to outbox:
no mutation/write stores, nested sends, further raises, publication or providers.
Successful enqueue/promotion leaves sticky receipts in every ancestor;
`Result.Fail` with such work poisons the unit before frame resolution, even if
absorbed. Event-free refusals retain ADR-0040 semantics. Terminal paths discard
source state; joiners neither drain nor clear the owner.
- `SaveChangesAsync` **flushes** the context; it does not commit. `TransactionBehavior` owns
the commit boundary, so the aggregate change and the outbox row land together because they
share that transaction — not because one `SaveChanges` wrote both.
Expand Down Expand Up @@ -155,13 +188,21 @@ fails the build.

### Step 4: Consumer — handler + inbox guard

**Inherited target, pending P02b-2 G1 and P02b-3 delivery implementation.** This
example does not decide inbox ownership, deduplication identity or delivery scope.
The accepted first flow uses independent Customization `card@1`/`plain@1`
subscriptions for `TenantCreatedIntegrationEventV1`, not an Audit mirror. G1 must
prevent one subscription from suppressing its sibling before those handlers ship.
P02b-3 owns create-only preservation, per-effect MUST audit and the semantic
key/version Seeder handoff described in ADR-0057.

In `<Consumer>.Application/IntegrationEvents/<HandlerName>.cs`:

```csharp
public sealed class CreateAuditEntryOnEnrollmentCreated(
public sealed class QueueWelcomeNotificationOnEnrollmentCreated(
IInboxGuard inbox,
AuditDbContext db,
ILogger<CreateAuditEntryOnEnrollmentCreated> logger)
NotificationsDbContext db,
ILogger<QueueWelcomeNotificationOnEnrollmentCreated> logger)
: IIntegrationEventHandler<EnrollmentCreatedIntegrationEventV1>
{
public async Task HandleAsync(
Expand All @@ -174,8 +215,9 @@ public sealed class CreateAuditEntryOnEnrollmentCreated(
}

// ... business logic ...
var entry = AuditEntry.FromIntegrationEvent(@event);
db.AuditEntries.Add(entry);
var dispatch = NotificationDispatch.QueueWelcome(
@event.LearnerId, @event.EnrollmentId);
db.NotificationDispatches.Add(dispatch);

inbox.MarkAsProcessed(@event.EventId, @event.GetType().Name);
await db.SaveChangesAsync(ct); // atomic business write + inbox marker
Expand All @@ -185,7 +227,8 @@ public sealed class CreateAuditEntryOnEnrollmentCreated(

Rules:

- **Every** `IIntegrationEventHandler` invokes `IInboxGuard.IsAlreadyProcessedAsync`
- Under the inherited consumer target, **every** `IIntegrationEventHandler` invokes
`IInboxGuard.IsAlreadyProcessedAsync`
before any business logic. The architecture test
`Integration_Event_Handlers_Use_InboxGuard` holds every handler to it from
[Phase 02b](../../../docs/roadmap/phase-02b-events-auth.md), which ships the guard; it
Expand Down Expand Up @@ -222,7 +265,7 @@ construction-free registry discovers and registers its concrete handlers:
```csharp
builder.AddLearnStackCrossCuttingFoundation(
deploymentMode,
typeof(CreateAuditEntryOnEnrollmentCreated).Assembly);
typeof(QueueWelcomeNotificationOnEnrollmentCreated).Assembly);
```

Today `InProcessEventBus` resolves the concrete
Expand All @@ -234,7 +277,13 @@ remains identical across transports.

### Step 6: Tests

Two tests minimum:
Follow the canonical producer rules and remaining consumer obligations in
[Standards 21](../../../docs/standards/21-architecture-tests-catalogue.md#events-jobs-and-correlation):
announced transaction/admission, sticky failure receipts, owner completion,
outbox-only handlers and identifier-only payloads; extend existing partition and
row-context proofs. Use real production subjects and planted violations, including
no-listener trace context, failed-save attribution, absorbed descendant refusal,
byte boundaries, SQL NULL metadata and terminal reuse. For delivered flows:

1. **Producer test** — exercise the handler, assert an `outbox_messages` row exists
with the right `type`, `topic`, `payload`.
Expand All @@ -245,9 +294,9 @@ Two tests minimum:

- `dotnet build` and `dotnet test` pass.
- `LearnStack.Tests.Architecture` is green — `Integration_Event_TopicNames_FollowConvention`
runs today; `Integration_Events_Inherit_From_IntegrationEventBase` and
`Integration_Event_Handlers_Use_InboxGuard` are registered for
[Phase 02b](../../../docs/roadmap/phase-02b-events-auth.md) and run from there. Check
and `Integration_Events_Inherit_From_IntegrationEventBase` run today.
`Integration_Event_Handlers_Use_InboxGuard` remains Registered for P02b-3's
consumer delivery after G1. Check
[the catalogue](../../../docs/standards/21-architecture-tests-catalogue.md) for their
status rather than assuming a net is under you.
- An integration test confirms the round-trip: handler enqueues → outbox row
Expand All @@ -260,10 +309,11 @@ Two tests minimum:
- **Crossing modules with a domain event.** Domain events stay inside the module
that owns the aggregate. The moment another module needs to react, it must be an
integration event.
- **Forgetting `IInboxGuard`.** The architecture test catches it, but the symptom
in production (Kafka redelivery → duplicate work) is silent until then.
- **Forgetting `IInboxGuard`.** The consumer rule remains Registered until
P02b-3; do not infer enforcement from its name. Consumer delivery must prove
redelivery cannot duplicate its business effect.
- **Writing the outbox row in a separate transaction.** Use `IOutbox.EnqueueAsync`
inside the ambient `DbContext`; never `new TransactionScope`.
through the enrolled ambient `IUnitOfWork`; never `new TransactionScope`.
- **Supplying a second topic at enqueue or subscription time.** The concrete
event's `Topic` override is the one source; persistence, envelope and transport
forward it unchanged.
Expand Down
22 changes: 21 additions & 1 deletion .claude/skills/code-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Walk the diff for correctness issues. Common shapes:
| **Race condition** | Read-modify-write without optimistic concurrency (`row_version`) or a transaction. Cross-instance state without `ICacheService` invalidation. |
| **Resource leak** | `DbContext` / `HttpClient` / file stream not disposed; long-lived `DaprClient` reuse via DI, not `new`. |
| **Idempotency** | Command with external side effects (LiveKit room, Stripe charge) missing `Idempotency-Key` handling. Integration-event consumer missing `IInboxGuard.IsAlreadyProcessedAsync`. |
| **Outbox atomicity** | Outbox row written in the same `DbContext` SaveChanges as the aggregate. No separate transaction. |
| **Outbox atomicity** | Immediate INSERT through the enrolled, announced ambient `IUnitOfWork`; source state/outbox/MUST audit share its transaction. SaveChanges flushes only. ADR-0057 generation, tenant, read-only, payload/metadata and duplicate refusals hold. |
| **Tenant context restoration** | Background job + integration-event handler restores `TenantContext` before any work. |
| **Edge cases** | Empty list, zero, negative, max-int, very-long string. Test or guard. |
| **Time / clock** | Hard-coded UTC / DateTime.Now / DateTime.UtcNow vs `IClock` injection. |
Expand Down Expand Up @@ -282,6 +282,26 @@ Author intent: <one-paragraph restatement>
— a separate rule per [ADR-0038](../../../docs/decisions/0038-cross-cutting-port-and-event-contracts.md)
and [29-dapr-integration.md § 8](../../../docs/architecture/29-dapr-integration.md);
architecture test `Dapr_SDK_Types_NotImportedOutsideInfrastructure`.
- **ADR-0057 producer boundaries.** Domain handlers only translate to outbox;
inspect helper/interface paths for mutations, write stores, nested sends,
further raises, notification publication and providers. Successful-save events
drain FIFO at one owner site before invariants/MUST audit/commit, with a total
256 bound. Sticky produced-work receipts reach all ancestors; absorbed
`Result.Fail` poisons before frame resolution. Event-free control still commits.
Verify failed-save attribution and cleanup on every terminal/same-scope path.
- **ADR-0057 payload and execution.** Additional fields are identifiers only;
concrete-runtime frozen JSON fits the inclusive 262,144-byte UTF-8 cap; JSON
metadata is SQL NULL. Trusted execution supplies no-listener Activity and
causation/causal actor; transaction announcement supplies tenant/organization.
No caller header/snapshot establishes authority. Common non-MediatR audit
entry refuses unregistered direct mutation and reconciles only at the outer
frame; producer receipts are distinct. P02b-3 delivery/inbox remains open.
- **Real flow and evidence.** Completed provisioning emits
`TenantCreatedIntegrationEventV1`; P02b-1 claims pending rows only. P02b-3 owns
separate create-only `card@1`/`plain@1` subscriptions and Seeder identity
handoff after G1; no settings invalidation or Audit mirror is the sample.
Read current producer/consumer statuses in the catalogue; require production
censuses and planted offenders rather than zero-subject passes.
- **No direct write to `audit_log` / `outbox_messages` /
`platform_entitlement_cache`.** Use `IAuditStore` / `IOutbox` /
`IEntitlementProvider.RefreshAsync`.
Expand Down
Loading
Loading