diff --git a/.agents/skills/neohaskell-concept-derivation/SKILL.md b/.agents/skills/neohaskell-concept-derivation/SKILL.md index 2fec895f2..b348e1357 100644 --- a/.agents/skills/neohaskell-concept-derivation/SKILL.md +++ b/.agents/skills/neohaskell-concept-derivation/SKILL.md @@ -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 = ` 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. diff --git a/.agents/skills/neohaskell-implementer/SKILL.md b/.agents/skills/neohaskell-implementer/SKILL.md index c070292e2..8b39623a2 100644 --- a/.agents/skills/neohaskell-implementer/SKILL.md +++ b/.agents/skills/neohaskell-implementer/SKILL.md @@ -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) diff --git a/.agents/skills/neohaskell-pipeline/SKILL.md b/.agents/skills/neohaskell-pipeline/SKILL.md index 964187523..09761eeb4 100644 --- a/.agents/skills/neohaskell-pipeline/SKILL.md +++ b/.agents/skills/neohaskell-pipeline/SKILL.md @@ -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 diff --git a/.changes/cli-project-defaults.md b/.changes/cli-project-defaults.md new file mode 100644 index 000000000..193faac9a --- /dev/null +++ b/.changes/cli-project-defaults.md @@ -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. diff --git a/.changes/core-derive-helpers.md b/.changes/core-derive-helpers.md new file mode 100644 index 000000000..151234c2e --- /dev/null +++ b/.changes/core-derive-helpers.md @@ -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. diff --git a/.changes/human-documentation.md b/.changes/human-documentation.md new file mode 100644 index 000000000..7078fea24 --- /dev/null +++ b/.changes/human-documentation.md @@ -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. diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 42065f096..3ceb1fdbf 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -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) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml deleted file mode 100644 index 6dd72e518..000000000 --- a/.github/workflows/translate.yml +++ /dev/null @@ -1,131 +0,0 @@ -# Continuous documentation translation for the Starlight docs site. -# -# NOTE: Auto-commits from this workflow trigger all push-to-main workflows (known consequence of monorepo) -# NOTE: GROQ_API_KEY must be added to this repo's GitHub Actions secrets -# (Settings → Secrets → Actions → New repository secret: GROQ_API_KEY) -# NOTE: The ADR pages under website/src/content/docs/adrs/** are CANONICAL ENGLISH -# and are excluded from translation. The landing page (index.mdx) and the -# per-ADR detail pages are generated from docs/decisions/ by -# website/scripts/generate-adrs.mjs; the detail pages are gitignored and -# never land on main, so they are never seen here. Localized ADR landing -# pages are maintained by hand and link to the English records. -name: Translate Documentation - -on: - push: - branches: [main] - paths: - - 'website/src/content/docs/**' - - '!website/src/content/docs/es/**' - - '!website/src/content/docs/ru/**' - - '!website/src/content/docs/hy/**' - - '!website/src/content/docs/fr/**' - - '!website/src/content/docs/ja/**' - - '!website/src/content/docs/adrs/**' - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - translate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: website/.genaiscript/cache/** - key: continuous-translation-${{ github.run_id }} - restore-keys: | - continuous-translation- - - - uses: pelikhan/action-continuous-translation@923d1c9a88c47993a6d10c509c810f853c9205e6 # v0 - with: - openai_api_key: ${{ secrets.GROQ_API_KEY }} - openai_api_base: https://api.groq.com/openai/v1 - model_alias: "translation: openai:llama-3.3-70b-versatile" - lang: es,ru,fr,ja - source: en - # Canonical-English ADR pages are excluded — they are generated from - # docs/decisions/ and must not be machine-translated. - files: | - website/src/content/docs/**/*.{md,mdx} - !website/src/content/docs/adrs/** - - - name: Translate to Armenian (unsupported by GenAIScript) - working-directory: website - env: - GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} - run: | - docs_root="src/content/docs" - for file in $(find "$docs_root" -maxdepth 2 -name "*.mdx" -not -path "*/*.*.*" | sort); do - # Skip files already in locale directories, and the canonical-English - # ADR pages (adrs/** is maintained in English, never translated). - rel="${file#${docs_root}/}" - first_dir="${rel%%/*}" - case "$first_dir" in es|ru|hy|fr|ja|adrs) continue ;; esac - - target_dir="${docs_root}/hy/$(dirname "$rel")" - target_file="${target_dir}/$(basename "$file")" - [ "$target_dir" = "${docs_root}/hy/." ] && target_dir="${docs_root}/hy" && target_file="${target_dir}/$(basename "$file")" - mkdir -p "$target_dir" - - echo "Translating $file to Armenian..." - content=$(cat "$file") - response=$(curl --fail --silent --show-error https://api.groq.com/openai/v1/chat/completions \ - -H "Authorization: Bearer $GROQ_API_KEY" \ - -H "Content-Type: application/json" \ - -d "$(jq -n --arg content "$content" '{ - model: "llama-3.3-70b-versatile", - messages: [ - {role: "system", content: "You are an expert translator. Translate the following MDX documentation file to Armenian (Հայերեն). Preserve ALL MDX/JSX syntax, frontmatter YAML structure, import statements, component tags, code blocks, and URLs exactly as-is. Only translate human-readable text content, titles, and descriptions. Output ONLY the translated file, no explanations."}, - {role: "user", content: $content} - ], - temperature: 0.3, - max_tokens: 4096 - }')") - translated=$(echo "$response" | jq -r '.choices[0].message.content // empty') - if [ -n "$translated" ]; then - echo "$translated" > "$target_file" - echo "Created: $target_file" - else - echo "Failed to translate $file" - echo "$response" | jq '.error' 2>/dev/null - exit 1 - fi - done - - - name: Reorganize translations to Starlight locale directories - working-directory: website - run: | - for lang in es ru hy fr ja; do - find src/content/docs -maxdepth 3 \( -name "*.${lang}.mdx" -o -name "*.${lang}.md" \) | while read file; do - ext="${file##*.}" - # Strip the .lang.ext suffix to get the base name - base=$(basename "$file" ".${lang}.${ext}") - dir=$(dirname "$file") - docs_root="src/content/docs" - rel_dir="${dir#${docs_root}}" - if [ -z "$rel_dir" ]; then - target_dir="${docs_root}/${lang}" - else - target_dir="${docs_root}/${lang}${rel_dir}" - fi - mkdir -p "$target_dir" - mv "$file" "${target_dir}/${base}.${ext}" - echo "Moved: $file -> ${target_dir}/${base}.${ext}" - done - done - - - uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 - with: - file_pattern: 'website/src/content/docs/** website/translations/**' - commit_message: '[translation] auto-translate docs' - commit_user_name: genaiscript diff --git a/AGENTS.md b/AGENTS.md index 1f29c28d7..eb16bebf0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ -Newcomer-friendly Haskell dialect. Monorepo: core library (`core/` → `nhcore`), reference app + acceptance tests (`testbed/` → `nhtestbed`), outbound integrations (`integrations/` → `nhintegrations`), Rust installer (`installer/` → `neo-install`), Rust Neo CLI (`neo/` → the `neo` binary), Astro website (`website/`). Architecture: event-sourcing + CQRS in `core/service/`. (LSP package removed 2026-07-08 — unused.) +Newcomer-friendly Haskell dialect. Monorepo: core library (`core/` → `nhcore`), reference app + acceptance tests (`testbed/` → `nhtestbed`), outbound integrations (`integrations/` → `nhintegrations`), Rust installer (`installer/` → `neo-install`), and Rust Neo CLI (`neo/` → the `neo` binary`). The public Astro website lives in the separate `neohaskell/website-v2` repository. Architecture: event-sourcing + CQRS in `core/service/`. (LSP package removed 2026-07-08 — unused.) This file is the **agent-specific contract**: the hard rule, dialect style, and the pipeline/verification gates. Two companion guides own the rest — do not duplicate them here: diff --git a/README.md b/README.md index 4d4e7f6f2..b15f53790 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,10 @@ different parts of NeoHaskell: - `testbed/` → `nhtestbed` — reference app + acceptance tests - `integrations/` → `nhintegrations` — outbound integrations - `installer/` → `neo-install` — the Rust installer -- `website/` — the Astro documentation site +- `neo/` → `neo` — the Rust CLI and bundled IDE + +The public Astro website and documentation now live in the separate +[`neohaskell/website-v2`](https://github.com/neohaskell/website-v2) repository. This README is the canonical guide for **human contributors**: environment setup, the full build/test commands, and Postgres. Two companion guides own the @@ -41,10 +44,9 @@ rest: regeneration. Architecture decisions live in `docs/decisions/` (indexed in -`docs/decisions/README.md`). The website turns each record into its own page -automatically: the generator runs on `pnpm dev`/`build`/`check` (no manual step), -and `./dev adr-website` regenerates or (with `--check`) verifies the tracked ADR -landing page. +`docs/decisions/README.md`). The standalone website fetches the latest `main` +commit from this repository when it builds, then generates the public ADR pages +and runs its documentation evidence checks locally. ## Installing the required tools diff --git a/codemap/MAP.md b/codemap/MAP.md index 796620176..5d345cac1 100644 --- a/codemap/MAP.md +++ b/codemap/MAP.md @@ -32,7 +32,7 @@ vocabulary); for NEW code use the extension points. Full contract: codemap/READM | `outbound-integrations` | Concrete outbound integrations | `integrations/**` | 🔒 | | `testbed` | Reference app + acceptance tests | `testbed/**` | | | `dev-pipeline` | The Continuous Generation pipeline itself | `scripts/**`… | | -| `website` | Astro/Starlight docs site (monorepo dir, NOT a separate repo) | `website/**` | | +| `website` | Migration pointer for the standalone Astro/Starlight docs site in neohaskell/website-v2. The public site owns English content, es/fr/hy/ja/ru translations, landing routes, and evidence checks; its CI fetches the latest NeoHaskell main snapshot before generating ADR pages and validating source hashes. | `website/**` | | | `installer` | Rust installer (neo-install binary). | `installer/**` | | | `ci-cd` | Build/CI/CD | `.github/workflows/*.yml`… | | | `governance-docs` | Plans, ADRs (docs/decisions/), agent guide, contributor guide. | `docs/**`… | | diff --git a/codemap/api-hot.md b/codemap/api-hot.md index 59e563880..57918a0b5 100644 --- a/codemap/api-hot.md +++ b/codemap/api-hot.md @@ -283,7 +283,7 @@ codemap/signatures/ · type-directed search: ./dev api "" - `fmap :: Functor f => (a -> b) -> f a -> f b` - `map :: Mappable mappable => (typeA -> typeB) -> mappable typeA -> mappable typeB` -*cut: 101 more modules (AtomicVar (26), Integration.Http (26), Decider (21), Result (21), Lock (20), …) — full surface: codemap/signatures/* +*cut: 103 more modules (AtomicVar (26), Integration.Http (26), Decider (21), Result (21), Lock (20), …) — full surface: codemap/signatures/* --- @@ -318,7 +318,7 @@ when WRITING TESTS; they are not feature-code frequency signal. - `contains :: Text -> Text -> Bool` - `toBytes :: Text -> Bytes` -- `fromLinkedList :: LinkedList Char -> Text` +- `fromLinkedList :: LinkedList Char -> Text` - `length :: Text -> Int` - `repeat :: Int -> Text -> Text` - `toLinkedList :: Text -> LinkedList Char` @@ -336,6 +336,17 @@ when WRITING TESTS; they are not feature-code frequency signal. - `decimal :: Parser Int` - `anyChar :: Parser Char` +## Json + +- `toJSON :: ToJSON a => a -> Value` +- `decodeText :: FromJSON value => Text -> Result Text value` +- `encodeText :: ToJSON value => value -> Text` +- `object :: [(Text, Value)] -> Value` +- `null :: Value` +- `encode :: ToJSON value => value -> Value` +- `decode :: FromJSON value => Value -> Result Text value` +- `yield :: value -> Parser value` + ## Layout - `text :: Text -> Blueprint annotation` @@ -347,17 +358,6 @@ when WRITING TESTS; they are not feature-code frequency signal. - `withAnnotation :: ann -> Blueprint ann -> Blueprint ann` - `empty :: Blueprint annotation` -## Json - -- `toJSON :: ToJSON a => a -> Value` -- `decodeText :: FromJSON value => Text -> Result Text value` -- `encodeText :: ToJSON value => value -> Text` -- `object :: [(Text, Value)] -> Value` -- `null :: Value` -- `encode :: ToJSON value => value -> Value` -- `decode :: FromJSON value => Value -> Result Text value` -- `withObject :: Text -> (Object -> Parser value) -> Value -> Parser value` - ## ConcurrentVar - `containing :: value -> Task w (ConcurrentVar value)` @@ -372,9 +372,9 @@ when WRITING TESTS; they are not feature-code frequency signal. ## Uuid - `generate :: Task w Uuid` -- `nil :: Uuid` +- `nil :: Uuid` - `toText :: Uuid -> Text` -- `generateV5 :: Uuid -> Text -> Uuid` +- `generateV5 :: Uuid -> Text -> Uuid` - `fromText :: Text -> Maybe Uuid` -*cut: 137 more modules (Map (257), Service.Application (250), Set (233), Stream (212), AsyncTask (211), …) — full surface: codemap/signatures/* +*cut: 138 more modules (Map (257), Service.Application (250), Set (233), Stream (212), AsyncTask (211), …) — full surface: codemap/signatures/* diff --git a/codemap/capabilities.yaml b/codemap/capabilities.yaml index 87f0bb5ae..90369bfc2 100644 --- a/codemap/capabilities.yaml +++ b/codemap/capabilities.yaml @@ -177,7 +177,7 @@ capabilities: # ─── non-Haskell surfaces ────────────────────────────────────────────── - id: website owns: [website/**] - responsibility: Astro/Starlight docs site (monorepo dir, NOT a separate repo) — English source in src/content/docs/, locale translations (es/fr/hy/ja/ru) via translate.yml; the ADR landing page + per-ADR pages are generated from docs/decisions/ by website/scripts/generate-adrs.mjs (npm predev/prebuild/precheck, or ./dev adr-website). + responsibility: Migration pointer for the standalone Astro/Starlight docs site in neohaskell/website-v2. The public site owns English content, es/fr/hy/ja/ru translations, landing routes, and evidence checks; its CI fetches the latest NeoHaskell main snapshot before generating ADR pages and validating source hashes. aliases: [docs site, documentation website, starlight, astro, translations, adr website] tests: [] diff --git a/codemap/phrasebook.md b/codemap/phrasebook.md index 67d55e478..5710950a9 100644 --- a/codemap/phrasebook.md +++ b/codemap/phrasebook.md @@ -185,6 +185,15 @@ Set (fromList [1,2,3]) "886313e1-3b8a-5372-9b90-0c9aee199e5d" ``` +## entities + +### Service.Entity.TH + +```haskell +>>> TH.nameBase (TH.mkName "Cart") +"Cart" +``` + ## postgres-infra ### Service.Infra.Postgres.SslMode @@ -228,4 +237,4 @@ True --- -*31 example sessions · modules with ZERO doctests: 315 (the documentation backlog — see codemap/.doc-ratchet)* +*32 example sessions · modules with ZERO doctests: 315 (the documentation backlog — see codemap/.doc-ratchet)* diff --git a/codemap/signatures/nhcore-core.txt b/codemap/signatures/nhcore-core.txt index 2db002f0e..acf6d8f47 100644 --- a/codemap/signatures/nhcore-core.txt +++ b/codemap/signatures/nhcore-core.txt @@ -545,6 +545,10 @@ data Uuid data Var value data Version decideImpl :: Command command => DecideFunction (IsMultiTenant command) command (EntityOf command) (EventOf (EntityOf command)) +deriveCommand :: Name -> DecsQ +deriveEntity :: Name -> Name -> DecsQ +deriveEvent :: Name -> DecsQ +deriveOutboundIntegration :: Name -> DecsQ getEntityIdImpl :: Command command => GetEntityIdFunction (IsMultiTenant command) command (EntityIdType (EntityOf command)) hashVal :: KnownHash s => proxy s -> Int infixr 5 ++ diff --git a/codemap/signatures/nhcore-service.txt b/codemap/signatures/nhcore-service.txt index 77f80d8d1..fcdbe6904 100644 --- a/codemap/signatures/nhcore-service.txt +++ b/codemap/signatures/nhcore-service.txt @@ -378,6 +378,7 @@ instance GHC.Show.Show Service.CommandExecutor.Core.ExecutionResult module Service.CommandExecutor.TH command :: Name -> DecsQ +deriveCommand :: Name -> DecsQ deriveKnownHash :: String -> DecsQ instance GHC.Classes.Eq Service.CommandExecutor.TH.MultiTenancyMode instance GHC.Generics.Generic Service.CommandExecutor.TH.FunctionInfo @@ -396,6 +397,9 @@ type family EntityOf command type family EventOf entityType updateImpl :: Entity entity => EventOf entity -> entity -> entity +module Service.Entity.TH +deriveEntity :: Name -> Name -> DecsQ + module Service.EntityFetcher module Service.EntityFetcher.Core @@ -568,6 +572,7 @@ new :: Int64 -> Maybe StreamPosition newtype StreamPosition module Service.Event.TH +deriveEvent :: Name -> DecsQ event :: Name -> DecsQ module Service.EventStore @@ -1371,6 +1376,7 @@ handleEventImpl :: OutboundIntegration handler => EntityOf handler -> HandledEve type HandledEvent handler module Service.OutboundIntegration.TH +deriveOutboundIntegration :: Name -> DecsQ outboundIntegration :: Name -> DecsQ module Service.Query diff --git a/core/core/Core.hs b/core/core/Core.hs index 31cae6633..12628ff02 100644 --- a/core/core/Core.hs +++ b/core/core/Core.hs @@ -3,6 +3,7 @@ -- This module is automatically imported in all the NeoHaskell files, module Core ( module Reexported, + deriveEntity, ) where import Appendable as Reexported ((++)) @@ -34,10 +35,15 @@ import Schema as Reexported (Schema (..), FieldSchema (..), ToSchema (..)) -- Command's auth method is named 'canExecuteImpl' (paired with 'execute') to -- coexist with 'Query.canAccessImpl' under one Core re-export. import Service.Command as Reexported (Command (IsMultiTenant, getEntityIdImpl, decideImpl, canExecuteImpl), NameOf) +import Service.CommandExecutor.TH as Reexported (deriveCommand) import Service.Entity as Reexported (Entity (..), EntityOf, EventOf) +import Service.Entity.TH (deriveEntity) import Service.Event as Reexported (InsertionType (..), StreamId, ToStreamId (..)) +import Service.Event.TH as Reexported (deriveEvent) import Service.OutboundIntegration.Core as Reexported (OutboundIntegration (..)) +import Service.OutboundIntegration.TH as Reexported (deriveOutboundIntegration) import Service.Query as Reexported (EntitiesOf, Query (..), QueryAction (..), QueryOf (..)) +import Service.Query.TH as Reexported (deriveQuery) import Service.ServiceDefinition.Core as Reexported ( Service, ) diff --git a/core/nhcore.cabal b/core/nhcore.cabal index 934e21aa7..638662bc2 100644 --- a/core/nhcore.cabal +++ b/core/nhcore.cabal @@ -228,6 +228,7 @@ library Service.CommandExecutor.TH Service.Entity Service.Entity.Core + Service.Entity.TH Service.EntityFetcher Service.EntityFetcher.Core Service.Error @@ -506,6 +507,14 @@ test-suite nhcore-test Service.CommandExecutor.TH.SumCommandFixture Service.CommandExecutor.THSpec Service.Event.THSpec + Service.Entity.THSpec + Service.Entity.TH.FreshFixture + Service.Entity.TH.NoShowFixture + Service.Entity.TH.PreservedFixture + Service.Entity.TH.MissingInitialStateFixture + Service.Entity.TH.MissingUpdateFixture + Service.Entity.TH.MissingRoutingFixture + Service.Entity.TH.ConflictingFamiliesFixture Service.Command.AuthSpec Service.Command.CanAccessSpec Service.Command.CanAccess.NoCanAccessFixture @@ -572,6 +581,14 @@ test-suite nhcore-test-service Service.CommandHandlerSpec Service.CommandSpec Service.Event.THSpec + Service.Entity.THSpec + Service.Entity.TH.FreshFixture + Service.Entity.TH.NoShowFixture + Service.Entity.TH.PreservedFixture + Service.Entity.TH.MissingInitialStateFixture + Service.Entity.TH.MissingUpdateFixture + Service.Entity.TH.MissingRoutingFixture + Service.Entity.TH.ConflictingFamiliesFixture Service.EventStore.InMemorySpec Service.EventStore.SimpleSpec Service.EventStore.Postgres.PoolBudgetSpec diff --git a/core/service/Service/CommandExecutor/TH.hs b/core/service/Service/CommandExecutor/TH.hs index 6df29a81e..0c9dd2828 100644 --- a/core/service/Service/CommandExecutor/TH.hs +++ b/core/service/Service/CommandExecutor/TH.hs @@ -1,10 +1,13 @@ module Service.CommandExecutor.TH ( + deriveCommand, command, deriveKnownHash, ) where import Control.Monad.Fail qualified as MonadFail -import Core +import Appendable ((++)) +import Basics -- HOOK-ALLOW: Core reexports these markers, so TH must use the lower-level prelude. +import Maybe (Maybe (..)) import Data.Hashable qualified as Hashable import Data.List qualified as GhcList import GHC.Base (String) @@ -250,6 +253,12 @@ Please ensure you have `import Core` at the top of your module. {-# INLINE deriveKnownHash #-} +-- | Derive the command instances from its decision and routing declarations. +deriveCommand :: TH.Name -> THLib.DecsQ +deriveCommand = command +{-# INLINE deriveCommand #-} + + command :: TH.Name -> THLib.DecsQ command someName = do let orError errMsg mVal = diff --git a/core/service/Service/Entity/TH.hs b/core/service/Service/Entity/TH.hs new file mode 100644 index 000000000..17a287c5f --- /dev/null +++ b/core/service/Service/Entity/TH.hs @@ -0,0 +1,179 @@ +-- | Derive entity plumbing while keeping its state transition in application code. +-- +-- Define @initialState@, @update@, and @getEventEntityId@ before calling +-- @deriveEntity ''CartEntity ''CartEvent@. Existing instances take precedence; +-- companions are required only for the corresponding missing business instances. +-- A missing 'Default.Default' delegates to the entity's initial state. +-- +-- Unlike event and command derivation, entity derivation does not require Show. +-- +-- >>> TH.nameBase (TH.mkName "Cart") +-- "Cart" +module Service.Entity.TH (deriveEntity) where + +import Appendable ((++)) +import Basics (Generic, (==), fmt, pure) -- HOOK-ALLOW: TH.Q compiler support below Core; Task cannot replace this Applicative. +import Char (Char) +import Control.Monad.Fail qualified as MonadFail -- HOOK-ALLOW: TH.Q reports compile-time errors through MonadFail; not a runtime Task. +import Default qualified +import Json qualified +import Language.Haskell.TH.Lib qualified as THLib +import Language.Haskell.TH.Syntax qualified as TH +import Maybe (Maybe (..)) +import Service.Command.Core qualified as Command +import Service.Entity.Core qualified as Entity +import Service.TH.Boilerplate (emitEmptyInstance, emitInstanceIfMissing, emitStockDeriving) + + +-- | Derive Generic, JSON, naming, entity/event associations, Default, Entity, +-- and event routing. Existing custom names, encodings, identifier types, and +-- instances are preserved. Incompatible event/entity associations fail early. +deriveEntity :: TH.Name -> TH.Name -> THLib.DecsQ +deriveEntity entityName eventName = do + nameDeclarations <- emitNameIfMissing entityName + eventOfDeclarations <- emitCheckedFamily ''Entity.EventOf entityName eventName + entityOfDeclarations <- emitCheckedFamily ''Entity.EntityOf eventName entityName + genericDeclarations <- emitStockDeriving ''Generic entityName + fromJsonDeclarations <- emitEmptyInstance ''Json.FromJSON entityName + toJsonDeclarations <- emitEmptyInstance ''Json.ToJSON entityName + entityDeclarations <- + emitInstanceIfMissing ''Entity.Entity entityName (emitEntityInstance entityName) + defaultDeclarations <- + emitInstanceIfMissing ''Default.Default entityName (emitDefaultInstance entityName) + routingDeclarations <- + emitInstanceIfMissing ''Entity.Event eventName (emitRoutingInstance eventName) + pure -- HOOK-ALLOW: return compiler declarations in TH.Q, not a runtime Task. + ( nameDeclarations + ++ eventOfDeclarations + ++ entityOfDeclarations + ++ genericDeclarations + ++ fromJsonDeclarations + ++ toJsonDeclarations + ++ entityDeclarations + ++ defaultDeclarations + ++ routingDeclarations + ) + + +-- | Emit the entity name mapping when the application has not supplied one. +emitNameIfMissing :: TH.Name -> THLib.DecsQ +emitNameIfMissing entityName = do + existing <- TH.reifyInstances ''Command.NameOf [TH.ConT entityName] + case existing of + [] -> do + let nameValue = TH.LitT (TH.StrTyLit (TH.nameBase entityName)) + pure [familyDeclaration ''Command.NameOf entityName nameValue] -- HOOK-ALLOW: TH.Q declaration generation. + _ -> pure [] -- HOOK-ALLOW: TH.Q declaration generation. + + +-- | Emit or validate a type-family mapping required by entity derivation. +emitCheckedFamily :: TH.Name -> TH.Name -> TH.Name -> THLib.DecsQ +emitCheckedFamily familyName argumentName expectedName = do + existing <- TH.reifyInstances familyName [TH.ConT argumentName] + case existing of + [] -> + pure [familyDeclaration familyName argumentName (TH.ConT expectedName)] -- HOOK-ALLOW: TH.Q declaration generation. + [TH.TySynInstD (TH.TySynEqn _ _ existingType)] -> + checkExistingMapping familyName argumentName expectedName existingType + _ -> conflictingFamily familyName argumentName expectedName + + +-- | Confirm that an existing type-family mapping matches the marker arguments. +checkExistingMapping :: TH.Name -> TH.Name -> TH.Name -> TH.Type -> THLib.DecsQ +checkExistingMapping familyName argumentName expectedName existingType = do + actual <- resolveNullaryAliases existingType + expected <- resolveNullaryAliases (TH.ConT expectedName) + if actual == expected + then pure [] -- HOOK-ALLOW: TH.Q declaration generation. + else conflictingFamily familyName argumentName expectedName + + +-- | Build a type-family declaration for a derived entity relationship. +familyDeclaration :: TH.Name -> TH.Name -> TH.Type -> TH.Dec +familyDeclaration familyName argumentName resultType = + TH.TySynInstD + ( TH.TySynEqn + Nothing + (TH.ConT familyName `TH.AppT` TH.ConT argumentName) + resultType + ) + + +-- | Report an incompatible existing entity relationship at compile time. +conflictingFamily :: TH.Name -> TH.Name -> TH.Name -> THLib.DecsQ +conflictingFamily familyName argumentName expectedName = + MonadFail.fail + [fmt|deriveEntity: existing #{TH.nameBase familyName} mapping for '#{TH.nameBase argumentName}' must resolve to '#{TH.nameBase expectedName}'. Make the marker arguments and the existing type-family equation agree.|] + + +-- Only expand ordinary nullary synonyms; do not attempt to evaluate arbitrary +-- type-family applications. GHC already rejects cyclic type synonyms. +resolveNullaryAliases :: TH.Type -> TH.Q TH.Type +resolveNullaryAliases originalType = + case originalType of + TH.ConT name -> do + information <- TH.reify name + resolveReifiedAlias originalType information + _ -> pure originalType -- HOOK-ALLOW: TH.Q type inspection. + + +-- | Resolve a reified nullary type synonym without evaluating type families. +resolveReifiedAlias :: TH.Type -> TH.Info -> TH.Q TH.Type +resolveReifiedAlias originalType information = + case information of + TH.TyConI (TH.TySynD _ [] expandedType) -> resolveNullaryAliases expandedType + _ -> pure originalType -- HOOK-ALLOW: TH.Q type inspection. + + +-- | Emit the application-owned state and update methods for an entity. +emitEntityInstance :: TH.Name -> THLib.DecsQ +emitEntityInstance entityName = do + initialStateName <- lookupCompanion entityName "initialState" + updateName <- lookupCompanion entityName "update" + let methods = + [ methodDeclaration 'Entity.initialStateImpl initialStateName + , methodDeclaration 'Entity.updateImpl updateName + ] + pure [instanceDeclaration ''Entity.Entity entityName methods] -- HOOK-ALLOW: TH.Q declaration generation. + + +-- | Emit the default value instance from the entity's initial state. +emitDefaultInstance :: TH.Name -> THLib.DecsQ +emitDefaultInstance entityName = do + let methods = [methodDeclaration 'Default.def 'Entity.initialStateImpl] + pure [instanceDeclaration ''Default.Default entityName methods] -- HOOK-ALLOW: TH.Q declaration generation. + + +-- | Emit event-to-entity routing from the application companion function. +emitRoutingInstance :: TH.Name -> THLib.DecsQ +emitRoutingInstance eventName = do + routingName <- lookupCompanion eventName "getEventEntityId" + let methods = [methodDeclaration 'Entity.getEventEntityIdImpl routingName] + pure [instanceDeclaration ''Entity.Event eventName methods] -- HOOK-ALLOW: TH.Q declaration generation. + + +-- | Find a required application companion or report an actionable error. +lookupCompanion :: TH.Name -> [Char] -> TH.Q TH.Name +lookupCompanion typeName companion = do + found <- TH.lookupValueName companion + case found of + Just name -> pure name -- HOOK-ALLOW: TH.Q name lookup. + Nothing -> + MonadFail.fail + [fmt|deriveEntity: missing '#{companion}' companion for '#{TH.nameBase typeName}'. Define the application function before the deriveEntity marker, or provide the corresponding Entity/Event instance before it. The marker does not invent state transitions or routing rules.|] + + +-- | Build an instance declaration from its class, type, and methods. +instanceDeclaration :: TH.Name -> TH.Name -> [TH.Dec] -> TH.Dec +instanceDeclaration className typeName methods = + TH.InstanceD + Nothing + [] + (TH.ConT className `TH.AppT` TH.ConT typeName) + methods + + +-- | Build a method declaration that delegates to an application function. +methodDeclaration :: TH.Name -> TH.Name -> TH.Dec +methodDeclaration methodName implementationName = + TH.FunD methodName [TH.Clause [] (TH.NormalB (TH.VarE implementationName)) []] diff --git a/core/service/Service/Event/TH.hs b/core/service/Service/Event/TH.hs index 1863c0eef..095eee4ca 100644 --- a/core/service/Service/Event/TH.hs +++ b/core/service/Service/Event/TH.hs @@ -1,19 +1,20 @@ -- | Template Haskell marker for event types. -- --- The 'event' marker emits Show, Generic, Json.FromJSON, and Json.ToJSON +-- The 'deriveEvent' marker emits Show, Generic, Json.FromJSON, and Json.ToJSON -- instances for the named type, skipping any that are already in scope. -- -- Usage: -- -- > data CartCreated = CartCreated { cartId :: Uuid } -- > --- > event ''CartCreated +-- > deriveEvent ''CartCreated -- -- CONVENTION — \"marker last\": Any custom instance you wish to write -- (e.g. a hand-crafted 'Json.ToJSON') must be declared /before/ the --- @event@ call. Declaring it after produces a GHC \"Duplicate instance\" +-- @deriveEvent@ call. Declaring it after produces a GHC \"Duplicate instance\" -- error because the marker will have already emitted the derived version. module Service.Event.TH ( + deriveEvent, event, ) where @@ -25,6 +26,12 @@ import Service.TH.Boilerplate (emitJsonAndDerivingBoilerplate) -- | Derive Show, Generic, Json.FromJSON, and Json.ToJSON for an event type. -- -- Each class is emitted only when it is not already in scope (idempotent). +deriveEvent :: TH.Name -> THLib.DecsQ +deriveEvent = emitJsonAndDerivingBoilerplate +{-# INLINE deriveEvent #-} + + +-- | Compatibility name for 'deriveEvent'. event :: TH.Name -> THLib.DecsQ -event = emitJsonAndDerivingBoilerplate +event = deriveEvent {-# INLINE event #-} diff --git a/core/service/Service/OutboundIntegration/TH.hs b/core/service/Service/OutboundIntegration/TH.hs index 5ad1a3531..3b26adf20 100644 --- a/core/service/Service/OutboundIntegration/TH.hs +++ b/core/service/Service/OutboundIntegration/TH.hs @@ -1,9 +1,12 @@ module Service.OutboundIntegration.TH ( + deriveOutboundIntegration, outboundIntegration, ) where import Control.Monad.Fail qualified as MonadFail -import Core +import Appendable ((++)) +import Basics -- HOOK-ALLOW: Core reexports these markers, so TH must use the lower-level prelude. +import Maybe (Maybe (..)) import GHC.Base (String) import Language.Haskell.TH.Lib qualified as THLib import Language.Haskell.TH.Ppr qualified as THPpr @@ -17,7 +20,6 @@ import Service.CommandExecutor.TH (deriveKnownHash) -- -- @ -- data ReserveStockOnItemAdded = ReserveStockOnItemAdded --- deriving (Generic, Typeable, Show) -- -- type instance EntityOf ReserveStockOnItemAdded = CartEntity -- @@ -26,7 +28,7 @@ import Service.CommandExecutor.TH (deriveKnownHash) -- ItemAdded {stockId, quantity} -> Integration.batch [...] -- _ -> Integration.none -- --- outboundIntegration ''ReserveStockOnItemAdded +-- deriveOutboundIntegration ''ReserveStockOnItemAdded -- @ -- -- Validates at compile time: @@ -42,6 +44,12 @@ import Service.CommandExecutor.TH (deriveKnownHash) -- to the event ADT type (EventOf entity) -- * @type instance NameOf HandlerType = "HandlerType"@ -- * @instance KnownHash "HandlerType"@ +deriveOutboundIntegration :: TH.Name -> THLib.DecsQ +deriveOutboundIntegration = outboundIntegration +{-# INLINE deriveOutboundIntegration #-} + + +-- | Compatibility name for 'deriveOutboundIntegration'. outboundIntegration :: TH.Name -> THLib.DecsQ outboundIntegration handlerName = do let handlerNameStr = TH.nameBase handlerName diff --git a/core/service/Service/Query/TH.hs b/core/service/Service/Query/TH.hs index 3db168d47..5ac4a8603 100644 --- a/core/service/Service/Query/TH.hs +++ b/core/service/Service/Query/TH.hs @@ -3,7 +3,9 @@ module Service.Query.TH ( ) where import Control.Monad.Fail qualified as MonadFail -import Core +import Appendable ((++)) +import Basics -- HOOK-ALLOW: Core reexports these markers, so TH must use the lower-level prelude. +import Maybe (Maybe (..)) import Data.Hashable qualified as Hashable import Data.List qualified as GhcList import GHC.Base (String) diff --git a/core/service/Service/TH/Boilerplate.hs b/core/service/Service/TH/Boilerplate.hs index 4a6cb00a7..1a1b481d1 100644 --- a/core/service/Service/TH/Boilerplate.hs +++ b/core/service/Service/TH/Boilerplate.hs @@ -12,7 +12,9 @@ module Service.TH.Boilerplate ( emitJsonAndDerivingBoilerplate, ) where -import Core +import Appendable ((++)) +import Basics -- HOOK-ALLOW: Core reexports these markers, so TH must use the lower-level prelude. +import Maybe (Maybe (..)) import Data.Aeson qualified as Json import Language.Haskell.TH.Lib qualified as THLib import Language.Haskell.TH.Syntax qualified as TH diff --git a/core/test-service/Main.hs b/core/test-service/Main.hs index 3df0c30c3..f00a9f639 100644 --- a/core/test-service/Main.hs +++ b/core/test-service/Main.hs @@ -26,6 +26,7 @@ import Service.Integration.AdapterSpec qualified import Service.Integration.DispatchRegistrySpec qualified import Service.Integration.SelectionSpec qualified import Service.Integration.WireupSpec qualified +import Service.Entity.THSpec qualified import Service.Event.THSpec qualified import Service.Query.DefinitionSpec qualified import Service.Query.EndpointSpec qualified @@ -80,6 +81,7 @@ main = Hspec.hspec do Hspec.describe "Service.Integration.DispatchRegistry" Service.Integration.DispatchRegistrySpec.spec Hspec.describe "Service.Integration.Selection" Service.Integration.SelectionSpec.spec Hspec.describe "Service.Integration.Wireup" Service.Integration.WireupSpec.spec + Hspec.describe "Service.Entity.TH" Service.Entity.THSpec.spec Hspec.describe "Service.Event.TH" Service.Event.THSpec.spec Hspec.describe "Service.Query.Definition" Service.Query.DefinitionSpec.spec Hspec.describe "Service.Query.Endpoint" Service.Query.EndpointSpec.spec diff --git a/core/test-service/Service/Query/DefinitionSpec.hs b/core/test-service/Service/Query/DefinitionSpec.hs index dcfe3d161..6bbcccffa 100644 --- a/core/test-service/Service/Query/DefinitionSpec.hs +++ b/core/test-service/Service/Query/DefinitionSpec.hs @@ -17,7 +17,6 @@ import Service.EventStore.Core (EventStore (..), ReadStreamMessage (..)) import Service.EventStore.InMemory qualified as EventStoreInMemory import Service.Query.Definition (QueryDefinition (..), createDefinitionWithStore) import Service.Query.Registry qualified as Registry -import Service.Query.TH (deriveQuery) import Service.QueryObjectStore.Core (QueryObjectStore) import Service.QueryObjectStore.InMemory qualified as QOSInMemory import Stream qualified diff --git a/core/test/OutboundIntegrationSpec.hs b/core/test/OutboundIntegrationSpec.hs index c8cc14e39..dad6d0686 100644 --- a/core/test/OutboundIntegrationSpec.hs +++ b/core/test/OutboundIntegrationSpec.hs @@ -28,7 +28,6 @@ import Service.EventStore.InMemory qualified as InMemory import Service.Integration.DispatchRegistry qualified as DispatchRegistry import Service.Integration.Types (OutboundRunner (..)) import Service.OutboundIntegration.Core (OutboundIntegration (..)) -import Service.OutboundIntegration.TH (outboundIntegration) import Task qualified import Test import Text qualified @@ -132,7 +131,7 @@ handleEvent _entity event = -- | TH splice: generates OutboundIntegration instance, NameOf, and KnownHash. -- IMPORTANT: handleEvent and all type instances MUST appear BEFORE this splice. -outboundIntegration ''TestHandlerA +deriveOutboundIntegration ''TestHandlerA -- ============================================================================ diff --git a/core/test/Service/CommandExecutor/TH/FreshCommandFixture.hs b/core/test/Service/CommandExecutor/TH/FreshCommandFixture.hs index 9d5a24354..1cf986d82 100644 --- a/core/test/Service/CommandExecutor/TH/FreshCommandFixture.hs +++ b/core/test/Service/CommandExecutor/TH/FreshCommandFixture.hs @@ -19,7 +19,6 @@ import Decider qualified import Json qualified import Language.Haskell.TH.Syntax qualified as TH import Service.Auth (RequestContext) -import Service.CommandExecutor.TH (command) import Uuid qualified @@ -69,7 +68,7 @@ decide :: FreshCommand -> Maybe THTestEntity -> RequestContext -> Decision THTes decide _ _ _ = Decider.acceptNew [THTestEntityCreated Uuid.nil] -command ''FreshCommand +deriveCommand ''FreshCommand -- TH probes ----------------------------------------------------------------- diff --git a/core/test/Service/Entity/TH/ConflictingFamiliesFixture.hs b/core/test/Service/Entity/TH/ConflictingFamiliesFixture.hs new file mode 100644 index 000000000..c11a9e0bc --- /dev/null +++ b/core/test/Service/Entity/TH/ConflictingFamiliesFixture.hs @@ -0,0 +1,51 @@ +module Service.Entity.TH.ConflictingFamiliesFixture where + +import Core +import Language.Haskell.TH.Syntax qualified as TH +import Service.Entity.Core qualified as Entity +import Uuid qualified + +data FamilyEntity = FamilyEntity +data DeclaredEvent = DeclaredEvent +data RequestedEvent = RequestedEvent + +type instance EventOf FamilyEntity = DeclaredEvent +type instance EntityOf RequestedEvent = FamilyEntity + +instance Entity FamilyEntity where + initialStateImpl = FamilyEntity + updateImpl _ entity = entity + +instance Default FamilyEntity where + def = FamilyEntity + +instance Entity.Event RequestedEvent where + getEventEntityIdImpl _ = Uuid.nil + +$(TH.recover + [d| rejectedEventOfConflict :: Bool; rejectedEventOfConflict = True |] + do + _ <- deriveEntity ''FamilyEntity ''RequestedEvent -- HOOK-ALLOW: inspect TH failure without splicing the generated declarations. + [d| rejectedEventOfConflict :: Bool; rejectedEventOfConflict = False |]) + +data SecondEntity = SecondEntity +data SecondEvent = SecondEvent + +type instance EventOf SecondEntity = SecondEvent +type instance EntityOf SecondEvent = FamilyEntity + +instance Entity SecondEntity where + initialStateImpl = SecondEntity + updateImpl _ entity = entity + +instance Default SecondEntity where + def = SecondEntity + +instance Entity.Event SecondEvent where + getEventEntityIdImpl _ = Uuid.nil + +$(TH.recover + [d| rejectedEntityOfConflict :: Bool; rejectedEntityOfConflict = True |] + do + _ <- deriveEntity ''SecondEntity ''SecondEvent -- HOOK-ALLOW: inspect TH failure without splicing the generated declarations. + [d| rejectedEntityOfConflict :: Bool; rejectedEntityOfConflict = False |]) diff --git a/core/test/Service/Entity/TH/FreshFixture.hs b/core/test/Service/Entity/TH/FreshFixture.hs new file mode 100644 index 000000000..e4b6f7748 --- /dev/null +++ b/core/test/Service/Entity/TH/FreshFixture.hs @@ -0,0 +1,29 @@ +module Service.Entity.TH.FreshFixture where + +import Core +import Uuid qualified + +-- This local type deliberately shadows the event-routing class's short name. +data Event = Changed {eventId :: Uuid, delta :: Int} + +deriveEvent ''Event + +data Counter = Counter {entityId :: Uuid, count :: Int} + +initialState :: Counter +initialState = Counter {entityId = Uuid.nil, count = 10} + +update :: Event -> Counter -> Counter +update change counter = + counter {entityId = change.eventId, count = counter.count + change.delta} + +getEventEntityId :: Event -> Uuid +getEventEntityId change = change.eventId + +deriveEntity ''Counter ''Event + +-- Repeated application must leave the existing generated instances intact. +deriveEntity ''Counter ''Event + +injectedEvent :: Event +injectedEvent = event (Changed {eventId = Uuid.nil, delta = 3}) diff --git a/core/test/Service/Entity/TH/MissingInitialStateFixture.hs b/core/test/Service/Entity/TH/MissingInitialStateFixture.hs new file mode 100644 index 000000000..de5a9e6c6 --- /dev/null +++ b/core/test/Service/Entity/TH/MissingInitialStateFixture.hs @@ -0,0 +1,22 @@ +module Service.Entity.TH.MissingInitialStateFixture where + +import Core +import Language.Haskell.TH.Syntax qualified as TH +import Uuid qualified + +data MissingEntity = MissingEntity +data MissingEvent = MissingEvent + +deriveEvent ''MissingEvent + +update :: MissingEvent -> MissingEntity -> MissingEntity +update _ entity = entity + +getEventEntityId :: MissingEvent -> Uuid +getEventEntityId _ = Uuid.nil + +$(TH.recover + [d| rejectedMissingCompanion :: Bool; rejectedMissingCompanion = True |] + do + _ <- deriveEntity ''MissingEntity ''MissingEvent -- HOOK-ALLOW: inspect TH failure without splicing the generated declarations. + [d| rejectedMissingCompanion :: Bool; rejectedMissingCompanion = False |]) diff --git a/core/test/Service/Entity/TH/MissingRoutingFixture.hs b/core/test/Service/Entity/TH/MissingRoutingFixture.hs new file mode 100644 index 000000000..89db8622a --- /dev/null +++ b/core/test/Service/Entity/TH/MissingRoutingFixture.hs @@ -0,0 +1,21 @@ +module Service.Entity.TH.MissingRoutingFixture where + +import Core +import Language.Haskell.TH.Syntax qualified as TH + +data MissingEntity = MissingEntity +data MissingEvent = MissingEvent + +deriveEvent ''MissingEvent + +initialState :: MissingEntity +initialState = MissingEntity + +update :: MissingEvent -> MissingEntity -> MissingEntity +update _ entity = entity + +$(TH.recover + [d| rejectedMissingCompanion :: Bool; rejectedMissingCompanion = True |] + do + _ <- deriveEntity ''MissingEntity ''MissingEvent -- HOOK-ALLOW: inspect TH failure without splicing the generated declarations. + [d| rejectedMissingCompanion :: Bool; rejectedMissingCompanion = False |]) diff --git a/core/test/Service/Entity/TH/MissingUpdateFixture.hs b/core/test/Service/Entity/TH/MissingUpdateFixture.hs new file mode 100644 index 000000000..17fb06ede --- /dev/null +++ b/core/test/Service/Entity/TH/MissingUpdateFixture.hs @@ -0,0 +1,22 @@ +module Service.Entity.TH.MissingUpdateFixture where + +import Core +import Language.Haskell.TH.Syntax qualified as TH +import Uuid qualified + +data MissingEntity = MissingEntity +data MissingEvent = MissingEvent + +deriveEvent ''MissingEvent + +initialState :: MissingEntity +initialState = MissingEntity + +getEventEntityId :: MissingEvent -> Uuid +getEventEntityId _ = Uuid.nil + +$(TH.recover + [d| rejectedMissingCompanion :: Bool; rejectedMissingCompanion = True |] + do + _ <- deriveEntity ''MissingEntity ''MissingEvent -- HOOK-ALLOW: inspect TH failure without splicing the generated declarations. + [d| rejectedMissingCompanion :: Bool; rejectedMissingCompanion = False |]) diff --git a/core/test/Service/Entity/TH/NoShowFixture.hs b/core/test/Service/Entity/TH/NoShowFixture.hs new file mode 100644 index 000000000..6aeab0506 --- /dev/null +++ b/core/test/Service/Entity/TH/NoShowFixture.hs @@ -0,0 +1,36 @@ +module Service.Entity.TH.NoShowFixture where + +import Core +import Json qualified +import Language.Haskell.TH.Syntax qualified as TH +import Uuid qualified + +-- An entity field can be serializable without being printable. +data QuietValue = QuietValue {amount :: Int} + deriving (Generic) + +instance Json.FromJSON QuietValue +instance Json.ToJSON QuietValue + +data QuietEvent = QuietChanged {amount :: Int} + +deriveEvent ''QuietEvent + +data QuietEntity = QuietEntity {secret :: QuietValue} + +initialState :: QuietEntity +initialState = QuietEntity {secret = QuietValue {amount = 0}} + +update :: QuietEvent -> QuietEntity -> QuietEntity +update change _ = QuietEntity {secret = QuietValue {amount = change.amount}} + +getEventEntityId :: QuietEvent -> Uuid +getEventEntityId _ = Uuid.nil + +deriveEntity ''QuietEntity ''QuietEvent + +$(do + instances <- TH.reifyInstances ''Show [TH.ConT ''QuietEntity] + case instances of + [] -> [d| hasEntityShow :: Bool; hasEntityShow = False |] + _ : _ -> [d| hasEntityShow :: Bool; hasEntityShow = True |]) diff --git a/core/test/Service/Entity/TH/PreservedFixture.hs b/core/test/Service/Entity/TH/PreservedFixture.hs new file mode 100644 index 000000000..7ccf9d0f8 --- /dev/null +++ b/core/test/Service/Entity/TH/PreservedFixture.hs @@ -0,0 +1,60 @@ +module Service.Entity.TH.PreservedFixture where + +import Core +import Json qualified +import Service.Entity.Core qualified as Entity + +-- Custom behavior deliberately has no marker companion functions. +data CustomEntity = CustomEntity {count :: Int} + deriving (Generic) + +data CustomEvent = CustomChanged {customId :: Text, delta :: Int} + +deriveEvent ''CustomEvent + +type instance NameOf CustomEntity = "custom-counter" +type instance EventOf CustomEntity = CustomEvent +type instance EntityOf CustomEvent = CustomEntity + +instance Json.ToJSON CustomEntity where + toJSON entity = Json.encode entity.count + +instance Json.FromJSON CustomEntity where + parseJSON value = do + count <- Json.parseJSON value + Json.yield CustomEntity {count} + +instance Default CustomEntity where + def = CustomEntity {count = 77} + +instance Entity CustomEntity where + type EntityIdType CustomEntity = Text + initialStateImpl = CustomEntity {count = 11} + updateImpl change entity = entity {count = entity.count + change.delta + 100} + +instance Entity.Event CustomEvent where + getEventEntityIdImpl change = change.customId + +deriveEntity ''CustomEntity ''CustomEvent + +-- Default can reuse an existing Entity implementation without local companions. +data ExistingEntity = ExistingEntity {count :: Int} +data ExistingEvent = ExistingChanged {customId :: Text, delta :: Int} + +deriveEvent ''ExistingEvent + +type ExistingEventAlias = ExistingEvent +type ExistingEntityAlias = ExistingEntity + +type instance EventOf ExistingEntity = ExistingEventAlias +type instance EntityOf ExistingEvent = ExistingEntityAlias + +instance Entity ExistingEntity where + type EntityIdType ExistingEntity = Text + initialStateImpl = ExistingEntity {count = 33} + updateImpl change entity = ExistingEntity {count = entity.count + change.delta} + +instance Entity.Event ExistingEvent where + getEventEntityIdImpl change = change.customId + +deriveEntity ''ExistingEntity ''ExistingEvent diff --git a/core/test/Service/Entity/THSpec.hs b/core/test/Service/Entity/THSpec.hs new file mode 100644 index 000000000..15477d166 --- /dev/null +++ b/core/test/Service/Entity/THSpec.hs @@ -0,0 +1,98 @@ +module Service.Entity.THSpec where + +import Core +import Data.Proxy qualified as Proxy -- HOOK-ALLOW: compiler type-level reflection probe; no Core proxy wrapper. +import GHC.TypeLits qualified as TypeLits -- HOOK-ALLOW: inspect generated type-level names in this compiler-boundary test. +import Json qualified +import Service.Entity.Core qualified as Entity +import Service.Entity.TH.ConflictingFamiliesFixture qualified as Conflicts +import Service.Entity.TH.FreshFixture qualified as Fresh +import Service.Entity.TH.MissingInitialStateFixture qualified as MissingInitial +import Service.Entity.TH.MissingRoutingFixture qualified as MissingRouting +import Service.Entity.TH.MissingUpdateFixture qualified as MissingUpdate +import Service.Entity.TH.NoShowFixture qualified as NoShow +import Service.Entity.TH.PreservedFixture qualified as Preserved +import Test +import Text qualified +import Uuid qualified + +spec :: Spec Unit +spec = do + describe "Core derivation helpers" do + it "derives an event while retaining Core.event injection" \_ -> do + Fresh.injectedEvent.delta |> shouldBe 3 + Json.encode Fresh.injectedEvent + |> shouldBe (Json.encode (Fresh.Changed {eventId = Uuid.nil, delta = 3})) + + it "uses the entity type name by default" \_ -> do + TypeLits.symbolVal (Proxy.Proxy @(NameOf Fresh.Counter)) + |> Text.fromLinkedList + |> shouldBe "Counter" + + describe "derived entity behavior" do + it "uses the supplied initial state for both Entity and Default" \_ -> do + (initialStateImpl @Fresh.Counter).count |> shouldBe 10 + (def @Fresh.Counter).count |> shouldBe 10 + (def @Fresh.Counter).entityId |> shouldBe Uuid.nil + + it "delegates updates and routes the event despite a local Event type" \_ -> do + let routedId = Uuid.generateV5 Uuid.nil "entity-derivation-test" + let change = Fresh.Changed {eventId = routedId, delta = 4} + let initial = Fresh.Counter {entityId = Uuid.nil, count = 5} + let updated = updateImpl change initial + updated.count |> shouldBe 9 + updated.entityId |> shouldBe routedId + Entity.getEventEntityIdImpl change |> shouldBe routedId + + it "roundtrips generated entity JSON without requiring entity Show" \_ -> do + let original = Fresh.Counter {entityId = Uuid.generateV5 Uuid.nil "entity-json-test", count = 12} + case Json.decode @Fresh.Counter (Json.encode original) of + Err reason -> Test.fail reason + Ok decoded -> do + decoded.entityId |> shouldBe original.entityId + decoded.count |> shouldBe original.count + + it "supports serializable fields that have no Show instance" \_ -> do + NoShow.hasEntityShow |> shouldBe False + let original = updateImpl (NoShow.QuietChanged {amount = 8}) (def @NoShow.QuietEntity) + case Json.decode @NoShow.QuietEntity (Json.encode original) of + Err reason -> Test.fail reason + Ok decoded -> decoded.secret.amount |> shouldBe 8 + + describe "entity derivation boundaries" do + it "preserves custom JSON, name, default, entity behavior and identifier type" \_ -> do + TypeLits.symbolVal (Proxy.Proxy @(NameOf Preserved.CustomEntity)) + |> Text.fromLinkedList + |> shouldBe "custom-counter" + (def @Preserved.CustomEntity).count |> shouldBe 77 + (initialStateImpl @Preserved.CustomEntity).count |> shouldBe 11 + let change = Preserved.CustomChanged {customId = "customer-17", delta = 2} + let initial = Preserved.CustomEntity {count = 3} + (updateImpl change initial).count |> shouldBe 105 + Entity.getEventEntityIdImpl change |> shouldBe "customer-17" + Json.encode initial |> shouldBe (Json.encode (3 :: Int)) + case Json.decode @Preserved.CustomEntity (Json.encode (6 :: Int)) of + Err reason -> Test.fail reason + Ok decoded -> decoded.count |> shouldBe 6 + + it "derives Default from an existing Entity with alias mappings and no companions" \_ -> do + (def @Preserved.ExistingEntity).count |> shouldBe 33 + let original = Preserved.ExistingEntity {count = 14} + case Json.decode @Preserved.ExistingEntity (Json.encode original) of + Err reason -> Test.fail reason + Ok decoded -> decoded.count |> shouldBe 14 + + it "requires initialState when deriving a missing Entity instance" \_ -> do + MissingInitial.rejectedMissingCompanion |> shouldBe True + + it "requires update when deriving a missing Entity instance" \_ -> do + MissingUpdate.rejectedMissingCompanion |> shouldBe True + + it "requires getEventEntityId when deriving missing event routing" \_ -> do + MissingRouting.rejectedMissingCompanion |> shouldBe True + + it "rejects an EventOf equation that names another event type" \_ -> do + Conflicts.rejectedEventOfConflict |> shouldBe True + + it "rejects an EntityOf equation that names another entity" \_ -> do + Conflicts.rejectedEntityOfConflict |> shouldBe True diff --git a/core/test/Service/Query/THSpec.hs b/core/test/Service/Query/THSpec.hs index 92d9ecead..50cfdebca 100644 --- a/core/test/Service/Query/THSpec.hs +++ b/core/test/Service/Query/THSpec.hs @@ -9,7 +9,6 @@ import GHC.TypeLits (symbolVal) import Json qualified import Language.Haskell.TH.Syntax qualified as TH import Service.AccessControl (AccessError) -import Service.Query.TH (deriveQuery) import Test import Text qualified import Uuid qualified diff --git a/dev b/dev index 20bda4b57..6154e9aab 100755 --- a/dev +++ b/dev @@ -33,10 +33,8 @@ usage: ./dev [args] codemap-regen workflow (check-metadata, check-symlinks, reconcile, check-staged-diff; --self-test) adr-check validate the ADR index in docs/decisions/README.md - adr-website [--check] (re)generate the website ADR pages — landing + one page - per ADR — from docs/decisions/ (also runs from pnpm - predev/prebuild/precheck; --check: fail if the tracked - landing page is out of sync) + website-migration-check [--check|--self-test] + verify the standalone website-v2 migration boundary hiedb [--rebuild] incrementally index emitted .hie files; --rebuild is the explicit cold full-build path who-calls [mod] cached symbol references, or an immediate git-grep fallback @@ -127,7 +125,7 @@ case "${VERB}" in codemap-check) exec scripts/codemap-check "$@" ;; codemap-regen-guard) exec scripts/codemap-regen-guard.py "$@" ;; adr-check) exec scripts/adr-index-check "$@" ;; - adr-website) exec scripts/adr-website "$@" ;; + website-migration-check) exec scripts/website-migration-check "$@" ;; hiedb) exec scripts/refresh-hiedb "$@" ;; who-calls) exec scripts/who-calls "$@" ;; regression-smoke) exec scripts/regression-scaffold-smoke "$@" ;; diff --git a/docs/changes/013-human-documentation.md b/docs/changes/013-human-documentation.md new file mode 100644 index 000000000..f3b9341a5 --- /dev/null +++ b/docs/changes/013-human-documentation.md @@ -0,0 +1,68 @@ +# Change 013: Teach NeoHaskell with progressive depth and a practice project + +Publish the approved human documentation methodology and a source-grounded learning +journey from evaluating NeoHaskell through building, connecting, operating, and +evolving applications with an AI coding partner. Use ecommerce as the continuing +example and practice project, with general-purpose concepts leading the topic pages. +Preserve direct consultation and an accessible evaluator path; no existing company's +story is required. Agent-facing documentation is separate. + +The reader installs the released CLI, creates one project with `neo new`, and +continues it through the cart, IDE, tests, integrations, and deployment. Complete +tutorial checkpoints support source and runtime verification. Framework-repository +commands belong only in the contribution branch. + +The introduction develops the philosophy before code through longer conceptual +chapters and editable visual explanations. History, correction, feature slices, +shared modeling, and human-agent trust form the accessible foundation. Diagram +sources, exports, text alternatives, and review evidence join the maintenance gate. +Worked Event Models use the canonical command/event/read-model colours and trace +concrete values across a timeline. Actual IDE screenshots connect the notation to +the tools; binary image hashes and text alternatives join the documentation checks. + +```yaml spec +issue: adhoc:human-documentation +kind: feature +touches: [website, ci-cd, governance-docs] +breaking: false +new-dependency: true +new-capability: false +new-extension-point: false +``` + +## Contract delta + +No Haskell or Rust public APIs change. The Neo CLI's generated project preset +adds `DerivingStrategies`, so framework declaration helpers work without per-file +language pragmas. A regression checks the real rendered template and inheritance +by application, library, and test components. The published 0.10.0 binary lacks +this default; pragma-free tutorial verification uses the corrected local CLI, +and the installation page identifies the next-release requirement. + +```diff signatures +``` + +## Criteria + +| ID | Behavior | Proving test | Level | Boundary | +|----|----------|--------------|-------|----------| +| C1 | The monorepo delegates public website ownership to `neohaskell/website-v2` and retains only a migration pointer | `script:scripts/website-migration-check#--check` | unit | none | +| C2 | The migration boundary rejects obsolete in-repository website payload, workflows, and ADR integration | `script:scripts/website-migration-check#--self-test` | unit | none | + +## User impact + +Jess can learn NeoHaskell through a cohesive ecommerce practice project, apply the +concepts to her own domain, evaluate business tradeoffs without reading every +implementation section, and consult individual topics. Existing +getting-started and deployment routes remain usable. The source examples distinguish +implemented public capabilities from shop-specific designs. The authoring plan and review record remain in the standalone +[`neohaskell/website-v2`](https://github.com/neohaskell/website-v2) repository; +they explicitly separate automated verification from a future human reader trial +and live external-provider testing. + +## ADR + +[ADR-0078](../decisions/0078-documentation-image-zoom.md) records the Starlight +image-zoom dependency. Diagrams and screenshots enlarge in a dialog on the +current page, with keyboard controls and captions. Built-page verification +requires an accessible zoom trigger for every registered illustration. diff --git a/docs/changes/015-core-derive-markers.md b/docs/changes/015-core-derive-markers.md new file mode 100644 index 000000000..9114ae84a --- /dev/null +++ b/docs/changes/015-core-derive-markers.md @@ -0,0 +1,79 @@ +# Change 015: Expose consistent concept derivation helpers through Core + +Application authors import `Core` and use `deriveEvent`, `deriveCommand`, +`deriveEntity`, `deriveQuery`, and `deriveOutboundIntegration`. Existing marker +names remain compatible. Nick explicitly requested these exports and confirmed +adding entity derivation in the local session on 2026-09-16. + +```yaml spec +issue: adhoc:core-derive-markers +kind: feature +touches: [core-primitives, commands, entities, queries, event-store, service-th, integration-runtime, website, governance-docs] +breaking: false +new-dependency: false +new-capability: false +new-extension-point: false +``` + +## Contract delta + +```diff signatures ++ Service.Event.TH: deriveEvent :: Name -> DecsQ ++ Service.CommandExecutor.TH: deriveCommand :: Name -> DecsQ ++ Service.Entity.TH: deriveEntity :: Name -> Name -> DecsQ ++ Service.OutboundIntegration.TH: deriveOutboundIntegration :: Name -> DecsQ +``` + +`Core` reexports all five canonical helpers; its value-level `event` remains +available. `deriveQuery` retains its current signature. The other existing TH +names remain available from their defining modules. + +`deriveEntity ''CartEntity ''CartEvent` generates Generic/JSON, NameOf, EventOf, +EntityOf for the event, Default, Entity, and event-routing instances. Application +code provides `initialState`, `update`, and `getEventEntityId` before the marker. +Existing instances are preserved, including custom EntityIdType and JSON +instances. Entity derivation does not impose Show on entity fields. Missing +companions receive actionable compile-time errors. Business decisions, reducers, +event routing, and query projections remain application code. + +## Criteria + +| ID | Behavior | Proving test | Level | Boundary | +|----|----------|--------------|-------|----------| +| C1 | Core exposes event derivation while retaining value-level event injection | `hspec:nhcore-test-service:core/test/Service/Entity/THSpec.hs#derives an event while retaining Core.event injection` | unit | none | +| C2 | Entity derivation delegates state, update, routing and default behavior | `hspec:nhcore-test-service:core/test/Service/Entity/THSpec.hs#delegates updates and routes the event despite a local Event type` | unit | none | +| C3 | Missing entity companions fail and existing instances remain valid | `hspec:nhcore-test-service:core/test/Service/Entity/THSpec.hs#requires initialState when deriving a missing Entity instance`
`hspec:nhcore-test-service:core/test/Service/Entity/THSpec.hs#preserves custom JSON, name, default, entity behavior and identifier type` | unit | none | +| C4 | Canonical command, query and integration exports compile with existing behavior | `hspec:nhcore-test-service:core/test/Service/CommandExecutor/THSpec.hs#[impl-driven] emits Show instance on fresh command (has Generic, missing Show/ToJSON/FromJSON)`
`hspec:nhcore-test-service:core/test/Service/Query/THSpec.hs#generates Query instance for UserOrders`
`hspec:nhcore-test-core:core/test/OutboundIntegrationSpec.hs#generates a compilable OutboundIntegration instance` | unit | none | +| C5 | The monorepo retains the standalone website pointer and rejects removed website paths or stale local integration references | `script:scripts/website-migration-check#--check` | unit | none | + +## User impact + +New examples need no separate TH imports. Entities need one declaration helper +beside their application functions. Existing applications need no migration; +the previous names still work. Tutorial downloads require the framework version +that contains these new helpers. + +## ADR + +Not required — additive helpers within existing capabilities, without new +dependencies or extension-point registrations. + +## Implementation plan + +- Break Core import back-edges in CommandExecutor/TH, Query/TH, + OutboundIntegration/TH and TH/Boilerplate using Basics, Appendable and Maybe. +- Add aliases in the existing TH modules and reexports in core/core/Core.hs. +- Copy-adapt Service/Entity/TH.hs from Event/TH.hs and Query/TH.hs; use canonical + quoted Entity.Core names rather than a consumer's potentially shadowed Event. +- Copy-adapt core/test/Service/Entity/THSpec.hs from Service/Event/THSpec.hs; + register its fixtures in core/nhcore.cabal and core/test-service/Main.hs. +- Migrate selected existing marker fixtures to prove Core exports while keeping + legacy fixtures. Remove redundant direct deriveQuery imports. +- Update website examples, documentation excerpts, authoring rules and concept + skill guidance. Regenerate archives, evidence hashes and codemap artifacts. + +Resolved primitives: TH.reifyInstances, TH.lookupValueName, TH.recover, +emitInstanceIfMissing, emitStockDeriving, emitEmptyInstance, Entity(..), Event(..), +NameOf, EntityOf, EventOf, Default(..). Existing test neighbors prove the marker +and JSON conventions. The HIE index was unavailable; bounded caller lookup was +used as instructed by who-calls. diff --git a/docs/changes/015-core-derive-markers.perf-review.md b/docs/changes/015-core-derive-markers.perf-review.md new file mode 100644 index 000000000..4ff42e04c --- /dev/null +++ b/docs/changes/015-core-derive-markers.perf-review.md @@ -0,0 +1,12 @@ +# Performance design review: Core concept derivation helpers + +Spec: docs/changes/015-core-derive-markers.md | Date: 2026-09-16 + +| # | Checklist | Finding | Grounding | Verdict | +|---|-----------|---------|-----------|---------| +| 1 | P1 | Helpers execute at compile time. Generated entity methods delegate to application functions as handwritten instances do. | No new runtime dispatch boundary. | informational | +| 2 | P2, P3, P5, P6 | No runtime algorithm, accumulator, lock or container changes. | Demoted: no exercised new hot path. | informational | +| 3 | P4 | Entity codecs retain Generic JSON defaults and preserve explicit codecs. | Same serialization mechanism as existing entities. | informational | +| 4 | P7 | No speed claim; no new runtime benchmark budget. | Compile-time API consistency is the goal. | informational | + +**Blockers:** 0. No performance-related plan amendment. diff --git a/docs/decisions/0078-documentation-image-zoom.md b/docs/decisions/0078-documentation-image-zoom.md new file mode 100644 index 000000000..2eb0a55e2 --- /dev/null +++ b/docs/decisions/0078-documentation-image-zoom.md @@ -0,0 +1,43 @@ +# ADR-0078: Enlarge documentation images in a Starlight dialog + +## Status + +Accepted + +## Context + +Diagrams and IDE screenshots carry much of the documentation's explanation. +Opening the image file directly leaves the lesson and its reading position. +The maintainer requested an Astro Starlight plugin that enlarges images in place. + +## Decision + +Use [starlight-image-zoom](https://github.com/HiDeoo/starlight-image-zoom), pinned +to 0.15.0, whose declared peer range includes the website's Starlight 0.41.3. +Version 0.16.0 requires Starlight 0.42.0 or newer; upgrading the documentation +framework is separate work. The package and its resolved dependencies live in +the existing pnpm lockfile. + +Configure Astro's supported Unified Markdown processor through a direct, pinned +`@astrojs/markdown-remark` 7.2.1 dependency (already resolved by the existing +Starlight installation). Image Zoom 0.15 uses rehype and cannot process the default +Sätteri pipeline. This keeps the existing Astro/Starlight versions while using the +plugin's supported integration. The full site build and rendered-link checks cover +the renderer change; future upgrades can reconsider Sätteri when all versions align. + +Register the plugin in Starlight's configuration. Render illustrations as ordinary +Markdown images, since linked images are intentionally excluded by the plugin. +Keep editable Draw.io downloads as separate links. Use the plugin's dialog, +keyboard triggers, captions, Escape dismissal, and focus restoration rather than +maintaining our own lightbox implementation. Without JavaScript, the ordinary +inline image and its alternative text remain available. + +## Consequences + +- Clicking an illustration enlarges it without navigating away from the lesson. +- The plugin owns dialog behaviour; CSS preserves readable diagram backgrounds. +- Dependency updates must respect the installed Starlight version. +- Built-page checks require a zoom controller and accessible trigger for every + documentation diagram and screenshot. Browser review checks opening, dismissal, + keyboard use, and preservation of the current page and scroll position. +- No application runtime or public API changes. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 54fb35d38..ef1a0d635 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -91,6 +91,7 @@ ADRs document significant architectural decisions made during the development of | [0075](0075-change-process-v2.md) | Change process v2: five coarse steps, one human gate, verified auto-merge | Superseded | | [0076](0076-restore-resumable-change-pipeline.md) | Restore the resumable contract-delta change pipeline | Implemented | | [0077](0077-local-release-notes-and-main-publication.md) | Local release notes and coordinated publication from main | Implemented | +| [0078](0078-documentation-image-zoom.md) | Documentation Image Zoom | Accepted | '; - -// ── pure helpers (unit-tested in --self-test) ────────────────────────────── - -/** Rows of the ADR index table: [{num, file, slug, title, status}]. */ -export function parseIndex(text) { - const entries = []; - for (const line of text.split('\n')) { - const m = ROW_RE.exec(line); - if (!m) continue; // header / separator / prose - const [, num, file, title, status] = m; - entries.push({ num, file: file.trim(), slug: file.trim().replace(/\.md$/, ''), title: title.trim(), status: status.trim() }); - } - return entries; -} - -/** Quote a string as a YAML double-quoted scalar (titles carry ':' and '`'). */ -export function yamlQuote(s) { - return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; -} - -/** Normalize a path written relative to docs/decisions/ (resolves ./ and ../). */ -export function resolveDocsPath(rel) { - const stack = []; - for (const part of `docs/decisions/${rel}`.split('/')) { - if (part === '' || part === '.') continue; - if (part === '..') stack.pop(); - else stack.push(part); - } - return stack.join('/'); -} - -/** Rewrite the links in an ADR body so they work on the website. - * NNNN-slug.md[#a] → /adrs/NNNN-slug/[#a] (internal ADR page) - * README.md[#a] → /adrs/[#a] (this landing page) - * other X.md[#a] → GitHub source blob (files not on the site) - * Code fences are left untouched so example snippets are preserved verbatim. */ -export function rewriteLinks(body) { - const out = []; - let inFence = false; - for (const line of body.split('\n')) { - if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; out.push(line); continue; } - if (inFence) { out.push(line); continue; } - let l = line; - // README (index) links first — before the generic .md catch-all. - l = l.replace(/\]\((?:\.\/)?README\.md(#[^)]*)?\)/g, (_m, anchor) => `](/adrs/${anchor || ''})`); - // ADR-to-ADR links → internal detail route. - l = l.replace(/\]\((?:\.\/)?(\d{4}-[A-Za-z0-9-]+)\.md(#[^)]*)?\)/g, - (_m, slug, anchor) => `](/adrs/${slug}/${anchor || ''})`); - // Any remaining relative *.md link points outside the site → GitHub source. - l = l.replace(/\]\((?!https?:|\/|#|mailto:)([^)\s]+?\.md)(#[^)]*)?\)/g, - (_m, rel, anchor) => `](${GITHUB_BLOB}/${resolveDocsPath(rel)}${anchor || ''})`); - out.push(l); - } - return out.join('\n'); -} - -/** Split an ADR into its title (first H1) and the remaining body. */ -export function extractTitle(raw, fallback) { - const lines = raw.split('\n'); - for (let i = 0; i < lines.length; i++) { - const m = /^#\s+(.+?)\s*$/.exec(lines[i]); // H1 only ('## …' is a section) - if (m) return { title: m[1], body: [...lines.slice(0, i), ...lines.slice(i + 1)].join('\n') }; - } - return { title: fallback, body: raw }; -} - -/** A faithful Starlight page for one ADR: frontmatter + transcribed body. */ -export function renderDetail(entry, raw) { - const { title, body } = extractTitle(raw, entry.title); - const transcribed = rewriteLinks(body).replace(/^\n+/, '').replace(/\s+$/, ''); - return `---\ntitle: ${yamlQuote(title)}\n---\n\n${GENERATED_DETAIL_MARKER}\n\n${transcribed}\n`; -} - -/** The tracked ADR landing page — internal links, deterministic ordering. */ -export function renderIndex(entries) { - const rows = entries.map((e) => `| [${e.num}](/adrs/${e.slug}/) | ${e.title} | ${e.status} |`); - return [ - '---', - 'title: Architecture Decision Records', - 'description: The architectural decisions behind NeoHaskell, with links to each record.', - '---', - '', - 'Architecture Decision Records (ADRs) capture the significant architectural decisions made while building NeoHaskell. Each record documents the **context** (why the decision was needed), the **decision** (what was chosen and why), and the **consequences** (what follows from it).', - '', - `ADRs are immutable: when a decision is reversed, a new ADR is written that references the original. Each record below is published as its own page, transcribed from the [decisions log](${README_URL}) in the repository — the records themselves are maintained in English.`, - '', - GENERATED_NOTICE, - '', - '| ADR | Title | Status |', - '| --- | ----- | ------ |', - ...rows, - '', - ].join('\n'); -} - -// ── filesystem actions ───────────────────────────────────────────────────── - -function readEntries() { - return parseIndex(readFileSync(INDEX, 'utf8')); -} - -function writeDetails(entries) { - mkdirSync(ADRS_DIR, { recursive: true }); - const wanted = new Set(); - for (const e of entries) { - wanted.add(e.slug); - const raw = readFileSync(join(DECISIONS, e.file), 'utf8'); - writeFileSync(join(ADRS_DIR, `${e.slug}.md`), renderDetail(e, raw), 'utf8'); - } - return pruneStale(wanted); -} - -/** Remove stale generated detail pages only. A manually authored Markdown file - * without GENERATED_DETAIL_MARKER is never touched. */ -function pruneStale(wantedSlugs) { - const removed = []; - if (!existsSync(ADRS_DIR)) return removed; - for (const f of readdirSync(ADRS_DIR)) { - if (!f.endsWith('.md')) continue; // index.mdx and anything non-.md is safe - if (wantedSlugs.has(f.slice(0, -3))) continue; - const path = join(ADRS_DIR, f); - if (!readFileSync(path, 'utf8').includes(GENERATED_DETAIL_MARKER)) continue; - unlinkSync(path); - removed.push(f); - } - return removed.sort(); -} - -function cmdWrite() { - const entries = readEntries(); - mkdirSync(ADRS_DIR, { recursive: true }); - writeFileSync(INDEX_PAGE, renderIndex(entries), 'utf8'); - const removed = writeDetails(entries); - const extra = removed.length ? `, pruned ${removed.length} stale` : ''; - console.log(`adr-website: wrote index.mdx + ${entries.length} ADR pages${extra}`); - return 0; -} - -function cmdCheck() { - const entries = readEntries(); - // Regenerate the (gitignored) detail pages so --check also proves every ADR - // still transcribes without error; then gate the one TRACKED file for drift. - writeDetails(entries); - const want = renderIndex(entries); - if (!existsSync(INDEX_PAGE)) { - console.error(`adr-website: FAIL — ${rel(INDEX_PAGE)} is missing; run ./dev adr-website`); - return 1; - } - if (readFileSync(INDEX_PAGE, 'utf8') !== want) { - console.error(`adr-website: FAIL — ${rel(INDEX_PAGE)} is out of sync with docs/decisions/README.md`); - console.error(' regenerate with: ./dev adr-website'); - return 1; - } - console.log(`adr-website: OK — ADR landing page in sync (${entries.length} ADRs), detail pages regenerated`); - return 0; -} - -function rel(p) { - return p.startsWith(REPO_ROOT) ? p.slice(REPO_ROOT.length + 1) : p; -} - -// ── self-test (fixtures; no writes to the real tree) ─────────────────────── - -function selfTest() { - let ok = true; - const check = (name, cond) => { console.log(` ${cond ? 'ok ' : 'FAIL'} ${name}`); ok = ok && cond; }; - - const sample = - '# Architecture Decision Records (ADRs)\n\n' + - '| ADR | Title | Status |\n' + - '| --- | ----- | ------ |\n' + - '| [0001](0001-initial-architecture-baseline.md) | Initial Baseline | Accepted |\n' + - '| [0055](0055-declarative-integrations-with-fakes.md) | Declarative Integrations | Proposed |\n\n' + - '## Creating New ADRs\n'; - const entries = parseIndex(sample); - check('parses exactly the two data rows (header/separator skipped)', entries.length === 2); - check('captures number/file/slug/title/status', - entries[0].num === '0001' && entries[0].slug === '0001-initial-architecture-baseline' && - entries[0].title === 'Initial Baseline' && entries[0].status === 'Accepted'); - check('preserves Proposed status', entries[1].status === 'Proposed'); - - const idx = renderIndex(entries); - check('index rows link INTERNALLY to /adrs//', idx.includes('| [0001](/adrs/0001-initial-architecture-baseline/) |')); - check('index carries no GitHub blob link in its rows', !/\]\(https:\/\/github\.com[^)]*\)\s*\|/.test(idx)); - check('index carries the generated banner', idx.includes('GENERATED by website/scripts/generate-adrs.mjs')); - check('index has valid frontmatter', idx.startsWith('---\ntitle: Architecture Decision Records')); - check('index render is deterministic', renderIndex(entries) === idx); - - const t = extractTitle('# ADR-0069: Local, never pushed\n\n## Status\n\nAccepted\n', 'fallback'); - check('extractTitle takes the first H1', t.title === 'ADR-0069: Local, never pushed'); - check('extractTitle keeps ## sections in the body', t.body.includes('## Status') && !t.body.includes('# ADR-0069')); - - const rw = rewriteLinks('See [x](0067-contract-delta-spec-gate.md) and [y](0067-contract-delta-spec-gate.md#status) and [i](README.md) and [p](../plans/foo.md).'); - check('rewrites ADR link to internal route', rw.includes('](/adrs/0067-contract-delta-spec-gate/)')); - check('rewrites ADR link + anchor', rw.includes('](/adrs/0067-contract-delta-spec-gate/#status)')); - check('rewrites README link to /adrs/', rw.includes('](/adrs/)')); - check('rewrites off-site .md link to GitHub source', rw.includes(`](${GITHUB_BLOB}/docs/plans/foo.md)`)); - - const fenced = rewriteLinks('```\n[keep](0001-x.md)\n```\n[go](0001-real-slug.md)'); - check('leaves links inside code fences untouched', fenced.includes('[keep](0001-x.md)')); - check('still rewrites links outside fences', fenced.includes('](/adrs/0001-real-slug/)')); - - const detail = renderDetail({ slug: '0069-x', title: 'fallback' }, '# ADR-0069: Title: with colon\n\nBody [ref](0001-a.md).\n'); - check('detail frontmatter quotes a title containing a colon', detail.includes('title: "ADR-0069: Title: with colon"')); - check('detail carries the generated marker used for safe pruning', detail.includes(GENERATED_DETAIL_MARKER)); - check('detail strips the source H1', !detail.includes('# ADR-0069: Title: with colon\n')); - check('detail rewrites body links', detail.includes('](/adrs/0001-a/)')); - - check('yamlQuote escapes embedded quotes', yamlQuote('a "b" c') === '"a \\"b\\" c"'); - check('resolveDocsPath normalizes ../', resolveDocsPath('../plans/x.md') === 'docs/plans/x.md'); - - console.log('adr-website: self-test', ok ? 'OK' : 'FAILED'); - return ok ? 0 : 1; -} - -// ── entry point ───────────────────────────────────────────────────────────── - -function main(argv) { - const mode = argv[0]; - if (mode === '--self-test') return selfTest(); - if (mode === '--check') return cmdCheck(); - if (!mode) return cmdWrite(); - console.error(`usage: generate-adrs.mjs [--check|--self-test] (got ${JSON.stringify(argv)})`); - return 2; -} - -process.exit(main(process.argv.slice(2))); diff --git a/website/src/content.config.ts b/website/src/content.config.ts deleted file mode 100644 index d9ee8c9d1..000000000 --- a/website/src/content.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineCollection } from 'astro:content'; -import { docsLoader } from '@astrojs/starlight/loaders'; -import { docsSchema } from '@astrojs/starlight/schema'; - -export const collections = { - docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), -}; diff --git a/website/src/content/docs/adrs/index.mdx b/website/src/content/docs/adrs/index.mdx deleted file mode 100644 index 68e22e57a..000000000 --- a/website/src/content/docs/adrs/index.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Architecture Decision Records -description: The architectural decisions behind NeoHaskell, with links to each record. ---- - -Architecture Decision Records (ADRs) capture the significant architectural decisions made while building NeoHaskell. Each record documents the **context** (why the decision was needed), the **decision** (what was chosen and why), and the **consequences** (what follows from it). - -ADRs are immutable: when a decision is reversed, a new ADR is written that references the original. Each record below is published as its own page, transcribed from the [decisions log](https://github.com/neohaskell/NeoHaskell/blob/main/docs/decisions/README.md) in the repository — the records themselves are maintained in English. - -{/* GENERATED by website/scripts/generate-adrs.mjs from docs/decisions/README.md — do not edit by hand. - Regenerate: ./dev adr-website (or it runs automatically on pnpm predev/prebuild/precheck) - Verify (CI): ./dev adr-website --check */} - -| ADR | Title | Status | -| --- | ----- | ------ | -| [0001](/adrs/0001-initial-architecture-baseline/) | Initial Architecture Baseline | Accepted | -| [0002](/adrs/0002-webapi-adapter-architecture/) | WebAPI Adapter Architecture | Accepted | -| [0003](/adrs/0003-command-abstraction-and-flow/) | Command Abstraction and Flow | Accepted | -| [0004](/adrs/0004-eventstore-abstraction/) | EventStore Abstraction | Accepted | -| [0005](/adrs/0005-service-module-reorganization/) | Service Module Reorganization | Accepted | -| [0006](/adrs/0006-entity-snapshot-cache/) | Entity Snapshot Cache | Accepted | -| [0007](/adrs/0007-queries-read-models/) | Queries (Read Models) | Accepted | -| [0008](/adrs/0008-integration-pattern/) | Integration Pattern | Accepted | -| [0009](/adrs/0009-jwt-authentication-middleware/) | JWT Authentication Middleware | Accepted | -| [0010](/adrs/0010-oauth2-provider-architecture/) | OAuth2 Provider Integration | Accepted | -| [0011](/adrs/0011-file-upload-architecture/) | File Upload Architecture | Accepted | -| [0012](/adrs/0012-postgres-file-state-store/) | PostgreSQL FileStateStore | Accepted | -| [0013](/adrs/0013-automatic-schema-generation/) | Automatic Schema Generation | Accepted | -| [0014](/adrs/0014-webtransport-openapi-integration/) | WebTransport OpenAPI Integration | Accepted | -| [0015](/adrs/0015-http-outbound-integration/) | HTTP Outbound Integration | Accepted | -| [0016](/adrs/0016-redacted-type-for-sensitive-data/) | Redacted Type for Sensitive Data | Accepted | -| [0017](/adrs/0017-toschema-auto-derivation-in-command-th/) | ToSchema Auto-Derivation in Command TH | Accepted | -| [0018](/adrs/0018-http-localhost-exception-for-oauth2-discovery/) | HTTP Localhost Exception for OAuth2 Discovery | Accepted | -| [0019](/adrs/0019-webtransport-request-body-size-coordination/) | WebTransport Request Body Size Coordination | Accepted | -| [0020](/adrs/0020-openapi-grouping-and-complete-endpoints/) | OpenAPI Grouping and Complete Endpoint Coverage | Accepted | -| [0021](/adrs/0021-declarative-config-dsl/) | Declarative Config DSL | Accepted | -| [0022](/adrs/0022-decimal-type/) | Decimal Type for Financial Calculations | Accepted | -| [0023](/adrs/0023-ai-pdf-transcription/) | AI-Powered OCR via Multimodal Models | Accepted | -| [0024](/adrs/0024-cors-support/) | CORS Support for WebTransport | Accepted | -| [0025](/adrs/0025-auto-health-endpoint/) | Auto Health Endpoint for WebTransport Apps | Accepted | -| [0026](/adrs/0026-line-buffering-for-containerized-deployments/) | Line Buffering for Containerized Deployments | Accepted | -| [0027](/adrs/0027-postgres-pool-health/) | PostgreSQL Pool Health for Serverless Databases | Accepted | -| [0028](/adrs/0028-structured-logging/) | Structured Logging with fast-logger | Accepted | -| [0029](/adrs/0029-worker-crash-recovery/) | Worker Crash Recovery for Outbound Integrations | Accepted | -| [0030](/adrs/0030-configurable-dispatcher-config/) | Configurable Dispatcher Config | Accepted | -| [0031](/adrs/0031-token-query-param-oauth2/) | Token Query Parameter for OAuth2 Browser Redirects | Accepted | -| [0032](/adrs/0032-simple-event-store/) | SimpleEventStore with Optional JSONL Persistence | Accepted | -| [0033](/adrs/0033-config-dependent-oauth2-provider/) | Config-Dependent OAuth2 Provider Factory | Accepted | -| [0034](/adrs/0034-cli-transport/) | CliTransport — Command-Line Interface Transport | Accepted | -| [0035](/adrs/0035-config-dependent-application-builders/) | Config-Dependent Application Builder Functions | Accepted | -| [0036](/adrs/0036-wave1-security-hardening/) | Wave 1 Security Hardening | Accepted | -| [0037](/adrs/0037-postgres-listen-keepalive-reconnect/) | PostgreSQL LISTEN Connection Keepalive and Reconnection | Accepted | -| [0038](/adrs/0038-fix-flaky-subscription-test/) | Fix Flaky Subscription Test Ordering Assertion | Accepted | -| [0039](/adrs/0039-fix-listen-notify-connection-leak/) | Fix LISTEN/NOTIFY Connection Leak in Test Teardown | Accepted | -| [0040](/adrs/0040-neoql-mvp/) | NeoQL MVP — Field Access and Equality Filtering | Accepted | -| [0041](/adrs/0041-audio-transcription-integration/) | Audio Transcription Integration via Multimodal Models | Accepted | -| [0042](/adrs/0042-parser-library/) | Parser Library — Beginner-Friendly megaparsec Wrapper | Accepted | -| [0043](/adrs/0043-neohaskell-comment-syntax-parsing/) | NeoHaskell Comment Syntax Parsing | Accepted | -| [0044](/adrs/0044-layout-library/) | Layout Library — Beginner-Friendly prettyprinter Wrapper | Accepted | -| [0045](/adrs/0045-integration-agent/) | Integration.Agent — Provider-Agnostic AI Agent via Tool Calling | Accepted | -| [0046](/adrs/0046-function-syntax-fun-keyword/) | Function Syntax — `fun` Keyword Parsing and Transpilation | Accepted | -| [0047](/adrs/0047-eventvariantof-typeclass/) | EventVariantOf Typeclass for Type-Safe Event Variant Wrapping | Accepted | -| [0048](/adrs/0048-file-upload-content-deduplication/) | File Upload Content Deduplication via SHA-256 | Accepted | -| [0049](/adrs/0049-outboundintegration-typeclass-dispatch-generation/) | OutboundIntegration Typeclass with Typed Event Dispatch | Accepted | -| [0050](/adrs/0050-internal-command-transport/) | Internal Command Transport | Accepted | -| [0051](/adrs/0051-query-pagination-result-limits/) | Query Pagination and Result Limits | Accepted | -| [0052](/adrs/0052-mcp-stdio-transport/) | McpTransport — MCP STDIO Transport for AI Assistant Integration | Accepted | -| [0053](/adrs/0053-openapi-query-pagination-schema/) | OpenAPI Query Pagination Schema | Accepted | -| [0054](/adrs/0054-multi-tenant-command-query-support/) | Multi-Tenant Support for Commands and Queries | Accepted | -| [0055](/adrs/0055-declarative-integrations-with-fakes/) | Declarative Integrations with Real/Fake Parity | Proposed | -| [0056](/adrs/0056-request-context-timestamp-as-datetime/) | Retype `RequestContext.timestamp` as `DateTime` | Proposed | -| [0057](/adrs/0057-th-marker-boilerplate/) | Fold JSON + `deriving` boilerplate into `command`, `event`, `deriveQuery` | Accepted | -| [0058](/adrs/0058-per-command-auth-options/) | Per-Command Access Control via `canAccess` | Proposed | -| [0059](/adrs/0059-async-query-rebuild-with-persistent-checkpoints/) | Async Query Rebuild with Persistent Checkpoints | Proposed | -| [0060](/adrs/0060-postgres-pool-budget/) | Explicit Postgres Connection-Pool Budget for Flexible Server B1ms | Proposed | -| [0061](/adrs/0061-listener-reconnect-catchup/) | Replay from Last globalPosition on LISTEN/NOTIFY Listener Reconnect | Proposed | -| [0062](/adrs/0062-shared-connection-settings-builder/) | Single Shared Postgres Connection-Settings Builder | Proposed | -| [0063](/adrs/0063-per-stream-subscription-connection-release/) | Release Per-Stream Subscription Connections on Unsubscribe | Proposed | -| [0064](/adrs/0064-optional-sslmode-tls-hardening/) | Optional `sslmode` TLS Hardening for Postgres Connections | Proposed | -| [0065](/adrs/0065-acs-email-integration/) | Azure Communication Services Email Integration | Proposed | -| [0066](/adrs/0066-two-database-api-search/) | Two-database API search (`./dev api`) | Accepted | -| [0067](/adrs/0067-contract-delta-spec-gate/) | Contract-delta spec gate and resumable draft-PR flow | Accepted | -| [0068](/adrs/0068-failure-asset-delta-and-learning-loop/) | Failure→asset-delta protocol and the learning loop | Accepted | -| [0069](/adrs/0069-security-reviews-are-local/) | Security design-review records are local-only, never pushed | Accepted | -| [0070](/adrs/0070-maintainer-codemap-regeneration/) | Maintainer-triggered codemap regeneration onto a contributor PR | Accepted | -| [0071](/adrs/0071-thread-query-name-through-query-object-store/) | Thread the query name through the QueryObjectStore factory | Accepted | -| [0072](/adrs/0072-neo-cutover-and-onboarding-slo/) | Neo CLI cutover — archive-as-human-gate and the clean-machine onboarding SLO | Accepted | -| [0073](/adrs/0073-deterministic-uuid-v5/) | Deterministic UUID v5 as a Core primitive and a Decision combinator | Implemented | -| [0074](/adrs/0074-dependabot-auto-merge/) | Dependabot patch/minor auto-merges; majors stop for a human | Accepted | -| [0075](/adrs/0075-change-process-v2/) | Change process v2: five coarse steps, one human gate, verified auto-merge | Superseded | -| [0076](/adrs/0076-restore-resumable-change-pipeline/) | Restore the resumable contract-delta change pipeline | Implemented | -| [0077](/adrs/0077-local-release-notes-and-main-publication/) | Local release notes and coordinated publication from main | Implemented | diff --git a/website/src/content/docs/es/adrs/index.mdx b/website/src/content/docs/es/adrs/index.mdx deleted file mode 100644 index 6c71798a6..000000000 --- a/website/src/content/docs/es/adrs/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Registros de Decisiones Arquitectónicas -description: Las decisiones arquitectónicas detrás de NeoHaskell. ---- - -Los Registros de Decisiones Arquitectónicas (ADR) documentan las decisiones arquitectónicas significativas tomadas al construir NeoHaskell. - -Los registros ADR se mantienen de forma canónica **en inglés** y no se traducen, para conservar una única fuente de verdad. Consulta la lista completa, con una página por cada decisión: - -- **[Registros de Decisiones Arquitectónicas (en inglés)](/adrs/)** diff --git a/website/src/content/docs/es/getting-started/index.mdx b/website/src/content/docs/es/getting-started/index.mdx deleted file mode 100644 index 2d665c40b..000000000 --- a/website/src/content/docs/es/getting-started/index.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Comenzando -description: Cómo configurar un proyecto NeoHaskell. ---- - -NeoHaskell vive en el monorepo [neohaskell/NeoHaskell](https://github.com/neohaskell/NeoHaskell). Este sitio de documentación es intencionadamente pequeño; las instrucciones autoritativas de instalación, compilación y pruebas están en el `README.md` del repositorio (en inglés). - -## Inicio rápido - -1. Instala [Determinate Nix](https://determinate.systems/nix). -2. Clona el repositorio y entra en su shell de desarrollo con `nix develop`. -3. Ejecuta `./dev` para ver los comandos de compilación y prueba disponibles. - -Para el flujo de trabajo completo —Postgres, la suite de pruebas y el proceso de contribución— consulta el [README del repositorio](https://github.com/neohaskell/NeoHaskell#readme). diff --git a/website/src/content/docs/es/index.mdx b/website/src/content/docs/es/index.mdx deleted file mode 100644 index bf04e3cfb..000000000 --- a/website/src/content/docs/es/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: NeoHaskell -description: Un dialecto de Haskell accesible para principiantes, con event sourcing garantizado por el compilador. -template: splash -hero: - tagline: Un dialecto de Haskell accesible para principiantes. Escribe tu lógica de negocio con garantías del compilador; todo lo demás se conecta a su alrededor. - actions: - - text: Comenzar - link: /es/getting-started/ - icon: right-arrow - - text: Ver en GitHub - link: https://github.com/neohaskell/NeoHaskell - icon: external - variant: minimal ---- - -## ¿Qué es NeoHaskell? - -NeoHaskell es un dialecto de Haskell diseñado para principiantes y personas pragmáticas. Convierte el event sourcing en una característica del lenguaje: eventos, entidades, comandos, consultas, servicios e integraciones son primitivas del lenguaje, no patrones de librería. Si una transición de estado es incorrecta, el programa no compila. - -## Adónde ir después - -- **[Comenzando](/es/getting-started/)** — configura un proyecto NeoHaskell. -- **[Registros de Decisiones Arquitectónicas](/es/adrs/)** — las decisiones detrás del lenguaje. -- **[NeoHaskell en GitHub](https://github.com/neohaskell/NeoHaskell)** — código, issues y la documentación completa. diff --git a/website/src/content/docs/fr/adrs/index.mdx b/website/src/content/docs/fr/adrs/index.mdx deleted file mode 100644 index 252eba502..000000000 --- a/website/src/content/docs/fr/adrs/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Dossiers de Décisions Architecturales -description: Les décisions architecturales derrière NeoHaskell. ---- - -Les Dossiers de Décisions Architecturales (ADR) documentent les décisions architecturales importantes prises lors de la construction de NeoHaskell. - -Les dossiers ADR sont maintenus de manière canonique **en anglais** et ne sont pas traduits, afin de conserver une source de vérité unique. Consultez la liste complète, avec une page par décision : - -- **[Dossiers de Décisions Architecturales (en anglais)](/adrs/)** diff --git a/website/src/content/docs/fr/getting-started/index.mdx b/website/src/content/docs/fr/getting-started/index.mdx deleted file mode 100644 index 50a15ee5e..000000000 --- a/website/src/content/docs/fr/getting-started/index.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Commencer -description: Comment configurer un projet NeoHaskell. ---- - -NeoHaskell vit dans le monorepo [neohaskell/NeoHaskell](https://github.com/neohaskell/NeoHaskell). Ce site de documentation est volontairement minimal ; les instructions de référence pour l'installation, la compilation et les tests se trouvent dans le `README.md` du dépôt (en anglais). - -## Démarrage rapide - -1. Installez [Determinate Nix](https://determinate.systems/nix). -2. Clonez le dépôt et entrez dans son shell de développement avec `nix develop`. -3. Lancez `./dev` pour lister les commandes de compilation et de test disponibles. - -Pour le flux de travail complet — Postgres, la suite de tests et le processus de contribution — consultez le [README du dépôt](https://github.com/neohaskell/NeoHaskell#readme). diff --git a/website/src/content/docs/fr/index.mdx b/website/src/content/docs/fr/index.mdx deleted file mode 100644 index e9296c1a8..000000000 --- a/website/src/content/docs/fr/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: NeoHaskell -description: Un dialecte de Haskell accessible aux débutants, avec event sourcing garanti par le compilateur. -template: splash -hero: - tagline: Un dialecte de Haskell accessible aux débutants. Écrivez votre logique métier avec les garanties du compilateur ; tout le reste se branche autour. - actions: - - text: Commencer - link: /fr/getting-started/ - icon: right-arrow - - text: Voir sur GitHub - link: https://github.com/neohaskell/NeoHaskell - icon: external - variant: minimal ---- - -## Qu'est-ce que NeoHaskell ? - -NeoHaskell est un dialecte de Haskell conçu pour les débutants et les pragmatiques. Il fait de l'event sourcing une fonctionnalité du langage : événements, entités, commandes, requêtes, services et intégrations sont des primitives du langage, pas des motifs de bibliothèque. Si une transition d'état est incorrecte, le programme ne compile pas. - -## Où aller ensuite - -- **[Commencer](/fr/getting-started/)** — configurez un projet NeoHaskell. -- **[Dossiers de Décisions Architecturales](/fr/adrs/)** — les décisions derrière le langage. -- **[NeoHaskell sur GitHub](https://github.com/neohaskell/NeoHaskell)** — code, tickets et documentation complète. diff --git a/website/src/content/docs/getting-started/index.mdx b/website/src/content/docs/getting-started/index.mdx deleted file mode 100644 index 6e499c285..000000000 --- a/website/src/content/docs/getting-started/index.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Getting Started -description: How to set up a NeoHaskell project. ---- - -NeoHaskell lives in the [neohaskell/NeoHaskell](https://github.com/neohaskell/NeoHaskell) -monorepo. This documentation site is intentionally small; the authoritative setup, -build, and test instructions live in the repository's `README.md`. - -## Quick start - -1. Install [Determinate Nix](https://determinate.systems/nix). -2. Clone the repository and enter its development shell: - - ```bash - git clone https://github.com/neohaskell/NeoHaskell.git - cd NeoHaskell - nix develop - ``` - -3. Run `./dev` to list the available build and test commands. - -For the full workflow — Postgres, the test suite, and the contribution process — see -the [repository README](https://github.com/neohaskell/NeoHaskell#readme). diff --git a/website/src/content/docs/guides/deployment.mdx b/website/src/content/docs/guides/deployment.mdx deleted file mode 100644 index e78261676..000000000 --- a/website/src/content/docs/guides/deployment.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: "Deployment" -description: "Probe configuration and rollout checks for NeoHaskell services with event-sourced read models." -sidebar: - order: 1 ---- - -## Start with a startup probe - -A NeoHaskell process has two distinct startup signals: - -- `GET /health` is **liveness**: the HTTP process has bound its port. -- `GET /ready` is **readiness**: every registered query projection has caught up. - -Configure a startup probe first. Without one, an orchestrator can apply the -steady-state liveness budget while the process is still connecting to its -database and initializing transports. - -```yaml -startupProbe: - httpGet: - path: /health - port: 8080 - periodSeconds: 5 - failureThreshold: 12 - timeoutSeconds: 2 - -livenessProbe: - httpGet: - path: /health - port: 8080 - periodSeconds: 10 - failureThreshold: 3 - timeoutSeconds: 2 - -readinessProbe: - httpGet: - path: /ready - port: 8080 - periodSeconds: 5 - failureThreshold: 3 - timeoutSeconds: 2 -``` - -The startup budget is explicit arithmetic: -`periodSeconds × failureThreshold = 5 × 12 = 60 seconds`. Size that budget for -bounded process/database initialization, not for event-log replay. Historical -query replay runs after live subscription registration and must not delay the -HTTP bind; `/ready` remains `503` until replay and overlapping live events are -fully drained. - -## Rollout contract - -1. Start the new revision without routing traffic to it. -2. Wait for `/health` to prove the process is accepting connections. -3. Keep it out of service while `/ready` is `503`. -4. Route traffic only after `/ready` becomes `200`. -5. Run post-deploy smoke tests against the **new revision identity**, not only - the shared ingress. A shared ingress may still route to the old revision and - report green while the rollout has failed. - -Alert separately on startup-probe failure, a prolonged `/ready` 503, and a -rollout whose active revision did not change. - -## Postgres connections - -NeoHaskell owns one bounded client pool for each configured Postgres subsystem. -The EventStore reuses its pool for reads and writes; it does not construct a new -pool for every replayed event. - -`LISTEN/NOTIFY` requires a direct, session-preserving Postgres connection. Do not -route listener connections through a transaction-mode PgBouncer endpoint. Neon -scale-to-zero and split pooled/direct endpoint support are tracked in -[issue #857](https://github.com/neohaskell/NeoHaskell/issues/857). - -## Shutdown limitation - -SIGTERM cancellation and checkpoint flushing during an active rebuild are still -tracked in [issue #662](https://github.com/neohaskell/NeoHaskell/issues/662). -Do not extend termination grace indefinitely to wait for replay; use the probe -contract above and follow that issue for the completed shutdown behavior. diff --git a/website/src/content/docs/guides/index.mdx b/website/src/content/docs/guides/index.mdx deleted file mode 100644 index 255621719..000000000 --- a/website/src/content/docs/guides/index.mdx +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Guides -description: Practical guidance for building and operating NeoHaskell services. -sidebar: - order: 0 ---- - -import { Card, CardGrid } from '@astrojs/starlight/components'; - - - - Configure startup, liveness, and readiness probes; verify that a rollout - reached the new revision; and preserve direct Postgres listener sessions. - - [Read →](/guides/deployment/) - - diff --git a/website/src/content/docs/hy/adrs/index.mdx b/website/src/content/docs/hy/adrs/index.mdx deleted file mode 100644 index 480770c03..000000000 --- a/website/src/content/docs/hy/adrs/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Կառուցվածքային Որոշումների Ձայնագրություններ -description: NeoHaskell-ի հիմքում ընկած ճարտարապետական որոշումները։ ---- - -Կառուցվածքային Որոշումների Ձայնագրությունները (ADR) փաստագրում են NeoHaskell-ը կառուցելիս կայացված նշանակալի ճարտարապետական որոշումները։ - -ADR-ի գրառումները պահվում են կանոնական **անգլերենով** և չեն թարգմանվում՝ մեկ ճշմարտության աղբյուր ունենալու համար։ Տե՛ս ամբողջ ցանկը՝ յուրաքանչյուր որոշման համար առանձին էջով. - -- **[Կառուցվածքային Որոշումների Ձայնագրություններ (անգլերեն)](/adrs/)** diff --git a/website/src/content/docs/hy/getting-started/index.mdx b/website/src/content/docs/hy/getting-started/index.mdx deleted file mode 100644 index fb41dfd80..000000000 --- a/website/src/content/docs/hy/getting-started/index.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Սկսել -description: NeoHaskell-ի հետ աշխատելու համար անհրաժեշտ քայլերը։ ---- - -NeoHaskell-ը գտնվում է [neohaskell/NeoHaskell](https://github.com/neohaskell/NeoHaskell) մոնոշտեմարանում։ Այս փաստաթղթերի կայքը միտումնավոր փոքր է. տեղադրման, կառուցման և թեստավորման հիմնական հրահանգները պահվում են շտեմարանի `README.md`-ում (անգլերեն)։ - -## Արագ սկիզբ - -1. Տեղադրեք [Determinate Nix](https://determinate.systems/nix)։ -2. Կլոնավորեք շտեմարանը և մուտք գործեք նրա մշակման կեղև՝ `nix develop`։ -3. Գործարկեք `./dev`՝ հասանելի հրամանները տեսնելու համար։ - -Ամբողջական աշխատանքային հոսքի համար տե՛ս [շտեմարանի README-ն](https://github.com/neohaskell/NeoHaskell#readme)։ diff --git a/website/src/content/docs/hy/index.mdx b/website/src/content/docs/hy/index.mdx deleted file mode 100644 index 8ef9d6af6..000000000 --- a/website/src/content/docs/hy/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: NeoHaskell -description: Հասքելի սկսնակների համար հարմար բարբառ՝ կոմպիլյատորով երաշխավորված event sourcing-ով։ -template: splash -hero: - tagline: Հասքելի սկսնակների համար հարմար բարբառ։ Գրեք Ձեր բիզնես-տրամաբանությունը կոմպիլյատորի երաշխիքներով, մնացած ամեն ինչը միանում է դրա շուրջ։ - actions: - - text: Սկսել - link: /hy/getting-started/ - icon: right-arrow - - text: Դիտել GitHub-ում - link: https://github.com/neohaskell/NeoHaskell - icon: external - variant: minimal ---- - -## Ի՞նչ է NeoHaskell-ը - -NeoHaskell-ը Հասքելի բարբառ է՝ նախատեսված սկսնակների և գործնական մարդկանց համար։ Այն event sourcing-ը դարձնում է լեզվի առաջնային հատկություն. իրադարձությունները, էությունները, հրամանները, հարցումները, ծառայությունները և ինտեգրումները լեզվի պարզունակ տարրեր են, ոչ թե գրադարանային ձևանմուշներ։ - -## Ուր գնալ հետո - -- **[Սկսել](/hy/getting-started/)** — կարգավորեք NeoHaskell նախագիծ։ -- **[Կառուցվածքային Որոշումների Ձայնագրություններ](/hy/adrs/)** — լեզվի հիմքում ընկած որոշումները։ -- **[NeoHaskell GitHub-ում](https://github.com/neohaskell/NeoHaskell)** — կոդ, խնդիրներ և ամբողջական փաստաթղթեր։ diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx deleted file mode 100644 index f9970f6ba..000000000 --- a/website/src/content/docs/index.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: NeoHaskell -description: A newcomer-friendly dialect of Haskell with compiler-enforced event sourcing. -template: splash -hero: - tagline: A newcomer-friendly dialect of Haskell. Write your business logic with compiler guarantees; everything else plugs in around it. - actions: - - text: View on GitHub - link: https://github.com/neohaskell/NeoHaskell - icon: external - - text: Architecture Decision Records - link: /adrs/ - icon: open-book - variant: minimal ---- - -## What is NeoHaskell? - -NeoHaskell is a dialect of Haskell designed for beginners and pragmatists. It makes -event sourcing a first-class language feature: events, entities, commands, queries, -services, and integrations are language primitives, not library patterns. If a state -transition is wrong, the program does not compile. - -You write the **core** — the business rules that make your product yours. NeoHaskell -handles the **shell** around them: HTTP endpoints, JSON schemas, CLI parsers, event -persistence, and a complete audit trail. - -## Where to go next - -- **[Getting started](/getting-started/)** — set up a NeoHaskell project. -- **[Deployment guide](/guides/deployment/)** — configure startup, liveness, and readiness probes. -- **[Architecture Decision Records](/adrs/)** — the decisions behind the language, - generated from the repository's decision log. -- **[NeoHaskell on GitHub](https://github.com/neohaskell/NeoHaskell)** — source, issues, - and the full documentation in the repository. diff --git a/website/src/content/docs/ja/adrs/index.mdx b/website/src/content/docs/ja/adrs/index.mdx deleted file mode 100644 index e3ac9b458..000000000 --- a/website/src/content/docs/ja/adrs/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: アーキテクチャ決定記録 -description: NeoHaskell の背後にあるアーキテクチャ上の決定。 ---- - -アーキテクチャ決定記録(ADR)は、NeoHaskell の構築中に行われた重要なアーキテクチャ上の決定を記録します。 - -ADR の記録は、唯一の信頼できる情報源とするため、正規の **英語** で管理され、翻訳されません。決定ごとに 1 ページの完全な一覧をご覧ください。 - -- **[アーキテクチャ決定記録(英語)](/adrs/)** diff --git a/website/src/content/docs/ja/getting-started/index.mdx b/website/src/content/docs/ja/getting-started/index.mdx deleted file mode 100644 index cf6e66ac4..000000000 --- a/website/src/content/docs/ja/getting-started/index.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: はじめに -description: NeoHaskell プロジェクトのセットアップ方法。 ---- - -NeoHaskell は [neohaskell/NeoHaskell](https://github.com/neohaskell/NeoHaskell) モノレポにあります。このドキュメントサイトは意図的に小さく作られており、信頼できるセットアップ・ビルド・テストの手順はリポジトリの `README.md`(英語)にあります。 - -## クイックスタート - -1. [Determinate Nix](https://determinate.systems/nix) をインストールします。 -2. リポジトリをクローンし、`nix develop` で開発シェルに入ります。 -3. `./dev` を実行して、利用可能なビルド・テストコマンドを確認します。 - -完全なワークフロー(Postgres、テストスイート、コントリビューションの流れ)については、[リポジトリの README](https://github.com/neohaskell/NeoHaskell#readme) を参照してください。 diff --git a/website/src/content/docs/ja/index.mdx b/website/src/content/docs/ja/index.mdx deleted file mode 100644 index 26c378c36..000000000 --- a/website/src/content/docs/ja/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: NeoHaskell -description: 初心者にやさしい Haskell の方言。コンパイラが保証するイベントソーシングを備えています。 -template: splash -hero: - tagline: 初心者にやさしい Haskell の方言。ビジネスロジックをコンパイラの保証付きで書き、それ以外はすべてその周りに組み込みます。 - actions: - - text: はじめる - link: /ja/getting-started/ - icon: right-arrow - - text: GitHub で見る - link: https://github.com/neohaskell/NeoHaskell - icon: external - variant: minimal ---- - -## NeoHaskell とは - -NeoHaskell は、初心者と実用主義者のために設計された Haskell の方言です。イベントソーシングを言語の第一級の機能にします。イベント、エンティティ、コマンド、クエリ、サービス、インテグレーションはライブラリのパターンではなく、言語のプリミティブです。状態遷移が誤っていれば、プログラムはコンパイルされません。 - -## 次に読む - -- **[はじめに](/ja/getting-started/)** — NeoHaskell プロジェクトをセットアップします。 -- **[アーキテクチャ決定記録](/ja/adrs/)** — 言語の背後にある決定。 -- **[GitHub の NeoHaskell](https://github.com/neohaskell/NeoHaskell)** — ソース、Issue、完全なドキュメント。 diff --git a/website/src/content/docs/ru/adrs/index.mdx b/website/src/content/docs/ru/adrs/index.mdx deleted file mode 100644 index 9511d89df..000000000 --- a/website/src/content/docs/ru/adrs/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Записи об архитектурных решениях -description: Архитектурные решения, стоящие за NeoHaskell. ---- - -Записи об архитектурных решениях (ADR) документируют значимые архитектурные решения, принятые при создании NeoHaskell. - -Записи ADR ведутся канонически **на английском языке** и не переводятся, чтобы существовал единый источник истины. Полный список — по одной странице на каждое решение: - -- **[Записи об архитектурных решениях (на английском)](/adrs/)** diff --git a/website/src/content/docs/ru/getting-started/index.mdx b/website/src/content/docs/ru/getting-started/index.mdx deleted file mode 100644 index a0b128936..000000000 --- a/website/src/content/docs/ru/getting-started/index.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Начало работы -description: Как настроить проект NeoHaskell. ---- - -NeoHaskell находится в монорепозитории [neohaskell/NeoHaskell](https://github.com/neohaskell/NeoHaskell). Этот сайт документации намеренно небольшой; авторитетные инструкции по установке, сборке и тестированию находятся в файле `README.md` репозитория (на английском языке). - -## Быстрый старт - -1. Установите [Determinate Nix](https://determinate.systems/nix). -2. Клонируйте репозиторий и войдите в его среду разработки с помощью `nix develop`. -3. Запустите `./dev`, чтобы увидеть доступные команды сборки и тестирования. - -Полный рабочий процесс — Postgres, набор тестов и процесс участия — описан в [README репозитория](https://github.com/neohaskell/NeoHaskell#readme). diff --git a/website/src/content/docs/ru/index.mdx b/website/src/content/docs/ru/index.mdx deleted file mode 100644 index db2c6ffbb..000000000 --- a/website/src/content/docs/ru/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: NeoHaskell -description: Дружелюбный к новичкам диалект Haskell с событийным источником, гарантированным компилятором. -template: splash -hero: - tagline: Дружелюбный к новичкам диалект Haskell. Пишите бизнес-логику с гарантиями компилятора; всё остальное подключается вокруг неё. - actions: - - text: Начать - link: /ru/getting-started/ - icon: right-arrow - - text: Смотреть на GitHub - link: https://github.com/neohaskell/NeoHaskell - icon: external - variant: minimal ---- - -## Что такое NeoHaskell? - -NeoHaskell — это диалект Haskell, созданный для новичков и прагматиков. Он делает событийный источник (event sourcing) полноценной возможностью языка: события, сущности, команды, запросы, сервисы и интеграции являются примитивами языка, а не шаблонами библиотек. Если переход состояния неверен, программа не компилируется. - -## Куда двигаться дальше - -- **[Начало работы](/ru/getting-started/)** — настройте проект NeoHaskell. -- **[Записи об архитектурных решениях](/ru/adrs/)** — решения, стоящие за языком. -- **[NeoHaskell на GitHub](https://github.com/neohaskell/NeoHaskell)** — исходный код, задачи и полная документация. diff --git a/website/tsconfig.json b/website/tsconfig.json deleted file mode 100644 index 8bf91d3bb..000000000 --- a/website/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"] -}