You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -81,6 +84,9 @@ You choose tools for **roles**. Only the directories for selected roles are crea
81
84
|`formal-methods`| Specification & verification | no |
82
85
83
86
87
+
**Fullstack tools.** Some tools (e.g. Scalus) implement both on-chain and off-chain in one language. Pick such a tool for both roles — `--fullstack scalus`, or `--on-chain scalus --off-chain scalus` — and instead of two folders you get a single unified **`protocol/`** component (one build, shared types). It still writes the standard `blueprint/plutus.json` and reads `.env`, so it composes with devnet, formal-methods, and infrastructure exactly like a normal on-chain component.
88
+
89
+
84
90
## Status
85
91
86
92
Early prototype. Tools currently in the registry (✅ available · ⬜ planned). Infrastructure is multi-tool and provisioned via `cardano-up`; every other role takes one tool.
Copy file name to clipboardExpand all lines: docs/ADDING_A_TOOL.md
+32Lines changed: 32 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -49,6 +49,28 @@ template = "mytool/on-chain"
49
49
template = "mytool/off-chain"
50
50
```
51
51
52
+
### Fullstack tools (one component for both on-chain and off-chain)
53
+
54
+
If your tool implements **both** on-chain and off-chain in one language and one build (e.g. Scalus), you can offer a **fullstack** experience: add a `[fullstack]` table with a third template. When a user assigns your tool to both roles (`--fullstack mytool`, or `--on-chain mytool --off-chain mytool`), the two collapse into a single unified **`protocol/`** component built from that template, instead of two folders that hand off through `blueprint/plutus.json`.
55
+
56
+
```toml
57
+
[roles.on-chain]
58
+
template = "mytool/on-chain"# used when mytool fills on-chain only (e.g. with a different off-chain tool)
59
+
60
+
[roles.off-chain]
61
+
template = "mytool/off-chain"# used when mytool fills off-chain only
62
+
63
+
[fullstack]
64
+
template = "mytool/fullstack"# used when mytool fills BOTH → one protocol/ component
65
+
```
66
+
67
+
All three shapes are first-class, so you provide three templates. Rules:
68
+
-`[fullstack]`**requires both**`[roles.on-chain]` and `[roles.off-chain]` (validated at load).
69
+
-`protocol` is **not** a role — you never write `[roles.protocol]`. It is a fused component the CLI derives from the two role assignments.
70
+
- The `protocol/` component must still honor the interface contract (Step 3): its `build`**writes `../blueprint/plutus.json`** and it reads/writes `../.env`, so it composes with devnet/formal/infra like a normal on-chain producer. Internally it may share types between its on-chain and off-chain halves and skip the blueprint round-trip — that private short-cut is the whole point — but the external seam is mandatory.
71
+
- Your tool's `detect` signatures must be present in the fullstack template too, so `doctor` recognizes the tool inside a `protocol/` directory.
72
+
- Adding a fullstack tool is still **pure data** — no Rust changes.
const indexerUrl =process.env.INDEXER_URL; // set → a local endpoint is up
157
179
```
158
180
181
+
**Fullstack tools** (a `[fullstack]` template, rendered into `protocol/`) must satisfy the on-chain contract from the fused component: `build`**writes `../blueprint/plutus.json`**, and the component reads/writes `../.env` like an off-chain consumer. Internally it may link its on-chain and off-chain halves directly (shared types, no blueprint round-trip); the blueprint file is written for the *other* roles (devnet/formal/infra), which still consume it. The three mandatory Justfile targets (`build`/`test`/`clean`) apply, and `dev` is optional as usual.
182
+
159
183
**Tools that provision a local chain endpoint** must write the connection details to `../.env` during `dev`. This applies to **infrastructure** services and, equally, to a **local devnet in the devnet role** (e.g. Yaci DevKit) — the seam is the `.env` keys, not the role. Use the standard variable names:
160
184
161
185
| Variable | Meaning |
@@ -220,6 +244,7 @@ Any rendered file (one whose `source` ends in `.jinja`) can reference the follow
220
244
{# Flags for conditional sections #}
221
245
{{ has_on_chain }}
222
246
{{ has_off_chain }}
247
+
{{ has_fullstack }} {# one tool fills both on-chain + off-chain → protocol/ #}
223
248
{{ has_infra }}
224
249
{{ has_devnet }}
225
250
{{ has_formal_methods }}
@@ -235,6 +260,13 @@ Any rendered file (one whose `source` ends in `.jinja`) can reference the follow
235
260
{{ off_chain.language }}
236
261
{{ off_chain.dir }} {# "off-chain" #}
237
262
263
+
{# Fullstack: set instead of on_chain/off_chain when one tool fills both.
264
+
When has_fullstack is true, has_on_chain and has_off_chain are false. #}
- Each role maps to a kebab string (`on-chain`, `formal-methods`, …) for TOML/flags, a `Display` name for humans, and a contract directory (`dir()` → §4).
107
107
-**The enum is the sole source of truth for the role vocabulary: roles are *not* defined by the repository data.** A tool's `[roles.<kebab>]` blocks merely *reference* existing roles; the registry cannot introduce a new one. Role strings are validated against the enum at load time via `Role::from_kebab` (an unknown role → `RegistryError::UnknownRole`). What the registry data determines is which *tools* exist and which of these fixed roles each can fill, not the set of roles itself.
108
108
- Adding a role is therefore a deliberate code change touching every site that names roles: a new `Role` variant + `Role::ALL` + `from_kebab`/`as_kebab`/`dir()`/`Display`, a `contract::DIR_*` constant, `TemplateContext` handling, a CLI flag, and the web query params. Adding a *tool*, by contrast, is pure data. The role set is small and grows rarely.
109
+
- The **fullstack `protocol/`** component (§3.2) is a related but distinct kind of code change: it adds a `contract::DIR_PROTOCOL` constant and `TemplateContext` handling, but **no**`Role` variant — it is a *fused component* derived from two existing roles, not a sixth role. New fullstack *tools* remain pure data (a `[fullstack]` table + a template dir).
109
110
110
111
### 3.2 Tools
111
112
@@ -115,12 +116,15 @@ pub struct ToolDef {
115
116
publanguages:Vec<String>,
116
117
pubnix_packages:Vec<String>, // toolchains for the Nix dev shell
117
118
pubroles:HashMap<Role, RoleConfig>, // which roles this tool can fill
pubstructRoleConfig { pubtemplate:String } // path under templates/
120
122
```
121
123
122
124
`system_deps` is declared in the tool TOML and consumed by the **doctor** (§8). One tool can fill multiple roles (e.g. Scalus: on-chain + off-chain), each with its own template path.
123
125
126
+
**Fullstack tools.** A tool that fills both on-chain and off-chain may also declare a `[fullstack]` template. When the *same* tool fills both roles, the two collapse into one **fused `protocol/` component** (built from `fullstack.template`) instead of two folders — the value of a same-language stack (shared types, one build, no `plutus.json` round-trip between the halves). This is a tool *capability*, **not a new role**: `protocol` has no `Role` variant and `Role::ALL` stays at five; the collapse is *derived* from the selection at planning time, mirroring the infrastructure aggregation (§6.2). The `protocol/` component still conforms to the interface contract — its `build` writes the blueprint and it reads/writes `.env` — so it composes with every other role like a normal on-chain producer (§4).
127
+
124
128
### 3.3 Selection (the resolved user choice)
125
129
126
130
```rust
@@ -157,10 +161,16 @@ The contract is a set of constants every template conforms to. It is the seam th
`DIR_PROTOCOL` is the one directory not backed by a `Role` (§3.2): a fullstack tool's fused
170
+
component. Its `build` still produces `BLUEPRINT_PATH` — it is the on-chain producer for its
171
+
project — so it is a full contract citizen; fullstack only privatizes the *internal*
172
+
on-chain↔off-chain link between its two halves.
173
+
164
174
**Compliance checklist (enforced mechanically by contract-compliance tests):**
165
175
166
176
-**Every template** ships a `Justfile` exposing `build`, `test`, `clean`, and works **independently** (its `just build` succeeds with no other roles present). A no-op-for-this-tool target among those three still exists (printing a message is fine). **`dev` is optional** — provided only when the tool has a real watch/daemon/devnet mode. The **top level** aggregates only `build`/`test`/`clean`; `dev`, where present, is per-component (§6.2 / TECH_SPEC §7.2), never aggregated.
@@ -225,7 +235,10 @@ Walks `selection.assignments`, resolves each tool against the registry, and buil
2.**Blueprint dir**: `blueprint/.gitkeep`, emitted whenever the selection includes any blueprint-producing-or-consuming role: i.e., any role **except** infrastructure (equivalently: present unless the project is infrastructure-only).
228
-
3.**Role layers**: for each assignment, read the template's `manifest.toml` and add its files. **Infrastructure is special — it aggregates**: all selected infra tools share one driver template (`_infra/cardano-up`) emitted **once** at `infra/`, rendered over the full set (`TemplateContext.infra_tools`). This is because the infra engine (`cardano-up`) manages the whole stack as a single unit, not per service. Every other role is one tool → one directory. See `docs/proposals/infra-via-cardano-up.md`.
238
+
3.**Role layers**: for each assignment, read the template's `manifest.toml` and add its files. Two cases aggregate instead of emitting one directory per assignment:
239
+
-**Fullstack collapses**: when the same tool fills on-chain + off-chain and declares a `[fullstack]` template (§3.2), the two assignments emit **one**`protocol/` component (`fullstack.template`), emitted once on the first of the pair; the second is skipped. `has_fullstack` is set; `has_on_chain`/`has_off_chain` are not.
240
+
-**Infrastructure is special — it aggregates**: all selected infra tools share one driver template (`_infra/cardano-up`) emitted **once** at `infra/`, rendered over the full set (`TemplateContext.infra_tools`). This is because the infra engine (`cardano-up`) manages the whole stack as a single unit, not per service.
241
+
Every other role is one tool → one directory.
229
242
4.**Optional layer**: `flake.nix` + `.envrc` when `nix` is set.
230
243
231
244
No I/O: only embedded assets are read. `render` is set from the `.jinja` extension.
@@ -290,7 +303,7 @@ doctor/
290
303
-**Recipes live in data.** Per-dep recipes are an embedded TOML file (`registry/deps.toml`), keyed by dep id: `binaries` (presence check), `docs` (universal fallback), and an ordered `install` list of `{ installer = arg }` methods. Installer names are validated against the code enum at load (unknown installer → load error, like an unknown `Role`). See §8.1 for why code/data split this way.
291
304
- **Resolver (`resolve`, pure, recursive).** A dep is present if any of its `binaries` is on `PATH`. For a missing dep, the walk is **two-pass over the ordered `install` methods**: Pass 1 returns the first method whose installer is **detected** (a one-step command); only if none is directly available does Pass 2 walk the methods again and, for the first **bootstrappable** installer, recurse to satisfy one of its `bootstrap` deps and prepend those steps. The result is an ordered, possibly multi-step **plan** (e.g. `aiken` missing with no `nix`/`aikup` → install `aikup` via `npm`, then `aikup install latest`). Two passes — rather than bootstrapping each method before trying later ones — are exactly why the `nix` path needs no `aikup` when `nix` is present (a single method is still chosen per dep). Cycle detection guards the walk; `docs` is the fallback when nothing resolves (advice never empty, FR-20). Version constraints are out of scope for v1 (presence only); doctor output is **host-dependent by design** (not part of the byte-identical generation contract). Full algorithm in TECH_SPEC §9.4.
292
305
-**Infrastructure deps** install via `cardano-up` (the `CardanoUp` installer); `cardano-up` is itself a dep in `registry/deps.toml` (bootstrappable via its own installer methods). Auto-installing it arrives with the DX.05 install command; bootstrapping `cardano-up` when absent may follow post-RC (ROADMAP).
293
-
-**Project scan (no metadata file).** The standalone doctor derives its target set by scanning the cwd: each contract role directory present is matched against the `detect` signatures of the tools that fill that role; an identified tool contributes its `system_deps`, and an unmatched directory is reported as *unrecognized*. Signatures are tool-author **data** in `registry/tools/<tool>.toml` (`detect = [...]`), either a bare path (existence) or `{ file, contains }` (content) — the content form keeps generic filenames like `package.json` from mislabeling foreign projects without claiming to validate viability. Full algorithm + schema in TECH_SPEC §9.6.
306
+
-**Project scan (no metadata file).** The standalone doctor derives its target set by scanning the cwd: each contract role directory present is matched against the `detect` signatures of the tools that fill that role; an identified tool contributes its `system_deps`, and an unmatched directory is reported as *unrecognized*. A **`protocol/`** directory (the fullstack fused component, §3.2) is scanned by a dedicated branch against the tools that declare a `[fullstack]` template; the identified tool is a real registry tool, so its `system_deps` feed the required set through the normal `registry.get` path (unlike the synthetic infra driver). Signatures are tool-author **data** in `registry/tools/<tool>.toml` (`detect = [...]`), either a bare path (existence) or `{ file, contains }` (content) — the content form keeps generic filenames like `package.json` from mislabeling foreign projects without claiming to validate viability. Full algorithm + schema in TECH_SPEC §9.6.
294
307
-**Boundary:**`mod.rs`/`installers.rs`/`catalog.rs` are pure and unit-tested with synthetic `Environment`s; only `probe.rs` touches the system (PATH/OS probes + the project scan). `doctor` depends on `registry`/`contract`, never on `cli`.
0 commit comments