framework: a design with one example through every layer #87

Open
opened 2026-10-05 21:07:18 +00:00 by orion · 13 comments
Collaborator

Context

A REST controller, the service, a repository and the database, so readability for somebody from Spring or Symfony can be judged before the framework exists.

Source

Acceptance criteria

  • The worked example in FRAMEWORK.md
  • The owner signs off before building

Dependencies

None known.

Moved from the planning lists of the docs; the design record stays the specification.

## Context A REST controller, the service, a repository and the database, so readability for somebody from Spring or Symfony can be judged before the framework exists. ## Source - [FRAMEWORK.md section 3](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/FRAMEWORK.md#L39) ## Acceptance criteria - [ ] The worked example in FRAMEWORK.md - [ ] The owner signs off before building ## Dependencies None known. _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), source FRAMEWORK.md section 3, not a duplicate. A design issue: the worked example is written by the loop in refine; the record asks for the owner's sign-off before building (product direction), which refine files as an owner decision once the example exists. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: in scope (author orion, label loop), source FRAMEWORK.md section 3, not a duplicate. A design issue: the worked example is written by the loop in refine; the record asks for the owner's sign-off before building (product direction), which refine files as an owner decision once the example exists. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261029 until 2026-10-29T10:20Z

[loop:torbscript] claim refine by session:loop-torbscript-20261029 until 2026-10-29T10:20Z
Author
Collaborator

[loop:torbscript] decided by the loop: the worked example is written to this specification, in one docs-only PR; no owner decision is needed before it is written. The owner's sign-off (second acceptance criterion) is a step after the record exists and is filed then, see "Sign-off" below. Why: every choice below follows WEB.md section 13, URI.md section 11 or the guideline in FRAMEWORK.md section 1, so none of it is product direction.

What the PR delivers

  1. docs/design/FRAMEWORK.md section 3 becomes "The worked example" (the stub's "Before it is built" text is replaced; the status line says the example exists and the framework is still planned). One scenario through every layer, the orders of WEB.md (Route, NewOrder, Shop), so the record and WEB.md read as one story.
  2. A checkable package examples/framework/ (a workspace member through examples/*, so tier A's check . gates it; no gate change). It holds the example's source files plus stand-ins for the parts of std that do not exist yet (web handler/request types where std/web is missing, a database driver in the shape of URI.md section 11's Connect/Connection, the module DSL). The record quotes excerpts of these files; the files are the source of truth, so the record cannot drift from code that checks. A layer that cannot be expressed today is shown in the record marked "does not check yet: " and listed as a language/library gap (the way WEB.md lists F1..F13), never silently hand-waved. No compiler or std change.
  3. A side-by-side for the judging reader: the same controller, service and repository in Spring Boot (Java) and Symfony (PHP), then a table "their word / our word / what differs and why" (@Autowired / trait-typed constructor field; @Transactional / closure; application-prod.yml / profile receiver script; @GetMapping / route(template, to:); Doctrine mapping / mapping DSL; @WebMvcTest / controller test slice; and so on).

The layers the example must cover (acceptance criteria of the PR)

  • Configuration with profiles: a receiver script (config.trb, SCRIPTS.md, examples/config-dsl) per profile (dev, prod); the database is a service URI (postgres://..., URI.md section 11), secrets by secret, never inline.
  • Module wiring: a module DSL on receiver closures (the project.trb style, FRAMEWORK.md section 1) assembles repository -> service -> controller by constructors and traits; no annotation, reflection or scanning. Shows the compile error for a missing dependency at the line that forgot it (a real diagnostic if the checker gives one on the stand-ins, otherwise the intended message, marked as such).
  • REST controller: routes as URI templates route("/orders/{id}", to: Route.Order) (WEB.md D5), handlers (Request) => Task<Result<Response, WebError>>, JSON in and out through Encode/Decode, form/body validation by types (WEB.md section 3.5), error mapping From<OrderError> for WebError.
  • Middleware/filter: one (logging or auth) with wrap, as WEB.md section 3.3.
  • Service: a trait with trait-typed fields as dependencies, the business rule (e.g. stock/total check), a transaction as a closure over the connection (no annotation), a typed OrderError, one domain event (OrderPlaced) published and consumed without strings or registration by scanning.
  • Repository: a trait (the hexagonal port) plus the database adapter; queries are quoted expressions as in examples/query-provider (filter { _.customer == name }), the table mapping is a builder DSL, not annotations.
  • Database and migrations: connection from the URI through Connect, one migration written in a typed DSL, run at startup in the dev profile.
  • Tests: a controller slice (fake service, app.answer(request) as WEB.md section 3.9), a service test over an in-memory repository, a repository slice against an in-memory driver - each starting one layer without the others.
  • A readability section that judges the result honestly against the Spring/Symfony reading: where a Spring developer is at home, where it differs on purpose (compile-time wiring, panics end the process per WEB.md D10), and what it costs in lines.

Constraints for the implementer

  • Docs plus one example package only; does not touch the seed (no syntax change, no new native, no driver flag, no std name looked up by string). Never Closes #87 in the PR: use Refs #87 (see "Sign-off").
  • Reuse decided records, do not reopen them: WEB.md section 13 (routing by URI template, builder-only markup, Elm-style live UI), URI.md section 11 (Connect/Connection/Cache stay out of std until a second driver exists, so the database driver is a stand-in in the shape written there), ENCODING.md, SCRIPTS.md. If the example finds a contradiction in one of them, write it into the record as a finding.
  • Out of scope: ORM implementation, actual std/web, std/orm or DI code, a real database driver, the live UI layer, caches and message brokers beyond naming where they would plug in.
  • Format and rules: TorbScript style of CLAUDE.md (no semicolons, no squeezed one-liners), torb format --check on the touched .trb files, lines <= 120, keep each file's line endings. English; no private links. Commits: docs(framework): worked example through every layer and, if split, docs(examples): add the framework example package (max 100 characters, Conventional Commits). docs/design/index.md already lists the record; if the docs skill gate reports drift, regenerate skills/ as CLAUDE.md says (CI shows it; do not build locally). Gates run in forge CI only.
  • Model: the record is an architecture document, so the contract's Opus-for-architecture rule applies; the implementer may start on Opus.

Sign-off (the second acceptance criterion)

Approving a framework design is product direction, an owner question per the contract. It cannot be answered before the record exists, and it is not part of the PR. So: the PR merges with Refs #87; the issue stays open after the merge; the next tick files [loop:torbscript] owner decision: (what the example shows, plain-language options: approve as is / change a named point / stop, with a recommendation) plus a blocking question_ask block, and closes the issue once the answer is recorded.

[loop:torbscript] decided by the loop: **the worked example is written to this specification, in one docs-only PR; no owner decision is needed before it is written.** The owner's sign-off (second acceptance criterion) is a step *after* the record exists and is filed then, see "Sign-off" below. Why: every choice below follows WEB.md section 13, URI.md section 11 or the guideline in FRAMEWORK.md section 1, so none of it is product direction. ## What the PR delivers 1. **`docs/design/FRAMEWORK.md` section 3 becomes "The worked example"** (the stub's "Before it is built" text is replaced; the status line says the example exists and the framework is still planned). One scenario through every layer, the **orders** of WEB.md (`Route`, `NewOrder`, `Shop`), so the record and WEB.md read as one story. 2. **A checkable package `examples/framework/`** (a workspace member through `examples/*`, so tier A's `check .` gates it; no gate change). It holds the example's source files plus stand-ins for the parts of `std` that do not exist yet (web handler/request types where `std/web` is missing, a database driver in the shape of URI.md section 11's `Connect`/`Connection`, the module DSL). The record quotes excerpts of these files; the files are the source of truth, so the record cannot drift from code that checks. A layer that cannot be expressed today is shown in the record marked "does not check yet: <what is missing>" and listed as a language/library gap (the way WEB.md lists F1..F13), never silently hand-waved. No compiler or `std` change. 3. **A side-by-side for the judging reader**: the same controller, service and repository in Spring Boot (Java) and Symfony (PHP), then a table "their word / our word / what differs and why" (`@Autowired` / trait-typed constructor field; `@Transactional` / closure; `application-prod.yml` / profile receiver script; `@GetMapping` / `route(template, to:)`; Doctrine mapping / mapping DSL; `@WebMvcTest` / controller test slice; and so on). ## The layers the example must cover (acceptance criteria of the PR) - [ ] **Configuration with profiles**: a receiver script (`config.trb`, SCRIPTS.md, examples/config-dsl) per profile (`dev`, `prod`); the database is a service URI (`postgres://...`, URI.md section 11), secrets by `secret`, never inline. - [ ] **Module wiring**: a module DSL on receiver closures (the `project.trb` style, FRAMEWORK.md section 1) assembles repository -> service -> controller by constructors and traits; **no annotation, reflection or scanning**. Shows the compile error for a missing dependency at the line that forgot it (a real diagnostic if the checker gives one on the stand-ins, otherwise the intended message, marked as such). - [ ] **REST controller**: routes as URI templates `route("/orders/{id}", to: Route.Order)` (WEB.md D5), handlers `(Request) => Task<Result<Response, WebError>>`, JSON in and out through `Encode`/`Decode`, form/body validation by types (WEB.md section 3.5), error mapping `From<OrderError> for WebError`. - [ ] **Middleware/filter**: one (logging or auth) with `wrap`, as WEB.md section 3.3. - [ ] **Service**: a trait with trait-typed fields as dependencies, the business rule (e.g. stock/total check), a **transaction** as a closure over the connection (no annotation), a typed `OrderError`, one **domain event** (`OrderPlaced`) published and consumed without strings or registration by scanning. - [ ] **Repository**: a trait (the hexagonal port) plus the database adapter; queries are quoted expressions as in `examples/query-provider` (`filter { _.customer == name }`), the table mapping is a builder DSL, not annotations. - [ ] **Database and migrations**: connection from the URI through `Connect`, one migration written in a typed DSL, run at startup in the `dev` profile. - [ ] **Tests**: a controller slice (fake service, `app.answer(request)` as WEB.md section 3.9), a service test over an in-memory repository, a repository slice against an in-memory driver - each starting one layer without the others. - [ ] **A readability section** that judges the result honestly against the Spring/Symfony reading: where a Spring developer is at home, where it differs on purpose (compile-time wiring, panics end the process per WEB.md D10), and what it costs in lines. ## Constraints for the implementer - Docs plus one example package only; **does not touch the seed** (no syntax change, no new native, no driver flag, no std name looked up by string). Never `Closes #87` in the PR: use `Refs #87` (see "Sign-off"). - Reuse decided records, do not reopen them: WEB.md section 13 (routing by URI template, builder-only markup, Elm-style live UI), URI.md section 11 (`Connect`/`Connection`/`Cache` stay out of `std` until a second driver exists, so the database driver is a stand-in in the shape written there), ENCODING.md, SCRIPTS.md. If the example finds a contradiction in one of them, write it into the record as a finding. - Out of scope: ORM implementation, actual `std/web`, `std/orm` or DI code, a real database driver, the live UI layer, caches and message brokers beyond naming where they would plug in. - Format and rules: TorbScript style of CLAUDE.md (no semicolons, no squeezed one-liners), `torb format --check` on the touched `.trb` files, lines <= 120, keep each file's line endings. English; no private links. Commits: `docs(framework): worked example through every layer` and, if split, `docs(examples): add the framework example package` (max 100 characters, Conventional Commits). `docs/design/index.md` already lists the record; if the docs skill gate reports drift, regenerate `skills/` as CLAUDE.md says (CI shows it; do not build locally). Gates run in forge CI only. - Model: the record is an architecture document, so the contract's Opus-for-architecture rule applies; the implementer may start on Opus. ## Sign-off (the second acceptance criterion) Approving a framework design is product direction, an owner question per the contract. It cannot be answered before the record exists, and it is not part of the PR. So: the PR merges with `Refs #87`; **the issue stays open after the merge**; the next tick files `[loop:torbscript] owner decision:` (what the example shows, plain-language options: approve as is / change a named point / stop, with a recommendation) plus a blocking `question_ask` block, and closes the issue once the answer is recorded.
Author
Collaborator

[loop:torbscript] refine: the requirements are written up in the comment above (deliverable: FRAMEWORK.md section 3 plus a checkable examples/framework/ package, layer-by-layer acceptance checklist, constraints, Spring/Symfony side-by-side). The earlier wait on the full ready buffer is gone. No owner decision is needed to write the record; the owner sign-off is filed after the PR merges (the issue stays open until then, PR says Refs #87). Label ready set. Worker usage (RE, sonnet): about 75k tokens (estimate). next: ready

[loop:torbscript] refine: the requirements are written up in the comment above (deliverable: FRAMEWORK.md section 3 plus a checkable `examples/framework/` package, layer-by-layer acceptance checklist, constraints, Spring/Symfony side-by-side). The earlier wait on the full ready buffer is gone. No owner decision is needed to write the record; the owner sign-off is filed after the PR merges (the issue stays open until then, PR says `Refs #87`). Label `ready` set. Worker usage (RE, sonnet): about 75k tokens (estimate). next: ready
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T17:51Z

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T17:51Z
Author
Collaborator

[loop:torbscript] release implement: the previous worker ended without a branch or PR (lease expired 2026-10-29T17:51Z), nothing was lost

[loop:torbscript] release implement: the previous worker ended without a branch or PR (lease expired 2026-10-29T17:51Z), nothing was lost
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261029-0858 until 2026-10-29T23:55:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261029-0858 until 2026-10-29T23:55:00Z
Author
Collaborator

[loop:torbscript] release implement: worker timed out without a PR (2026-10-29T18:25Z); #87 returns to the ready list, no slot free this tick

[loop:torbscript] release implement: worker timed out without a PR (2026-10-29T18:25Z); #87 returns to the ready list, no slot free this tick
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261029-0858 until 2026-10-29T20:40:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261029-0858 until 2026-10-29T20:40:00Z
Author
Collaborator

[loop:torbscript] release implement: PR #604 opened, head b3ed7db0, CI not yet seen (nothing built locally), left: owner sign-off on the readability judgement; fixes for the first CI run (check, format --check, test)

[loop:torbscript] release implement: PR #604 opened, head b3ed7db0, CI not yet seen (nothing built locally), left: owner sign-off on the readability judgement; fixes for the first CI run (check, format --check, test)
Author
Collaborator

[loop:torbscript] claim fix by session:loop-torbscript-20261029-0858 until 2026-10-30T01:30:00Z

[loop:torbscript] claim fix by session:loop-torbscript-20261029-0858 until 2026-10-30T01:30:00Z
Author
Collaborator

[loop:torbscript] release fix: PR #604's first CI run (2346, tier A) went red because examples/framework had never been compiled. The fixes are folded into the three owning commits, and the new head is 0f86e5cc (still on base 5ba7fdae; tools/check-commits.sh is clean).

Fixed:

  • OrderRepository, Orders and Endpoint are now shared traits. The adapter, the service and the controller each hold the Database object, and trait rule 17 does not let a value of an ordinary trait hide an object.
  • web.trb: return Fail WebError.badRequest("...") now has parentheses. Without them the file did not parse.
  • Tests (assert quotes its condition): every assert that touched a shared object, a non-Encode value or a { _ => ... } closure now binds the answer to a const first. An unused expect is now const _ = .... The rollback test now inserts through the repository instead of database.insert inside database.transaction, which broke the exclusivity rule.
  • torb format applied to the 7 files of the package.
  • FRAMEWORK.md section 3 now matches the example: the port is a shared trait and the prose says why, the call syntax is the formatted one, the controller excerpt is the same as web.trb, and gap G11 (the assert quotation in tests) was added to 3.8.
  • test compiler/tests failed only on "The whole repository ... nothing is reported", which is the same check . errors and not a flake.

Verified locally with the nightly torb 0.1.0 of 2026-10-29, not the PR's own build: torb check examples/framework reports 15 files and no problems, format --check changes 0 of 15 files, and torb test in the VM passes 14 of 14.

Unverified: the native tests (there is no C compiler here), the repo-wide gates and docs checks, and whether the branch's own compiler behaves differently from the nightly.

next: waiting (CI on PR #604)

[loop:torbscript] release fix: PR #604's first CI run (2346, tier A) went red because examples/framework had never been compiled. The fixes are folded into the three owning commits, and the new head is `0f86e5cc` (still on base `5ba7fdae`; `tools/check-commits.sh` is clean). **Fixed:** - `OrderRepository`, `Orders` and `Endpoint` are now `shared trait`s. The adapter, the service and the controller each hold the `Database` object, and trait rule 17 does not let a value of an ordinary trait hide an object. - `web.trb`: `return Fail WebError.badRequest("...")` now has parentheses. Without them the file did not parse. - Tests (`assert` quotes its condition): every assert that touched a shared object, a non-`Encode` value or a `{ _ => ... }` closure now binds the answer to a `const` first. An unused `expect` is now `const _ = ...`. The rollback test now inserts through the repository instead of `database.insert` inside `database.transaction`, which broke the exclusivity rule. - `torb format` applied to the 7 files of the package. - FRAMEWORK.md section 3 now matches the example: the port is a `shared trait` and the prose says why, the call syntax is the formatted one, the controller excerpt is the same as `web.trb`, and gap G11 (the `assert` quotation in tests) was added to 3.8. - `test compiler/tests` failed only on "The whole repository ... nothing is reported", which is the same `check .` errors and not a flake. **Verified locally** with the nightly `torb 0.1.0` of 2026-10-29, not the PR's own build: `torb check examples/framework` reports 15 files and no problems, `format --check` changes 0 of 15 files, and `torb test` in the VM passes 14 of 14. **Unverified:** the native tests (there is no C compiler here), the repo-wide gates and docs checks, and whether the branch's own compiler behaves differently from the nightly. next: waiting (CI on PR #604)
Author
Collaborator

[loop:torbscript] wait: PR #604 is merged (worked example in examples/framework and FRAMEWORK.md); the one open criterion is the owner sign-off on the readability judgement, which is Torben's call.
next: waiting (owner sign-off, re-check 2026-11-06)

[loop:torbscript] wait: PR #604 is merged (worked example in examples/framework and FRAMEWORK.md); the one open criterion is the owner sign-off on the readability judgement, which is Torben's call. next: waiting (owner sign-off, re-check 2026-11-06)
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#87
No description provided.