Skip to content

Commit 615ba0b

Browse files
authored
Merge pull request #32 from input-output-hk/add-fullstack
feat: add `fullstack` option that combines `on-chain` and `off-chain`…
2 parents 526ac03 + 5b3d20f commit 615ba0b

33 files changed

Lines changed: 1022 additions & 223 deletions

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ cardano-init
5858
# One-shot (non-interactive)
5959
cardano-init --name my-protocol --on-chain aiken --off-chain meshjs --devnet yaci
6060

61+
# Fullstack: one tool for both on-chain and off-chain, as a single `protocol/` component
62+
cardano-init --name my-protocol --fullstack scalus
63+
6164
# Preview what would be generated, without writing
6265
cardano-init --name my-protocol --on-chain aiken --dry-run
6366

@@ -81,6 +84,9 @@ You choose tools for **roles**. Only the directories for selected roles are crea
8184
| `formal-methods` | Specification & verification | no |
8285

8386

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+
8490
## Status
8591

8692
Early prototype. Tools currently in the registry (✅ available · ⬜ planned). Infrastructure is multi-tool and provisioned via `cardano-up`; every other role takes one tool.

docs/ADDING_A_TOOL.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@ template = "mytool/on-chain"
4949
template = "mytool/off-chain"
5050
```
5151

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.
73+
5274
### Valid role names
5375

5476
| Role key | Description |
@@ -156,6 +178,8 @@ dotenv.config({ path: "../.env" });
156178
const indexerUrl = process.env.INDEXER_URL; // set → a local endpoint is up
157179
```
158180

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+
159183
**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:
160184

161185
| Variable | Meaning |
@@ -220,6 +244,7 @@ Any rendered file (one whose `source` ends in `.jinja`) can reference the follow
220244
{# Flags for conditional sections #}
221245
{{ has_on_chain }}
222246
{{ has_off_chain }}
247+
{{ has_fullstack }} {# one tool fills both on-chain + off-chain → protocol/ #}
223248
{{ has_infra }}
224249
{{ has_devnet }}
225250
{{ has_formal_methods }}
@@ -235,6 +260,13 @@ Any rendered file (one whose `source` ends in `.jinja`) can reference the follow
235260
{{ off_chain.language }}
236261
{{ off_chain.dir }} {# "off-chain" #}
237262
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. #}
265+
{{ fullstack.tool_id }}
266+
{{ fullstack.tool_name }}
267+
{{ fullstack.language }}
268+
{{ fullstack.dir }} {# "protocol" #}
269+
238270
{{ devnet.tool_id }}
239271
{{ devnet.dir }} {# "devnet" #}
240272

docs/ARCHITECTURE.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ pub enum Role { OnChain, OffChain, Infrastructure, Devnet, FormalMethods }
106106
- 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).
107107
- **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.
108108
- 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).
109110

110111
### 3.2 Tools
111112

@@ -115,12 +116,15 @@ pub struct ToolDef {
115116
pub languages: Vec<String>,
116117
pub nix_packages: Vec<String>, // toolchains for the Nix dev shell
117118
pub roles: HashMap<Role, RoleConfig>, // which roles this tool can fill
119+
pub fullstack: Option<RoleConfig>, // unified on-chain+off-chain template (opt-in)
118120
}
119121
pub struct RoleConfig { pub template: String } // path under templates/
120122
```
121123

122124
`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.
123125

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+
124128
### 3.3 Selection (the resolved user choice)
125129

126130
```rust
@@ -157,10 +161,16 @@ The contract is a set of constants every template conforms to. It is the seam th
157161
pub const BLUEPRINT_PATH: &str = "blueprint/plutus.json";
158162
pub const DIR_ON_CHAIN = "on-chain"; DIR_OFF_CHAIN = "off-chain";
159163
pub const DIR_INFRA = "infra"; DIR_DEVNET = "devnet"; DIR_FORMAL_METHODS = "formal-methods";
164+
pub const DIR_PROTOCOL = "protocol"; // fused on-chain+off-chain component (fullstack, §3.2)
160165
pub const ENV_INDEXER_URL = "INDEXER_URL"; ENV_INDEXER_PORT = "INDEXER_PORT";
161166
pub const ENV_NODE_SOCKET_PATH = "NODE_SOCKET_PATH"; ENV_NETWORK = "CARDANO_NETWORK";
162167
```
163168

169+
`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+
164174
**Compliance checklist (enforced mechanically by contract-compliance tests):**
165175

166176
- **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
225235
Produces the ordered `FilePlan`:
226236
1. **Base layer** (always): `Justfile`, `README.md`, `.gitignore`, `.env`.
227237
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.
229242
4. **Optional layer**: `flake.nix` + `.envrc` when `nix` is set.
230243

231244
No I/O: only embedded assets are read. `render` is set from the `.jinja` extension.
@@ -290,7 +303,7 @@ doctor/
290303
- **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.
291304
- **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.
292305
- **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.
294307
- **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`.
295308

296309
### 8.1 The code/data split

0 commit comments

Comments
 (0)