Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f614b84
docs: plan the progressive human documentation journey
NickSeagull Sep 15, 2026
cbd74f0
docs: write and verify the progressive human learning journey
NickSeagull Sep 15, 2026
a1b647e
chore: record documentation verification handoff
NickSeagull Sep 15, 2026
920a9f5
docs: reserve a fresh specification number after base update
NickSeagull Sep 15, 2026
1567382
docs: record final checks and remaining runtime verification
NickSeagull Sep 15, 2026
3c76b1d
docs: teach general concepts through an ecommerce practice project
NickSeagull Sep 16, 2026
468124b
docs: deepen the introduction with editable visual explanations
NickSeagull Sep 16, 2026
67fc4e0
docs: illustrate event models and capture the visual IDE
NickSeagull Sep 16, 2026
b3a512b
feat(docs): enlarge illustrations in an image zoom dialog
NickSeagull Sep 16, 2026
35c3667
docs: grow one reader-owned project through progressive examples
NickSeagull Sep 16, 2026
bf69934
chore: sync release infrastructure from main
NickSeagull Sep 16, 2026
c864e18
feat(core): specify consistent concept derivation helpers
NickSeagull Sep 16, 2026
45e6fcc
feat(core): expose consistent derivation helpers through Core
NickSeagull Sep 16, 2026
dd1ec68
docs: use Core derivation helpers throughout the learning journey
NickSeagull Sep 16, 2026
e3617b9
docs: assemble complete files after progressive examples
NickSeagull Sep 17, 2026
bb1ce20
docs: update the workflow lesson cross-reference
NickSeagull Sep 17, 2026
327b999
docs: introduce NeoHaskell with a visual main website
NickSeagull Sep 17, 2026
ba381c9
docs: align website branding and learning paths
NickSeagull Sep 17, 2026
0985b1e
refactor(website): complete standalone migration cleanup
NickSeagull Sep 18, 2026
c6203cb
docs(core): document entity derivation helpers
NickSeagull Sep 18, 2026
c661930
fix(starter): use Core query derivation export
NickSeagull Sep 18, 2026
77bc48e
fix(ci): resolve website migration review findings
NickSeagullBot Sep 22, 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
140 changes: 45 additions & 95 deletions .agents/skills/neohaskell-concept-derivation/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,124 +1,74 @@
---
name: neohaskell-concept-derivation
description: Derive NeoHaskell concepts (Query, Command, Event, Outbound integration) through their canonical Template Haskell marker instead of hand-writing the instances the marker generates. Use whenever creating or editing a query, command, event, entity, or any concept that has a TH marker — reach for deriveQuery / command / event / outboundIntegration and author ONLY the required companion functions and business-logic instances; never hand-write the Show/Generic/JSON/ToSchema/NameOf/EntitiesOf/Query/KnownHash instances the marker emits.
description: Declare NeoHaskell concepts with deriveEvent, deriveCommand, deriveEntity, deriveQuery, and deriveOutboundIntegration from Core. Author data and business functions; let the markers generate their mechanical instances.
---

# NeoHaskell concept derivation

Every core concept type (query, command, event, outbound integration) has a
Template Haskell **marker** that emits the mechanical instances it needs. The
marker is the canonical way to declare the concept. Your job is to author the
**data type** plus the **required companion functions and business-logic
instances** — never the boilerplate the marker owns.
Import `Core` for every concept marker. Use the canonical `deriveXXX` names;
legacy `event`, `command`, and `outboundIntegration` TH names remain compatibility
exports from their defining modules. `Core.event` is a value-level event injection
helper, distinct from `deriveEvent`.

## The rule

**Use the marker. Author only what the marker cannot: the data type, the
required companion functions, the required marker-specific type instances (a
command's `EntityOf`/`TransportsOf` — never the query `NameOf`/`EntitiesOf` the
marker generates), and the business-logic instances. Never hand-write an
instance the marker emits.**
Write the data type and business functions. Do not hand-write the instances the
marker generates. Custom instances belong before the marker so it can preserve
them; do not duplicate generated instances after it. Do not add source language
pragmas to reader-owned Neo projects: the CLI configures the compiler extensions.

Markers are idempotent (they call `Service.TH.Boilerplate.emitInstanceIfMissing`,
which reifies existing instances and skips them). So a hand-written
`deriving (Show, Generic)` or `instance ToJSON` next to a marker is *silently
redundant* at best — and a hard `Duplicate instance` GHC error if you place it
*after* the marker. Both are the noise this skill exists to remove.
| Concept | Marker from Core | Generates | Application code |
|---|---|---|---|
| Event payload or sum | `deriveEvent ''CartCreated` | Show, Generic, FromJSON, ToJSON | Data declaration; Eq only when separately needed |
| Command | `deriveCommand ''AddItem` | Show, Generic, JSON, ToSchema, NameOf, KnownHash, Command wiring | `EntityOf`, transport mapping when needed, `getEntityId`, `decide`, optional access control and multi-tenancy declarations |
| Entity | `deriveEntity ''CartEntity ''CartEvent` | Generic, JSON, NameOf, EventOf, event's EntityOf, Default, Entity and event-routing wiring | `initialState`, `update`, `getEventEntityId`; custom instances when needed |
| Query | `deriveQuery ''CartSummary [''CartEntity]` | Show, Generic, JSON, ToSchema, NameOf, EntitiesOf, KnownHash, Query wiring | `canAccess`, `canView`, optional `maxResults`; one `QueryOf` business projection per entity |
| Outbound handler | `deriveOutboundIntegration ''ReserveStock` | NameOf, KnownHash, OutboundIntegration and HandledEvent wiring | `EntityOf`, `handleEvent` |

## The markers (ground truth — read the module, don't recall)
Entity derivation does **not** require Show on fields. It preserves custom
instances and a custom NameOf. EventOf and EntityOf must agree with the supplied
entity and event types. An existing Entity instance retains its custom identifier
type. Outbound derivation does **not** generate JSON or Show instances.

| Concept | Marker | Module | Emits (skipped if already in scope) | You author |
|---|---|---|---|---|
| Query | `deriveQuery ''Q [''E1, ''E2]` | `Service.Query.TH` | `Show`, `Generic`, `FromJSON`, `ToJSON`, `ToSchema`, `NameOf Q = "Q"`, `EntitiesOf Q = '[E1,E2]`, `Query` (wires `canAccessImpl = canAccess`, `canViewImpl = canView`, and `maxResultsImpl = maxResults` if `maxResults` is defined), `KnownHash` | the data type; **`canAccess`** and **`canView`** (required, top-level); `maxResults :: Int` (optional cap); one **`QueryOf entity Q`** instance per contributing entity (business logic) |
| Command | `command ''C` | `Service.CommandExecutor.TH` | `Show`, `Generic`, `FromJSON`, `ToJSON`, `NameOf C = "C"`, `Command` (wires `getEntityIdImpl = getEntityId`, `decideImpl = decide`, and the multi-tenancy mode), `KnownHash` | the data type; **`getEntityId`** and **`decide`** (required); **`type instance EntityOf C = SomeEntity`** (required); `type instance TransportsOf C = '[WebTransport]` for HTTP commands |
| Event | `event ''E` | `Service.Event.TH` | `Show`, `Generic`, `FromJSON`, `ToJSON` | the data type; for a domain event driven through the store: `type instance EventOf/EntityOf` and `instance Event` (business `getEventEntityIdImpl`) |
| Outbound | `outboundIntegration ''O` | `Service.OutboundIntegration.TH` | `KnownHash` + the JSON/deriving boilerplate | the data type + the companion functions the module's error messages name — **read `Service/OutboundIntegration/TH.hs`** before use |
The defining modules are `Service.Event.TH`, `Service.CommandExecutor.TH`,
`Service.Entity.TH`, `Service.Query.TH`, and `Service.OutboundIntegration.TH`.
Read those modules for the exact companion contract and diagnostics.

`NameOf` is the **verbatim type name** (`"CartSummary"`, `"AddItem"`), NOT
kebab-case. Kebab-case is only for HTTP URLs; the marker uses the exact type
name as the dispatch/checkpoint key (see `Service.Query.TH`).
## Declaration order

### Entities have no marker
Put required companion functions and command/handler type-family declarations
**before** their marker. A splice sees preceding declarations.

An entity is business logic, so it is **hand-written**: the record + `deriving
(Generic)` + `instance FromJSON/ToJSON` + `type instance NameOf` +
`type instance EventOf` + `instance Entity` (`initialStateImpl`/`updateImpl`).
There is nothing to defer to a marker here — writing these instances is correct,
not a violation. (`type instance EntityOf <event> = <entity>` lives with the
entity too.)

## Canonical shapes

Query (`testbed/src/Testbed/Cart/Queries/CartSummary.hs` is the reference):
For queries, put the marker **before** `QueryOf` instances: the marker generates
NameOf, EntitiesOf and Query, which the projection instances may require. The
blanket advice "marker last in the module" is incorrect.

```haskell
data CartSummary = CartSummary
{ cartSummaryId :: Uuid, ownerId :: Text, itemCount :: Int }
-- no `deriving` clause for Show/Generic — the marker emits them

canAccess :: Maybe UserClaims -> Maybe AccessError
canAccess = AccessControl.authenticatedAccess -- secure default
canAccess = AccessControl.authenticatedAccess

canView :: Maybe UserClaims -> CartSummary -> Maybe AccessError
canView = AccessControl.ownerOnly (.ownerId)

-- business logic the marker cannot know:
deriveQuery ''CartSummary [''CartEntity]

instance QueryOf CartEntity CartSummary where
queryId cart = cart.cartId
combine cart _ = ...

deriveQuery ''CartSummary [''CartEntity] -- MARKER LAST
combine cart previous = ...
```

Command (`testbed/src/Testbed/Cart/Commands/AddItem.hs`):
Keep each entity, event payload, command, query and integration in its appropriate
application module. Small documentation fragments may omit module/import headers;
the complete project must retain its own imports and compile as separate files.

```haskell
data AddItem = AddItem { cartId :: Uuid, quantity :: Int }

getEntityId :: AddItem -> Maybe Uuid
getEntityId cmd = Just cmd.cartId

decide :: AddItem -> Maybe CartEntity -> RequestContext -> Decision CartEvent
decide cmd entity _ctx = ...
## Verification

type instance EntityOf AddItem = CartEntity
type instance TransportsOf AddItem = '[WebTransport]

command ''AddItem -- MARKER LAST
```
- No redundant Show/Generic/JSON/ToSchema or marker-owned wiring.
- Companions are in scope before the splice; projections follow query derivation.
- No confusion between `Core.event` (injection) and `deriveEvent` (derivation).
- New examples use the Core exports, not separate TH imports.
- Compile via `./dev check`; run relevant concept specs via `./dev test`.
- Existing legacy-name tests continue to cover compatibility.

## Common pitfalls

1. **Hand-writing marker-owned instances** — `deriving (Show, Generic)`,
`instance ToJSON/FromJSON`, `instance ToSchema`, `type instance NameOf`,
`type instance EntitiesOf`, `instance Query`, or `instance KnownHash`
alongside the marker. Delete them; the marker owns them. After the marker
they are a `Duplicate instance` error.
2. **Assuming `NameOf` is kebab-case.** It is the exact type name.
3. **Missing companion / type-family.** `deriveQuery` without `canAccess`/
`canView`, or `command` without `getEntityId`/`decide`/`EntityOf`, fails at
compile with a message naming exactly what to add — add it, don't fight it.
4. **Marker not last.** Put the companions, the `type instance` wiring, and the
business-logic instances (`QueryOf`/`Entity`/`Event`) all *before* the marker
call — the marker is the LAST declaration for the type. The marker *requires*
the declarations it wires to precede it: `deriveQuery` resolves
`canAccess`/`canView` via `lookupValueName` at splice time (it does **not**
reify `QueryOf`), and `command` needs `getEntityId`/`decide`/`EntityOf`.
Keeping everything above the marker is the one rule that always holds.
5. **Copying a pre-marker module.** Some older modules (e.g.
`Testbed.Cart.Core`) hand-write everything and predate the markers — do not
copy that style for a new concept.

## Verification checklist

- [ ] The data type has NO `deriving` clause for `Show`/`Generic` and NO
hand-written `ToJSON`/`FromJSON`/`ToSchema`.
- [ ] Required companions are defined ABOVE the marker (query → `canAccess`,
`canView`; command → `getEntityId`, `decide`).
- [ ] Required `type instance`s are ABOVE the marker (command → `EntityOf`,
`TransportsOf`; query passes its entity list as the marker's 2nd arg).
- [ ] Business-logic instances (`QueryOf`, `Entity`, `Event`) are hand-written
(correct — the marker does not own these).
- [ ] The marker call is the LAST declaration for the type.
- [ ] It compiles: `./dev check` (or run the concept's spec with `./dev test`).
A missing-companion TH error tells you precisely what to add.
This skill is checked by `./dev neo-skills-check`; implementation and export
contracts are covered by the registered service and integration TH specs.
6 changes: 4 additions & 2 deletions .agents/skills/neohaskell-implementer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ Stack Overflow Haskell. Before writing:
**Creating or editing a concept (query, command, event, outbound integration)?**
These are declared through a Template Haskell marker — **load the
`neohaskell-concept-derivation` skill and follow it**: use the marker
(`deriveQuery` / `command` / `event` / `outboundIntegration`) and author only
(`deriveQuery` / `deriveCommand` / `deriveEvent` / `deriveEntity` /
`deriveOutboundIntegration` from `Core`) and author only
the required companion functions and business-logic instances; never hand-write
the `Show`/`Generic`/JSON/`ToSchema`/`NameOf`/`EntitiesOf`/`Query`/`KnownHash`
instances the marker emits. Do not copy the boilerplate of a pre-marker module.
(Entities are the exception — no marker; hand-write their `Entity` instance.)
Use `deriveEntity` for entity wiring; keep `initialState`, `update`, and
`getEventEntityId` as application functions.

## Dialect (portable gates: hlint → GHC; optional edit feedback)

Expand Down
6 changes: 4 additions & 2 deletions .agents/skills/neohaskell-pipeline/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,10 @@ the activity vocabulary is `localization`, `index`, `test-scaffolding`,
ready-for-review (this triggers the full CI matrix; drafts run only cheap
checks). PR body: spec link, criteria → test mapping, review records. **Flip
the ADR's `## Status` to `Implemented`** here (and the matching row in
`docs/decisions/README.md`, then `./dev adr-website` to resync the landing
page); run `./dev adr-check`. GATE 2 is the maintainer's normal review.
`docs/decisions/README.md`). The standalone website fetches that main commit
and regenerates its ADR landing page in `neohaskell/website-v2`; run
`./dev adr-check` here and `pnpm verify` in the website repository. GATE 2 is
the maintainer's normal review.
11. **ci → GATE 2 → telemetry-only finalization** — each CI round: watch
checks → read every new bot comment → triage → push the
fix → wait for re-review. Repeat until CodeRabbit has no outstanding
Expand Down
15 changes: 15 additions & 0 deletions .changes/cli-project-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
group: platform
component: CLI
impact: compatible
category: Fixed
---

## Summary

Applications can use the framework's declaration helpers without adding language
pragmas to their source files. Neo now enables the required deriving strategy in
the project settings it generates for application, library, and test components.

After updating Neo, run `neo build` and `neo test` from your application directory.
Neo refreshes the generated settings; no manual Cabal configuration is needed.
17 changes: 17 additions & 0 deletions .changes/core-derive-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
group: platform
component: Framework
impact: compatible
category: Added
---

## Summary

Declare events, commands, entities, queries, and outbound integrations with
`deriveEvent`, `deriveCommand`, `deriveEntity`, `deriveQuery`, and
`deriveOutboundIntegration`, all available through `import Core`.

The new `deriveEntity ''CartEntity ''CartEvent` helper generates routine entity
instances and connects your `initialState`, `update`, and `getEventEntityId`
functions. Existing custom instances remain supported. The previous marker
names still work, so existing applications need no changes.
28 changes: 28 additions & 0 deletions .changes/human-documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
group: platform
component: Framework
impact: none
category: Improved
---

## Summary

The human documentation now teaches NeoHaskell from first concepts through
application building, integrations, AI features, deployment, and recovery.
An ecommerce practice project connects the examples; readers can apply the concepts
to their own applications or consult individual topics. Each page starts with an
understandable problem and develops the implementation details gradually.

The introduction explains the philosophy in depth before code, using diagrams to
connect intent, history, corrections, feature boundaries, and working with an agent.
Worked event-model diagrams and real IDE screenshots show how requests, facts,
and read models relate, from a first cart to a selected command's fields.
Select an image to enlarge it in a dialog; close it to continue reading in place.

Install the released Neo CLI and create your own project once with `neo new`.
Build the cart there, explore it in the IDE, and keep growing the same application
with `neo build`, `neo run`, and `neo test` throughout the guide.

The website adds in-page image enlargement and a runnable learning journey;
application behaviour and public APIs are unchanged by this documentation work. Begin at the website's Start and evaluate section, or use the
capability reference to find a specific topic.
6 changes: 0 additions & 6 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,6 @@ jobs:
# Pure-stdlib python check: every docs/decisions/NNNN-*.md is indexed
# exactly once with a status matching its own `## Status` section.
- run: ./dev adr-check
# Drift gate: the website ADR pages are generated from that same
# (now-audited) index by website/scripts/generate-adrs.mjs (Node, on the
# runner's default toolchain — no pnpm install, the generator is zero-dep).
# Regenerates the gitignored per-ADR pages (proving each still transcribes)
# and fails if the tracked landing page was hand-edited or left stale.
- run: ./dev adr-website --check

spec:
name: spec check (contract-delta gate)
Expand Down
Loading
Loading