Skip to content

Add modular-monolith architecture design and incremental implementation plan - #423

Closed
mforce wants to merge 10 commits into
mainfrom
codex/plan-cluckwork-modular-monolith-re-architecture
Closed

mforce wants to merge 10 commits into
mainfrom
codex/plan-cluckwork-modular-monolith-re-architecture

Conversation

@mforce

@mforce mforce commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Motivation

  • Capture a proposed hybrid modular-monolith architecture that enforces module contracts, a small shared kernel, and a single AppDbContext while preserving current runtime/DB topology.
  • Provide a practical, incremental delivery path and safety-first rollout rules to avoid behavioral or schema regressions during refactor.
  • Define concrete enforcement requirements (guards/tests) to prevent accidental compile- or runtime coupling across modules.

Description

  • Add docs/architecture/modular-monolith-design.md which documents the recommended design, vocabulary, module responsibilities, cross-module seams, EF/Core strategy, enforcement requirements, and an objective finish line.
  • Add docs/architecture/modular-monolith-implementation-plan.md which provides a phased, incremental implementation plan with per-phase commits, verification commands, rollback rules, and a recommended Finance pilot.
  • The documents enumerate specific architecture guards and mutation-tested checks (project/contract/dag/ownership/signature guards), and include a verification command set such as dotnet build, dotnet test, npm --prefix web run typecheck, and the git diff migration check for the migration baseline.
  • No production code, migrations or API surface changes are made in this PR; the files are documentation and planning artifacts that prescribe future changes and required tests.

Testing

  • This is a documentation-only change; no automated unit/integration tests were executed as part of this PR.
  • The implementation plan includes a concrete verification command set (dotnet build, dotnet test, npm --prefix web run typecheck, and others) that should be run as part of subsequent structural commits.
  • Future implementation phases are required to include the mutation-proof guards described in the documents and must record red mutation evidence before being accepted.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f934431f8b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

entities or the context, and cannot call `SaveChanges`. Events may later populate read
models, but are not required to manufacture module purity.
6. **Identity/platform paths.** Access owns token/security state and credential epoch.
Tenant resolution is a Platform adapter calling Access/Farm contracts; middleware order

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep tenant resolution claim-only

For the auth modularization, routing tenant resolution through Access/Farm contracts changes a load-bearing request boundary: today TenantResolutionMiddleware populates TenantContext directly from the JWT account_id claim before the credential and must-change-password middleware run. If this step starts calling module contracts, those calls either execute tenant-filtered data access before TenantContext exists or require bypassing filters in the request path, weakening the fail-closed tenant boundary. Keep tenant identity resolution in Platform from the claim, and call Farm only after the context is resolved.

AGENTS.md reference: AGENTS.md:L46-L48

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e7b37f5. §3.5 item 6 now states tenant identity resolution stays claim-only in Platform (TenantResolutionMiddleware populates TenantContext from the JWT account_id claim, unchanged) and is never routed through Access/Farm contracts to establish it — contracts are only called after TenantContext already exists.

Comment on lines +254 to +255
`GetFlockProductionEligibility(flockId,date)` before its transaction. This is a read-only
precondition; the inventory transaction covers its item/lot/usage/movement writes. No

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep flock eligibility inside the transaction

For the feed-usage refactor, moving GetFlockProductionEligibility before the inventory transaction widens a race that the current handler explicitly avoids: RecordFeedUsageHandler performs the flock read and CanRecordProductionOn check inside ExecuteInTransactionAsync after taking the item lock (src/Cluckwork.Application/Features/Inventory/RecordFeedUsage/RecordFeedUsageHandler.cs:46, :68-80). Following this plan would allow a flock to pass eligibility, then be archived/depleted before the lot consumption and usage rows commit, changing current behavior despite the plan's no-contract-change rule. Keep that Flock seam enlisted/invoked inside the inventory transaction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e7b37f5. §2.3 and §3.5 item 3 now show GetFlockProductionEligibility enlisted inside the inventory transaction, after the item lock — matching RecordFeedUsageHandler.cs today — instead of as a precondition before the transaction opens. Removes the archive/deplete race you flagged.

mforce added 2 commits August 5, 2026 23:48
…seam

Two Codex review findings on PR #423 against
docs/architecture/modular-monolith-design.md:

- Record feed usage: the eligibility check must stay enlisted inside
  the inventory transaction after the item lock, matching what
  RecordFeedUsageHandler does today, not run as a precondition
  before the transaction opens (which would reopen a flock
  archive/deplete race between check and commit).
- Identity/platform paths: tenant identity resolution stays
  claim-only in Platform via TenantResolutionMiddleware, never
  routed through Access/Farm contracts to establish TenantContext,
  preserving the fail-closed ordering against CredentialEpochMiddleware.

Updated the rendered diagrams (docs/architecture/modular-monolith-diagrams.html)
to match the corrected feed-usage seam.
@mforce

mforce commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

@codex both P2 findings from your last review — the feed-usage eligibility-check race and the tenant-resolution-through-contracts ordering hazard — are addressed in e7b37f5. Replied on each thread with specifics. Please re-check.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7b37f56ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +256 to +258
after the item lock is already held, so a flock cannot be archived or depleted between
eligibility passing and the lot-consumption/usage rows committing. Moving the check outside
the transaction would reopen that race and change today's behavior. No event is suitable:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not claim the transaction prevents flock lifecycle races

The new e7b37f5 wording still overstates what enlistment provides: RecordFeedUsageHandler explicitly says the flock row is not locked, and FlockRepository.GetByIdAsync performs an ordinary read, so under Read Committed an archive/deplete can commit after eligibility is read because feed usage never writes the flock or its version. Fresh evidence is the handler's own comment that this only “shrinks the race to the commit itself”; preserve the in-transaction ordering without claiming it closes the race, otherwise the prescribed lifecycle race guard may encode a nonexistent invariant.

AGENTS.md reference: AGENTS.md:L99-L103

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 41cf6fd. §3.5 item 3 no longer claims enlistment closes the race — it now says explicitly that it shrinks the race to the commit itself, matching RecordFeedUsageHandler's own comment, and explains that leaving the flock row unlocked is deliberate (business-valid either way), not a gap. Good catch — I'd read that exact handler comment earlier and still let the doc overclaim past it.

| 2.1 Contract project | add `src/Cluckwork.Modules.Finance.Contracts` with use-case commands/results and `IFinanceModule`; add contract tests | existing endpoint DTO maps 1:1 through an API adapter; signature guard | project is additive |
| 2.2 Implementation shell | add `src/Cluckwork.Modules.Finance.Implementation`; internal registration extension; wrap existing handlers behind contract without moving behavior | temporary `LegacyFinanceModuleAdapter`; handler unit tests plus module-interface tests | DI switches back to legacy handlers |
| 2.3 Move domain/application | move Expense/category model, handlers and validators mechanically; preserve namespaces first, then separate namespace-only commit; preserve the account `FOR SHARE` currency lock through a synchronous lock-aware Farm port | type forwards or narrow legacy repository adapters; build/test after each move | revert individual move; no schema/model configuration edit |
| 2.4 Move persistence adapter | move expense configurations/repositories behind internal Finance implementation while single context remains Platform-owned | EF model digest before/after identical; integration CRUD/tenant/version/audit tests | restore old registrations/files |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Place cross-owner EF mappings outside module implementations

When Phase 2.4 moves the current ExpenseConfiguration behind Finance implementation, its HasOne<Flock>() relationship still requires the Flock aggregate type. That either gives Finance the peer entity reference forbidden by the proposed module guard, or dropping the relationship changes the EF model digest this phase requires to remain identical. Define a privileged persistence-composition location for cross-owner relationships before the pilot rather than treating this as a mechanical configuration move.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 41cf6fd. Added a rule to design §7: a relationship configuration whose fluent API needs a peer module's entity type (HasOne() for Expense.FlockId) is cross-owner composition and stays in Platform, not in either module's Implementation assembly — authoring location only, FK/index/migration output unchanged. Updated the Phase 2.4 plan row (implementation-plan.md) to point at it so the pilot doesn't hit this as a surprise.

…er EF configs in Platform

Two more Codex findings on PR #423, against the fix in e7b37f5:

- Record feed usage: the previous wording claimed enlisting the
  eligibility check inside the transaction meant a flock "cannot be
  archived or depleted between eligibility passing and commit." False —
  RecordFeedUsageHandler's own comment says the flock row is deliberately
  not locked (a same-day deplete racing a same-day feed is business-valid
  either way), so enlistment only shrinks the race to the commit itself,
  it doesn't close it. Corrected to say that, and why it's deliberate
  rather than a gap.
- EF/data strategy: Phase 2.4 moving ExpenseConfiguration behind Finance
  implementation didn't account for its HasOne<Flock>() relationship,
  which needs the Flock CLR type in its fluent API even though Expense's
  own domain model only carries the FlockId. That's the same peer-entity
  reference the module guard forbids, and stripping it to comply would
  change the EF model digest the pilot phase requires to stay identical.
  Added a rule to design §7: a relationship configuration whose generic
  parameter names a peer module's entity type is cross-owner composition
  and stays in Platform, not in either module's Implementation assembly —
  authoring location only, FK/index/migration output unchanged. Updated
  the Phase 2.4 plan row to point at it.
@mforce

mforce commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

@codex both P2 findings from this round — the feed-usage race-closure overclaim and the cross-owner EF relationship-configuration placement — are addressed in 41cf6fd. Replied on each thread with specifics. Please re-check.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41cf6fd167

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

| 2.1 Contract project | add `src/Cluckwork.Modules.Finance.Contracts` with use-case commands/results and `IFinanceModule`; add contract tests | existing endpoint DTO maps 1:1 through an API adapter; signature guard | project is additive |
| 2.2 Implementation shell | add `src/Cluckwork.Modules.Finance.Implementation`; internal registration extension; wrap existing handlers behind contract without moving behavior | temporary `LegacyFinanceModuleAdapter`; handler unit tests plus module-interface tests | DI switches back to legacy handlers |
| 2.3 Move domain/application | move Expense/category model, handlers and validators mechanically; preserve namespaces first, then separate namespace-only commit; preserve the account `FOR SHARE` currency lock through a synchronous lock-aware Farm port | type forwards or narrow legacy repository adapters; build/test after each move | revert individual move; no schema/model configuration edit |
| 2.4 Move persistence adapter | move Expense's own configuration/repository behind internal Finance implementation; its cross-owner `Expense`→`Flock` relationship configuration stays in Platform's composition (design §7), not in Finance.Implementation, since Finance may not reference the `Flock` type | EF model digest before/after identical; integration CRUD/tenant/version/audit tests | restore old registrations/files |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Introduce an acyclic persistence boundary

In Phase 2.4, placing the relationship configuration in Platform does not compile with the declared project topology: Platform must reference Finance.Implementation to configure its moved Expense entity, while the moved Finance repository must reference Platform's internal AppDbContext, creating a Platform.Persistence ↔ Finance.Implementation project cycle. Fresh evidence after 41cf6fd is that the new placement rule combines with this same row's requirement to move both the entity-facing configuration and repository behind Finance. Define a separate persistence-contract/model boundary, or keep the entity and its persistence adapter on one side of the dependency before starting the pilot.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9a0cb45. You're right the previous fix didn't compile — corrected the actual mechanism: cross-owner FKs are never expressed through EF's fluent HasOne() at all (that generic parameter is what forces the cycle). The owning entity's config declares the FK as a plain scalar column (no reference to the peer type needed), and the constraint itself is a raw migrationBuilder.AddForeignKey by table/column name — needs no C# reference to either module's entity type by either side. Matches #283's existing raw-SQL posture. No Platform composition file, no cycle.

| same ambient transaction
+--> Flock Management.AppendMortalityMovement
+--> Egg Operations creates lots + egg-ledger opening movements
+--> Platform commits once, then publishes read/audit events

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep audit appends inside the transaction

If an implementer follows this workflow literally, AuditEvents are published only after the business commit, whereas the current IAuditWriter adds them to the same AppDbContext and §3.5.4 explicitly says audit participates in the ambient transaction. A crash or publication failure after commit would therefore leave the mutation without its audit row and could not roll it back. Separate the transactional audit append from any optional after-commit read/integration event in both diagrams.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9a0cb45. §2.3's diagrams now say the audit row is appended in the SAME transaction before Platform commits — only the read-model publish happens after commit. Also added an explicit note below the diagrams stating this matches §3.5 item 4 and §6's Platform row, so it can't drift back to the ambiguous 'read/audit events' phrasing.

…the transaction

Two more Codex findings on PR #423, round 3:

- Phase 2.4's previous fix (cross-owner relationship config lives in "a
  Platform-owned composition file") doesn't compile: Platform would need
  Finance.Implementation for the Expense type, while Finance's repository
  needs Platform's AppDbContext — an unavoidable cycle. Real fix: never
  express a cross-owner FK through EF's fluent HasOne<T>() at all: the
  owning entity's own configuration declares the FK column as a plain
  scalar (already true — Expense carries only FlockId, no navigation),
  and the FK constraint itself is a raw migrationBuilder.AddForeignKey
  operation by table/column name — no C# reference to either module's
  entity type needed by either side. Matches #283's existing raw-SQL
  posture and §3.3's own statement that cross-owner FKs are database
  constraints, not EF navigations. Updated design §7 and the Phase 2.4
  plan row together.

- §2.3's diagrams said "Platform commits once, then publishes read/audit
  events" — bundling audit with the after-commit read publish, which
  contradicts §3.5 item 4 ("audit participates in the ambient
  transaction") and §6's own Platform row ("audit append can enlist").
  A literal implementer would ship audit as a decoupled after-commit
  event, losing audit rows on any post-commit crash/failure. Fixed both
  diagram lines and added an explicit note: the audit row is one of the
  atomic writes, appended in the SAME transaction, rolled back with it
  on failure — only the read-model publish happens after commit.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex both round-3 findings addressed in 9a0cb45 — the Phase 2.4 persistence-boundary cycle (real fix this time: raw FK migration, not a Platform composition file) and the audit-ordering diagram contradiction. Please re-check.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a0cb45015

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

| 2.1 Contract project | add `src/Cluckwork.Modules.Finance.Contracts` with use-case commands/results and `IFinanceModule`; add contract tests | existing endpoint DTO maps 1:1 through an API adapter; signature guard | project is additive |
| 2.2 Implementation shell | add `src/Cluckwork.Modules.Finance.Implementation`; internal registration extension; wrap existing handlers behind contract without moving behavior | temporary `LegacyFinanceModuleAdapter`; handler unit tests plus module-interface tests | DI switches back to legacy handlers |
| 2.3 Move domain/application | move Expense/category model, handlers and validators mechanically; preserve namespaces first, then separate namespace-only commit; preserve the account `FOR SHARE` currency lock through a synchronous lock-aware Farm port | type forwards or narrow legacy repository adapters; build/test after each move | revert individual move; no schema/model configuration edit |
| 2.4 Move persistence adapter | move Expense's own configuration/repository behind internal Finance implementation, configuring `FlockId` as a plain scalar column (no `HasOne<Flock>()` — that generic parameter would force a `Platform.Persistence` ↔ `Finance.Implementation` cycle, design §7); the `Expense`→`Flock` FK constraint itself is a raw `migrationBuilder.AddForeignKey` operation by table/column name, needing no reference to either module's entity type | EF model digest before/after identical; integration CRUD/tenant/version/audit tests | restore old registrations/files |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not recreate the existing expense foreign key

Fresh evidence after 9a0cb45 is that 20260801190854_InitialCreate.cs already creates both FK_Expenses_Flocks_FlockId and IX_Expenses_FlockId, while this phase now removes HasOne<Flock>() and prescribes another AddForeignKey. A new migration would fail on every fresh or upgraded database because the named constraint already exists; omitting the migration instead removes the relationship and convention-created index from EF's runtime model, so the required before/after model digest cannot remain identical. Preserve this relationship in EF metadata through an acyclic non-generic mechanism rather than emitting the existing schema again.

AGENTS.md reference: AGENTS.md:L50-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — the round-3 raw-migration fix was itself wrong since InitialCreate already creates FK_Expenses_Flocks_FlockId + IX_Expenses_FlockId. Fixed in 596fe08: §7 now specifies a string-based non-generic HasOne("Cluckwork.Domain.Flock") relationship, using the fully-qualified type name as a string (not typeof(Flock), not the generic HasOne()) — reproduces the existing FK/index byte-for-byte so EF's model digest is unchanged and no migration is emitted at all, while still needing no compile-time reference to either module's Implementation assembly. Implementation-plan.md's Phase 2.4 row updated to match.

A1[Flock Management.AppendMortalityMovement]
A2["Egg Operations creates lots +&lt;br/&gt;egg-ledger opening movements"]
end
P1["Platform commits once,&lt;br/&gt;then publishes read/audit events"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep audit append inside the HTML transaction diagrams

Fresh evidence after 9a0cb45 is that the companion HTML diagram still places publication of read/audit events on the arrow explicitly labeled after commit (and repeats this for Commerce at line 442). Readers using this standalone architecture artifact are therefore still instructed to decouple the audit append, contradicting the corrected Markdown and allowing a crash after the business commit to leave no audit row; show the audit append inside each transaction and only read-event publication after commit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 596fe08 — both P1 (Submit daily entry) and P2 (Confirm/void sale) panels now show the audit append inside the transaction subgraph, and the after-commit box only covers read-event publication, matching the corrected Markdown.

#423 round 5)

Phase 2.4's raw AddForeignKey would recreate a constraint InitialCreate
already emits, failing on every migrated DB. Replace with a string-based
non-generic HasOne(...) relationship that reproduces the existing FK/index
byte-for-byte, needing no migration and no cross-module type reference.

Also bring the HTML diagrams' two after-commit boxes in line with the
design doc: audit append is inside the transaction, only read-event
publication happens after commit.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex both round-5 findings addressed in 596fe08 — the expense-FK fix now uses a string-based non-generic relationship instead of a migration that would collide with InitialCreate, and the HTML diagrams' audit-append timing now matches the design doc. Please re-check.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 596fe08b93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +392 to +393
`builder.HasOne("Cluckwork.Domain.Flock").WithMany().HasForeignKey("FlockId")
.HasConstraintName("FK_Expenses_Flocks_FlockId")` — using the *fully-qualified type name as a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the actual Flock entity name

The string-based relationship targets Cluckwork.Domain.Flock, but the configured CLR entity and frozen snapshot use Cluckwork.Domain.Flocks.Flock. Following this Phase 2.4 example therefore does not bind the expense relationship to the existing Flock entity and will fail the promised model-equivalence check or model construction; use the exact current fully qualified name (and describe the dependency as Flock Management rather than Farm).

AGENTS.md reference: AGENTS.md:L50-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed by checking src/Cluckwork.Domain/Flocks/Flock.cs directly — the type is Cluckwork.Domain.Flocks.Flock, not Cluckwork.Domain.Flock. Fixed in 470ec7f — §7's example and implementation-plan.md's Phase 2.4 row both corrected, and 'Farm's assembly' relabeled to 'Flock Management's assembly' per your note.

Comment on lines +392 to +393
`builder.HasOne("Cluckwork.Domain.Flock").WithMany().HasForeignKey("FlockId")
.HasConstraintName("FK_Expenses_Flocks_FlockId")` — using the *fully-qualified type name as a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the Restrict delete behavior

The replacement chain omits the existing .OnDelete(DeleteBehavior.Restrict) from ExpenseConfiguration, while InitialCreate also records ReferentialAction.Restrict. For this optional FK, EF's convention defaults to ClientSetNull, so implementing the shown chain does not reproduce the runtime model byte-for-byte and the required before/after model guard should fail; include the explicit Restrict behavior in the non-generic mapping.

AGENTS.md reference: AGENTS.md:L50-L50

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed by checking ExpenseConfiguration.cs — it does have .OnDelete(DeleteBehavior.Restrict) on the Flock FK. Fixed in 470ec7f — added .OnDelete(DeleteBehavior.Restrict) to the string-based example in both design.md §7 and implementation-plan.md's Phase 2.4 row.

direction TB
A1[Flock Management.AppendMortalityMovement]
A2["Egg Operations creates lots +&lt;br/&gt;egg-ledger opening movements"]
A3["Platform appends the audit row"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep unaudited commands behaviorally unchanged

The timing is now correct, but following this panel still changes behavior: the current SubmitDailyEntryHandler and ConfirmSaleHandler neither inject nor call IAuditWriter; only the adjust/void paths shown elsewhere write audit rows. Adding this step during Phases 5–6 would create new records visible through audit views despite the plan's behavior-compatibility rule, so show the append only for currently audited commands or authorize it as a separate product change.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — checked the actual handlers: SubmitDailyEntryHandler and ConfirmSaleHandler inject no IAuditWriter; only AdjustDailyEntryHandler, VoidDailyEntryHandler, VoidSaleHandler and VoidPaymentHandler do. Fixed in 470ec7f: split the combined 'ConfirmSale / VoidSale' panel into separate Confirm sale (no audit) and Void sale (audit) panels in both the Markdown §2.3 diagram and the HTML companion, and removed the audit-append claim from the Submit daily entry panel. Added a note explaining why (this plan's no-behavior-change rule).

…or unaudited commands (#423 round 6)

- §7's string-based FK example named a type that doesn't exist
  (Cluckwork.Domain.Flock instead of Cluckwork.Domain.Flocks.Flock)
  and omitted ExpenseConfiguration's existing OnDelete(Restrict) —
  either would fail the promised model-equivalence check. Fixed in
  design.md and implementation-plan.md's Phase 2.4 row.
- §2.3's cross-module write diagram (Markdown and the HTML companion)
  showed every command appending an audit row inside its transaction.
  SubmitDailyEntryHandler and ConfirmSaleHandler inject no
  IAuditWriter today and write none — only Adjust/Void paths do. Split
  Confirm/Void sale into two panels and removed the false audit claim
  from Submit and Confirm, so the diagram matches current behavior
  rather than implying a product change this refactor doesn't
  authorize.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex all three round-6 findings addressed in 470ec7f — the Flock FQN and OnDelete(Restrict) are fixed in the FK example, and the audit-row diagrams (Markdown + HTML) now only show it where SubmitDailyEntry/ConfirmSale/AdjustDailyEntry/VoidDailyEntry/VoidSale/VoidPayment actually inject IAuditWriter today. Please re-check.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 470ec7fb12

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Commerce.ConfirmSale
| same ambient transaction
+--> Egg Operations.ReserveFifo
+--> Commerce updates order, allocation and payment state

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep payment mutations out of sale confirmation

When Phases 5–6 are implemented from this workflow, this step incorrectly folds payment mutation into confirm/void: ConfirmSaleHandler does not access payments at all, while VoidSaleHandler only checks AnyNonVoidedByOrderAsync and returns SalesOrder.HasPayments, requiring payments to be voided separately through VoidPaymentHandler. The same claim is repeated in the HTML diagram and §3.5; following it would change the independent payment lifecycle despite the plan's behavioral-compatibility rule, so describe the void payment gate and leave payment recording/voiding as separate commands.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed by checking both handlers directly: ConfirmSaleHandler never references payments; VoidSaleHandler only calls IPaymentRepository.AnyNonVoidedByOrderAsync as a guard (rejects with SalesOrder.HasPayments if one exists) and never mutates one. Fixed in 633e18b — §2.3 diagram (both Markdown and the HTML companion) and §3.5 item 2 now describe void's payment interaction as a gate, not a mutation, and note payments are recorded/voided through their own separate command (VoidPaymentHandler).

…und 7)

ConfirmSaleHandler never references payments; VoidSaleHandler only
checks IPaymentRepository.AnyNonVoidedByOrderAsync as a guard (rejects
the void if a payment exists) and never mutates one — payments are
recorded/voided through their own separate command (VoidPaymentHandler).
Fixed the §2.3 diagram (Markdown + HTML), §3.5 item 2, to describe
VoidSale's payment interaction as a gate, not a mutation, for the same
no-behavior-change reason as the prior audit-row fix.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex round-7 finding addressed in 633e18b — confirm/void sale no longer claims to mutate payments; void's payment interaction is now described as a gate. Please re-check.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 633e18bb7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

|---|---|---|---|
| 2.1 Contract project | add `src/Cluckwork.Modules.Finance.Contracts` with use-case commands/results and `IFinanceModule`; add contract tests | existing endpoint DTO maps 1:1 through an API adapter; signature guard | project is additive |
| 2.2 Implementation shell | add `src/Cluckwork.Modules.Finance.Implementation`; internal registration extension; wrap existing handlers behind contract without moving behavior | temporary `LegacyFinanceModuleAdapter`; handler unit tests plus module-interface tests | DI switches back to legacy handlers |
| 2.3 Move domain/application | move Expense/category model, handlers and validators mechanically; preserve namespaces first, then separate namespace-only commit; preserve the account `FOR SHARE` currency lock through a synchronous lock-aware Farm port | type forwards or narrow legacy repository adapters; build/test after each move | revert individual move; no schema/model configuration edit |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Convert infrastructure callers during the Finance pilot

Moving the Expense models and handlers behind Finance.Implementation at this phase leaves production callers that are not scheduled for conversion until much later: SimulationDataSeeder directly invokes CreateExpense*Handler and queries db.ExpenseCategories/db.Expenses (SimulationDataSeeder.cs:15-16,1034-1054), while reporting/export queries also use those typed sets. After this move, those callers must either reference the supposedly hidden implementation or retain compatibility exposure through Phases 9–10, contradicting this phase's completion criterion that Finance is reached solely through its contract and compatibility adapters are zero. Convert these callers during the pilot, or defer internalizing the Expense types until their contract-based replacements exist.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed by checking SimulationDataSeeder.cs directly — it does DI-inject CreateExpenseCategoryHandler/CreateExpenseHandler and query db.ExpenseCategories/db.Expenses for its idempotent-seeding checks and count verification. Fixed in 559caa0, but not via your first option (converting during the pilot): Phase 10 item 3 already schedules 'convert demo/simulation seeders to module bootstrap/orchestration contracts one dataset at a time,' so pulling this single caller's conversion into Phase 2 would duplicate that work. Instead named it as an explicit, tracked compatibility exception (owner: the seeder; deletion phase: 10.3) per the plan's own compatibility-adapter policy, with the 1.3 guard required to allow-list it by name — so the 'zero adapters' completion criterion is honest rather than silently false for eight phases. The read-side (ExportQueries/ReportQueries also querying Expenses tables, which you mentioned) isn't touched by this — that's already covered by design.md §7's Insights read-only-facade allowance and Phase 9's planned move, not a gap.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e96c264 — ReportQueries/ExportQueries/CurrencyBoundRowProbe are now each their own named, tracked compatibility exception (same policy as SimulationDataSeeder), with per-caller resolution points: ReportQueries/ExportQueries close at Phase 9 (already scheduled to move them, per implementation-plan.md's existing Phase 9 item 2); CurrencyBoundRowProbe spans Finance/Commerce/Inventory and stays allow-listed through Phase 7 (the last of the three it references). The 1.3 guard's allow-list now needs to name all four. Docs-only change; no code/tests to verify against.

…ity exception (#423 round 8)

SimulationDataSeeder DI-injects CreateExpenseCategoryHandler/
CreateExpenseHandler directly and queries db.ExpenseCategories/
db.Expenses for its idempotent-seeding checks and count verification
— a real caller of the types Phase 2.3/2.4 make internal to
Finance.Implementation. Phase 10 item 3 already schedules converting
demo/simulation seeders generally, so pulling this one caller's
conversion into Phase 2 would duplicate that work. Instead: name it
explicitly as a tracked compatibility exception (owner: the seeder,
deletion phase: 10.3) per the plan's own compatibility-adapter policy,
with the 1.3 guard allow-listing it by name — so the "zero adapters"
completion criterion stays honest instead of silently false for eight
phases.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex round-8 finding addressed in 559caa0 — SimulationDataSeeder is now a named, tracked compatibility exception through Phase 10.3 rather than silently contradicting Phase 2's zero-adapters completion criterion. Please re-check.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 559caa020a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +64 to +65
name rather than fail on it, so the guard's own "zero bypasses" claim stays true about every
*other* caller while this one is visible and dated, not accidentally exempted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track every deferred Finance caller

Fresh evidence after 559caa0 is that the allow-list names only SimulationDataSeeder, but ReportQueries (:225-252), ExportQueries (:235-240), and CurrencyBoundRowProbe (:21) also access the typed Finance DbSets and are not converted until later phases. When Phase 2.3/2.4 moves those types behind Finance.Implementation, these callers either stop compiling or become additional implementation bypasses, so the guard cannot truthfully report zero bypasses for every other caller; convert them during the pilot or give each a tracked, expiring exception.

AGENTS.md reference: AGENTS.md:L99-L103

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e96c264 — same fix as the sibling round-8 finding on the implementation-plan.md diff. All three callers you named are now explicit exceptions with owners and resolution phases (ReportQueries/ExportQueries at Phase 9, CurrencyBoundRowProbe at Phase 7), and the completion criterion now says "four named, dated exceptions" instead of the one. Docs-only change.

…s named Phase 2 exceptions (#423 round 8)

codex found SimulationDataSeeder's own exception didn't cover two more real
production callers of Finance's soon-internal Expense types. Names each with
its own resolution point instead of one shared deadline: ReportQueries/
ExportQueries close at Phase 9 (already scheduled to move them);
CurrencyBoundRowProbe spans Finance/Commerce/Inventory and can't close until
the last of those (Phase 7) lands.
@mforce

mforce commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@codex round-8 findings both addressed in e96c264 — ReportQueries, ExportQueries, and CurrencyBoundRowProbe are now each their own named, tracked compatibility exception with a per-caller resolution phase, and the guard/completion language now says four exceptions instead of one.

@chatgpt-codex-connector

Copy link
Copy Markdown

No findings. The round-8 issue is addressed:

Checks

  • ✅ git show --format=fuller --no-ext-diff e96c264 -- docs/architecture/modular-monolith-implementation-plan.md
  • ✅ rg -n '\b(Expenses|ExpenseCategories)\b' src tests --glob '*.cs'
  • ✅ rg -n 'SimulationDataSeeder|ReportQueries|ExportQueries|CurrencyBoundRowProbe|four named|Phase 9 item 2|Phase 7' docs/architecture/modular-monolith-implementation-plan.md
  • ✅ git status --short (clean)

No changes were needed, so I did not create a commit or a new pull request.

View task →

@mforce

mforce commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Converting to issue #514 to track separately — docs preserved there (full text + permalinks to this PR's final commit). Closing without merge.

@mforce mforce closed this Aug 12, 2026
@mforce
mforce deleted the codex/plan-cluckwork-modular-monolith-re-architecture branch August 12, 2026 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant