Cratis code review
A review is worth the reader's time only when it separates what must change from
what could. Everything below is a criterion an analyzer does not already
enforce — if the build is clean and this list is clean, the change is sound on
the axes a reviewer can judge.
Review the change, not the file. A pre-existing violation in a line the
change did not touch is a note, never a blocker.
Verified product sources
| Package |
Version |
Purpose |
Cratis.Arc.Core |
22.10.4 |
Command, query, validation and analyzer surface (ARC0001–ARC0015) |
Cratis.Chronicle |
16.45.2 |
Event, projection, read-model and constraint surface |
Cratis.Fundamentals |
7.18.2 |
ConceptAs<T>, IInstancesOf<T>, the DI conventions |
Reverify against the owning product repository before asserting a framework
contract this file does not already state.
Route near misses
- A focused authentication, authorization, data-exposure, or event-sourcing
security audit: use
cratis-security-review.
- A focused Chronicle, database, .NET or React scalability analysis: use
cratis-performance-review. The performance items below are the ones a
general reviewer should catch in passing; do not duplicate the specialist's
findings when both have run.
- Deciding whether the behavior is right at all: that is modeling, not review.
Step 1 — Run the gates first
A review that reports what the build already says is noise. Confirm the change
builds clean in Debug and Release, its specifications pass, and lint and the
TypeScript build are clean. Report a gate failure as the finding and stop —
there is nothing to review under a red build.
Step 2 — Architecture
- Each slice is its own folder,
<Module>/<Feature>/<Slice>/<Slice>.cs, with the
backend artifacts together. No top-level Features/ wrapper.
- Namespace mirrors the folder path under the source root.
- Commands are
record types with Handle() on the record. No separate handler
class.
- Business rejection returns a
ValidationResult or
Result<TEvent, ValidationResult>, or comes from a validator. Never thrown
from Provide() or Handle() — a throw is HTTP 500, not a validation error.
- Fetched or computed handler data is in
Provide(), not inline in Handle().
- Events are
record types: past tense, no mutable and no nullable properties,
never carrying the event-source id, and each has an XML <summary>.
- Identity concepts derive from
EventSourceId<T>, not ConceptAs<Guid>.
- Domain values are concepts, not raw
Guid, string, or int.
- Projections consume events, never read models. AutoMap is on by default —
.AutoMap() appears only inside a scope disabled with .NoAutoMap().
- A
[Projection] id, once given explicitly and deployed, is permanent. The
argument is optional; adding one to an existing projection after the fact
changes its identity.
- Model-bound query custom paths use
[Path("...")], never ASP.NET [Route].
- No service locator:
IServiceProvider is not injected. Implementation sets
come from IInstancesOf<T>, never IEnumerable<T>.
- No explicit singleton registration where
[Singleton] suffices.
- Logging lives in a
*Logging.cs partial with [LoggerMessage], not inline in
domain code.
- No shared mutable state between commands.
Step 3 — C# style
- File-scoped namespaces;
using directives sorted, none unused.
is null / is not null, never == null / != null.
var over an explicit type.
- No
Async, Impl, Service, Manager or Helper postfix on a class name.
- No regions.
- Custom exception types only — never
InvalidOperationException,
ArgumentException or another built-in (ARC0012 flags this on Arc
artifacts). The XML doc starts with "The exception that is thrown when …".
- Every public type, method and property carries a multiline XML doc.
<summary> is never collapsed onto one line. Every parameter has a <param>;
every non-void method has <returns>; every throw has an <exception cref>.
- The copyright header is on every file; the file ends with one newline.
Step 4 — TypeScript and components
const over let over var; no unused imports.
- No
any. Use unknown with a type guard; widen through
value as unknown as TargetType rather than (x as any).
- No
@ts-ignore or @ts-expect-error without a comment saying why.
- Full descriptive names — never
e, idx, prev, dir, pos.
CommandDialog from @cratis/components/CommandDialog for command dialogs;
Dialog from @cratis/components/Dialogs for data-only dialogs. Never
Dialog from primereact/dialog.
- No hard-coded hex or rgb colors — PrimeReact CSS variables only. No
!important without a justifying comment.
- Components live in the slice folder. No barrel
index.ts that re-exports one
component, and no technical hooks/ / utils/ / types/ grouping at feature
level.
- The copyright header is on every file.
Step 5 — Performance, in passing
Performance is part of an ordinary review, not only a separate pass. Flag the
degradations a reviewer can see without measuring:
- A projection joining on a read model; a reactor re-querying the event log
inside a handler instead of using the event data.
- A new projection that could not replay all historical events; events carrying
large blobs.
- A query that does not filter on an indexed field; a growable list that does
not return
IQueryable<T> for server-side paging; hydrating a collection only
to count it.
- An N+1 pattern; a response payload with fields no client reads.
- React: a growable list rendering every row; an inline object or array literal
passed as a prop, changing identity every render; wrong
useEffect
dependencies.
- .NET:
.ToList() before .Where(); an IEnumerable<T> enumerated more than
once.
Step 6 — Specification coverage
- Every State Change command has a happy-path specification.
- Every validation rule has a failure specification asserting both
ShouldNotBeSuccessful() and ShouldHaveValidationErrors().
- Every business-rule rejection has a specification.
- Every constraint has an
EventScenario specification asserting the constraint
name, not its message.
- No specification asserts on a presentation message string.
- Nothing trivial is specified — a property getter, a constructor pass-through,
a delegation.
- No specification sleeps to let the system catch up.
Step 7 — Report
Open with one line:
Review result: Approved / Approved with comments / Changes requested
Then, per file:
### <file path>
**[BLOCKING]** Line N: `problematic code`
Because: <the consequence, not the rule number>
Fix:
<corrected code>
Close with what passed and what must change. Two rules make the report usable:
- A blocking finding names a consequence. "Violates the style guide" is not
a reason. "Throws on a recoverable path, so the caller sees a 500 instead of a
validation error" is.
- Say what you did not review. A report listing only findings reads as if
everything was checked. Name the files, the paths, and the axes you skipped.
What breaks
- The review restates the compiler. The gates were not run first, so
analyzer output is being reported as review findings.
- Every finding is blocking. The distinction is what makes the report
actionable; if everything blocks, nothing is prioritized.
- A convention is reported as a framework contract. The slice folder shape
and the single-file default are house conventions;
Handle() on the record and
the [Path] attribute are contracts. Saying "the framework requires this" of a
convention loses the reader's trust for the findings that are contracts.
How it is proven
The build, the specifications, lint and the TypeScript build are all green
before the report is written, and the report names both what was reviewed and
what was not.
1---2name: cratis-code-review3description: Review changed code in a Cratis application against the architecture, style, and specification-coverage criteria that the compiler cannot check, and produce a structured report with blocking issues separated from suggestions. Use when asked to review, check, or validate a change. Do not substitute it for a focused security audit and do not restate specialist performance findings.4license: MIT5---67# Cratis code review89A review is worth the reader's time only when it separates what must change from10what could. Everything below is a criterion an analyzer does **not** already11enforce — if the build is clean and this list is clean, the change is sound on12the axes a reviewer can judge.1314Review the **change**, not the file. A pre-existing violation in a line the15change did not touch is a note, never a blocker.1617## Verified product sources1819| Package | Version | Purpose |20| --- | --- | --- |21| `Cratis.Arc.Core` | `22.10.4` | Command, query, validation and analyzer surface (`ARC0001`–`ARC0015`) |22| `Cratis.Chronicle` | `16.45.2` | Event, projection, read-model and constraint surface |23| `Cratis.Fundamentals` | `7.18.2` | `ConceptAs<T>`, `IInstancesOf<T>`, the DI conventions |2425Reverify against the owning product repository before asserting a framework26contract this file does not already state.2728## Route near misses2930- A focused authentication, authorization, data-exposure, or event-sourcing31 security audit: use `cratis-security-review`.32- A focused Chronicle, database, .NET or React scalability analysis: use33 `cratis-performance-review`. The performance items below are the ones a34 general reviewer should catch in passing; do not duplicate the specialist's35 findings when both have run.36- Deciding whether the behavior is right at all: that is modeling, not review.3738## Step 1 — Run the gates first3940A review that reports what the build already says is noise. Confirm the change41builds clean in Debug and Release, its specifications pass, and lint and the42TypeScript build are clean. Report a gate failure as the finding and stop —43there is nothing to review under a red build.4445## Step 2 — Architecture4647- Each slice is its own folder, `<Module>/<Feature>/<Slice>/<Slice>.cs`, with the48 backend artifacts together. **No top-level `Features/` wrapper.**49- Namespace mirrors the folder path under the source root.50- Commands are `record` types with `Handle()` on the record. No separate handler51 class.52- Business rejection returns a `ValidationResult` or53 `Result<TEvent, ValidationResult>`, or comes from a validator. **Never thrown54 from `Provide()` or `Handle()`** — a throw is HTTP 500, not a validation error.55- Fetched or computed handler data is in `Provide()`, not inline in `Handle()`.56- Events are `record` types: past tense, no mutable and no nullable properties,57 never carrying the event-source id, and each has an XML `<summary>`.58- Identity concepts derive from `EventSourceId<T>`, not `ConceptAs<Guid>`.59- Domain values are concepts, not raw `Guid`, `string`, or `int`.60- Projections consume events, never read models. AutoMap is on by default —61 `.AutoMap()` appears only inside a scope disabled with `.NoAutoMap()`.62- A `[Projection]` id, once given explicitly and deployed, is permanent. The63 argument is optional; adding one to an existing projection after the fact64 changes its identity.65- Model-bound query custom paths use `[Path("...")]`, never ASP.NET `[Route]`.66- No service locator: `IServiceProvider` is not injected. Implementation sets67 come from `IInstancesOf<T>`, never `IEnumerable<T>`.68- No explicit singleton registration where `[Singleton]` suffices.69- Logging lives in a `*Logging.cs` partial with `[LoggerMessage]`, not inline in70 domain code.71- No shared mutable state between commands.7273## Step 3 — C# style7475- File-scoped namespaces; `using` directives sorted, none unused.76- `is null` / `is not null`, never `== null` / `!= null`.77- `var` over an explicit type.78- No `Async`, `Impl`, `Service`, `Manager` or `Helper` postfix on a class name.79- No regions.80- Custom exception types only — never `InvalidOperationException`,81 `ArgumentException` or another built-in (`ARC0012` flags this on Arc82 artifacts). The XML doc starts with "The exception that is thrown when …".83- Every public type, method and property carries a multiline XML doc.84 `<summary>` is never collapsed onto one line. Every parameter has a `<param>`;85 every non-void method has `<returns>`; every throw has an `<exception cref>`.86- The copyright header is on every file; the file ends with one newline.8788## Step 4 — TypeScript and components8990- `const` over `let` over `var`; no unused imports.91- No `any`. Use `unknown` with a type guard; widen through92 `value as unknown as TargetType` rather than `(x as any)`.93- No `@ts-ignore` or `@ts-expect-error` without a comment saying why.94- Full descriptive names — never `e`, `idx`, `prev`, `dir`, `pos`.95- `CommandDialog` from `@cratis/components/CommandDialog` for command dialogs;96 `Dialog` from `@cratis/components/Dialogs` for data-only dialogs. **Never**97 `Dialog` from `primereact/dialog`.98- No hard-coded hex or rgb colors — PrimeReact CSS variables only. No99 `!important` without a justifying comment.100- Components live in the slice folder. No barrel `index.ts` that re-exports one101 component, and no technical `hooks/` / `utils/` / `types/` grouping at feature102 level.103- The copyright header is on every file.104105## Step 5 — Performance, in passing106107Performance is part of an ordinary review, not only a separate pass. Flag the108degradations a reviewer can see without measuring:109110- A projection joining on a read model; a reactor re-querying the event log111 inside a handler instead of using the event data.112- A new projection that could not replay all historical events; events carrying113 large blobs.114- A query that does not filter on an indexed field; a growable list that does115 not return `IQueryable<T>` for server-side paging; hydrating a collection only116 to count it.117- An N+1 pattern; a response payload with fields no client reads.118- React: a growable list rendering every row; an inline object or array literal119 passed as a prop, changing identity every render; wrong `useEffect`120 dependencies.121- .NET: `.ToList()` before `.Where()`; an `IEnumerable<T>` enumerated more than122 once.123124## Step 6 — Specification coverage125126- Every State Change command has a happy-path specification.127- Every validation rule has a failure specification asserting **both**128 `ShouldNotBeSuccessful()` and `ShouldHaveValidationErrors()`.129- Every business-rule rejection has a specification.130- Every constraint has an `EventScenario` specification asserting the constraint131 **name**, not its message.132- No specification asserts on a presentation message string.133- Nothing trivial is specified — a property getter, a constructor pass-through,134 a delegation.135- No specification sleeps to let the system catch up.136137## Step 7 — Report138139Open with one line:140141> **Review result: Approved / Approved with comments / Changes requested**142143Then, per file:144145```146### <file path>147148**[BLOCKING]** Line N: `problematic code`149Because: <the consequence, not the rule number>150Fix:151<corrected code>152```153154Close with what passed and what must change. Two rules make the report usable:155156- **A blocking finding names a consequence.** "Violates the style guide" is not157 a reason. "Throws on a recoverable path, so the caller sees a 500 instead of a158 validation error" is.159- **Say what you did not review.** A report listing only findings reads as if160 everything was checked. Name the files, the paths, and the axes you skipped.161162## What breaks163164- **The review restates the compiler.** The gates were not run first, so165 analyzer output is being reported as review findings.166- **Every finding is blocking.** The distinction is what makes the report167 actionable; if everything blocks, nothing is prioritized.168- **A convention is reported as a framework contract.** The slice folder shape169 and the single-file default are house conventions; `Handle()` on the record and170 the `[Path]` attribute are contracts. Saying "the framework requires this" of a171 convention loses the reader's trust for the findings that are contracts.172173## How it is proven174175The build, the specifications, lint and the TypeScript build are all green176*before* the report is written, and the report names both what was reviewed and177what was not.