From 7f680b342a31d32c9399e5d68f0a4ed07a54153e Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:14:02 +0200 Subject: [PATCH 1/9] chore(plan): story for the ESS tutorial and onboarding - story:ess-tutorial-and-onboarding (active): a public tutorial from nothing to a conforming ESS specification with real output, held to the newest CLIs by agentplugins-check and a trial; onboarding pages that name the five shipped plugins and install them through b10x; release 0.17.0. Context cites the survey of origin/main d6c2ae1. - Scope recorded: website/docs, the checker, the Tools workflow, trials/ess-tutorial, verified.json, plugins/ess, README.md, SETUP.md, b10x.docs.yaml. aep plan artifact validate: valid. --- .../story/ess-tutorial-and-onboarding.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .engineering/planning/story/ess-tutorial-and-onboarding.md diff --git a/.engineering/planning/story/ess-tutorial-and-onboarding.md b/.engineering/planning/story/ess-tutorial-and-onboarding.md new file mode 100644 index 0000000..6bde6aa --- /dev/null +++ b/.engineering/planning/story/ess-tutorial-and-onboarding.md @@ -0,0 +1,65 @@ +--- +format: aep.planning-md/3 +id: story:ess-tutorial-and-onboarding +kind: story +status: active +title: A public tutorial takes a developer from nothing to a conforming ESS specification; onboarding pages match b10x +summary: New tutorials/first-ess-specification page recorded with real output, held to the newest CLIs by the checker and a trial; onboarding pages corrected; released as 0.17.0. +relations: +- informed_by: epic:ahead-of-the-alternative +scope: +- confidence: cited + path: .github/workflows/tools.yml +- confidence: cited + path: README.md +- confidence: cited + path: SETUP.md +- confidence: cited + path: b10x.docs.yaml +- confidence: cited + path: crates/agentplugins-check +- confidence: cited + path: plugins/ess +- confidence: cited + path: trials/ess-tutorial +- confidence: cited + path: verified.json +- confidence: cited + path: website/docs +revision: 12 +transitions: +- {from: "draft", to: "proposed", at: "2026-09-28T09:14:02Z", actor: "human:timo", revision: 11} +- {from: "proposed", to: "active", at: "2026-09-28T09:14:02Z", actor: "human:timo", revision: 12} +--- +# Story: a public ESS tutorial, and onboarding pages that match b10x + +## Outcome +A developer who reads the agentplugins docs can go from nothing to a validated ESS specification +and a passing Go conformance run, with an agent doing the writing, and every step shows the output +it really printed. The onboarding pages name the five plugins that ship and install them the way +`b10x` does. + +## Context +Surveyed on `origin/main` d6c2ae1 (2026-09-28): +- No page in agentplugins, ess or aep walks from zero to a conformance run with real output; ESS + `getting-started.md` stops at the built-in `billing` target. +- `website/docs/install.md:39-77` hand-installs AEP 0.55.0 and expects `protocol 0.55.0`; + `golden-path.md:28` installs the `ess` plugin "from the ESS repository". +- `trust-and-scope.md:12-16` and `choose-a-plugin.md:12,32,50` name retired plugins ("AEP Plan", + "AEP Drive", "ESS Specify", "Beyond10x"); the retired-name check matches hyphenated ids only + (`crates/agentplugins-check/src/main.rs:332-453`). +- connectors is missing from `intro.md`, the docusaurus footer and `src/pages/index.tsx`. +- ess 0.38.0 is out and `verified.json` pins 0.37.0, so the Tools check fails on `main`. + +## Acceptance +- `website/docs/tutorials/first-ess-specification.md` exists, recorded with ess 0.38.0; its + committed specification validates, and `agentplugins-check tools` validates it and checks every + command the page spells against the newest releases. +- `trials/ess-tutorial`: a fresh isolated agent given only the page reaches `validate: valid` and 0 + failed scenarios under `go test`. +- `agentplugins-check` refuses the retired display names in `website/` and `README.md`. +- `task check`, `task site-build` and `tools` pass; release 0.17.0 publishes its assets. + +## Out of Scope +The AEP tutorial (later); the `website` repository's `experiences.json`; the ess repository's +own guides. From 18838a29712b30ff540cb54e40bd0c348acf571a Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:24:58 +0200 Subject: [PATCH 2/9] docs(ess): verify the skills against ess 0.38.0 - verified.json pins ess 0.38.0 (was 0.37.0). agentplugins-check tools: 35 spelled ess commands exist in 0.38.0; the syntax example validates (14 scenarios, 0 refusals). - ESS trial round on 0.38.0, all within the baseline: ess-new 16 tool calls (38), validate valid; ess-retrofit 26 (36), 20 scenarios, 0 refusals; ess-pipeline 10 (11), 14 scenarios, 4/4 outputs; ess-full-package 66 (53), 31 scenarios, 0 refusals, 9/9 outputs, go test 0 failed. - Fixes for the text the agents quoted as confusing: - specifying: a request that names the domain and asks for a result ("make sure it validates") counts as a finished-specification request, so it is not interviewed; the closing check allows a named refusal where synthesis cannot arrange the model (the per-holder limit). - later-formats: the per-holder example's Join sets packets_out: 0 and the at-limit refusal is expected; do not give Join a starting count to reach it. - testing-conformance: `--out ` writes /essconform, the import path the generated README shows; ESS_REPORT_FORMAT=2 is required from ess-conformance/8 on; a refused command sets Outcome to the refusing outcome's name (the generated comment says empty). - The three ess-side defects (the Outcome comment, the IR version in synthesize --help, the report variable read as optional) are filed as beyond10x/ess#186. --- plugins/ess/skills/specifying/SKILL.md | 11 +++++++---- .../specifying/references/later-formats.md | 7 ++++--- plugins/ess/skills/testing-conformance/SKILL.md | 17 ++++++++++++----- verified.json | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/plugins/ess/skills/specifying/SKILL.md b/plugins/ess/skills/specifying/SKILL.md index 04d9bda..32332a2 100644 --- a/plugins/ess/skills/specifying/SKILL.md +++ b/plugins/ess/skills/specifying/SKILL.md @@ -17,8 +17,9 @@ job: the domain is drafted first, so the noun has a typed home before stories ar When the request does not say what the noun is, what it relates to and how it changes state, and no source in the repository answers that, interview first: ask the open decisions in numbered rounds, each with the answer you would take, and draft once none is open. When the request asks for a -finished specification, or no operator is there, that takes precedence: do not ask; take each -recommended answer and list the decisions in your report. +finished specification (it names the domain and asks for a result, such as "make sure it +validates"), or no operator is there, that takes precedence: do not ask; take each recommended +answer and list the decisions in your report. [references/interview.md](references/interview.md) is the procedure for both. Where an OpenAPI document already describes it, do not hand-write the domain. Draft it from the @@ -278,8 +279,10 @@ strict v1 reader such as `infra-ir/1`, add an old-reader compatibility test; unk currently refused. Finish by running the repository's full gate, where it has one, and report the exact command and -exit status. Where it has none, `ess specify validate` plus `ess verify conform synthesize` with 0 -refusals is the check; report both outputs. +exit status. Where it has none, `ess specify validate` plus `ess verify conform synthesize` is the +check: 0 refusals, or each refusal named in your report with its code where the model needs a +construct synthesis cannot arrange (the per-holder limit in +[references/later-formats.md](references/later-formats.md) is one). Report both outputs. ## Agents diff --git a/plugins/ess/skills/specifying/references/later-formats.md b/plugins/ess/skills/specifying/references/later-formats.md index 87b5827..780e9fe 100644 --- a/plugins/ess/skills/specifying/references/later-formats.md +++ b/plugins/ess/skills/specifying/references/later-formats.md @@ -192,9 +192,10 @@ entities: seeds.lending.PacketReturned: {member_id: input.member_id} ``` -The creating command (`Join`) sets `packets_out` from its input under `when: {packets_out: {gte: 0, -lte: 5}}`. Synthesis arranges a member only through that `sets:` and does not repeat `BorrowPacket`, -so with `packets_out: 0` fixed at creation the `at-limit` scenario is refused (`ESS-SYNTH-003`). +The creating command (`Join`) sets `packets_out: 0`. Synthesis arranges a member only through that +`sets:` and does not repeat `BorrowPacket`, so the `at-limit` scenario is refused (`ESS-SYNTH-003`). +That refusal is the expected result: name it in your report. Do not give `Join` a starting count +only so synthesis can reach the limit; no member joins holding packets. One outcome changes one record: a `Packet` moving to `OnLoan` is a second command the caller sends, and the specification cannot require both to happen together. diff --git a/plugins/ess/skills/testing-conformance/SKILL.md b/plugins/ess/skills/testing-conformance/SKILL.md index e0e36ee..f7126c9 100644 --- a/plugins/ess/skills/testing-conformance/SKILL.md +++ b/plugins/ess/skills/testing-conformance/SKILL.md @@ -274,13 +274,20 @@ ess verify conform synthesize --path --target go --out ess verify conform synthesize --path --target typescript --out ``` -Generate the Go package **inside the implementation's own module** (`--out /conformance`) so -its test can import it; a copy outside the module has no `go.mod` to import from. The package's +Generate the Go package **inside the implementation's own module**: `--out ` writes +`/essconform`, the import path its `README.md` shows (`/essconform`). A copy +outside the module has no `go.mod` to import from. The package's `README.md` lists the methods and the wiring test. A method you cannot answer returns -`ErrUnsupported` (a skip), never a made-up result. Keep a suite for a domain nobody has implemented; -run it once something answers it. +`ErrUnsupported` (a skip), never a made-up result. A refused command still sets `Outcome` to the +refusing outcome's name beside `Error`: the runner compares it for refusals too, whatever the +generated comment on `CommandResult` says (beyond10x/ess#186). +Keep a suite for a domain nobody has implemented; run it once something answers it. -Three version facts decide whether these packages run a suite at all: +Four version facts decide whether these packages run a suite at all: + +- Suites from `ess-conformance/8` on run only with `ESS_REPORT_FORMAT=2` set: without it `go test` + stops before the first scenario (`suite/8 through /21 require explicit ESS_REPORT_FORMAT=2 before + execution`). Set it in the command, `ESS_REPORT_FORMAT=2 go test ./...`, or in the CI job. - A command with `fixture_inputs:` needs the target to supply the values: implement `FixtureValues` (Go) or `fixtureValues` (TypeScript). Without it every scenario using a fixture is diff --git a/verified.json b/verified.json index 7b00b05..34379aa 100644 --- a/verified.json +++ b/verified.json @@ -1,5 +1,5 @@ { "aep": "0.63.1", - "ess": "0.37.0", + "ess": "0.38.0", "worktree": "0.8.2" } From 1393d10427d756ef70f804e93ecf8f8de651466b Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:38:18 +0200 Subject: [PATCH 3/9] docs: tutorial, your first ESS specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New website/docs/tutorials/first-ess-specification.md, typed `tutorial` on the public website by its path, listed under a Tutorials sidebar category. An agent with ess@b10x interviews, writes a lending-library specification, and a Go implementation is held to the suite it synthesizes: install, interview, the files, validate and a deliberate refusal, docs/OpenAPI/graph, synthesis and go test, a planted bug turning the suite red, the toolchain pin and a Taskfile check. - Every output block is a real run on 2026-09-28 with ess 0.38.0 in an isolated sandbox (claude -p, resumed across the interview); paths are shortened to `~` and the marketplace line to beyond10x/agentplugins, which the page says. Recorded outputs: validate `library v1 — 3 file(s), valid`; synthesis `17 scenario(s) (0 authored), 0 refusal(s)`; go test 17 passed; the planted bug fails exactly Book/state/OnLoan/refuses/BorrowBook. - The committed spec/ and impl/ under website/docs/tutorials/first-ess-specification/ are the recorded files; the page quotes each whole. - Deviations from the plan: the domain has no per-member cap. The first recording asked for one: each borrow split into three commands and synthesis refused 3 scenarios (ESS-SYNTH-003), so the cap is a closing section instead. `ess verify conform mutate` runs only against ess's built-in targets, so step 7 points to ess:hardening for it. - A fresh agent given only the page (trial ess-tutorial) reached 17/17; the four places it found the page and its run disagreeing are fixed. --- .../docs/tutorials/first-ess-specification.md | 1200 +++++++++++++++++ .../impl/conformance_test.go | 191 +++ .../first-ess-specification/impl/go.mod | 3 + .../first-ess-specification/impl/library.go | 156 +++ .../spec/components.yaml | 21 + .../spec/domains/lending.yaml | 314 +++++ .../spec/ess-inputs.yaml | 7 + .../first-ess-specification/spec/system.yaml | 6 + website/sidebars.ts | 5 + 9 files changed, 1903 insertions(+) create mode 100644 website/docs/tutorials/first-ess-specification.md create mode 100644 website/docs/tutorials/first-ess-specification/impl/conformance_test.go create mode 100644 website/docs/tutorials/first-ess-specification/impl/go.mod create mode 100644 website/docs/tutorials/first-ess-specification/impl/library.go create mode 100644 website/docs/tutorials/first-ess-specification/spec/components.yaml create mode 100644 website/docs/tutorials/first-ess-specification/spec/domains/lending.yaml create mode 100644 website/docs/tutorials/first-ess-specification/spec/ess-inputs.yaml create mode 100644 website/docs/tutorials/first-ess-specification/spec/system.yaml diff --git a/website/docs/tutorials/first-ess-specification.md b/website/docs/tutorials/first-ess-specification.md new file mode 100644 index 0000000..fde5a2f --- /dev/null +++ b/website/docs/tutorials/first-ess-specification.md @@ -0,0 +1,1200 @@ +--- +title: Your first ESS specification +sidebar_label: Your first ESS specification +description: With an agent, write an Executable System Specification for a small lending library, validate it, and hold a Go implementation to the conformance suite it generates. +--- + +# Your first ESS specification + +An **Executable System Specification** (ESS) says what a system does: its records, the commands that +change them, the answers a command may refuse with, the events it publishes and the views it serves. +The `ess` command-line tool checks that the specification is consistent, generates documentation and +an OpenAPI description from it, and writes a **conformance suite**: tests that any implementation, +in any language, must pass. + +In this tutorial an agent writes the specification for you, you check it with `ess`, and a small Go +implementation is held to the suite. It takes about 30 minutes. + +**What you end with:** + +- a specification of a lending library in `spec/`, which `ess specify validate` accepts; +- documentation and an OpenAPI description generated from it; +- a Go implementation in `impl/` that passes all 17 scenarios the specification obliges; +- a check that fails when the implementation breaks a rule, which you will see happen. + +Every output block on this page is what the command printed when this page was recorded, on +2026-09-28, with `ess` 0.38.0, the `ess` plugin from this marketplace and Go 1.27. Paths are +shortened to `~` for the home directory. The files are in the +[repository](https://github.com/beyond10x/agentplugins/tree/main/website/docs/tutorials/first-ess-specification); +copy them if you want to follow without an agent. + +## What you need + +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or Codex. +- [Go](https://go.dev/dl/) 1.23 or newer, for step 6. +- An empty directory. This page uses `library/`. + +## 1. Install `ess` and its plugin + +Give your agent one sentence: + +```text +Set up Beyond10x: follow https://github.com/beyond10x/agentplugins/releases/latest/download/SETUP.md +``` + +It installs the `b10x` setup tool and asks what you want to do; answer *write specifications*. It +lists every change before it makes one. If you prefer to run it +yourself, with `b10x` already on your `PATH`: + +```console +$ b10x init ess --host claude --out plan.json +``` + +```text +b10x setup plan (hosts: claude) + +Products: + [ ] aep Plan governed work in an artifact store and deliver it in reviewed waves. + [x] ess Write, retrofit, validate and conformance-test Executable System Specifications. + [ ] worktree Create, lease, finish and safely clean isolated Git worktrees. + [ ] connectors Set up providers and invoke governed integrations through the connectors CLI. (optional) + +Findings: + change claude b10x marketplace `b10x` is not registered; add beyond10x/agentplugins + change claude b10x@b10x not installed; install 0.16.0 + change claude ess@b10x not installed; install 0.16.0 + change - ess not on PATH; install the newest release 0.38.0 + note - install method prebuilt archives (cargo is on PATH); choose with --method cargo or --method prebuilt + +Actions (4): + 1. register marketplace `b10x`: claude plugin marketplace add beyond10x/agentplugins + 2. install `b10x@b10x`: claude plugin install b10x@b10x --scope user + 3. install `ess@b10x`: claude plugin install ess@b10x --scope user + 4. install `ess`: install ess 0.38.0 into ~/.local/bin (prebuilt archive) + +Next: + /ess:init starts ess here (this session, before a restart: `b10x skill ess:init`) + /b10x:upgrade checks everything later; /b10x:init adds a product + +Apply after the user confirms this list: b10x setup apply --plan plan.json --yes +``` + +`b10x` shows the plan first and changes nothing. (This recording registered a local copy of the +marketplace; the lines above show the name yours registers, `beyond10x/agentplugins`.) Apply it +after you have read the list: + +```console +$ b10x setup apply --plan plan.json --yes +``` + +```text +snapshot: ~/.local/state/b10x/setup/1790586877 + ok 1/4 register marketplace `b10x`: claude plugin marketplace add beyond10x/agentplugins + ok 2/4 install `b10x@b10x`: claude plugin install b10x@b10x --scope user + ok 3/4 install `ess@b10x`: claude plugin install ess@b10x --scope user + ok 4/4 install `ess`: install ess 0.38.0 into ~/.local/bin (prebuilt archive) + +After: +b10x setup plan (hosts: claude) + +Products: + [ ] aep Plan governed work in an artifact store and deliver it in reviewed waves. + [x] ess Write, retrofit, validate and conformance-test Executable System Specifications. (present) + [ ] worktree Create, lease, finish and safely clean isolated Git worktrees. + [ ] connectors Set up providers and invoke governed integrations through the connectors CLI. (optional) + +Findings: + ok claude b10x@b10x current (0.16.0) + ok claude ess@b10x current (0.16.0) + ok - ess 0.38.0 at ~/.local/bin/ess is the newest release + +Nothing to change. + +Also runs (refreshes the marketplace snapshot; changes no version): + - refresh: claude plugin marketplace update b10x + +Next: + /ess:init starts ess here (this session, before a restart: `b10x skill ess:init`) + /b10x:upgrade checks everything later; /b10x:init adds a product + +Converged. New plugins load in a new session: restart Claude Code (or /reload-plugins) or start a new Codex thread. In this session, `b10x skill ` lists a plugin's skills and `b10x skill :` prints one. +``` + +Restart Claude Code so it loads the new plugin. The `ess` plugin gives the agent the +`ess:specifying` skill, which is what writes the specification in the next step. + +## 2. Ask for the specification + +Describe the system in plain words, and ask the agent to put its questions first: + +```text +I want an ESS specification for a small lending library. The library adds books and registers +members. A member borrows a book and later returns it. A book that is on loan can't be lent again, +and a book can only be withdrawn from the collection while it is on the shelf. Put it in spec/. +Before you write anything, ask me the decisions you need from me, then stop and wait for my answers. +``` + +`ess:specifying` does not guess what your words mean. It asks the open decisions in numbered rounds, +each with the answer it would take, and it looks up anything it can find for itself. This is the +first round it asked, unedited: + +
+The agent's questions (10) + +The directory is empty and `ess` 0.38.0 is installed, so there is nothing existing to build on. Here's the first round of decisions. Each one has the answer I'd pick. Reply with the numbers you want to change, or "all recommended". + +**Q1: Is a "book" a physical copy or a title?** +A copy means each physical item is tracked on its own. A title means one record per title, with its copies underneath. +*Recommended: a physical copy.* The request only talks about one book being on the shelf or on loan. Titles and copies would add an entity the request doesn't mention. + +**Q2: Is a loan its own record, or just a state of the book?** +- **Own record:** a `Loan` entity that points to the book and the member, and goes from `Active` to `Returned`. This keeps a history of who borrowed what. +- **State only:** the book's state says `OnLoan` and stores who has it. There's no history. + +*Recommended: its own record.* A borrow has to say who borrowed, and a loan record is the natural place to keep that. + +**Q3: What states does a book go through?** +*Recommended:* it starts `OnShelf` when added. Borrowing moves it to `OnLoan`, and returning moves it back to `OnShelf`. Withdrawing moves it from `OnShelf` to `Withdrawn`. `Withdrawn` is final and the book is kept, not deleted. Borrowing a book that's on loan, or withdrawing one that's on loan, is refused with a named error. + +**Q4: What does a book record?** +*Recommended:* `title` and `author` (both text), with a generated ID. Should there be an ISBN too? + +**Q5: What does a member record, and can a member leave?** +*Recommended:* only `name`, with a generated ID. Members are registered and never cancelled, because the request doesn't mention leaving. + +**Q6: Is there a limit on how many books a member can borrow at once?** +*Recommended: no limit.* + +**Q7: Are there due dates or overdue loans?** +*Recommended: no.* A loan is just active until the book comes back. + +**Q8: What does a return need?** +*Recommended: only the book.* Any active loan on that book is closed, and the returning member isn't checked. The other option is to require the loan ID or the member, and refuse it if they don't match. + +**Q9: What can be looked up?** +*Recommended:* +- the catalogue: every book with its state +- the list of members +- the active loans + +These lists are also what the conformance checks use to see the book, member and loan rules. + +**Q10: Should the spec say how the library is reached, e.g. over HTTP?** +This is what lets me generate an OpenAPI description from the spec. +*Recommended: yes.* One library service, reached over HTTP. + +I'll stop here until you answer. If any answer leads to a follow-up question (for example, an ISBN in Q4 raises whether it has to be unique), I'll ask that in a short second round before I write anything to `spec/`. + +
+ +Answer the numbers you want to change. This page answered: + +```text +All recommended, except Q2: state only. The book records which member has it; no separate loan +record. No ISBN. Go ahead: write the specification in spec/, validate it, and tell me what you wrote. +``` + +Q2 matters most. With a separate loan record, a borrow changes two records, and an ESS command +changes one; see [When a rule spans two records](#when-a-rule-spans-two-records) at the end. + +The agent then wrote the specification, validated it and reported one decision back: the +specification cannot check that the member in `BorrowBook` is registered, because a command can only +be guarded by the record it acts on. This page answered *leave it to the implementation*, and the +agent recorded that as a comment on `BorrowBook`. + +## 3. Read what it wrote + +Four files; `validate` counts the three that make up the specification, and `ess-inputs.yaml` lists +them. `system.yaml` names the system and its one domain: + +```yaml title="spec/system.yaml" +format: ess/15 +system: library +version: v1 + +domains: + - library.lending +``` + +`components.yaml` says which service accepts the commands and publishes the events. It is what lets +`ess` generate an OpenAPI description: + +```yaml title="spec/components.yaml" +components: + - component: library-service + summary: Holds the collection and the members, and lends books to members. + owns: + domains: + - library.lending + accepts: + commands: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + publishes: + events: + - library.lending.BookAdded + - library.lending.MemberRegistered + - library.lending.BookBorrowed + - library.lending.BookReturned + - library.lending.BookWithdrawn + reached_by: network +``` + +`ess-inputs.yaml` lists the files that make up the specification, so nothing generated is ever read +back in as input. Its `requires:` line pins the `ess` release; [step 8](#8-keep-it-true) explains it: + +```yaml title="spec/ess-inputs.yaml" +format: ess-inputs/2 +requires: ess 0.38.0 +specification: + - system.yaml + - components.yaml + - domains/lending.yaml +scenarios: [] +``` + +`domains/lending.yaml` is the domain itself. Read it in this order: + +- **`entities`**: `Book` and `Member`. A book's `lifecycle` lists its states (`OnShelf`, `OnLoan`, + `Withdrawn`) and the three `transitions` between them. `borrower_id` is a `references` relation + to `Member`: the book points at a member and does not own one. +- **`commands`**: each one has `outcomes`. `borrowed` moves the book through `lend`; `wrong-state` + refuses with `BookStateConflict` when the book is not on the shelf. That refusal is the rule *a + book on loan can't be lent again*, written once. +- **`events`** and **`views`**: what the service publishes, and what a caller can read back. + +
+spec/domains/lending.yaml (the whole file) + +```yaml title="spec/domains/lending.yaml" +domain: library.lending + +summary: A small lending library. Books are added to the collection, members are registered, and a + member borrows a book and later returns it. + +naming: + wire: lending + display: Lending + +types: + - name: library.lending.BookId + kind: newtype + of: Uuid + + - name: library.lending.MemberId + kind: newtype + of: Uuid + +entities: + # One physical book. There is no loan record: while the book is OnLoan it records which member + # has it, and the record is cleared when the book comes back. + - name: library.lending.Book + identity: + name: book_id + type: library.lending.BookId + fields: + - name: title + type: String + - name: author + type: String + - name: borrower_id + type: Optional + relations: + - name: borrower + kind: references + target: library.lending.Member + cardinality: one + via: borrower_id + lifecycle: + initial: OnShelf + states: [OnShelf, OnLoan, Withdrawn] + terminal: [Withdrawn] + transitions: + - name: lend + from: [OnShelf] + to: OnLoan + - name: return + from: [OnLoan] + to: OnShelf + - name: withdraw + from: [OnShelf] + to: Withdrawn + + # Members are registered and never leave. + - name: library.lending.Member + identity: + name: member_id + type: library.lending.MemberId + fields: + - name: name + type: String + lifecycle: + initial: Active + states: [Active] + terminal: [Active] + +actors: + - name: library.lending.Librarian + may: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + naming: + display: Librarian + +errors: + - name: library.lending.BookStateConflict + summary: The book is not in a state this command acts from, so nothing changed. + fields: + - name: state + type: library.lending.Book.State + + - name: library.lending.BookNotFound + summary: No book in the collection has this identity. + fields: + - name: book_id + type: library.lending.BookId + +commands: + - name: library.lending.AddBook + naming: + wire: add-book + display: Add a book + input: + - name: title + type: String + - name: author + type: String + outcomes: + - name: added + creates: library.lending.Book + instance: book_id + sets: + title: input.title + author: input.author + borrower_id: {cleared: true} + emits: + - library.lending.BookAdded + payload: + library.lending.BookAdded: + book_id: {generated: true} + title: input.title + author: input.author + summary: The book is in the collection, on the shelf. + + - name: library.lending.RegisterMember + naming: + wire: register-member + display: Register a member + input: + - name: name + type: String + outcomes: + - name: registered + creates: library.lending.Member + instance: member_id + sets: + name: input.name + emits: + - library.lending.MemberRegistered + payload: + library.lending.MemberRegistered: + member_id: {generated: true} + name: input.name + summary: The member is registered and may borrow. + + - name: library.lending.BorrowBook + naming: + wire: borrow-book + display: Borrow a book + input: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + # The borrowing member must be a registered member. That is a condition on another entity + # (Member), which a command outcome cannot guard on. Decided: the implementation checks it; + # this specification does not, and no scenario covers it. + outcomes: + - name: borrowed + moves: library.lending.Book.lend + instance: book_id + sets: + borrower_id: input.member_id + emits: + - library.lending.BookBorrowed + payload: + library.lending.BookBorrowed: + book_id: input.book_id + member_id: input.member_id + summary: The book is on loan to the member. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or withdrawn, so it was not lent. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was lent. + + # A return names only the book; the member who brings it back is not checked. + - name: library.lending.ReturnBook + naming: + wire: return-book + display: Return a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: returned + moves: library.lending.Book.return + instance: book_id + sets: + borrower_id: {cleared: true} + emits: + - library.lending.BookReturned + payload: + library.lending.BookReturned: + book_id: input.book_id + summary: The book is back on the shelf and no member has it. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is not on loan, so nothing was returned. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was returned. + + - name: library.lending.WithdrawBook + naming: + wire: withdraw-book + display: Withdraw a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: withdrawn + moves: library.lending.Book.withdraw + instance: book_id + emits: + - library.lending.BookWithdrawn + payload: + library.lending.BookWithdrawn: + book_id: input.book_id + summary: The book has left the collection for good. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or already withdrawn, so it was not withdrawn. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was withdrawn. + +events: + - name: library.lending.BookAdded + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + + - name: library.lending.MemberRegistered + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + + - name: library.lending.BookBorrowed + fields: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + + - name: library.lending.BookReturned + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookWithdrawn + fields: + - name: book_id + type: library.lending.BookId + +views: + # The whole collection, in every state, with who has each book. + - name: library.lending.Catalogue + source: library.lending.Book + consistency: read_your_writes + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + - name: state + type: library.lending.Book.State + - name: borrower_id + type: Optional + naming: + wire: catalogue + display: Catalogue + + - name: library.lending.Members + source: library.lending.Member + consistency: read_your_writes + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + naming: + wire: members + display: Members + + # The loans in force: every book on loan and the member who has it. + - name: library.lending.BooksOnLoan + source: library.lending.Book + consistency: read_your_writes + filter: state == OnLoan + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: borrower_id + type: Optional + naming: + wire: books-on-loan + display: Books on loan +``` + +
+ +## 4. Validate it + +```console +$ ess specify validate --path spec +``` + +```text +library v1 — 3 file(s), valid +``` + +Now make a mistake, to see what a refusal looks like. In `BorrowBook`, change +`moves: library.lending.Book.lend` to `moves: library.lending.Book.loan`, a transition that does not +exist, and validate again: + +```text +$ ess specify validate --path spec +spec was refused: + - [undeclared_reference] command.library.lending.BorrowBook.outcomes.borrowed.moves: outcome `borrowed` of `library.lending.BorrowBook` takes `loan`, which `library.lending.Book` does not declare as a transition (hint: `library.lending.Book` declares: lend, return, withdraw) + - [missing_causation] entity library.lending.Book.transitions[0]: `lend` moves `library.lending.Book` to `OnLoan`, and no command outcome takes it, so nothing in this specification can make that state change happen (hint: give some outcome `moves: library.lending.Book.lend`, or delete the transition) + - error[ESS-COMMAND-001]: outcome `borrowed` of `library.lending.BorrowBook` takes `loan`, which `library.lending.Book` does not declare as a transition + `ess-domain` refuses this as `undeclared_reference` + help: `library.lending.Book` declares: lend, return, withdraw + --> domains/lending.yaml:139:5 + - error[ESS-ENTITY-005]: `lend` moves `library.lending.Book` to `OnLoan`, and no command outcome takes it, so nothing in this specification can make that state change happen + `ess-domain` refuses this as `missing_causation` + help: give some outcome `moves: library.lending.Book.lend`, or delete the transition + --> domains/lending.yaml:22:5 +``` + +It names the file, the line where the declaration starts, what is wrong, and what the book does +declare. It also reports the +consequence: nothing takes the `lend` transition any more. Change it back, and `validate` prints +`valid` again. + +## 5. Generate from it + +The same specification produces documentation and an OpenAPI description: + +```console +$ ess generate --kind openapi --path spec --out out +$ ess generate --kind docs --path spec --out out +``` + +```text +openapi/library-service.yaml — 25269 byte(s) +1 artifact(s), written to out +docs/crossings.md — 1204 byte(s) +docs/domains/library-lending.md — 16915 byte(s) +docs/index.md — 3379 byte(s) +docs/interactions.md — 1318 byte(s) +docs/topology.md — 1076 byte(s) +5 artifact(s), written to out +``` + +`ess specify graph --path spec --format mermaid` draws who may send which command and what each one +publishes: + +```mermaid +flowchart TB + subgraph who["who may ask"] + who0["library.lending.Librarian"] + end + subgraph unit0["library-service"] + cmd0["library.lending.AddBook"] + cmd1["library.lending.BorrowBook"] + cmd2["library.lending.RegisterMember"] + cmd3["library.lending.ReturnBook"] + cmd4["library.lending.WithdrawBook"] + evt0["library.lending.BookAdded"] + evt1["library.lending.BookBorrowed"] + evt2["library.lending.BookReturned"] + evt3["library.lending.BookWithdrawn"] + evt4["library.lending.MemberRegistered"] + end + who0 -->|"may invoke"| cmd0 + who0 -->|"may invoke"| cmd1 + who0 -->|"may invoke"| cmd2 + who0 -->|"may invoke"| cmd3 + who0 -->|"may invoke"| cmd4 + cmd0 -->|"added"| evt0 + cmd1 -->|"borrowed"| evt1 + cmd2 -->|"registered"| evt4 + cmd3 -->|"returned"| evt2 + cmd4 -->|"withdrawn"| evt3 +``` + +## 6. Hold an implementation to it + +Generate the conformance suite as a Go package inside the implementation's module: + +```console +$ ess verify conform synthesize --path spec --target go --out impl +``` + +```text +17 scenario(s) (0 authored), 0 refusal(s), 7 file(s) written to impl +``` + +`0 refusal(s)` means every rule in the specification became a check. A refusal would name a rule the +suite cannot test; read those before trusting a green run. + +The package, `impl/essconform`, holds the scenarios and a runner. You supply a `Target`: the methods +that let the runner send a command to your implementation and read a view back. Ask the agent: + +```text +Write a minimal in-memory Go implementation of the library in impl/ (module example.com/library) +that implements the suite's Target, and run `go test ./...` in impl/. Don't edit the generated +suite to make it pass. +``` + +It wrote three files: `go.mod`, `library.go` (the library) and `conformance_test.go` (the `Target`, +mapping the five commands and three views onto the library). + +
+impl/library.go + +```go title="impl/library.go" +// Package library is a minimal in-memory lending library: books are added and withdrawn, members +// are registered, and a member borrows a book and returns it. +package library + +import ( + "crypto/rand" + "errors" + "fmt" +) + +// BookState is where a book is in its lifecycle. +type BookState string + +const ( + OnShelf BookState = "OnShelf" + OnLoan BookState = "OnLoan" + Withdrawn BookState = "Withdrawn" +) + +// Book is one physical book. Borrower is set only while the book is OnLoan. +type Book struct { + ID string + Title string + Author string + State BookState + Borrower *string +} + +// Member is a registered member. +type Member struct { + ID string + Name string +} + +// ErrBookNotFound is returned when no book has the given identity. +var ErrBookNotFound = errors.New("book not found") + +// ErrUnknownMember is returned when a borrow names a member nobody registered. +var ErrUnknownMember = errors.New("member not registered") + +// StateConflictError is returned when a book is not in a state the operation acts from. +type StateConflictError struct { + State BookState +} + +func (e *StateConflictError) Error() string { + return fmt.Sprintf("book is %s", e.State) +} + +// Library holds the collection and the members in memory. It is not safe for concurrent use. +type Library struct { + books map[string]*Book + members map[string]*Member + // order keeps listings stable in insertion order. + bookOrder []string + memberOrder []string +} + +// New returns an empty library. +func New() *Library { + return &Library{books: map[string]*Book{}, members: map[string]*Member{}} +} + +// AddBook puts a new book on the shelf. +func (l *Library) AddBook(title, author string) Book { + b := &Book{ID: newID(), Title: title, Author: author, State: OnShelf} + l.books[b.ID] = b + l.bookOrder = append(l.bookOrder, b.ID) + return *b +} + +// RegisterMember registers a new member. +func (l *Library) RegisterMember(name string) Member { + m := &Member{ID: newID(), Name: name} + l.members[m.ID] = m + l.memberOrder = append(l.memberOrder, m.ID) + return *m +} + +// Borrow lends a book on the shelf to a member. +// +// requireMember makes an unregistered member a refusal. The specification leaves that check to the +// implementation and its conformance suite borrows for members it never registered, so the +// conformance target turns it off. +func (l *Library) Borrow(bookID, memberID string, requireMember bool) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + if requireMember { + if _, ok := l.members[memberID]; !ok { + return ErrUnknownMember + } + } + b.State = OnLoan + b.Borrower = &memberID + return nil +} + +// Return puts a book on loan back on the shelf. +func (l *Library) Return(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnLoan { + return &StateConflictError{State: b.State} + } + b.State = OnShelf + b.Borrower = nil + return nil +} + +// Withdraw takes a book on the shelf out of the collection for good. +func (l *Library) Withdraw(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + b.State = Withdrawn + return nil +} + +// Books lists every book, in every state. +func (l *Library) Books() []Book { + out := make([]Book, 0, len(l.bookOrder)) + for _, id := range l.bookOrder { + out = append(out, *l.books[id]) + } + return out +} + +// Members lists every member. +func (l *Library) Members() []Member { + out := make([]Member, 0, len(l.memberOrder)) + for _, id := range l.memberOrder { + out = append(out, *l.members[id]) + } + return out +} + +func newID() string { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + panic(err) + } + b[6] = b[6]&0x0f | 0x40 + b[8] = b[8]&0x3f | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} +``` + +
+ +
+impl/conformance_test.go + +```go title="impl/conformance_test.go" +package library_test + +import ( + "errors" + "fmt" + "os" + "testing" + + "example.com/library" + "example.com/library/essconform" +) + +func TestConformance(t *testing.T) { + // The suite runs only once a report format is chosen; choose it here so plain `go test ./...` + // runs it. ESS_REPORT_OUT, when set, still decides where the report goes. + if os.Getenv("ESS_REPORT_FORMAT") == "" { + t.Setenv("ESS_REPORT_FORMAT", "2") + } + essconform.Run(t, func() essconform.Target { return newTarget() }) +} + +// target drives one in-memory Library through the suite's Target interface. +type target struct { + lib *library.Library +} + +func newTarget() *target { return &target{lib: library.New()} } + +func (t *target) Identity() (essconform.Identity, error) { + return essconform.Identity{Name: "example.com/library", Version: "v1"}, nil +} + +func (t *target) BeginScenario(essconform.ScenarioContext) error { return nil } +func (t *target) EndScenario(essconform.ScenarioContext) error { return nil } + +func (t *target) ExecuteCommand(req essconform.CommandRequest) (essconform.CommandResult, error) { + in := func(name string) (string, error) { + v, ok := req.Input[name].(string) + if !ok { + return "", fmt.Errorf("%s: input %q is not text", req.Command, name) + } + return v, nil + } + event := func(name string, payload map[string]essconform.Node) []essconform.ObservedEvent { + return []essconform.ObservedEvent{{Event: name, Payload: payload}} + } + + switch req.Command { + case "library.lending.AddBook": + title, err := in("title") + if err != nil { + return essconform.CommandResult{}, err + } + author, err := in("author") + if err != nil { + return essconform.CommandResult{}, err + } + b := t.lib.AddBook(title, author) + return essconform.CommandResult{ + Outcome: "added", + DirectEvents: event("library.lending.BookAdded", map[string]essconform.Node{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + }), + }, nil + + case "library.lending.RegisterMember": + name, err := in("name") + if err != nil { + return essconform.CommandResult{}, err + } + m := t.lib.RegisterMember(name) + return essconform.CommandResult{ + Outcome: "registered", + DirectEvents: event("library.lending.MemberRegistered", map[string]essconform.Node{ + "member_id": m.ID, "name": m.Name, + }), + }, nil + + case "library.lending.BorrowBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + memberID, err := in("member_id") + if err != nil { + return essconform.CommandResult{}, err + } + // The member check is the implementation's own rule, outside the specification; the + // suite borrows for members it never registered, so it is off here. + if err := t.lib.Borrow(bookID, memberID, false); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "borrowed", + DirectEvents: event("library.lending.BookBorrowed", map[string]essconform.Node{ + "book_id": bookID, "member_id": memberID, + }), + }, nil + + case "library.lending.ReturnBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Return(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "returned", + DirectEvents: event("library.lending.BookReturned", map[string]essconform.Node{"book_id": bookID}), + }, nil + + case "library.lending.WithdrawBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Withdraw(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "withdrawn", + DirectEvents: event("library.lending.BookWithdrawn", map[string]essconform.Node{"book_id": bookID}), + }, nil + } + return essconform.CommandResult{}, fmt.Errorf("unknown command %q: %w", req.Command, essconform.ErrUnsupported) +} + +// refusal maps a library error onto the declared outcome and error. +func refusal(err error) (essconform.CommandResult, error) { + var conflict *library.StateConflictError + switch { + case errors.As(err, &conflict): + return essconform.CommandResult{Outcome: "wrong-state", Error: "library.lending.BookStateConflict"}, nil + case errors.Is(err, library.ErrBookNotFound): + return essconform.CommandResult{Outcome: "no-such-book", Error: "library.lending.BookNotFound"}, nil + } + return essconform.CommandResult{}, err +} + +func (t *target) QueryView(req essconform.ViewRequest) (essconform.ViewResult, error) { + var rows []essconform.Row + switch req.View { + case "library.lending.Catalogue": + for _, b := range t.lib.Books() { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + "state": string(b.State), "borrower_id": borrower(b), + }) + } + case "library.lending.BooksOnLoan": + for _, b := range t.lib.Books() { + if b.State == library.OnLoan { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "borrower_id": borrower(b), + }) + } + } + case "library.lending.Members": + for _, m := range t.lib.Members() { + rows = append(rows, essconform.Row{"member_id": m.ID, "name": m.Name}) + } + default: + return essconform.ViewResult{}, fmt.Errorf("unknown view %q: %w", req.View, essconform.ErrUnsupported) + } + return essconform.ViewResult{Rows: rows}, nil +} + +func borrower(b library.Book) essconform.Node { + if b.Borrower == nil { + return nil + } + return *b.Borrower +} + +// Every event is returned directly by the command that emits it; there is nothing to observe apart. +func (t *target) ObserveEvents(essconform.EventObservationRequest) ([]essconform.ObservedEvent, error) { + return nil, essconform.ErrUnsupported +} + +func (t *target) ConfigureExternalOutcome(essconform.ExternalOutcomeControl) error { + return essconform.ErrUnsupported +} + +func (t *target) RedeliverEvent(essconform.RedeliveryRequest) error { + return essconform.ErrUnsupported +} + +func (t *target) ObserveInvocations(essconform.InvocationObservationRequest) ([]essconform.Invocation, error) { + return nil, essconform.ErrUnsupported +} +``` + +
+ +Run the suite. The runner needs `ESS_REPORT_FORMAT=2` and stops before the first scenario without +it. This `conformance_test.go` sets it when it is missing; in your own test, set it the same way or +on the command line. + +```console +$ cd impl +$ ESS_REPORT_FORMAT=2 go test -v ./... +``` + +```text +--- PASS: TestConformance (0.01s) + --- PASS: TestConformance/library.lending.AddBook/outcome/added (0.00s) + --- PASS: TestConformance/library.lending.Book/state/OnLoan/refuses/library.lending.BorrowBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/OnLoan/refuses/library.lending.WithdrawBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/OnShelf/refuses/library.lending.ReturnBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/Withdrawn/refuses/library.lending.BorrowBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/Withdrawn/refuses/library.lending.ReturnBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/Withdrawn/refuses/library.lending.WithdrawBook (0.00s) + --- PASS: TestConformance/library.lending.Book/transition/lend/by/library.lending.BorrowBook/borrowed (0.00s) + --- PASS: TestConformance/library.lending.Book/transition/return/by/library.lending.ReturnBook/returned (0.00s) + --- PASS: TestConformance/library.lending.Book/transition/withdraw/by/library.lending.WithdrawBook/withdrawn (0.00s) + --- PASS: TestConformance/library.lending.BorrowBook/outcome/borrowed (0.00s) + --- PASS: TestConformance/library.lending.BorrowBook/outcome/no-such-book (0.00s) + --- PASS: TestConformance/library.lending.RegisterMember/outcome/registered (0.00s) + --- PASS: TestConformance/library.lending.ReturnBook/outcome/no-such-book (0.00s) + --- PASS: TestConformance/library.lending.ReturnBook/outcome/returned (0.00s) + --- PASS: TestConformance/library.lending.WithdrawBook/outcome/no-such-book (0.00s) + --- PASS: TestConformance/library.lending.WithdrawBook/outcome/withdrawn (0.00s) +PASS +ok example.com/library 0.017s +? example.com/library/essconform [no test files] +``` + +Seventeen scenarios, each named for the rule it checks, for example +`library.lending.Book/state/OnLoan/refuses/library.lending.BorrowBook`. + +## 7. Watch it catch a bug + +A passing suite only means something if it can fail. In `impl/library.go`, make `Borrow` accept a +book that is already on loan: + +```diff +- if b.State != OnShelf { ++ if b.State == Withdrawn { +``` + +```console +$ ESS_REPORT_FORMAT=2 go test ./... +``` + +```text +--- FAIL: TestConformance (0.04s) + conformance_test.go:19: library v1, 17 scenario(s), spec digest 297bae6c872cfd3000a51c7ee9caa176be4674b27863f83233cb1af4c98e693d + --- FAIL: TestConformance/library.lending.Book/state/OnLoan/refuses/library.lending.BorrowBook (0.00s) + runtime.go:1870: `library.lending.BorrowBook` does not move a `library.lending.Book` that is in `OnLoan`, and reports `library.lending.BookStateConflict` + runtime.go:2661: step 10: `library.lending.BorrowBook` took `borrowed`, and the specification says `wrong-state` +FAIL +FAIL example.com/library 0.046s +? example.com/library/essconform [no test files] +FAIL +``` + +The failing scenario names the rule, the step, and what the implementation did instead +(`took borrowed`, where the specification says `wrong-state`). Undo the change and the suite passes +again. + +To test the suite itself, rather than one bug you thought of, the `ess:hardening` skill runs +`ess verify conform mutate`: it changes the specification one rule at a time and checks that the +suite notices each change. + +## 8. Keep it true + +Pin the `ess` release the specification was written against. The `ess-inputs.yaml` in step 3 +already carries the pin; on your own project, add `requires:`, which needs `format: ess-inputs/2`: + +```yaml title="spec/ess-inputs.yaml" +format: ess-inputs/2 +requires: ess 0.38.0 +``` + +```console +$ cd spec && ess specify toolchain which +``` + +```text +ess 0.38.0 +reason: pin: `requires: ess 0.38.0` in ~/library/spec/ess-inputs.yaml +binary: ~/.local/bin/ess (this ess) +``` + +With the pin, an `ess` of a different release runs 0.38.0 for this project, and +`ess specify toolchain install` fetches it where it is missing. + +Then make the three checks part of your build, so a change to the specification or the code that +breaks the other fails there. With [Task](https://taskfile.dev): + +```yaml title="Taskfile.yml" +version: '3' + +tasks: + check: + cmds: + - ess specify validate --path spec + - ess verify conform synthesize --path spec --target go --out impl + - cmd: go test ./... + dir: impl + env: + ESS_REPORT_FORMAT: '2' +``` + +Regenerating the suite in the build means a changed specification is always tested as it now reads. + +## When a rule spans two records + +The first recording of this page asked for one more rule: *a member can hold at most three books at +a time*, with a separate loan record. The agent's model was valid, but each borrow became three +commands (one per record it changes: the member's count, the book, the loan), and synthesis refused +3 scenarios with `ESS-SYNTH-003`, because it cannot arrange a member who already holds three books. +The agent said so in its report instead of bending the model to make the number zero. + +That is how ESS treats a rule across two records today: one command changes one record, and a check +it cannot build is named, not skipped in silence. When your domain has such a rule, expect the agent +to show you the split and the refusals, and decide with it whether the rule belongs in the +specification or in the implementation. + +## Next + +- **Plan work around it.** Once a new noun has a specification, AEP plans the stories that build it. + The [golden path](../golden-path.md) walks an agent from a feature idea to a critiqued plan; a + tutorial like this one for AEP follows. +- **Specify a system that already runs.** `ess:retrofitting` derives a specification from an OpenAPI + document or from code, citing a source for every declaration. +- **Go deeper into ESS.** The [ESS documentation](https://beyond10x.github.io/docs/ess/) covers the + format, every command and the other targets (`--target typescript`). diff --git a/website/docs/tutorials/first-ess-specification/impl/conformance_test.go b/website/docs/tutorials/first-ess-specification/impl/conformance_test.go new file mode 100644 index 0000000..2242c10 --- /dev/null +++ b/website/docs/tutorials/first-ess-specification/impl/conformance_test.go @@ -0,0 +1,191 @@ +package library_test + +import ( + "errors" + "fmt" + "os" + "testing" + + "example.com/library" + "example.com/library/essconform" +) + +func TestConformance(t *testing.T) { + // The suite runs only once a report format is chosen; choose it here so plain `go test ./...` + // runs it. ESS_REPORT_OUT, when set, still decides where the report goes. + if os.Getenv("ESS_REPORT_FORMAT") == "" { + t.Setenv("ESS_REPORT_FORMAT", "2") + } + essconform.Run(t, func() essconform.Target { return newTarget() }) +} + +// target drives one in-memory Library through the suite's Target interface. +type target struct { + lib *library.Library +} + +func newTarget() *target { return &target{lib: library.New()} } + +func (t *target) Identity() (essconform.Identity, error) { + return essconform.Identity{Name: "example.com/library", Version: "v1"}, nil +} + +func (t *target) BeginScenario(essconform.ScenarioContext) error { return nil } +func (t *target) EndScenario(essconform.ScenarioContext) error { return nil } + +func (t *target) ExecuteCommand(req essconform.CommandRequest) (essconform.CommandResult, error) { + in := func(name string) (string, error) { + v, ok := req.Input[name].(string) + if !ok { + return "", fmt.Errorf("%s: input %q is not text", req.Command, name) + } + return v, nil + } + event := func(name string, payload map[string]essconform.Node) []essconform.ObservedEvent { + return []essconform.ObservedEvent{{Event: name, Payload: payload}} + } + + switch req.Command { + case "library.lending.AddBook": + title, err := in("title") + if err != nil { + return essconform.CommandResult{}, err + } + author, err := in("author") + if err != nil { + return essconform.CommandResult{}, err + } + b := t.lib.AddBook(title, author) + return essconform.CommandResult{ + Outcome: "added", + DirectEvents: event("library.lending.BookAdded", map[string]essconform.Node{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + }), + }, nil + + case "library.lending.RegisterMember": + name, err := in("name") + if err != nil { + return essconform.CommandResult{}, err + } + m := t.lib.RegisterMember(name) + return essconform.CommandResult{ + Outcome: "registered", + DirectEvents: event("library.lending.MemberRegistered", map[string]essconform.Node{ + "member_id": m.ID, "name": m.Name, + }), + }, nil + + case "library.lending.BorrowBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + memberID, err := in("member_id") + if err != nil { + return essconform.CommandResult{}, err + } + // The member check is the implementation's own rule, outside the specification; the + // suite borrows for members it never registered, so it is off here. + if err := t.lib.Borrow(bookID, memberID, false); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "borrowed", + DirectEvents: event("library.lending.BookBorrowed", map[string]essconform.Node{ + "book_id": bookID, "member_id": memberID, + }), + }, nil + + case "library.lending.ReturnBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Return(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "returned", + DirectEvents: event("library.lending.BookReturned", map[string]essconform.Node{"book_id": bookID}), + }, nil + + case "library.lending.WithdrawBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Withdraw(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "withdrawn", + DirectEvents: event("library.lending.BookWithdrawn", map[string]essconform.Node{"book_id": bookID}), + }, nil + } + return essconform.CommandResult{}, fmt.Errorf("unknown command %q: %w", req.Command, essconform.ErrUnsupported) +} + +// refusal maps a library error onto the declared outcome and error. +func refusal(err error) (essconform.CommandResult, error) { + var conflict *library.StateConflictError + switch { + case errors.As(err, &conflict): + return essconform.CommandResult{Outcome: "wrong-state", Error: "library.lending.BookStateConflict"}, nil + case errors.Is(err, library.ErrBookNotFound): + return essconform.CommandResult{Outcome: "no-such-book", Error: "library.lending.BookNotFound"}, nil + } + return essconform.CommandResult{}, err +} + +func (t *target) QueryView(req essconform.ViewRequest) (essconform.ViewResult, error) { + var rows []essconform.Row + switch req.View { + case "library.lending.Catalogue": + for _, b := range t.lib.Books() { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + "state": string(b.State), "borrower_id": borrower(b), + }) + } + case "library.lending.BooksOnLoan": + for _, b := range t.lib.Books() { + if b.State == library.OnLoan { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "borrower_id": borrower(b), + }) + } + } + case "library.lending.Members": + for _, m := range t.lib.Members() { + rows = append(rows, essconform.Row{"member_id": m.ID, "name": m.Name}) + } + default: + return essconform.ViewResult{}, fmt.Errorf("unknown view %q: %w", req.View, essconform.ErrUnsupported) + } + return essconform.ViewResult{Rows: rows}, nil +} + +func borrower(b library.Book) essconform.Node { + if b.Borrower == nil { + return nil + } + return *b.Borrower +} + +// Every event is returned directly by the command that emits it; there is nothing to observe apart. +func (t *target) ObserveEvents(essconform.EventObservationRequest) ([]essconform.ObservedEvent, error) { + return nil, essconform.ErrUnsupported +} + +func (t *target) ConfigureExternalOutcome(essconform.ExternalOutcomeControl) error { + return essconform.ErrUnsupported +} + +func (t *target) RedeliverEvent(essconform.RedeliveryRequest) error { + return essconform.ErrUnsupported +} + +func (t *target) ObserveInvocations(essconform.InvocationObservationRequest) ([]essconform.Invocation, error) { + return nil, essconform.ErrUnsupported +} diff --git a/website/docs/tutorials/first-ess-specification/impl/go.mod b/website/docs/tutorials/first-ess-specification/impl/go.mod new file mode 100644 index 0000000..4b2c9c4 --- /dev/null +++ b/website/docs/tutorials/first-ess-specification/impl/go.mod @@ -0,0 +1,3 @@ +module example.com/library + +go 1.23 diff --git a/website/docs/tutorials/first-ess-specification/impl/library.go b/website/docs/tutorials/first-ess-specification/impl/library.go new file mode 100644 index 0000000..db6e0a3 --- /dev/null +++ b/website/docs/tutorials/first-ess-specification/impl/library.go @@ -0,0 +1,156 @@ +// Package library is a minimal in-memory lending library: books are added and withdrawn, members +// are registered, and a member borrows a book and returns it. +package library + +import ( + "crypto/rand" + "errors" + "fmt" +) + +// BookState is where a book is in its lifecycle. +type BookState string + +const ( + OnShelf BookState = "OnShelf" + OnLoan BookState = "OnLoan" + Withdrawn BookState = "Withdrawn" +) + +// Book is one physical book. Borrower is set only while the book is OnLoan. +type Book struct { + ID string + Title string + Author string + State BookState + Borrower *string +} + +// Member is a registered member. +type Member struct { + ID string + Name string +} + +// ErrBookNotFound is returned when no book has the given identity. +var ErrBookNotFound = errors.New("book not found") + +// ErrUnknownMember is returned when a borrow names a member nobody registered. +var ErrUnknownMember = errors.New("member not registered") + +// StateConflictError is returned when a book is not in a state the operation acts from. +type StateConflictError struct { + State BookState +} + +func (e *StateConflictError) Error() string { + return fmt.Sprintf("book is %s", e.State) +} + +// Library holds the collection and the members in memory. It is not safe for concurrent use. +type Library struct { + books map[string]*Book + members map[string]*Member + // order keeps listings stable in insertion order. + bookOrder []string + memberOrder []string +} + +// New returns an empty library. +func New() *Library { + return &Library{books: map[string]*Book{}, members: map[string]*Member{}} +} + +// AddBook puts a new book on the shelf. +func (l *Library) AddBook(title, author string) Book { + b := &Book{ID: newID(), Title: title, Author: author, State: OnShelf} + l.books[b.ID] = b + l.bookOrder = append(l.bookOrder, b.ID) + return *b +} + +// RegisterMember registers a new member. +func (l *Library) RegisterMember(name string) Member { + m := &Member{ID: newID(), Name: name} + l.members[m.ID] = m + l.memberOrder = append(l.memberOrder, m.ID) + return *m +} + +// Borrow lends a book on the shelf to a member. +// +// requireMember makes an unregistered member a refusal. The specification leaves that check to the +// implementation and its conformance suite borrows for members it never registered, so the +// conformance target turns it off. +func (l *Library) Borrow(bookID, memberID string, requireMember bool) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + if requireMember { + if _, ok := l.members[memberID]; !ok { + return ErrUnknownMember + } + } + b.State = OnLoan + b.Borrower = &memberID + return nil +} + +// Return puts a book on loan back on the shelf. +func (l *Library) Return(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnLoan { + return &StateConflictError{State: b.State} + } + b.State = OnShelf + b.Borrower = nil + return nil +} + +// Withdraw takes a book on the shelf out of the collection for good. +func (l *Library) Withdraw(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + b.State = Withdrawn + return nil +} + +// Books lists every book, in every state. +func (l *Library) Books() []Book { + out := make([]Book, 0, len(l.bookOrder)) + for _, id := range l.bookOrder { + out = append(out, *l.books[id]) + } + return out +} + +// Members lists every member. +func (l *Library) Members() []Member { + out := make([]Member, 0, len(l.memberOrder)) + for _, id := range l.memberOrder { + out = append(out, *l.members[id]) + } + return out +} + +func newID() string { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + panic(err) + } + b[6] = b[6]&0x0f | 0x40 + b[8] = b[8]&0x3f | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} diff --git a/website/docs/tutorials/first-ess-specification/spec/components.yaml b/website/docs/tutorials/first-ess-specification/spec/components.yaml new file mode 100644 index 0000000..16b2f14 --- /dev/null +++ b/website/docs/tutorials/first-ess-specification/spec/components.yaml @@ -0,0 +1,21 @@ +components: + - component: library-service + summary: Holds the collection and the members, and lends books to members. + owns: + domains: + - library.lending + accepts: + commands: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + publishes: + events: + - library.lending.BookAdded + - library.lending.MemberRegistered + - library.lending.BookBorrowed + - library.lending.BookReturned + - library.lending.BookWithdrawn + reached_by: network diff --git a/website/docs/tutorials/first-ess-specification/spec/domains/lending.yaml b/website/docs/tutorials/first-ess-specification/spec/domains/lending.yaml new file mode 100644 index 0000000..0a332f3 --- /dev/null +++ b/website/docs/tutorials/first-ess-specification/spec/domains/lending.yaml @@ -0,0 +1,314 @@ +domain: library.lending + +summary: A small lending library. Books are added to the collection, members are registered, and a + member borrows a book and later returns it. + +naming: + wire: lending + display: Lending + +types: + - name: library.lending.BookId + kind: newtype + of: Uuid + + - name: library.lending.MemberId + kind: newtype + of: Uuid + +entities: + # One physical book. There is no loan record: while the book is OnLoan it records which member + # has it, and the record is cleared when the book comes back. + - name: library.lending.Book + identity: + name: book_id + type: library.lending.BookId + fields: + - name: title + type: String + - name: author + type: String + - name: borrower_id + type: Optional + relations: + - name: borrower + kind: references + target: library.lending.Member + cardinality: one + via: borrower_id + lifecycle: + initial: OnShelf + states: [OnShelf, OnLoan, Withdrawn] + terminal: [Withdrawn] + transitions: + - name: lend + from: [OnShelf] + to: OnLoan + - name: return + from: [OnLoan] + to: OnShelf + - name: withdraw + from: [OnShelf] + to: Withdrawn + + # Members are registered and never leave. + - name: library.lending.Member + identity: + name: member_id + type: library.lending.MemberId + fields: + - name: name + type: String + lifecycle: + initial: Active + states: [Active] + terminal: [Active] + +actors: + - name: library.lending.Librarian + may: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + naming: + display: Librarian + +errors: + - name: library.lending.BookStateConflict + summary: The book is not in a state this command acts from, so nothing changed. + fields: + - name: state + type: library.lending.Book.State + + - name: library.lending.BookNotFound + summary: No book in the collection has this identity. + fields: + - name: book_id + type: library.lending.BookId + +commands: + - name: library.lending.AddBook + naming: + wire: add-book + display: Add a book + input: + - name: title + type: String + - name: author + type: String + outcomes: + - name: added + creates: library.lending.Book + instance: book_id + sets: + title: input.title + author: input.author + borrower_id: {cleared: true} + emits: + - library.lending.BookAdded + payload: + library.lending.BookAdded: + book_id: {generated: true} + title: input.title + author: input.author + summary: The book is in the collection, on the shelf. + + - name: library.lending.RegisterMember + naming: + wire: register-member + display: Register a member + input: + - name: name + type: String + outcomes: + - name: registered + creates: library.lending.Member + instance: member_id + sets: + name: input.name + emits: + - library.lending.MemberRegistered + payload: + library.lending.MemberRegistered: + member_id: {generated: true} + name: input.name + summary: The member is registered and may borrow. + + - name: library.lending.BorrowBook + naming: + wire: borrow-book + display: Borrow a book + input: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + # The borrowing member must be a registered member. That is a condition on another entity + # (Member), which a command outcome cannot guard on. Decided: the implementation checks it; + # this specification does not, and no scenario covers it. + outcomes: + - name: borrowed + moves: library.lending.Book.lend + instance: book_id + sets: + borrower_id: input.member_id + emits: + - library.lending.BookBorrowed + payload: + library.lending.BookBorrowed: + book_id: input.book_id + member_id: input.member_id + summary: The book is on loan to the member. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or withdrawn, so it was not lent. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was lent. + + # A return names only the book; the member who brings it back is not checked. + - name: library.lending.ReturnBook + naming: + wire: return-book + display: Return a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: returned + moves: library.lending.Book.return + instance: book_id + sets: + borrower_id: {cleared: true} + emits: + - library.lending.BookReturned + payload: + library.lending.BookReturned: + book_id: input.book_id + summary: The book is back on the shelf and no member has it. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is not on loan, so nothing was returned. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was returned. + + - name: library.lending.WithdrawBook + naming: + wire: withdraw-book + display: Withdraw a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: withdrawn + moves: library.lending.Book.withdraw + instance: book_id + emits: + - library.lending.BookWithdrawn + payload: + library.lending.BookWithdrawn: + book_id: input.book_id + summary: The book has left the collection for good. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or already withdrawn, so it was not withdrawn. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was withdrawn. + +events: + - name: library.lending.BookAdded + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + + - name: library.lending.MemberRegistered + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + + - name: library.lending.BookBorrowed + fields: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + + - name: library.lending.BookReturned + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookWithdrawn + fields: + - name: book_id + type: library.lending.BookId + +views: + # The whole collection, in every state, with who has each book. + - name: library.lending.Catalogue + source: library.lending.Book + consistency: read_your_writes + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + - name: state + type: library.lending.Book.State + - name: borrower_id + type: Optional + naming: + wire: catalogue + display: Catalogue + + - name: library.lending.Members + source: library.lending.Member + consistency: read_your_writes + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + naming: + wire: members + display: Members + + # The loans in force: every book on loan and the member who has it. + - name: library.lending.BooksOnLoan + source: library.lending.Book + consistency: read_your_writes + filter: state == OnLoan + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: borrower_id + type: Optional + naming: + wire: books-on-loan + display: Books on loan diff --git a/website/docs/tutorials/first-ess-specification/spec/ess-inputs.yaml b/website/docs/tutorials/first-ess-specification/spec/ess-inputs.yaml new file mode 100644 index 0000000..c404043 --- /dev/null +++ b/website/docs/tutorials/first-ess-specification/spec/ess-inputs.yaml @@ -0,0 +1,7 @@ +format: ess-inputs/2 +requires: ess 0.38.0 +specification: + - system.yaml + - components.yaml + - domains/lending.yaml +scenarios: [] diff --git a/website/docs/tutorials/first-ess-specification/spec/system.yaml b/website/docs/tutorials/first-ess-specification/spec/system.yaml new file mode 100644 index 0000000..82586ba --- /dev/null +++ b/website/docs/tutorials/first-ess-specification/spec/system.yaml @@ -0,0 +1,6 @@ +format: ess/15 +system: library +version: v1 + +domains: + - library.lending diff --git a/website/sidebars.ts b/website/sidebars.ts index 6acbdd9..0810439 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -5,6 +5,11 @@ const sidebars: SidebarsConfig = { 'intro', 'choose-a-plugin', 'install', + { + type: 'category', + label: 'Tutorials', + items: ['tutorials/first-ess-specification'], + }, 'golden-path', 'structure', { From 992ff78e7179d97d61030920395c6215a4c01f8a Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:38:37 +0200 Subject: [PATCH 4/9] docs: onboarding matches b10x and the five plugins - install.md leads with b10x for the CLIs (`b10x init `, then `b10x setup apply`); the hand install of AEP 0.55.0 and its `protocol 0.55.0` check are gone; Metaharness installs with `b10x install metaharness`; the pin examples name current releases. - plugins/b10x.md: the `init`/`upgrade` skills replace the retired `installing`; `--out` is optional; `install --dir` and `setup plan`'s `--host/--method/--json/--out` are listed (crates/b10x/src/main.rs); connectors is a routing target. - choose-a-plugin.md and trust-and-scope.md name the five plugins by id (b10x, aep, ess, worktree, connectors) instead of "Beyond10x", "AEP Plan", "AEP Drive" and "ESS Specify"; the worktree section no longer says its skill is generated by the worktree CLI (owned here since 3c7d402). - golden-path.md installs aep and ess through b10x (it sent readers to the ESS repository for the plugin) and points newcomers at the tutorial; its recorded AEP 0.44.0 / ESS 0.5.1 output stays, labelled. - intro.md, the landing page and the footer list connectors and link the tutorial; README.md and SETUP.md point newcomers at it. - b10x.docs.yaml: the reference section links plugins/b10x/ (was plugins/beyond10x/), a guide section links the tutorial, and the summary names the current plugins. - b10x:routing resources: canonical /docs// URLs, the tutorial, one aep row instead of AEP Plan/AEP Drive, and connectors. --- README.md | 3 + SETUP.md | 4 + b10x.docs.yaml | 7 +- .../skills/routing/references/resources.md | 23 ++--- website/docs/choose-a-plugin.md | 20 +++-- website/docs/golden-path.md | 19 ++-- website/docs/install.md | 89 +++++-------------- website/docs/intro.md | 7 +- website/docs/plugins/b10x.md | 13 +-- website/docs/trust-and-scope.md | 14 +-- website/docusaurus.config.ts | 2 + website/src/pages/index.tsx | 2 + 12 files changed, 98 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 77d8975..b4a22ed 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ Most plugins drive a CLI of the same name. To have an agent install plugins and migrate older installs, tell it: *"Set up Beyond10x: follow https://github.com/beyond10x/agentplugins/releases/latest/download/SETUP.md"*. +New to ESS: [your first ESS specification](https://beyond10x.github.io/docs/agentplugins/tutorials/first-ess-specification/), +a tutorial from an empty directory to a passing conformance suite. + More: [install guide](website/docs/install.md) · [evals](evals/README.md) · [changelog](CHANGELOG.md) · [contributing](AGENTS.md) diff --git a/SETUP.md b/SETUP.md index 4a148ed..d026640 100644 --- a/SETUP.md +++ b/SETUP.md @@ -41,3 +41,7 @@ says so. Change nothing the user did not confirm. 3. **Continue with the product.** A plugin loads in the next session. To use it now, run `b10x skill :init` (for example `b10x skill ess:init`) and follow the printed text; `b10x skill ` lists the rest. +4. **Point a newcomer at the tutorial.** When the user chose to write specifications and has not + written one before, tell them about + https://beyond10x.github.io/docs/agentplugins/tutorials/first-ess-specification/: it walks + one specification from an empty directory to a passing conformance suite. diff --git a/b10x.docs.yaml b/b10x.docs.yaml index 5343233..34bd67e 100644 --- a/b10x.docs.yaml +++ b/b10x.docs.yaml @@ -57,9 +57,12 @@ surfaces: - kind: quickstart label: Install url: https://beyond10x.github.io/docs/agentplugins/install/ + - kind: guide + label: 'Tutorial: your first ESS specification' + url: https://beyond10x.github.io/docs/agentplugins/tutorials/first-ess-specification/ - kind: reference label: Plugin reference - url: https://beyond10x.github.io/docs/agentplugins/plugins/beyond10x/ + url: https://beyond10x.github.io/docs/agentplugins/plugins/b10x/ - kind: source label: Source url: https://github.com/beyond10x/agentplugins @@ -84,4 +87,4 @@ surfaces: order: 30 sidebar: autogenerated root: website - summary: Navigate and create portable plugins, or install focused AEP planning, AEP delivery, and ESS specification guidance from the curated b10x marketplace. + summary: Set up, navigate and create portable plugins, or install focused AEP planning and delivery, ESS specification, worktree and connectors guidance from the curated b10x marketplace; start with a tutorial that writes and tests a first ESS specification. diff --git a/plugins/b10x/skills/routing/references/resources.md b/plugins/b10x/skills/routing/references/resources.md index 9425705..e94c8b3 100644 --- a/plugins/b10x/skills/routing/references/resources.md +++ b/plugins/b10x/skills/routing/references/resources.md @@ -5,17 +5,18 @@ Use the narrowest link that answers the request. | Resource | Purpose | |---|---| | [Getting started](https://beyond10x.github.io/getting-started/) | Public entry point and adoption paths | -| [Agent Plugins](https://beyond10x.github.io/agentplugins/) | Marketplace overview, installation, and plugin selection | -| [Golden path](https://beyond10x.github.io/agentplugins/docs/golden-path) | One worked run, from a feature idea to a critiqued plan, on a repository that already exists | -| [b10x plugin](https://beyond10x.github.io/agentplugins/docs/plugins/b10x) | Setup, this router and the portable plugin-creation workflow | -| [AEP Plan plugin](https://beyond10x.github.io/agentplugins/docs/plugins/aep) | Governed planning skill and specialist planning agents | -| [AEP Drive plugin](https://beyond10x.github.io/agentplugins/docs/plugins/aep) | Development-wave roles and coordination | -| [ESS agent plugin](https://beyond10x.github.io/agentplugins/docs/plugins/ess) | ESS specification, retrofit, validation, projection and conformance guidance, released with the `ess` binary | -| [Worktree plugin](https://beyond10x.github.io/agentplugins/docs/plugins/worktree) | Managed Git worktree lifecycle and cleanup guidance, released with the `worktree` binary | -| [AEP](https://beyond10x.github.io/aep/) | Protocol concepts, commands, and adopter documentation | -| [AEP Service](https://beyond10x.github.io/aep-service/) | Hosted AEP service boundary and operations | -| [ESS](https://beyond10x.github.io/ess/) | Executable System Specification model, CLI, and adapters | -| [Entity Runtime](https://beyond10x.github.io/entity-runtime/) | Deterministic entity kernel and provider documentation | +| [Agent Plugins](https://beyond10x.github.io/docs/agentplugins/) | Marketplace overview, installation, and plugin selection | +| [Your first ESS specification](https://beyond10x.github.io/docs/agentplugins/tutorials/first-ess-specification/) | Tutorial: an agent writes a specification, `ess` validates it, a Go implementation passes its conformance suite | +| [Golden path](https://beyond10x.github.io/docs/agentplugins/golden-path/) | One worked run, from a feature idea to a critiqued plan, on a repository that already exists | +| [b10x plugin](https://beyond10x.github.io/docs/agentplugins/plugins/b10x/) | Setup, this router and the portable plugin-creation workflow | +| [aep plugin](https://beyond10x.github.io/docs/agentplugins/plugins/aep/) | Governed planning, delivery in waves or by the engine, and diagnosis | +| [ess plugin](https://beyond10x.github.io/docs/agentplugins/plugins/ess/) | ESS specification, retrofit, validation, projection and conformance guidance | +| [worktree plugin](https://beyond10x.github.io/docs/agentplugins/plugins/worktree/) | Managed Git worktree lifecycle and cleanup guidance | +| [connectors plugin](https://beyond10x.github.io/docs/agentplugins/plugins/connectors/) | Provider setup and governed integrations through the `connectors` CLI | +| [AEP](https://beyond10x.github.io/docs/aep/) | Protocol concepts, commands, and adopter documentation | +| [AEP Service](https://beyond10x.github.io/docs/aep-service/) | Hosted AEP service boundary and operations | +| [ESS](https://beyond10x.github.io/docs/ess/) | Executable System Specification model, CLI, and adapters | +| [Entity Runtime](https://beyond10x.github.io/docs/entity-runtime/) | Deterministic entity kernel and provider documentation | | [Worktree](https://github.com/beyond10x/worktree) | Embeddable Rust worktree lifecycle service and CLI | | [Marketplace source](https://github.com/beyond10x/agentplugins) | Plugin manifests, skills, agents, releases, and license | diff --git a/website/docs/choose-a-plugin.md b/website/docs/choose-a-plugin.md index bcd8b86..04b11a1 100644 --- a/website/docs/choose-a-plugin.md +++ b/website/docs/choose-a-plugin.md @@ -9,7 +9,7 @@ Start with the narrowest plugin that covers the task. ## Finding the right surface or creating a plugin -Use **Beyond10x** when you are unsure which specialist owns the work, need the public resource map, +Use **`b10x`** when you are unsure which specialist owns the work, need the public resource map, or want to create or port a plugin. Its plugin creator makes shared skills the canonical workflow so the capability works in Codex and Claude Code, while keeping host-specific wrappers explicit. @@ -29,22 +29,24 @@ Neither makes a draft plan implementation-ready. ## Specifying a system contract -Use **ESS Specify** when editing or reviewing an Executable System Specification, its typed IR, or -a deterministic schema/OpenAPI projection. It reports unsupported semantics instead of inventing -them. +Use **`ess`** (its `specifying` skill) when writing, editing or reviewing an Executable System +Specification, or generating documentation, OpenAPI or a conformance suite from one; `retrofitting` +derives one for a system that already runs, and `testing-conformance` holds an implementation to it. +It reports unsupported semantics instead of inventing them. New to ESS: start with the +[tutorial](tutorials/first-ess-specification.md). ## Managing repository workspaces -Use **Worktree** before an agent changes a repository or when linked worktrees need an -inventory or cleanup sweep. Its skill is generated by the public `worktree` CLI, so the documented -commands and the executable safety policy stay aligned. The plugin does not delete unmanaged trees +Use **`worktree`** before an agent changes a repository or when linked worktrees need an +inventory or cleanup sweep. Every command its skills spell is checked against the newest `worktree` +release, so the documented commands and the executable safety policy stay aligned. The plugin does not delete unmanaged trees or replace the standalone toolchain. ## Using configured integrations -Use **Connectors** to set up providers, diagnose readiness, and search, describe, and invoke +Use **`connectors`** to set up providers, diagnose readiness, and search, describe, and invoke admitted operations through the standalone `connectors` CLI. Both hosts load the same skill; credentials and grants remain owned by the Connector. See [installation](plugins/connectors.md). Installing more than one is reasonable when the work crosses those boundaries. Their scopes are -complementary. The Beyond10x front door routes to them; it does not absorb their instructions. +complementary. The `b10x` front door routes to them; it does not absorb their instructions. diff --git a/website/docs/golden-path.md b/website/docs/golden-path.md index c7ed83b..2d4c283 100644 --- a/website/docs/golden-path.md +++ b/website/docs/golden-path.md @@ -25,9 +25,18 @@ nobody has decided, and what the plan does with that instead of guessing. ## Prerequisites -Install `aep` from the marketplace — see [Install](./install.md) — and the -`ess` plugin from the [ESS repository](https://github.com/beyond10x/ess#point-your-agent-here), -and have the `aep` CLI on your PATH. Step 3 also uses the `ess` CLI. +Install the `aep` and `ess` plugins and their command-line tools with `b10x` (see +[Install](./install.md)): + +```console +$ b10x init aep,ess --out plan.json +$ b10x setup apply --plan plan.json --yes +``` + +Step 3 models a new noun in ESS. If you have not written an ESS specification before, [Your first +ESS specification](./tutorials/first-ess-specification.md) walks through one first. + +The recording on this page was made with these releases: ```shell-session $ aep --version @@ -36,8 +45,8 @@ $ ess --version ess 0.5.1 ``` -That build produced every output on this page. The binary prints `protocol` in `--version` and in -its `--help` usage lines; the command you install and type is `aep`. +That build produced every output on this page. It printed `protocol` in `--version` and in its +`--help` usage lines; current releases print `aep`, the command you install and type. ## 1. Adopt the repository diff --git a/website/docs/install.md b/website/docs/install.md index 6a6b4d2..44888b9 100644 --- a/website/docs/install.md +++ b/website/docs/install.md @@ -27,78 +27,37 @@ in both hosts; every one lives in this repository. The CLIs come from their own releases, prebuilt or with `cargo install`. The [Connectors guide](plugins/connectors.md) covers its separate CLI prerequisite. -## Before you install: put `aep` on your `PATH` +## The command-line tools -The `aep` plugin is an instruction surface for a program it does not ship. It drives the -`aep` CLI. Install it before the plugins so the first task does not stop at a missing command. The -AEP version pinned below publishes native archives for x86-64 and ARM64 Linux and macOS, plus a -`SHA256SUMS` file. Windows archives are not published. +The `aep`, `ess` and `worktree` plugins drive command-line tools they do not ship: `aep`, `ess` +and `worktree`. `b10x` installs each at its newest release, from the release's prebuilt archive +checked against its `SHA256SUMS`, or with `cargo`: -Select the native target once: - -```bash -case "$(uname -s):$(uname -m)" in - Linux:x86_64) B10X_TARGET=x86_64-unknown-linux-gnu ;; - Linux:aarch64) B10X_TARGET=aarch64-unknown-linux-gnu ;; - Darwin:x86_64) B10X_TARGET=x86_64-apple-darwin ;; - Darwin:arm64) B10X_TARGET=aarch64-apple-darwin ;; - *) echo "unsupported platform: $(uname -s) $(uname -m)" >&2; exit 1 ;; -esac +```console +$ b10x init ess --out plan.json # one product, or several: aep,ess,worktree +$ b10x setup apply --plan plan.json --yes ``` -Download AEP `0.55.0`, verify the selected archive against the release manifest, and install its -canonical `aep` command: +`b10x init` prints the plan and changes nothing; `b10x setup apply` carries it out after you have +read it, and snapshots every file it changes first. Archives exist for x86-64 and ARM64 Linux and +macOS; elsewhere `--method cargo` builds from the release tag. The +[tutorial](tutorials/first-ess-specification.md) shows the output of both commands. The `b10x` +front door itself needs none of the three. -```bash -B10X_AEP_VERSION=0.55.0 -B10X_AEP_ARCHIVE="aep-${B10X_AEP_VERSION}-${B10X_TARGET}.tar.gz" -B10X_AEP_RELEASE="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/beyond10x/aep/releases/download/${B10X_AEP_VERSION}" -curl --fail --location --remote-name "${B10X_AEP_RELEASE}/${B10X_AEP_ARCHIVE}" -curl --fail --location --output AEP-SHA256SUMS "${B10X_AEP_RELEASE}/SHA256SUMS" -if command -v sha256sum >/dev/null; then - grep " ${B10X_AEP_ARCHIVE}$" AEP-SHA256SUMS | sha256sum --check - -else - grep " ${B10X_AEP_ARCHIVE}$" AEP-SHA256SUMS | shasum --algorithm 256 --check -fi -tar -xzf "${B10X_AEP_ARCHIVE}" -mkdir -p "$HOME/.local/bin" -install -m 0755 "aep-${B10X_AEP_VERSION}-${B10X_TARGET}/aep" "$HOME/.local/bin/aep" -``` +### The `aep:implementing` skill's drive mode also needs Metaharness -If `$HOME/.local/bin` is not already on your `PATH`, add it in your shell profile. Building from -source remains a fallback: use the exact release tag with Cargo, never a moving branch. +Drive mode hands one story to `metaharness aep drive run`. Metaharness is optional in the `aep` +product, so `b10x init aep` does not install it; install it on its own: -Confirm it before installing anything: - -```bash -aep --version +```console +$ b10x install metaharness +$ metaharness aep drive run --help ``` -The expected line is `protocol 0.55.0`. (`aep` retains `protocol` as its version label for -compatibility.) `command not found` means the affected plugin will install and then stop at its -first CLI command. The `b10x` front door does not need the `aep` binary. - -### The `aep:implementing` skill also needs Metaharness - -AEP 0.55.0 refuses to run a model-backed step map itself and names `metaharness aep drive run` as -the command that does. Metaharness `0.7.0` includes this host; tags through `0.6.5` predate it. -This release provides source. With Rust 1.98 or newer, install the binary from its exact tag: - -```bash -cargo install --locked --git https://github.com/beyond10x/metaharness \ - --tag 0.7.0 metaharness-cli -metaharness aep drive run --help -``` - -Metaharness does not call the `aep` binary installed above — it links AEP as a library at whatever -`beyond10x/aep` git revision `crates/metaharness-aep/Cargo.toml` pins in the Metaharness tag you -install (at `0.7.0` that revision is `a23176ae`, which `git describe --tags` in the AEP repository -reports as `0.54.0-33-ga23176ae`, i.e. behind the `0.55.0` you put on `PATH`), so read that manifest -for the Metaharness↔AEP pair rather than assuming the two versions match. - -Wave mode, `aep:planning` and every other plugin need no Metaharness. +Metaharness links AEP as a library at the revision its own release pins, not the `aep` on your +`PATH`. Wave mode, `aep:planning` and every other plugin need no Metaharness. -`b10x-harness`, the Beyond10x agent loop Metaharness's `b10x` adapter runs, is optional as well: +`b10x-harness`, the agent loop Metaharness's `b10x` adapter runs, is optional as well: `b10x install b10x-harness` installs its newest release. It runs on Linux only, from a prebuilt archive where the release has one for the machine, otherwise built with `cargo`. @@ -133,7 +92,7 @@ The same plugins remain available from the Plugins surface. The authoritative de Codex will find is [`.agents/plugins/marketplace.json`](https://github.com/beyond10x/agentplugins/blob/main/.agents/plugins/marketplace.json) in this repository; Codex reads it together with the selected plugin's `.codex-plugin/plugin.json`. -The `aep` binary requirement above applies unchanged. +The command-line tools above apply unchanged. ## Upgrading @@ -151,8 +110,8 @@ repository rules still decide those boundaries. A repository can hold a CLI at one release instead of the newest: ```bash -b10x pin ess 0.32.0 # exactly this release -b10x pin aep 0.59 # the newest 0.59.x +b10x pin ess 0.38.0 # exactly this release +b10x pin aep 0.63 # the newest 0.63.x b10x unpin ess ``` diff --git a/website/docs/intro.md b/website/docs/intro.md index e110506..09e491f 100644 --- a/website/docs/intro.md +++ b/website/docs/intro.md @@ -17,6 +17,7 @@ directly when the work is already clear. | [`aep`](./plugins/aep.md) | Governed planning and delivery | `planning`, `migrating`, `implementing` and `diagnosing` skills; decomposer, plan critics, reverse engineer, story scoper, implementor, adversary, security reviewer | | [`ess`](./plugins/ess.md) | Executable System Specifications | `specifying`, `retrofitting`, `testing-conformance` and `hardening` skills; author, conformance, retrofitter agents | | [`worktree`](./plugins/worktree.md) | Git workspaces | managed worktrees, leases, recovery proof, and safe cleanup | +| [`connectors`](./plugins/connectors.md) | Integrations | the `integrating` skill for the `connectors` CLI: set up providers and invoke governed integrations | Every plugin starts with `/:init` and checks itself with `/:upgrade`; `/b10x:init` asks what you want to do and installs the matching plugins and CLIs. @@ -25,5 +26,7 @@ The marketplace contains instructions, not credentials. A plugin does not acquir write a repository, contact a service, or bypass an approval boundary merely because it is installed. -[Set up with one sentence](./install.md), [start with the front door](./plugins/b10x.md), [choose a specialist](./choose-a-plugin.md), -or go directly to [installation](./install.md). +New to ESS? [Your first ESS specification](./tutorials/first-ess-specification.md) takes you from an +empty directory to a validated specification and a passing conformance suite, with an agent doing +the writing. Otherwise [set up with one sentence](./install.md), [start with the front +door](./plugins/b10x.md) or [choose a specialist](./choose-a-plugin.md). diff --git a/website/docs/plugins/b10x.md b/website/docs/plugins/b10x.md index 2ecc29a..0bdde44 100644 --- a/website/docs/plugins/b10x.md +++ b/website/docs/plugins/b10x.md @@ -11,7 +11,7 @@ capabilities portable across Codex and Claude Code. It provides: -- an `installing` skill that drives the `b10x` binary: it reads both hosts, the binaries on `PATH` and +- `init` and `upgrade` skills that drive the `b10x` binary: it reads both hosts, the binaries on `PATH` and project settings, asks which products to have, lists every change and applies it after confirmation (see [Install](../install.md)); - a session-start hook that runs `b10x check` and prints one line per plugin/binary drift; @@ -25,15 +25,15 @@ It provides: | command | does | |---|---| -| `b10x init … [--method cargo\|prebuilt] [--host claude\|codex\|all] --out ` | plan the named products: plugins and CLIs installed or brought current; nothing else touched | -| `b10x upgrade […] --out ` | the same for installed products (all, or the named ones) | +| `b10x init … [--method cargo\|prebuilt] [--host claude\|codex\|all] [--json] [--out ]` | plan the named products: plugins and CLIs installed or brought current; nothing else touched | +| `b10x upgrade […] [--method …] [--host …] [--json] [--out ]` | the same for installed products (all, or the named ones) | | `b10x setup apply --plan --yes` | refuse a stale plan, snapshot every file it changes, run the actions, check that the result converged | | `b10x setup undo []` | restore the files the newest (or named) snapshot holds, then refresh both hosts' marketplaces | -| `b10x setup plan [--products …]` | the whole desired state at once: the named products installed, other catalog plugins removed | +| `b10x setup plan [--products …] [--host …] [--method …] [--json] [--out ]` | the whole desired state at once: the named products installed, other catalog plugins removed | | `b10x skill [:]` | list an installed plugin's skills, or print one — usable before a restart | | `b10x setup guide` | print `/b10x:init`, for an agent that has no plugin yet | | `b10x check` | the session-start drift check; prints only problems, refreshes the newest releases at most daily | -| `b10x install [--tag ] [--method cargo\|prebuilt]` | install one CLI (the repository's pin, else the newest) | +| `b10x install [--tag ] [--dir ] [--method cargo\|prebuilt]` | install one CLI (the repository's pin, else the newest) | | `b10x pin ` | pin a CLI for this repository in `b10x.toml` | | `b10x unpin ` | remove that pin | @@ -83,6 +83,7 @@ the governed workflow: - [`aep`](./aep.md) for plans, artifact stores and accepted development work; - [`ess`](./ess.md) for ESS specification, retrofit, validation and conformance; -- [`worktree`](./worktree.md) for managed Git worktrees and safe cleanup. +- [`worktree`](./worktree.md) for managed Git worktrees and safe cleanup; +- [`connectors`](./connectors.md) for setting up providers and invoking governed integrations. For the broader organization map, start at [Beyond10x getting started](https://beyond10x.github.io/getting-started/). diff --git a/website/docs/trust-and-scope.md b/website/docs/trust-and-scope.md index 5e14dfb..7fcaf02 100644 --- a/website/docs/trust-and-scope.md +++ b/website/docs/trust-and-scope.md @@ -9,11 +9,15 @@ credentials and cannot enlarge the authority granted by the host, operator, or r Every published plugin is intentionally narrow: -- Beyond10x routes work, links public resources, and creates portable plugins without copying the - specialist workflows. -- AEP Plan governs planning artifacts and lifecycle-aware planning work. -- AEP Drive coordinates accepted development work. -- ESS Specify specifies, validates and projects executable system contracts. +- `b10x` sets up the other plugins and their command-line tools, routes work, links public + resources, and creates portable plugins without copying the specialist workflows. +- `aep` governs planning artifacts and lifecycle-aware planning work, and coordinates accepted + development work. +- `ess` specifies, validates and projects executable system contracts, and holds implementations + to them. +- `worktree` creates, leases and safely cleans isolated Git worktrees. +- `connectors` sets up providers and invokes governed integrations; credentials stay with the + Connector. The repository gate checks that the two marketplace formats agree in their declared order, plugin and directory names match, required skills and agents exist, and no retired marketplace or diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 0295fb1..3fa7eac 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -57,12 +57,14 @@ const config: Config = { {title: 'Use it', items: [ {label: 'Choose a plugin', to: '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/docs/choose-a-plugin'}, {label: 'Install the marketplace', to: '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/docs/install'}, + {label: 'Tutorial: your first ESS specification', to: '/docs/tutorials/first-ess-specification'}, ]}, {title: 'Plugins', items: [ {label: 'b10x', to: '/docs/plugins/b10x'}, {label: 'ESS', to: '/docs/plugins/ess'}, {label: 'Worktree', to: '/docs/plugins/worktree'}, {label: 'AEP', to: '/docs/plugins/aep'}, + {label: 'Connectors', to: '/docs/plugins/connectors'}, ]}, {title: 'Project', items: [ {label: 'GitHub repository', href: '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/beyond10x/agentplugins'}, diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 6b7dfac..629b6e9 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -9,6 +9,7 @@ const plugins = [ ['ESS', 'Write, retrofit and conformance-test Executable System Specifications.', '/docs/plugins/ess'], ['AEP', 'Plan governed work, then scope and deliver it in waves with adversarial review.', '/docs/plugins/aep'], ['Worktree', 'Manage isolated Git worktrees with explicit recovery proof.', '/docs/plugins/worktree'], + ['Connectors', 'Set up providers and invoke governed integrations through the connectors CLI.', '/docs/plugins/connectors'], ]; export default function Home(): ReactNode { @@ -25,6 +26,7 @@ export default function Home(): ReactNode {
Choose a plugin Install + Your first ESS specification
From 834a8aa99754e9fc13bd730c6c6832440d93b150 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:38:44 +0200 Subject: [PATCH 5/9] feat(check): hold the tutorial to the newest CLIs; refuse retired display names - tools: commands spelled in website/docs/tutorials/*.md are checked against the newest releases like the skills' (ess: 42 commands, was 35). The tutorial's committed specification must validate with the newest ess, synthesize a Go suite with 0 refusals, and its committed implementation must pass it under go test. ESS_TOOLCHAIN_DELEGATED=1 keeps the page's `requires: ess` pin from handing the run to the pinned release, so the newest one is what is tested. First run: library v1 valid, 17 scenarios, 0 refusals, go test ok. - Retired names: "AEP Plan", "AEP Drive" and "ESS Specify", the display names public pages kept after the hyphenated ids were swept. New test a_retired_display_name_on_a_page_fails_the_check; with the ESS Specify entry removed it fails, restored it passes. --- crates/agentplugins-check/src/main.rs | 47 ++++++++++++ crates/agentplugins-check/src/tools.rs | 98 +++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/crates/agentplugins-check/src/main.rs b/crates/agentplugins-check/src/main.rs index a59710e..88fbd71 100644 --- a/crates/agentplugins-check/src/main.rs +++ b/crates/agentplugins-check/src/main.rs @@ -450,6 +450,24 @@ const RETIRED: &[Retired] = &[ new: "the b10x marketplace", wire_next: &[], }, + // Display names of the retired plugins. Public pages kept them after the ids were swept, + // because the sweep matched only the hyphenated ids (found 2026-09-28 in trust-and-scope.md, + // choose-a-plugin.md and the b10x routing resources). + Retired { + old: "AEP Plan", + new: "aep@b10x", + wire_next: &[], + }, + Retired { + old: "AEP Drive", + new: "aep@b10x", + wire_next: &[], + }, + Retired { + old: "ESS Specify", + new: "ess@b10x", + wire_next: &[], + }, ]; /// Directories never walked looking for a retired name, wherever they sit. @@ -1547,6 +1565,35 @@ one product only: `b10x upgrade ess` std::fs::remove_dir_all(&sandbox).expect("the sandbox is removable"); } + /// A retired plugin's display name on a public page fails like its id, and a longer word that + /// begins with it (`AEP Planning`) does not. + #[test] + fn a_retired_display_name_on_a_page_fails_the_check() { + let sandbox = std::env::temp_dir().join(format!( + "agentplugins-check-display-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let page = sandbox.join("website/docs/trust-and-scope.md"); + std::fs::create_dir_all(page.parent().expect("a page has a directory")) + .expect("the sandbox is writable"); + let name = ["ESS", "Specify"].join(" "); + std::fs::write(&page, format!("- {name} validates contracts.\n")) + .expect("the sandbox is writable"); + let error = retired_names(&sandbox).expect_err("a retired display name must fail"); + assert!( + error.contains("website/docs/trust-and-scope.md:1 names `ESS Specify`"), + "{error}" + ); + + let longer = ["AEP", "Planning"].join(" "); + std::fs::write(&page, format!("- {longer} is a topic, not a plugin.\n")) + .expect("the sandbox is writable"); + retired_names(&sandbox).expect("a longer word is not the retired name"); + + std::fs::remove_dir_all(&sandbox).expect("the sandbox is removable"); + } + /// The name a file is *filed under* is a reference too, and the sweep never looks at one. /// /// [`retired_names`] reads bytes and never paths, so the leftover an incomplete `git mv` makes diff --git a/crates/agentplugins-check/src/tools.rs b/crates/agentplugins-check/src/tools.rs index 685fff8..be84250 100644 --- a/crates/agentplugins-check/src/tools.rs +++ b/crates/agentplugins-check/src/tools.rs @@ -4,7 +4,9 @@ //! R5), so the only thing that can drift is a product release renaming or removing a command the //! skills still spell. This check downloads each product's newest release — the prebuilt archive, //! checked against its `SHA256SUMS` — and runs ` --help` for every command a -//! code span or code block in that plugin spells. ESS's syntax example must also still validate. +//! code span or code block in that plugin, or in a page under `website/docs/tutorials/`, spells. +//! ESS's syntax example must also still validate, and the public tutorial's specification must +//! validate, synthesize a Go suite with no refusals and pass it with its committed implementation. //! It runs on every pull request, every `main` push and daily; a red run is fixed by a skill edit. //! //! It also fails when a CLI's newest release is newer than `verified.json`, the release its skills @@ -309,6 +311,95 @@ fn syntax(root: &Path, ess: &Path, scratch: &Path) -> Result<(), String> { Ok(()) } +/// The public tutorial's committed specification and Go implementation. +pub const TUTORIAL: &str = "website/docs/tutorials/first-ess-specification"; + +/// Tutorial pages, whose spelled commands are held to the newest releases like the skills'. +pub const TUTORIALS: &str = "website/docs/tutorials"; + +fn copy(from: &Path, to: &Path) -> Result<(), String> { + std::fs::create_dir_all(to).map_err(|error| format!("{}: {error}", to.display()))?; + for entry in std::fs::read_dir(from).map_err(|error| format!("{}: {error}", from.display()))? { + let path = entry.map_err(|error| error.to_string())?.path(); + let target = to.join(path.file_name().unwrap_or_default()); + if path.is_dir() { + copy(&path, &target)?; + } else { + std::fs::copy(&path, &target) + .map_err(|error| format!("{}: {error}", path.display()))?; + } + } + Ok(()) +} + +/// Run in `dir` with `ESS_TOOLCHAIN_DELEGATED=1`, so a `requires:` pin in the tutorial's +/// `ess-inputs.yaml` does not hand the command to the pinned release: the newest one is under test. +fn run_in(dir: &Path, program: &str, arguments: &[&str]) -> Result { + let output = Command::new(program) + .args(arguments) + .current_dir(dir) + .env("ESS_TOOLCHAIN_DELEGATED", "1") + .env("ESS_REPORT_FORMAT", "2") + .output() + .map_err(|error| format!("running {program}: {error}"))?; + let text = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + if output.status.success() { + Ok(text) + } else { + Err(format!( + "{program} {} failed: {}", + arguments.join(" "), + text.trim() + )) + } +} + +/// The tutorial, as a reader runs it: its specification validates and synthesizes a Go suite with +/// no refusals, and its implementation passes that suite under `go test`. +fn tutorial(root: &Path, ess: &Path, scratch: &Path) -> Result<(), String> { + let dir = scratch.join("tutorial"); + copy(&root.join(TUTORIAL), &dir)?; + let ess = ess.to_string_lossy(); + let fail = |step: &str, error: String| format!("{TUTORIAL}: {step}: {error}"); + let validated = run_in(&dir, &ess, &["specify", "validate", "--path", "spec"]) + .map_err(|error| fail("the specification no longer validates", error))?; + println!("tools `ess`: tutorial — {}", validated.trim()); + let synthesized = run_in( + &dir, + &ess, + &[ + "verify", + "conform", + "synthesize", + "--path", + "spec", + "--target", + "go", + "--out", + "impl", + ], + ) + .map_err(|error| fail("synthesize failed", error))?; + if !synthesized.contains(" 0 refusal(s)") { + return Err(fail( + "the specification synthesizes with refusals", + synthesized.trim().to_owned(), + )); + } + println!("tools `ess`: tutorial — {}", synthesized.trim()); + let tested = run_in(&dir.join("impl"), "go", &["test", "./..."]) + .map_err(|error| fail("the implementation fails its suite", error))?; + println!( + "tools `ess`: tutorial — go test: {}", + tested.lines().next().unwrap_or_default().trim() + ); + Ok(()) +} + /// Check every plugin's spelled commands against its CLI's newest release. pub fn verify(root: &Path) -> Result<(), String> { let scratch = @@ -326,7 +417,9 @@ pub fn verify(root: &Path) -> Result<(), String> { problems.push(line); } let mut checked = 0; - for file in markdown(&root.join("plugins").join(plugin)) { + let mut files = markdown(&root.join("plugins").join(plugin)); + files.extend(markdown(&root.join(TUTORIALS))); + for file in files { let text = std::fs::read_to_string(&file).map_err(|error| error.to_string())?; for path in spelled(&text, cli) { checked += 1; @@ -348,6 +441,7 @@ pub fn verify(root: &Path) -> Result<(), String> { println!("tools `{cli}` {tag}: {checked} spelled command(s) checked"); if *cli == "ess" { syntax(root, &binary, &scratch)?; + tutorial(root, &binary, &scratch)?; } } if problems.is_empty() { From d6a9458dbc8e89281257838596111230589777aa Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:38:53 +0200 Subject: [PATCH 6/9] test(trials): ess-tutorial trial and baseline - trials/ess-tutorial: a fresh isolated agent with ess@b10x gets only a copy of the tutorial page (fixture/tutorial.md) and works steps 2-8. Measures tool calls, validate, synthesis, outputs and go test. - New trial kind ess-tutorial; the test the_tutorial_trial_carries_the_published_page fails when the fixture and website/docs/tutorials/first-ess-specification.md differ. - Baseline from the first accepted run: 35 tool calls, validate valid, 17 scenarios with 0 refusals, 5/5 outputs, go test 17 passed, 0 failed, 0 skipped. --- crates/agentplugins-check/src/trials.rs | 20 + trials/baseline.json | 20 + trials/ess-tutorial/fixture/tutorial.md | 1200 +++++++++++++++++++++++ trials/ess-tutorial/trial.yaml | 25 + 4 files changed, 1265 insertions(+) create mode 100644 trials/ess-tutorial/fixture/tutorial.md create mode 100644 trials/ess-tutorial/trial.yaml diff --git a/crates/agentplugins-check/src/trials.rs b/crates/agentplugins-check/src/trials.rs index eb77ce2..36f9d30 100644 --- a/crates/agentplugins-check/src/trials.rs +++ b/crates/agentplugins-check/src/trials.rs @@ -31,6 +31,8 @@ pub enum Kind { EssPipeline, /// Every ESS output, and an implementation held to the synthesized Go suite. EssFullPackage, + /// A fresh agent follows the public ESS tutorial, given only the page. + EssTutorial, /// AEP planning from an existing backlog. AepBacklog, /// Setting up `worktree` in a repository. @@ -403,6 +405,7 @@ mod tests { "ess-retrofit", "ess-pipeline", "ess-full-package", + "ess-tutorial", "aep-backlog", "worktree-onboarding", "upgrade-seeded", @@ -411,6 +414,23 @@ mod tests { } } + /// The tutorial trial hands its agent a copy of the public page; a copy that differs would + /// measure a page nobody reads. + #[test] + fn the_tutorial_trial_carries_the_published_page() { + let root = repository(); + let page = + std::fs::read_to_string(root.join("website/docs/tutorials/first-ess-specification.md")) + .expect("the tutorial page exists"); + let copy = + std::fs::read_to_string(root.join(TRIALS).join("ess-tutorial/fixture/tutorial.md")) + .expect("the trial fixture carries the page"); + assert!( + page == copy, + "trials/ess-tutorial/fixture/tutorial.md differs from the tutorial page; copy the page again" + ); + } + #[test] fn a_definition_that_does_not_hold_together_is_refused() { let root = scratch("bad"); diff --git a/trials/baseline.json b/trials/baseline.json index 7b99e76..8f21ae0 100644 --- a/trials/baseline.json +++ b/trials/baseline.json @@ -55,6 +55,26 @@ }, "unmapped": 3 }, + "ess-tutorial": { + "tool_calls": 35, + "validate": "valid", + "synthesis": { + "scenarios": 17, + "refusals": 0 + }, + "outputs": [ + "docs", + "implementation", + "openapi", + "spec", + "suite" + ], + "go_test": { + "passed": 17, + "failed": 0, + "skipped": 0 + } + }, "worktree-onboarding": { "tool_calls": 19 } diff --git a/trials/ess-tutorial/fixture/tutorial.md b/trials/ess-tutorial/fixture/tutorial.md new file mode 100644 index 0000000..fde5a2f --- /dev/null +++ b/trials/ess-tutorial/fixture/tutorial.md @@ -0,0 +1,1200 @@ +--- +title: Your first ESS specification +sidebar_label: Your first ESS specification +description: With an agent, write an Executable System Specification for a small lending library, validate it, and hold a Go implementation to the conformance suite it generates. +--- + +# Your first ESS specification + +An **Executable System Specification** (ESS) says what a system does: its records, the commands that +change them, the answers a command may refuse with, the events it publishes and the views it serves. +The `ess` command-line tool checks that the specification is consistent, generates documentation and +an OpenAPI description from it, and writes a **conformance suite**: tests that any implementation, +in any language, must pass. + +In this tutorial an agent writes the specification for you, you check it with `ess`, and a small Go +implementation is held to the suite. It takes about 30 minutes. + +**What you end with:** + +- a specification of a lending library in `spec/`, which `ess specify validate` accepts; +- documentation and an OpenAPI description generated from it; +- a Go implementation in `impl/` that passes all 17 scenarios the specification obliges; +- a check that fails when the implementation breaks a rule, which you will see happen. + +Every output block on this page is what the command printed when this page was recorded, on +2026-09-28, with `ess` 0.38.0, the `ess` plugin from this marketplace and Go 1.27. Paths are +shortened to `~` for the home directory. The files are in the +[repository](https://github.com/beyond10x/agentplugins/tree/main/website/docs/tutorials/first-ess-specification); +copy them if you want to follow without an agent. + +## What you need + +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or Codex. +- [Go](https://go.dev/dl/) 1.23 or newer, for step 6. +- An empty directory. This page uses `library/`. + +## 1. Install `ess` and its plugin + +Give your agent one sentence: + +```text +Set up Beyond10x: follow https://github.com/beyond10x/agentplugins/releases/latest/download/SETUP.md +``` + +It installs the `b10x` setup tool and asks what you want to do; answer *write specifications*. It +lists every change before it makes one. If you prefer to run it +yourself, with `b10x` already on your `PATH`: + +```console +$ b10x init ess --host claude --out plan.json +``` + +```text +b10x setup plan (hosts: claude) + +Products: + [ ] aep Plan governed work in an artifact store and deliver it in reviewed waves. + [x] ess Write, retrofit, validate and conformance-test Executable System Specifications. + [ ] worktree Create, lease, finish and safely clean isolated Git worktrees. + [ ] connectors Set up providers and invoke governed integrations through the connectors CLI. (optional) + +Findings: + change claude b10x marketplace `b10x` is not registered; add beyond10x/agentplugins + change claude b10x@b10x not installed; install 0.16.0 + change claude ess@b10x not installed; install 0.16.0 + change - ess not on PATH; install the newest release 0.38.0 + note - install method prebuilt archives (cargo is on PATH); choose with --method cargo or --method prebuilt + +Actions (4): + 1. register marketplace `b10x`: claude plugin marketplace add beyond10x/agentplugins + 2. install `b10x@b10x`: claude plugin install b10x@b10x --scope user + 3. install `ess@b10x`: claude plugin install ess@b10x --scope user + 4. install `ess`: install ess 0.38.0 into ~/.local/bin (prebuilt archive) + +Next: + /ess:init starts ess here (this session, before a restart: `b10x skill ess:init`) + /b10x:upgrade checks everything later; /b10x:init adds a product + +Apply after the user confirms this list: b10x setup apply --plan plan.json --yes +``` + +`b10x` shows the plan first and changes nothing. (This recording registered a local copy of the +marketplace; the lines above show the name yours registers, `beyond10x/agentplugins`.) Apply it +after you have read the list: + +```console +$ b10x setup apply --plan plan.json --yes +``` + +```text +snapshot: ~/.local/state/b10x/setup/1790586877 + ok 1/4 register marketplace `b10x`: claude plugin marketplace add beyond10x/agentplugins + ok 2/4 install `b10x@b10x`: claude plugin install b10x@b10x --scope user + ok 3/4 install `ess@b10x`: claude plugin install ess@b10x --scope user + ok 4/4 install `ess`: install ess 0.38.0 into ~/.local/bin (prebuilt archive) + +After: +b10x setup plan (hosts: claude) + +Products: + [ ] aep Plan governed work in an artifact store and deliver it in reviewed waves. + [x] ess Write, retrofit, validate and conformance-test Executable System Specifications. (present) + [ ] worktree Create, lease, finish and safely clean isolated Git worktrees. + [ ] connectors Set up providers and invoke governed integrations through the connectors CLI. (optional) + +Findings: + ok claude b10x@b10x current (0.16.0) + ok claude ess@b10x current (0.16.0) + ok - ess 0.38.0 at ~/.local/bin/ess is the newest release + +Nothing to change. + +Also runs (refreshes the marketplace snapshot; changes no version): + - refresh: claude plugin marketplace update b10x + +Next: + /ess:init starts ess here (this session, before a restart: `b10x skill ess:init`) + /b10x:upgrade checks everything later; /b10x:init adds a product + +Converged. New plugins load in a new session: restart Claude Code (or /reload-plugins) or start a new Codex thread. In this session, `b10x skill ` lists a plugin's skills and `b10x skill :` prints one. +``` + +Restart Claude Code so it loads the new plugin. The `ess` plugin gives the agent the +`ess:specifying` skill, which is what writes the specification in the next step. + +## 2. Ask for the specification + +Describe the system in plain words, and ask the agent to put its questions first: + +```text +I want an ESS specification for a small lending library. The library adds books and registers +members. A member borrows a book and later returns it. A book that is on loan can't be lent again, +and a book can only be withdrawn from the collection while it is on the shelf. Put it in spec/. +Before you write anything, ask me the decisions you need from me, then stop and wait for my answers. +``` + +`ess:specifying` does not guess what your words mean. It asks the open decisions in numbered rounds, +each with the answer it would take, and it looks up anything it can find for itself. This is the +first round it asked, unedited: + +
+The agent's questions (10) + +The directory is empty and `ess` 0.38.0 is installed, so there is nothing existing to build on. Here's the first round of decisions. Each one has the answer I'd pick. Reply with the numbers you want to change, or "all recommended". + +**Q1: Is a "book" a physical copy or a title?** +A copy means each physical item is tracked on its own. A title means one record per title, with its copies underneath. +*Recommended: a physical copy.* The request only talks about one book being on the shelf or on loan. Titles and copies would add an entity the request doesn't mention. + +**Q2: Is a loan its own record, or just a state of the book?** +- **Own record:** a `Loan` entity that points to the book and the member, and goes from `Active` to `Returned`. This keeps a history of who borrowed what. +- **State only:** the book's state says `OnLoan` and stores who has it. There's no history. + +*Recommended: its own record.* A borrow has to say who borrowed, and a loan record is the natural place to keep that. + +**Q3: What states does a book go through?** +*Recommended:* it starts `OnShelf` when added. Borrowing moves it to `OnLoan`, and returning moves it back to `OnShelf`. Withdrawing moves it from `OnShelf` to `Withdrawn`. `Withdrawn` is final and the book is kept, not deleted. Borrowing a book that's on loan, or withdrawing one that's on loan, is refused with a named error. + +**Q4: What does a book record?** +*Recommended:* `title` and `author` (both text), with a generated ID. Should there be an ISBN too? + +**Q5: What does a member record, and can a member leave?** +*Recommended:* only `name`, with a generated ID. Members are registered and never cancelled, because the request doesn't mention leaving. + +**Q6: Is there a limit on how many books a member can borrow at once?** +*Recommended: no limit.* + +**Q7: Are there due dates or overdue loans?** +*Recommended: no.* A loan is just active until the book comes back. + +**Q8: What does a return need?** +*Recommended: only the book.* Any active loan on that book is closed, and the returning member isn't checked. The other option is to require the loan ID or the member, and refuse it if they don't match. + +**Q9: What can be looked up?** +*Recommended:* +- the catalogue: every book with its state +- the list of members +- the active loans + +These lists are also what the conformance checks use to see the book, member and loan rules. + +**Q10: Should the spec say how the library is reached, e.g. over HTTP?** +This is what lets me generate an OpenAPI description from the spec. +*Recommended: yes.* One library service, reached over HTTP. + +I'll stop here until you answer. If any answer leads to a follow-up question (for example, an ISBN in Q4 raises whether it has to be unique), I'll ask that in a short second round before I write anything to `spec/`. + +
+ +Answer the numbers you want to change. This page answered: + +```text +All recommended, except Q2: state only. The book records which member has it; no separate loan +record. No ISBN. Go ahead: write the specification in spec/, validate it, and tell me what you wrote. +``` + +Q2 matters most. With a separate loan record, a borrow changes two records, and an ESS command +changes one; see [When a rule spans two records](#when-a-rule-spans-two-records) at the end. + +The agent then wrote the specification, validated it and reported one decision back: the +specification cannot check that the member in `BorrowBook` is registered, because a command can only +be guarded by the record it acts on. This page answered *leave it to the implementation*, and the +agent recorded that as a comment on `BorrowBook`. + +## 3. Read what it wrote + +Four files; `validate` counts the three that make up the specification, and `ess-inputs.yaml` lists +them. `system.yaml` names the system and its one domain: + +```yaml title="spec/system.yaml" +format: ess/15 +system: library +version: v1 + +domains: + - library.lending +``` + +`components.yaml` says which service accepts the commands and publishes the events. It is what lets +`ess` generate an OpenAPI description: + +```yaml title="spec/components.yaml" +components: + - component: library-service + summary: Holds the collection and the members, and lends books to members. + owns: + domains: + - library.lending + accepts: + commands: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + publishes: + events: + - library.lending.BookAdded + - library.lending.MemberRegistered + - library.lending.BookBorrowed + - library.lending.BookReturned + - library.lending.BookWithdrawn + reached_by: network +``` + +`ess-inputs.yaml` lists the files that make up the specification, so nothing generated is ever read +back in as input. Its `requires:` line pins the `ess` release; [step 8](#8-keep-it-true) explains it: + +```yaml title="spec/ess-inputs.yaml" +format: ess-inputs/2 +requires: ess 0.38.0 +specification: + - system.yaml + - components.yaml + - domains/lending.yaml +scenarios: [] +``` + +`domains/lending.yaml` is the domain itself. Read it in this order: + +- **`entities`**: `Book` and `Member`. A book's `lifecycle` lists its states (`OnShelf`, `OnLoan`, + `Withdrawn`) and the three `transitions` between them. `borrower_id` is a `references` relation + to `Member`: the book points at a member and does not own one. +- **`commands`**: each one has `outcomes`. `borrowed` moves the book through `lend`; `wrong-state` + refuses with `BookStateConflict` when the book is not on the shelf. That refusal is the rule *a + book on loan can't be lent again*, written once. +- **`events`** and **`views`**: what the service publishes, and what a caller can read back. + +
+spec/domains/lending.yaml (the whole file) + +```yaml title="spec/domains/lending.yaml" +domain: library.lending + +summary: A small lending library. Books are added to the collection, members are registered, and a + member borrows a book and later returns it. + +naming: + wire: lending + display: Lending + +types: + - name: library.lending.BookId + kind: newtype + of: Uuid + + - name: library.lending.MemberId + kind: newtype + of: Uuid + +entities: + # One physical book. There is no loan record: while the book is OnLoan it records which member + # has it, and the record is cleared when the book comes back. + - name: library.lending.Book + identity: + name: book_id + type: library.lending.BookId + fields: + - name: title + type: String + - name: author + type: String + - name: borrower_id + type: Optional + relations: + - name: borrower + kind: references + target: library.lending.Member + cardinality: one + via: borrower_id + lifecycle: + initial: OnShelf + states: [OnShelf, OnLoan, Withdrawn] + terminal: [Withdrawn] + transitions: + - name: lend + from: [OnShelf] + to: OnLoan + - name: return + from: [OnLoan] + to: OnShelf + - name: withdraw + from: [OnShelf] + to: Withdrawn + + # Members are registered and never leave. + - name: library.lending.Member + identity: + name: member_id + type: library.lending.MemberId + fields: + - name: name + type: String + lifecycle: + initial: Active + states: [Active] + terminal: [Active] + +actors: + - name: library.lending.Librarian + may: + - library.lending.AddBook + - library.lending.RegisterMember + - library.lending.BorrowBook + - library.lending.ReturnBook + - library.lending.WithdrawBook + naming: + display: Librarian + +errors: + - name: library.lending.BookStateConflict + summary: The book is not in a state this command acts from, so nothing changed. + fields: + - name: state + type: library.lending.Book.State + + - name: library.lending.BookNotFound + summary: No book in the collection has this identity. + fields: + - name: book_id + type: library.lending.BookId + +commands: + - name: library.lending.AddBook + naming: + wire: add-book + display: Add a book + input: + - name: title + type: String + - name: author + type: String + outcomes: + - name: added + creates: library.lending.Book + instance: book_id + sets: + title: input.title + author: input.author + borrower_id: {cleared: true} + emits: + - library.lending.BookAdded + payload: + library.lending.BookAdded: + book_id: {generated: true} + title: input.title + author: input.author + summary: The book is in the collection, on the shelf. + + - name: library.lending.RegisterMember + naming: + wire: register-member + display: Register a member + input: + - name: name + type: String + outcomes: + - name: registered + creates: library.lending.Member + instance: member_id + sets: + name: input.name + emits: + - library.lending.MemberRegistered + payload: + library.lending.MemberRegistered: + member_id: {generated: true} + name: input.name + summary: The member is registered and may borrow. + + - name: library.lending.BorrowBook + naming: + wire: borrow-book + display: Borrow a book + input: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + # The borrowing member must be a registered member. That is a condition on another entity + # (Member), which a command outcome cannot guard on. Decided: the implementation checks it; + # this specification does not, and no scenario covers it. + outcomes: + - name: borrowed + moves: library.lending.Book.lend + instance: book_id + sets: + borrower_id: input.member_id + emits: + - library.lending.BookBorrowed + payload: + library.lending.BookBorrowed: + book_id: input.book_id + member_id: input.member_id + summary: The book is on loan to the member. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or withdrawn, so it was not lent. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was lent. + + # A return names only the book; the member who brings it back is not checked. + - name: library.lending.ReturnBook + naming: + wire: return-book + display: Return a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: returned + moves: library.lending.Book.return + instance: book_id + sets: + borrower_id: {cleared: true} + emits: + - library.lending.BookReturned + payload: + library.lending.BookReturned: + book_id: input.book_id + summary: The book is back on the shelf and no member has it. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is not on loan, so nothing was returned. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was returned. + + - name: library.lending.WithdrawBook + naming: + wire: withdraw-book + display: Withdraw a book + input: + - name: book_id + type: library.lending.BookId + outcomes: + - name: withdrawn + moves: library.lending.Book.withdraw + instance: book_id + emits: + - library.lending.BookWithdrawn + payload: + library.lending.BookWithdrawn: + book_id: input.book_id + summary: The book has left the collection for good. + + - name: wrong-state + wrong_state: true + error: library.lending.BookStateConflict + summary: The book is on loan or already withdrawn, so it was not withdrawn. + + - name: no-such-book + unknown_instance: true + error: library.lending.BookNotFound + summary: No book has this identity, so nothing was withdrawn. + +events: + - name: library.lending.BookAdded + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + + - name: library.lending.MemberRegistered + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + + - name: library.lending.BookBorrowed + fields: + - name: book_id + type: library.lending.BookId + - name: member_id + type: library.lending.MemberId + + - name: library.lending.BookReturned + fields: + - name: book_id + type: library.lending.BookId + + - name: library.lending.BookWithdrawn + fields: + - name: book_id + type: library.lending.BookId + +views: + # The whole collection, in every state, with who has each book. + - name: library.lending.Catalogue + source: library.lending.Book + consistency: read_your_writes + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: author + type: String + - name: state + type: library.lending.Book.State + - name: borrower_id + type: Optional + naming: + wire: catalogue + display: Catalogue + + - name: library.lending.Members + source: library.lending.Member + consistency: read_your_writes + fields: + - name: member_id + type: library.lending.MemberId + - name: name + type: String + naming: + wire: members + display: Members + + # The loans in force: every book on loan and the member who has it. + - name: library.lending.BooksOnLoan + source: library.lending.Book + consistency: read_your_writes + filter: state == OnLoan + fields: + - name: book_id + type: library.lending.BookId + - name: title + type: String + - name: borrower_id + type: Optional + naming: + wire: books-on-loan + display: Books on loan +``` + +
+ +## 4. Validate it + +```console +$ ess specify validate --path spec +``` + +```text +library v1 — 3 file(s), valid +``` + +Now make a mistake, to see what a refusal looks like. In `BorrowBook`, change +`moves: library.lending.Book.lend` to `moves: library.lending.Book.loan`, a transition that does not +exist, and validate again: + +```text +$ ess specify validate --path spec +spec was refused: + - [undeclared_reference] command.library.lending.BorrowBook.outcomes.borrowed.moves: outcome `borrowed` of `library.lending.BorrowBook` takes `loan`, which `library.lending.Book` does not declare as a transition (hint: `library.lending.Book` declares: lend, return, withdraw) + - [missing_causation] entity library.lending.Book.transitions[0]: `lend` moves `library.lending.Book` to `OnLoan`, and no command outcome takes it, so nothing in this specification can make that state change happen (hint: give some outcome `moves: library.lending.Book.lend`, or delete the transition) + - error[ESS-COMMAND-001]: outcome `borrowed` of `library.lending.BorrowBook` takes `loan`, which `library.lending.Book` does not declare as a transition + `ess-domain` refuses this as `undeclared_reference` + help: `library.lending.Book` declares: lend, return, withdraw + --> domains/lending.yaml:139:5 + - error[ESS-ENTITY-005]: `lend` moves `library.lending.Book` to `OnLoan`, and no command outcome takes it, so nothing in this specification can make that state change happen + `ess-domain` refuses this as `missing_causation` + help: give some outcome `moves: library.lending.Book.lend`, or delete the transition + --> domains/lending.yaml:22:5 +``` + +It names the file, the line where the declaration starts, what is wrong, and what the book does +declare. It also reports the +consequence: nothing takes the `lend` transition any more. Change it back, and `validate` prints +`valid` again. + +## 5. Generate from it + +The same specification produces documentation and an OpenAPI description: + +```console +$ ess generate --kind openapi --path spec --out out +$ ess generate --kind docs --path spec --out out +``` + +```text +openapi/library-service.yaml — 25269 byte(s) +1 artifact(s), written to out +docs/crossings.md — 1204 byte(s) +docs/domains/library-lending.md — 16915 byte(s) +docs/index.md — 3379 byte(s) +docs/interactions.md — 1318 byte(s) +docs/topology.md — 1076 byte(s) +5 artifact(s), written to out +``` + +`ess specify graph --path spec --format mermaid` draws who may send which command and what each one +publishes: + +```mermaid +flowchart TB + subgraph who["who may ask"] + who0["library.lending.Librarian"] + end + subgraph unit0["library-service"] + cmd0["library.lending.AddBook"] + cmd1["library.lending.BorrowBook"] + cmd2["library.lending.RegisterMember"] + cmd3["library.lending.ReturnBook"] + cmd4["library.lending.WithdrawBook"] + evt0["library.lending.BookAdded"] + evt1["library.lending.BookBorrowed"] + evt2["library.lending.BookReturned"] + evt3["library.lending.BookWithdrawn"] + evt4["library.lending.MemberRegistered"] + end + who0 -->|"may invoke"| cmd0 + who0 -->|"may invoke"| cmd1 + who0 -->|"may invoke"| cmd2 + who0 -->|"may invoke"| cmd3 + who0 -->|"may invoke"| cmd4 + cmd0 -->|"added"| evt0 + cmd1 -->|"borrowed"| evt1 + cmd2 -->|"registered"| evt4 + cmd3 -->|"returned"| evt2 + cmd4 -->|"withdrawn"| evt3 +``` + +## 6. Hold an implementation to it + +Generate the conformance suite as a Go package inside the implementation's module: + +```console +$ ess verify conform synthesize --path spec --target go --out impl +``` + +```text +17 scenario(s) (0 authored), 0 refusal(s), 7 file(s) written to impl +``` + +`0 refusal(s)` means every rule in the specification became a check. A refusal would name a rule the +suite cannot test; read those before trusting a green run. + +The package, `impl/essconform`, holds the scenarios and a runner. You supply a `Target`: the methods +that let the runner send a command to your implementation and read a view back. Ask the agent: + +```text +Write a minimal in-memory Go implementation of the library in impl/ (module example.com/library) +that implements the suite's Target, and run `go test ./...` in impl/. Don't edit the generated +suite to make it pass. +``` + +It wrote three files: `go.mod`, `library.go` (the library) and `conformance_test.go` (the `Target`, +mapping the five commands and three views onto the library). + +
+impl/library.go + +```go title="impl/library.go" +// Package library is a minimal in-memory lending library: books are added and withdrawn, members +// are registered, and a member borrows a book and returns it. +package library + +import ( + "crypto/rand" + "errors" + "fmt" +) + +// BookState is where a book is in its lifecycle. +type BookState string + +const ( + OnShelf BookState = "OnShelf" + OnLoan BookState = "OnLoan" + Withdrawn BookState = "Withdrawn" +) + +// Book is one physical book. Borrower is set only while the book is OnLoan. +type Book struct { + ID string + Title string + Author string + State BookState + Borrower *string +} + +// Member is a registered member. +type Member struct { + ID string + Name string +} + +// ErrBookNotFound is returned when no book has the given identity. +var ErrBookNotFound = errors.New("book not found") + +// ErrUnknownMember is returned when a borrow names a member nobody registered. +var ErrUnknownMember = errors.New("member not registered") + +// StateConflictError is returned when a book is not in a state the operation acts from. +type StateConflictError struct { + State BookState +} + +func (e *StateConflictError) Error() string { + return fmt.Sprintf("book is %s", e.State) +} + +// Library holds the collection and the members in memory. It is not safe for concurrent use. +type Library struct { + books map[string]*Book + members map[string]*Member + // order keeps listings stable in insertion order. + bookOrder []string + memberOrder []string +} + +// New returns an empty library. +func New() *Library { + return &Library{books: map[string]*Book{}, members: map[string]*Member{}} +} + +// AddBook puts a new book on the shelf. +func (l *Library) AddBook(title, author string) Book { + b := &Book{ID: newID(), Title: title, Author: author, State: OnShelf} + l.books[b.ID] = b + l.bookOrder = append(l.bookOrder, b.ID) + return *b +} + +// RegisterMember registers a new member. +func (l *Library) RegisterMember(name string) Member { + m := &Member{ID: newID(), Name: name} + l.members[m.ID] = m + l.memberOrder = append(l.memberOrder, m.ID) + return *m +} + +// Borrow lends a book on the shelf to a member. +// +// requireMember makes an unregistered member a refusal. The specification leaves that check to the +// implementation and its conformance suite borrows for members it never registered, so the +// conformance target turns it off. +func (l *Library) Borrow(bookID, memberID string, requireMember bool) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + if requireMember { + if _, ok := l.members[memberID]; !ok { + return ErrUnknownMember + } + } + b.State = OnLoan + b.Borrower = &memberID + return nil +} + +// Return puts a book on loan back on the shelf. +func (l *Library) Return(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnLoan { + return &StateConflictError{State: b.State} + } + b.State = OnShelf + b.Borrower = nil + return nil +} + +// Withdraw takes a book on the shelf out of the collection for good. +func (l *Library) Withdraw(bookID string) error { + b, ok := l.books[bookID] + if !ok { + return ErrBookNotFound + } + if b.State != OnShelf { + return &StateConflictError{State: b.State} + } + b.State = Withdrawn + return nil +} + +// Books lists every book, in every state. +func (l *Library) Books() []Book { + out := make([]Book, 0, len(l.bookOrder)) + for _, id := range l.bookOrder { + out = append(out, *l.books[id]) + } + return out +} + +// Members lists every member. +func (l *Library) Members() []Member { + out := make([]Member, 0, len(l.memberOrder)) + for _, id := range l.memberOrder { + out = append(out, *l.members[id]) + } + return out +} + +func newID() string { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + panic(err) + } + b[6] = b[6]&0x0f | 0x40 + b[8] = b[8]&0x3f | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} +``` + +
+ +
+impl/conformance_test.go + +```go title="impl/conformance_test.go" +package library_test + +import ( + "errors" + "fmt" + "os" + "testing" + + "example.com/library" + "example.com/library/essconform" +) + +func TestConformance(t *testing.T) { + // The suite runs only once a report format is chosen; choose it here so plain `go test ./...` + // runs it. ESS_REPORT_OUT, when set, still decides where the report goes. + if os.Getenv("ESS_REPORT_FORMAT") == "" { + t.Setenv("ESS_REPORT_FORMAT", "2") + } + essconform.Run(t, func() essconform.Target { return newTarget() }) +} + +// target drives one in-memory Library through the suite's Target interface. +type target struct { + lib *library.Library +} + +func newTarget() *target { return &target{lib: library.New()} } + +func (t *target) Identity() (essconform.Identity, error) { + return essconform.Identity{Name: "example.com/library", Version: "v1"}, nil +} + +func (t *target) BeginScenario(essconform.ScenarioContext) error { return nil } +func (t *target) EndScenario(essconform.ScenarioContext) error { return nil } + +func (t *target) ExecuteCommand(req essconform.CommandRequest) (essconform.CommandResult, error) { + in := func(name string) (string, error) { + v, ok := req.Input[name].(string) + if !ok { + return "", fmt.Errorf("%s: input %q is not text", req.Command, name) + } + return v, nil + } + event := func(name string, payload map[string]essconform.Node) []essconform.ObservedEvent { + return []essconform.ObservedEvent{{Event: name, Payload: payload}} + } + + switch req.Command { + case "library.lending.AddBook": + title, err := in("title") + if err != nil { + return essconform.CommandResult{}, err + } + author, err := in("author") + if err != nil { + return essconform.CommandResult{}, err + } + b := t.lib.AddBook(title, author) + return essconform.CommandResult{ + Outcome: "added", + DirectEvents: event("library.lending.BookAdded", map[string]essconform.Node{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + }), + }, nil + + case "library.lending.RegisterMember": + name, err := in("name") + if err != nil { + return essconform.CommandResult{}, err + } + m := t.lib.RegisterMember(name) + return essconform.CommandResult{ + Outcome: "registered", + DirectEvents: event("library.lending.MemberRegistered", map[string]essconform.Node{ + "member_id": m.ID, "name": m.Name, + }), + }, nil + + case "library.lending.BorrowBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + memberID, err := in("member_id") + if err != nil { + return essconform.CommandResult{}, err + } + // The member check is the implementation's own rule, outside the specification; the + // suite borrows for members it never registered, so it is off here. + if err := t.lib.Borrow(bookID, memberID, false); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "borrowed", + DirectEvents: event("library.lending.BookBorrowed", map[string]essconform.Node{ + "book_id": bookID, "member_id": memberID, + }), + }, nil + + case "library.lending.ReturnBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Return(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "returned", + DirectEvents: event("library.lending.BookReturned", map[string]essconform.Node{"book_id": bookID}), + }, nil + + case "library.lending.WithdrawBook": + bookID, err := in("book_id") + if err != nil { + return essconform.CommandResult{}, err + } + if err := t.lib.Withdraw(bookID); err != nil { + return refusal(err) + } + return essconform.CommandResult{ + Outcome: "withdrawn", + DirectEvents: event("library.lending.BookWithdrawn", map[string]essconform.Node{"book_id": bookID}), + }, nil + } + return essconform.CommandResult{}, fmt.Errorf("unknown command %q: %w", req.Command, essconform.ErrUnsupported) +} + +// refusal maps a library error onto the declared outcome and error. +func refusal(err error) (essconform.CommandResult, error) { + var conflict *library.StateConflictError + switch { + case errors.As(err, &conflict): + return essconform.CommandResult{Outcome: "wrong-state", Error: "library.lending.BookStateConflict"}, nil + case errors.Is(err, library.ErrBookNotFound): + return essconform.CommandResult{Outcome: "no-such-book", Error: "library.lending.BookNotFound"}, nil + } + return essconform.CommandResult{}, err +} + +func (t *target) QueryView(req essconform.ViewRequest) (essconform.ViewResult, error) { + var rows []essconform.Row + switch req.View { + case "library.lending.Catalogue": + for _, b := range t.lib.Books() { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "author": b.Author, + "state": string(b.State), "borrower_id": borrower(b), + }) + } + case "library.lending.BooksOnLoan": + for _, b := range t.lib.Books() { + if b.State == library.OnLoan { + rows = append(rows, essconform.Row{ + "book_id": b.ID, "title": b.Title, "borrower_id": borrower(b), + }) + } + } + case "library.lending.Members": + for _, m := range t.lib.Members() { + rows = append(rows, essconform.Row{"member_id": m.ID, "name": m.Name}) + } + default: + return essconform.ViewResult{}, fmt.Errorf("unknown view %q: %w", req.View, essconform.ErrUnsupported) + } + return essconform.ViewResult{Rows: rows}, nil +} + +func borrower(b library.Book) essconform.Node { + if b.Borrower == nil { + return nil + } + return *b.Borrower +} + +// Every event is returned directly by the command that emits it; there is nothing to observe apart. +func (t *target) ObserveEvents(essconform.EventObservationRequest) ([]essconform.ObservedEvent, error) { + return nil, essconform.ErrUnsupported +} + +func (t *target) ConfigureExternalOutcome(essconform.ExternalOutcomeControl) error { + return essconform.ErrUnsupported +} + +func (t *target) RedeliverEvent(essconform.RedeliveryRequest) error { + return essconform.ErrUnsupported +} + +func (t *target) ObserveInvocations(essconform.InvocationObservationRequest) ([]essconform.Invocation, error) { + return nil, essconform.ErrUnsupported +} +``` + +
+ +Run the suite. The runner needs `ESS_REPORT_FORMAT=2` and stops before the first scenario without +it. This `conformance_test.go` sets it when it is missing; in your own test, set it the same way or +on the command line. + +```console +$ cd impl +$ ESS_REPORT_FORMAT=2 go test -v ./... +``` + +```text +--- PASS: TestConformance (0.01s) + --- PASS: TestConformance/library.lending.AddBook/outcome/added (0.00s) + --- PASS: TestConformance/library.lending.Book/state/OnLoan/refuses/library.lending.BorrowBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/OnLoan/refuses/library.lending.WithdrawBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/OnShelf/refuses/library.lending.ReturnBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/Withdrawn/refuses/library.lending.BorrowBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/Withdrawn/refuses/library.lending.ReturnBook (0.00s) + --- PASS: TestConformance/library.lending.Book/state/Withdrawn/refuses/library.lending.WithdrawBook (0.00s) + --- PASS: TestConformance/library.lending.Book/transition/lend/by/library.lending.BorrowBook/borrowed (0.00s) + --- PASS: TestConformance/library.lending.Book/transition/return/by/library.lending.ReturnBook/returned (0.00s) + --- PASS: TestConformance/library.lending.Book/transition/withdraw/by/library.lending.WithdrawBook/withdrawn (0.00s) + --- PASS: TestConformance/library.lending.BorrowBook/outcome/borrowed (0.00s) + --- PASS: TestConformance/library.lending.BorrowBook/outcome/no-such-book (0.00s) + --- PASS: TestConformance/library.lending.RegisterMember/outcome/registered (0.00s) + --- PASS: TestConformance/library.lending.ReturnBook/outcome/no-such-book (0.00s) + --- PASS: TestConformance/library.lending.ReturnBook/outcome/returned (0.00s) + --- PASS: TestConformance/library.lending.WithdrawBook/outcome/no-such-book (0.00s) + --- PASS: TestConformance/library.lending.WithdrawBook/outcome/withdrawn (0.00s) +PASS +ok example.com/library 0.017s +? example.com/library/essconform [no test files] +``` + +Seventeen scenarios, each named for the rule it checks, for example +`library.lending.Book/state/OnLoan/refuses/library.lending.BorrowBook`. + +## 7. Watch it catch a bug + +A passing suite only means something if it can fail. In `impl/library.go`, make `Borrow` accept a +book that is already on loan: + +```diff +- if b.State != OnShelf { ++ if b.State == Withdrawn { +``` + +```console +$ ESS_REPORT_FORMAT=2 go test ./... +``` + +```text +--- FAIL: TestConformance (0.04s) + conformance_test.go:19: library v1, 17 scenario(s), spec digest 297bae6c872cfd3000a51c7ee9caa176be4674b27863f83233cb1af4c98e693d + --- FAIL: TestConformance/library.lending.Book/state/OnLoan/refuses/library.lending.BorrowBook (0.00s) + runtime.go:1870: `library.lending.BorrowBook` does not move a `library.lending.Book` that is in `OnLoan`, and reports `library.lending.BookStateConflict` + runtime.go:2661: step 10: `library.lending.BorrowBook` took `borrowed`, and the specification says `wrong-state` +FAIL +FAIL example.com/library 0.046s +? example.com/library/essconform [no test files] +FAIL +``` + +The failing scenario names the rule, the step, and what the implementation did instead +(`took borrowed`, where the specification says `wrong-state`). Undo the change and the suite passes +again. + +To test the suite itself, rather than one bug you thought of, the `ess:hardening` skill runs +`ess verify conform mutate`: it changes the specification one rule at a time and checks that the +suite notices each change. + +## 8. Keep it true + +Pin the `ess` release the specification was written against. The `ess-inputs.yaml` in step 3 +already carries the pin; on your own project, add `requires:`, which needs `format: ess-inputs/2`: + +```yaml title="spec/ess-inputs.yaml" +format: ess-inputs/2 +requires: ess 0.38.0 +``` + +```console +$ cd spec && ess specify toolchain which +``` + +```text +ess 0.38.0 +reason: pin: `requires: ess 0.38.0` in ~/library/spec/ess-inputs.yaml +binary: ~/.local/bin/ess (this ess) +``` + +With the pin, an `ess` of a different release runs 0.38.0 for this project, and +`ess specify toolchain install` fetches it where it is missing. + +Then make the three checks part of your build, so a change to the specification or the code that +breaks the other fails there. With [Task](https://taskfile.dev): + +```yaml title="Taskfile.yml" +version: '3' + +tasks: + check: + cmds: + - ess specify validate --path spec + - ess verify conform synthesize --path spec --target go --out impl + - cmd: go test ./... + dir: impl + env: + ESS_REPORT_FORMAT: '2' +``` + +Regenerating the suite in the build means a changed specification is always tested as it now reads. + +## When a rule spans two records + +The first recording of this page asked for one more rule: *a member can hold at most three books at +a time*, with a separate loan record. The agent's model was valid, but each borrow became three +commands (one per record it changes: the member's count, the book, the loan), and synthesis refused +3 scenarios with `ESS-SYNTH-003`, because it cannot arrange a member who already holds three books. +The agent said so in its report instead of bending the model to make the number zero. + +That is how ESS treats a rule across two records today: one command changes one record, and a check +it cannot build is named, not skipped in silence. When your domain has such a rule, expect the agent +to show you the split and the refusals, and decide with it whether the rule belongs in the +specification or in the implementation. + +## Next + +- **Plan work around it.** Once a new noun has a specification, AEP plans the stories that build it. + The [golden path](../golden-path.md) walks an agent from a feature idea to a critiqued plan; a + tutorial like this one for AEP follows. +- **Specify a system that already runs.** `ess:retrofitting` derives a specification from an OpenAPI + document or from code, citing a source for every declaration. +- **Go deeper into ESS.** The [ESS documentation](https://beyond10x.github.io/docs/ess/) covers the + format, every command and the other targets (`--target typescript`). diff --git a/trials/ess-tutorial/trial.yaml b/trials/ess-tutorial/trial.yaml new file mode 100644 index 0000000..68e3bda --- /dev/null +++ b/trials/ess-tutorial/trial.yaml @@ -0,0 +1,25 @@ +name: ess-tutorial +kind: ess-tutorial +prompt: | + I'm new to ESS and I'm following the tutorial in `tutorial.md` here. Setup (step 1) is done. Work + through steps 2 to 8 in this directory as the page describes: write the specification in `spec/`, + validate it, generate its docs and OpenAPI into `out/`, synthesize the Go suite into `impl/`, write + the implementation there and run the suite. Where the page shows the questions and the answers it + gave, take the same answers instead of asking me. Do step 7's bug and undo it, then run + `ESS_REPORT_FORMAT=2 go test -v ./...` in `impl/` one last time. + + When you're done, tell me how many scenarios passed, failed and were skipped, quote any text on the + page that confused you or did not match what you saw, and paste the last validation output, the + last synthesis output and the last `go test` summary verbatim. +dir: library +fixture: fixture +setup: + - b10x init ess --host claude --out plan.json + - b10x setup apply --plan plan.json --yes +measures: [tool_calls, validate, synthesis, outputs, go_test] +outputs: + spec: spec/system.yaml + docs: out/docs + openapi: out/openapi + suite: impl/essconform + implementation: impl/go.mod From 2dcd8076e66bafa3cf195b1fa01338829dfd46f2 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:39:50 +0200 Subject: [PATCH 7/9] chore: release 0.17.0 - Workspace, all ten plugin manifests and the Skill version lines move to 0.17.0; CHANGELOG gains the 0.17.0 section. task check: exit 0 (141 tests, 17 eval cases, 5 plugins). agentplugins-check tools: exit 0 (aep 0.63.1, ess 0.38.0: 42 commands and the tutorial's specification, suite and go test, worktree 0.8.2). task site-build: exit 0. Trials on ess 0.38.0: ess-new, ess-retrofit, ess-pipeline, ess-full-package within the baseline; ess-tutorial 17/17 go test. --- CHANGELOG.md | 25 +++++++++++++++++++ Cargo.lock | 4 +-- Cargo.toml | 2 +- plugins/aep/.claude-plugin/plugin.json | 2 +- plugins/aep/.codex-plugin/plugin.json | 2 +- plugins/aep/skills/diagnosing/SKILL.md | 2 +- plugins/aep/skills/implementing/SKILL.md | 2 +- plugins/aep/skills/migrating/SKILL.md | 2 +- plugins/aep/skills/planning/SKILL.md | 2 +- plugins/b10x/.claude-plugin/plugin.json | 2 +- plugins/b10x/.codex-plugin/plugin.json | 2 +- plugins/connectors/.claude-plugin/plugin.json | 2 +- plugins/connectors/.codex-plugin/plugin.json | 2 +- plugins/ess/.claude-plugin/plugin.json | 2 +- plugins/ess/.codex-plugin/plugin.json | 2 +- plugins/worktree/.claude-plugin/plugin.json | 2 +- plugins/worktree/.codex-plugin/plugin.json | 2 +- 17 files changed, 42 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5a430e..fd34ee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [0.17.0] — 2026-09-28 + +A tutorial takes a developer from an empty directory to a validated ESS specification and a Go +implementation that passes its conformance suite, with an agent doing the writing. The onboarding +pages install through `b10x` and name the five plugins that ship. The skills are verified against +ess 0.38.0. + +- New page: [Your first ESS specification](website/docs/tutorials/first-ess-specification.md). + `ess:specifying` interviews, writes a lending-library specification, and `ess` validates it, + refuses a planted mistake, generates docs and OpenAPI, and synthesizes a Go suite of 17 + scenarios that the implementation passes and a planted bug fails. Every output is from a real + run. `agentplugins-check tools` validates the page's specification against the newest `ess` and + runs its suite, and the `ess-tutorial` trial has a fresh agent follow the page. +- `install.md` installs every command-line tool through `b10x init` and `b10x setup apply`, and + Metaharness through `b10x install metaharness`; the hand install of AEP 0.55.0 is gone. + `choose-a-plugin.md`, `trust-and-scope.md`, the b10x reference, the golden path's prerequisites, + the landing page, the footer and `b10x:routing`'s resources name `b10x`, `aep`, `ess`, + `worktree` and `connectors`. +- `agentplugins-check` refuses the retired display names "AEP Plan", "AEP Drive" and "ESS Specify". +- `verified.json` pins ess 0.38.0. From the trial round: `ess:specifying` counts "make sure it + validates" as a request for a finished specification and allows a named refusal where synthesis + cannot arrange the model; `ess:testing-conformance` generates the Go package at the module root, + requires `ESS_REPORT_FORMAT=2`, and has a refused command set `Outcome` (beyond10x/ess#186). +- This repository's planning store is `aep.project/5`. + ## [0.16.0] — 2026-09-28 The `ess`, `aep` and `worktree` plugins describe the newest CLI releases: ess 0.37.0, aep 0.63.1 diff --git a/Cargo.lock b/Cargo.lock index 5eac945..4ee420d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agentplugins-check" -version = "0.16.0" +version = "0.17.0" dependencies = [ "clap", "serde", @@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "b10x" -version = "0.16.0" +version = "0.17.0" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 9201641..3f62033 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/agentplugins-check", "crates/b10x"] [workspace.package] -version = "0.16.0" +version = "0.17.0" edition = "2021" rust-version = "1.85" license = "Apache-2.0" diff --git a/plugins/aep/.claude-plugin/plugin.json b/plugins/aep/.claude-plugin/plugin.json index de1484c..59e1031 100644 --- a/plugins/aep/.claude-plugin/plugin.json +++ b/plugins/aep/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "aep", "displayName": "AEP", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves: decomposition, plan critique, reverse engineering, story scoping, implementation and adversarial review.", - "version": "0.16.0", + "version": "0.17.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/aep/.codex-plugin/plugin.json b/plugins/aep/.codex-plugin/plugin.json index ab95013..bd31949 100644 --- a/plugins/aep/.codex-plugin/plugin.json +++ b/plugins/aep/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aep", - "version": "0.16.0", + "version": "0.17.0", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves.", "author": { "name": "Beyond10x" diff --git a/plugins/aep/skills/diagnosing/SKILL.md b/plugins/aep/skills/diagnosing/SKILL.md index 279223c..896024a 100644 --- a/plugins/aep/skills/diagnosing/SKILL.md +++ b/plugins/aep/skills/diagnosing/SKILL.md @@ -3,7 +3,7 @@ name: diagnosing description: Diagnose a hard bug or a performance regression by building a red-capable feedback loop before any hypothesis, then ranked falsifiable hypotheses, one-variable probes, a regression test at the right seam, and evidence recorded in the AEP store. Use when the user says diagnose, debug, "why is this failing", "this is slow", or reports something broken, throwing, flaky or slower than before. Not for a failing CI job whose cause is already named in its log, and not for raising conformance coverage, which is `ess:testing-conformance`. --- -**Skill version 0.16.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`. # Diagnosing a failure diff --git a/plugins/aep/skills/implementing/SKILL.md b/plugins/aep/skills/implementing/SKILL.md index 5750f17..00d3fc4 100644 --- a/plugins/aep/skills/implementing/SKILL.md +++ b/plugins/aep/skills/implementing/SKILL.md @@ -3,7 +3,7 @@ name: implementing description: Implement accepted AEP work, in one of two modes. A wave picks the stories that can be implemented at once, proposes the wave for approval, dispatches one implementor per story into its own worktree, sends each result to the adversary and merges what goes green. A drive hands one story to a governed `metaharness aep drive` run and reports the run id. Use when the operator asks to implement, build or deliver planned stories, to pick or start the next wave, to implement several stories in parallel or fan out across sub-agents, to drive a story or start a governed run, or asks why a wave's rules are instructions and a drive's are enforced. A wave proposes first and stops; a drive starts one run and reports; neither moves an artifact itself. --- -**Skill version 0.16.0** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. +**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`; a wave's stage-1 proposal quotes it. # Implementing accepted work diff --git a/plugins/aep/skills/migrating/SKILL.md b/plugins/aep/skills/migrating/SKILL.md index dfaa064..37ea32d 100644 --- a/plugins/aep/skills/migrating/SKILL.md +++ b/plugins/aep/skills/migrating/SKILL.md @@ -3,7 +3,7 @@ name: migrating description: Migrate a repository's legacy work tracking — story trees, TODO.md, plan and issue documents — into the governed AEP planning store, without deleting or rewriting the sources. Use when the user asks to migrate, import, port or convert an existing backlog into AEP, when a repository is adopting AEP and already has work written down somewhere, or when a store has been adopted beside a legacy backlog nobody retired. Read it before creating the first artifact in a repository that already tracks work in markdown. --- -**Skill version 0.16.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`. # Migrating legacy tracking into the store diff --git a/plugins/aep/skills/planning/SKILL.md b/plugins/aep/skills/planning/SKILL.md index cfe3f49..92474f8 100644 --- a/plugins/aep/skills/planning/SKILL.md +++ b/plugins/aep/skills/planning/SKILL.md @@ -3,7 +3,7 @@ name: planning description: Plan engineering work in a governed markdown artifact store — create, relate, move and validate epics, stories, tasks and initiatives through the `aep` CLI. Use when the user mentions planning, a backlog, an epic, a story, a task, decomposing or breaking down work, an artifact's status ("move this to active", "what is still in draft?", "why can't this be implemented?"), or when the project contains a `.engineering/planning/` directory. Use it at adoption too — the user asks to adopt AEP, to migrate from or replace the track plugin, to start a first backlog, or works in a repository with no `.engineering/` directory at all — because § 5 says how a first store is populated and it is worth nothing after one has been hand-written. Also use before editing any file under `.engineering/planning/`. --- -**Skill version 0.16.0** — the version in `.claude-plugin/plugin.json`. +**Skill version 0.17.0** — the version in `.claude-plugin/plugin.json`. # Planning in a governed artifact store diff --git a/plugins/b10x/.claude-plugin/plugin.json b/plugins/b10x/.claude-plugin/plugin.json index 5709eec..bc55130 100644 --- a/plugins/b10x/.claude-plugin/plugin.json +++ b/plugins/b10x/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "b10x", "displayName": "Beyond10x", "description": "Set up, upgrade and check the Beyond10x plugins and binaries, route work to them, and create portable plugins.", - "version": "0.16.0", + "version": "0.17.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/b10x/.codex-plugin/plugin.json b/plugins/b10x/.codex-plugin/plugin.json index 1de3b70..5bdc349 100644 --- a/plugins/b10x/.codex-plugin/plugin.json +++ b/plugins/b10x/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "b10x", - "version": "0.16.0", + "version": "0.17.0", "description": "Set up, upgrade and check the Beyond10x plugins and binaries, route work to them, and create portable plugins.", "author": { "name": "Beyond10x" diff --git a/plugins/connectors/.claude-plugin/plugin.json b/plugins/connectors/.claude-plugin/plugin.json index 4405013..a0a6cc3 100644 --- a/plugins/connectors/.claude-plugin/plugin.json +++ b/plugins/connectors/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.16.0", + "version": "0.17.0", "description": "Set up, inspect, and invoke governed integrations through the connectors CLI.", "author": { "name": "Beyond10x" }, "license": "Apache-2.0", diff --git a/plugins/connectors/.codex-plugin/plugin.json b/plugins/connectors/.codex-plugin/plugin.json index bb383fe..a314214 100644 --- a/plugins/connectors/.codex-plugin/plugin.json +++ b/plugins/connectors/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "connectors", - "version": "0.16.0", + "version": "0.17.0", "description": "Set up, inspect, and invoke governed integrations through the connectors CLI.", "author": { "name": "Beyond10x" }, "license": "Apache-2.0", diff --git a/plugins/ess/.claude-plugin/plugin.json b/plugins/ess/.claude-plugin/plugin.json index cd0e402..ca7b9e5 100644 --- a/plugins/ess/.claude-plugin/plugin.json +++ b/plugins/ess/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "ess", "displayName": "ESS", "description": "Write, retrofit, validate and project Executable System Specifications, and hold implementations to them with conformance suites.", - "version": "0.16.0", + "version": "0.17.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/ess/.codex-plugin/plugin.json b/plugins/ess/.codex-plugin/plugin.json index 3cd2139..e509402 100644 --- a/plugins/ess/.codex-plugin/plugin.json +++ b/plugins/ess/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ess", - "version": "0.16.0", + "version": "0.17.0", "description": "Write, retrofit, validate and project Executable System Specifications, and hold implementations to them with conformance suites.", "author": { "name": "Beyond10x" diff --git a/plugins/worktree/.claude-plugin/plugin.json b/plugins/worktree/.claude-plugin/plugin.json index f104d22..8835160 100644 --- a/plugins/worktree/.claude-plugin/plugin.json +++ b/plugins/worktree/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "worktree", "displayName": "Worktree", "description": "Create, lease, finish, audit and safely clean isolated Git worktrees through the worktree CLI.", - "version": "0.16.0", + "version": "0.17.0", "author": { "name": "Beyond10x" }, diff --git a/plugins/worktree/.codex-plugin/plugin.json b/plugins/worktree/.codex-plugin/plugin.json index 4dcfae7..23599a9 100644 --- a/plugins/worktree/.codex-plugin/plugin.json +++ b/plugins/worktree/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "worktree", - "version": "0.16.0", + "version": "0.17.0", "description": "Create, lease, finish, audit and safely clean isolated Git worktrees through the worktree CLI.", "author": { "name": "Beyond10x" From 16d7c16f1ab6447c22359090d6a4e34b0e7f5065 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:44:36 +0200 Subject: [PATCH 8/9] docs: keep the docs manifest summary within 240 characters - b10x-docs-check on PR #47 refused the new summary: "/surfaces/0/summary must NOT have more than 240 characters". The shorter one names the same plugins and the tutorial. --- b10x.docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b10x.docs.yaml b/b10x.docs.yaml index 34bd67e..5c4facf 100644 --- a/b10x.docs.yaml +++ b/b10x.docs.yaml @@ -87,4 +87,4 @@ surfaces: order: 30 sidebar: autogenerated root: website - summary: Set up, navigate and create portable plugins, or install focused AEP planning and delivery, ESS specification, worktree and connectors guidance from the curated b10x marketplace; start with a tutorial that writes and tests a first ESS specification. + summary: Set up the b10x plugins for AEP planning and delivery, ESS specification, worktrees and connectors, then write and test a first ESS specification with the tutorial. From b217b42e76bf682d7f2d0e69ea3d2e9a910df2f8 Mon Sep 17 00:00:00 2001 From: "b10x-bot[bot]" <316511680+b10x-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:48:39 +0200 Subject: [PATCH 9/9] docs: shell-session fences, as the docs system requires - b10x-docs-check refused 11 ```console fences ("console is ambiguous; use bash, shell-session, or text") in the tutorial, install.md and golden-path.md; they are shell-session. The trial fixture copies the page again. --- trials/ess-tutorial/fixture/tutorial.md | 16 ++++++++-------- website/docs/golden-path.md | 2 +- website/docs/install.md | 4 ++-- .../docs/tutorials/first-ess-specification.md | 16 ++++++++-------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/trials/ess-tutorial/fixture/tutorial.md b/trials/ess-tutorial/fixture/tutorial.md index fde5a2f..46d18d4 100644 --- a/trials/ess-tutorial/fixture/tutorial.md +++ b/trials/ess-tutorial/fixture/tutorial.md @@ -46,7 +46,7 @@ It installs the `b10x` setup tool and asks what you want to do; answer *write sp lists every change before it makes one. If you prefer to run it yourself, with `b10x` already on your `PATH`: -```console +```shell-session $ b10x init ess --host claude --out plan.json ``` @@ -83,7 +83,7 @@ Apply after the user confirms this list: b10x setup apply --plan plan.json --yes marketplace; the lines above show the name yours registers, `beyond10x/agentplugins`.) Apply it after you have read the list: -```console +```shell-session $ b10x setup apply --plan plan.json --yes ``` @@ -590,7 +590,7 @@ views: ## 4. Validate it -```console +```shell-session $ ess specify validate --path spec ``` @@ -626,7 +626,7 @@ consequence: nothing takes the `lend` transition any more. Change it back, and ` The same specification produces documentation and an OpenAPI description: -```console +```shell-session $ ess generate --kind openapi --path spec --out out $ ess generate --kind docs --path spec --out out ``` @@ -678,7 +678,7 @@ flowchart TB Generate the conformance suite as a Go package inside the implementation's module: -```console +```shell-session $ ess verify conform synthesize --path spec --target go --out impl ``` @@ -1068,7 +1068,7 @@ Run the suite. The runner needs `ESS_REPORT_FORMAT=2` and stops before the first it. This `conformance_test.go` sets it when it is missing; in your own test, set it the same way or on the command line. -```console +```shell-session $ cd impl $ ESS_REPORT_FORMAT=2 go test -v ./... ``` @@ -1110,7 +1110,7 @@ book that is already on loan: + if b.State == Withdrawn { ``` -```console +```shell-session $ ESS_REPORT_FORMAT=2 go test ./... ``` @@ -1144,7 +1144,7 @@ format: ess-inputs/2 requires: ess 0.38.0 ``` -```console +```shell-session $ cd spec && ess specify toolchain which ``` diff --git a/website/docs/golden-path.md b/website/docs/golden-path.md index 2d4c283..35ce98e 100644 --- a/website/docs/golden-path.md +++ b/website/docs/golden-path.md @@ -28,7 +28,7 @@ nobody has decided, and what the plan does with that instead of guessing. Install the `aep` and `ess` plugins and their command-line tools with `b10x` (see [Install](./install.md)): -```console +```shell-session $ b10x init aep,ess --out plan.json $ b10x setup apply --plan plan.json --yes ``` diff --git a/website/docs/install.md b/website/docs/install.md index 44888b9..86315e2 100644 --- a/website/docs/install.md +++ b/website/docs/install.md @@ -33,7 +33,7 @@ The `aep`, `ess` and `worktree` plugins drive command-line tools they do not shi and `worktree`. `b10x` installs each at its newest release, from the release's prebuilt archive checked against its `SHA256SUMS`, or with `cargo`: -```console +```shell-session $ b10x init ess --out plan.json # one product, or several: aep,ess,worktree $ b10x setup apply --plan plan.json --yes ``` @@ -49,7 +49,7 @@ front door itself needs none of the three. Drive mode hands one story to `metaharness aep drive run`. Metaharness is optional in the `aep` product, so `b10x init aep` does not install it; install it on its own: -```console +```shell-session $ b10x install metaharness $ metaharness aep drive run --help ``` diff --git a/website/docs/tutorials/first-ess-specification.md b/website/docs/tutorials/first-ess-specification.md index fde5a2f..46d18d4 100644 --- a/website/docs/tutorials/first-ess-specification.md +++ b/website/docs/tutorials/first-ess-specification.md @@ -46,7 +46,7 @@ It installs the `b10x` setup tool and asks what you want to do; answer *write sp lists every change before it makes one. If you prefer to run it yourself, with `b10x` already on your `PATH`: -```console +```shell-session $ b10x init ess --host claude --out plan.json ``` @@ -83,7 +83,7 @@ Apply after the user confirms this list: b10x setup apply --plan plan.json --yes marketplace; the lines above show the name yours registers, `beyond10x/agentplugins`.) Apply it after you have read the list: -```console +```shell-session $ b10x setup apply --plan plan.json --yes ``` @@ -590,7 +590,7 @@ views: ## 4. Validate it -```console +```shell-session $ ess specify validate --path spec ``` @@ -626,7 +626,7 @@ consequence: nothing takes the `lend` transition any more. Change it back, and ` The same specification produces documentation and an OpenAPI description: -```console +```shell-session $ ess generate --kind openapi --path spec --out out $ ess generate --kind docs --path spec --out out ``` @@ -678,7 +678,7 @@ flowchart TB Generate the conformance suite as a Go package inside the implementation's module: -```console +```shell-session $ ess verify conform synthesize --path spec --target go --out impl ``` @@ -1068,7 +1068,7 @@ Run the suite. The runner needs `ESS_REPORT_FORMAT=2` and stops before the first it. This `conformance_test.go` sets it when it is missing; in your own test, set it the same way or on the command line. -```console +```shell-session $ cd impl $ ESS_REPORT_FORMAT=2 go test -v ./... ``` @@ -1110,7 +1110,7 @@ book that is already on loan: + if b.State == Withdrawn { ``` -```console +```shell-session $ ESS_REPORT_FORMAT=2 go test ./... ``` @@ -1144,7 +1144,7 @@ format: ess-inputs/2 requires: ess 0.38.0 ``` -```console +```shell-session $ cd spec && ess specify toolchain which ```