From 46869b55c0e509d8e4785e7ceefd2e6a973a453a Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 9 Aug 2026 11:43:17 +0200 Subject: [PATCH 1/2] docs: finish the mint-helper conventions in the tutorial and callouts Convert the tutorial's four remaining casts to mint helpers, matching the castless-producer convention PR #50 set elsewhere, and rewrite the asymmetry paragraph it made stale. Harmonize the how-to (and one reference) import callouts to state domain vocabulary is the reader's own, the way test-domain-logic.md's callout already does. --- docs/how-to/evolve-an-entity.md | 3 +++ docs/how-to/http-contract.md | 3 +++ docs/how-to/model-an-aggregate.md | 3 +++ docs/how-to/persist-and-rehydrate.md | 3 +++ docs/reference/errors.md | 3 +++ docs/tutorial/getting-started.md | 27 ++++++++++++++++----------- 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/docs/how-to/evolve-an-entity.md b/docs/how-to/evolve-an-entity.md index 3700da5..9b2371b 100644 --- a/docs/how-to/evolve-an-entity.md +++ b/docs/how-to/evolve-an-entity.md @@ -16,6 +16,9 @@ the same: do the old rows still validate? > import { z } from "zod"; > import { Entity } from "@btravstack/entity"; > ``` +> +> Domain vocabulary — entities, brands, factories — is whatever your own +> domain declares. ## Add an optional field diff --git a/docs/how-to/http-contract.md b/docs/how-to/http-contract.md index 74db232..e5fa1f8 100644 --- a/docs/how-to/http-contract.md +++ b/docs/how-to/http-contract.md @@ -16,6 +16,9 @@ from the model. > import { P } from "unthrown"; > import { Entity } from "@btravstack/entity"; > ``` +> +> Domain vocabulary — entities, brands, factories — is whatever your own +> domain declares. ## Use the four `ZodObject` members directly diff --git a/docs/how-to/model-an-aggregate.md b/docs/how-to/model-an-aggregate.md index 176051f..404a363 100644 --- a/docs/how-to/model-an-aggregate.md +++ b/docs/how-to/model-an-aggregate.md @@ -16,6 +16,9 @@ entity rather than a bare schema. > import { match, P } from "unthrown"; > import { Entity } from "@btravstack/entity"; > ``` +> +> Domain vocabulary — entities, brands, factories — is whatever your own +> domain declares. ## Use the class as a field diff --git a/docs/how-to/persist-and-rehydrate.md b/docs/how-to/persist-and-rehydrate.md index f2cf034..11b9760 100644 --- a/docs/how-to/persist-and-rehydrate.md +++ b/docs/how-to/persist-and-rehydrate.md @@ -15,6 +15,9 @@ without the storage layer knowing about entity internals. > import { P } from "unthrown"; > import { Entity } from "@btravstack/entity"; > ``` +> +> Domain vocabulary — entities, brands, factories — is whatever your own +> domain declares. ## Write with `toJSON()` diff --git a/docs/reference/errors.md b/docs/reference/errors.md index c3373a8..3afe351 100644 --- a/docs/reference/errors.md +++ b/docs/reference/errors.md @@ -14,6 +14,9 @@ modelled as a value; a bug in domain code goes down the separate defect channel. > import { P } from "unthrown"; > import { Entity } from "@btravstack/entity"; > ``` +> +> Domain vocabulary — entities, brands, factories — is whatever your own +> domain declares. ## `Entity.InvalidEntity` diff --git a/docs/tutorial/getting-started.md b/docs/tutorial/getting-started.md index bb8d371..abc5823 100644 --- a/docs/tutorial/getting-started.md +++ b/docs/tutorial/getting-started.md @@ -38,12 +38,19 @@ const OrgId = z.uuid().brand("OrgId"); const Slug = z.string().min(1).brand("Slug"); const DisplayName = z.string().min(1).brand("DisplayName"); const Instant = z.iso.datetime().brand("Instant"); + +const slug = (value: string) => Slug.parse(value); +const name = (value: string) => DisplayName.parse(value); ``` The reason is the one every domain modeller already knows: with plain strings, `findOrg(slug, name)` type-checks with the arguments swapped. Branded, it does not. ([The full argument](/explanation/branded-fields).) +`slug` and `name` are **mint helpers** — a named `parse` declared beside the +vocabulary it mints, for the two fields a caller supplies by hand later in this +page. + ## 2. Declare the entity ```ts @@ -112,8 +119,8 @@ Now a create use case supplies only the caller's fields: ```ts const created = createOrganization({ - slug: "acme" as z.infer, - name: "Acme" as z.infer, + slug: slug("acme"), + name: name("Acme"), }); const org = created.getOrThrow(); @@ -126,11 +133,11 @@ startup still yields a fresh id per entity. And a test can bind fixed generators instead of stubbing globals. ([Why no I/O](/explanation/no-io).) Note the asymmetry between the two blocks. A generator hands its value to the -entity, which validates it, so `crypto.randomUUID()` needs nothing; a caller -field is a branded value you are supplying, so it has to be minted. The cast -above is the shortest spelling for a tutorial — real code declares a helper per -piece of vocabulary and writes `slug("acme")` -([Branded fields](/explanation/branded-fields#everywhere-else-parse-through-a-mint-helper)). +entity, which validates it, so `crypto.randomUUID()` needs no brand of its own; +a caller field is a branded value you are supplying, so it has to be minted — +which is exactly what `slug` and `name`, declared in step 1, are for. Real code +follows the same pattern for every piece of vocabulary written by hand. +([Branded fields](/explanation/branded-fields#everywhere-else-parse-through-a-mint-helper).) ::: tip `getOrThrow()` is for a tutorial It is the shortest way to get at a value while you are exploring. Real code @@ -143,7 +150,7 @@ The entity is immutable in both halves — the binding is non-writable and the value is deep-frozen: ```ts -org.name = "Other" as z.infer; // ✗ compile error — read-only property +org.name = name("Other"); // ✗ compile error — read-only property ``` And you cannot sidestep the entry points: @@ -251,9 +258,7 @@ re-derives](/explanation/computed-fields).) `update` returns a **new** entity — the original is untouched: ```ts -const renamed = org - .update({ name: "Acme Corp" as z.infer }) - .getOrThrow(); +const renamed = org.update({ name: name("Acme Corp") }).getOrThrow(); renamed.name; // "Acme Corp" renamed.shout; // "ACME CORP" — re-derived, never stale From e97e9e744b6de6ba4a25539338c4ff031970240f Mon Sep 17 00:00:00 2001 From: Benoit TRAVERS Date: Sun, 9 Aug 2026 15:00:11 +0200 Subject: [PATCH 2/2] docs: rename the argument-swap demo's variables off the mint helpers --- docs/tutorial/getting-started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/getting-started.md b/docs/tutorial/getting-started.md index abc5823..cf4d9ea 100644 --- a/docs/tutorial/getting-started.md +++ b/docs/tutorial/getting-started.md @@ -44,8 +44,8 @@ const name = (value: string) => DisplayName.parse(value); ``` The reason is the one every domain modeller already knows: with plain strings, -`findOrg(slug, name)` type-checks with the arguments swapped. Branded, it does -not. ([The full argument](/explanation/branded-fields).) +`findOrg(orgSlug, orgName)` type-checks with the arguments swapped. Branded, it +does not. ([The full argument](/explanation/branded-fields).) `slug` and `name` are **mint helpers** — a named `parse` declared beside the vocabulary it mints, for the two fields a caller supplies by hand later in this