Conversation
Rewrites the manifest store model to the §6.6 portable schema: - `[stores.<kind>]` now carries only logical `ids` (non-empty) and an optional `default` (required when >1 id, must be a declared id). The five per-adapter store config types collapse into one reusable `StoreDeclaration`. - The pre-rewrite store schema (`[stores.<kind>] name`, `[stores.config.defaults]`, `[stores.<kind>.adapters.*]`, `enabled`) is a hard load error whose message points at the migration guide. - Store helper methods resolve a store's name to its logical default id (interim — `EDGEZERO__*` env overrides arrive in Task 2.2). - `[stores.config.defaults]` and its axum dev-server seeding are gone. - Migrated `examples/app-demo/edgezero.toml` and the generated `edgezero.toml.hbs` template to the new schema. Scoped to store types only; `[adapters.*]`, the env layer, and adapter store registries are later Stage 2 tasks.
New `edgezero-core::env_config` module: parses `EDGEZERO__`-prefixed environment variables (`__` = key-path separator, segments lower-cased) into an `EnvConfig` value with accessors for store platform names + tuning, bind host/port, and logging level. - `from_env()` reads the process environment; `from_vars()` lets the Cloudflare adapter supply its `Env` binding (no `std::env` there). - `store_name(kind, id)` falls back to the logical id when unset. Additive only — wired into the runtime in later Stage 2 tasks.
…st_src from run_app
Couples the macro/runtime change with the adapter signature change so the
workspace and `examples/app-demo` stay buildable in a single commit.
- `Hooks::stores() -> StoresMetadata` replaces `config_store()`. The
`app!` macro emits portable `StoreMetadata { default, ids }` for
`[stores.config|kv|secrets]`. `Hooks::stores()` defaults to empty so
apps built without the macro still compile.
- `run_app::<A>()` no longer takes `manifest_src` on any adapter — axum,
cloudflare, fastly, spin. Each reads `A::stores()` and layers
`EDGEZERO__*` env config on top (logging level, bind host/port, store
platform names). All four entrypoint templates, all four
`app-demo-adapter-*` consumers, and `edgezero-cli/src/demo_server.rs`
drop `include_str!("edgezero.toml")`.
- axum `resolve_addr` reads `EDGEZERO__ADAPTER__HOST`/`PORT` only; the
`[adapters.axum.adapter]` fallback is gone (consistent with the §6.6
no-runtime-tables rule).
- cloudflare derives the exact `EDGEZERO__STORES__<KIND>__<ID>__NAME`
keys from baked metadata to query the worker `Env` (workers cannot
enumerate). Deprecated `run_app_with_manifest` is removed.
- spin drops `dispatch_with_manifest`; the KV label resolves from
`EDGEZERO__STORES__KV__<ID>__NAME` or the declared default id.
All five CI gates green + `examples/app-demo` tests pass.
Tasks 2.5–2.9 (async ConfigStore, store registries, extractors,
app-demo/templates/docs migration, ship gate) follow.
…stry, id-keyed RequestContext
Lands the runtime-API shape for §6.6 multi-store support. No adapter
yet builds a `StoreRegistry` — that arrives in Task 2.6. The new
RequestContext accessors are wired with a legacy single-handle
fallback so all four adapters keep compiling and tests stay green
through the transition.
- `ConfigStore::get` → `async` (`#[async_trait(?Send)]`). Required for
Cloudflare's KV-backed config store (§8 Task 2.6) which is async at
the SDK boundary. All four adapter impls + `FixedConfigStore` test
doubles + app-demo's `MapConfigStore` / `UnavailableConfigStore` are
updated; the contract-test macro switches to `block_on` (same pattern
as the KV contract macro). `ConfigStoreHandle::get` follows.
- New `KvError` variants with `From<KvError> for EdgeError` mappings:
- `Unsupported { operation }` → `EdgeError::not_implemented` (501).
Used by Spin TTL writes (§6.7), where `key_value::Store::set` has
no expiry parameter.
- `LimitExceeded { message }` → `service_unavailable` (503). Used by
Spin's `get_keys` cap (`max_list_keys`, §6.7).
A new `EdgeError::NotImplemented` variant + constructor backs the
501 mapping.
- New `edgezero_core::store_registry` module:
- `StoreRegistry<H> { by_id: BTreeMap<String, H>, default_id: String }`
with `default()`, `default_id()`, `ids()`, `named()`, `new()`.
- `KvRegistry` / `ConfigRegistry` / `SecretRegistry` type aliases.
- `BoundKvStore` / `BoundConfigStore` / `BoundSecretStore` aliases for
the existing handle types so future call sites can speak in
registry terms without coupling to the legacy names.
- `RequestContext` gains id-keyed accessors `kv_store(id)` /
`config_store(id)` / `secret_store(id)` and the `_default()` helpers.
Each reads from the matching registry in extensions when present
(strict lookup — unknown ids yield `None`); otherwise falls back to
the legacy single-handle stash so today's adapters continue to work.
The pre-existing no-arg `config_store()` is renamed to
`config_handle()` to free the symmetric name; the few in-tree
call sites (axum service + four adapter contract tests + app-demo's
config handler) are updated. Handler migration to the id-keyed API
lands in Task 2.8.
Tests added: 4 RequestContext registry/fallback cases (kv, config,
secret), 2 KvError → EdgeError mappings (Unsupported → 501,
LimitExceeded → 503), 6 StoreRegistry unit tests. All five CI gates
green plus `examples/app-demo` tests.
…rror variants
First slice of Task 2.6. The remaining adapters (fastly, spin, cloudflare)
follow in subsequent commits; the legacy single-handle path stays live
in parallel so the workspace and `examples/app-demo` keep building
through the transition.
axum:
- `EdgeZeroAxumService` and `AxumDevServer` gain
`with_{kv,config,secret}_registry` setters alongside the existing
single-handle ones. Both can coexist; the registries are inserted
into request extensions in addition to the legacy handles, and
`RequestContext` prefers a registry when present.
- `dev_server::run_app` builds the three registries from `A::stores()`
+ `EDGEZERO__STORES__*`:
- KV: one `PersistentKvStore` per declared id at
`.edgezero/kv-<slug>-<hash>.redb` (file name derived from the
platform name from `EDGEZERO__STORES__KV__<ID>__NAME` or the id
default).
- Config: one empty `AxumConfigStore` per declared id; the
`.edgezero/local-config-<id>.json` read path lands when stage 7's
`config push` is wired.
- Secrets: axum is `Single` (§6.6) — every declared secrets id maps
to the same env-backed `EnvSecretStore`.
spin:
- `SpinKvStore::put_bytes_with_ttl` now returns
`KvError::Unsupported { operation: "put_bytes_with_ttl" }` (was
`KvError::Validation`) — semantically correct per §6.7 and matches
the variant added in Task 2.5.
- `SpinKvStore::list_keys_page` now returns
`KvError::LimitExceeded` (was `KvError::Validation`). Paginated
listing with an env-driven `max_list_keys` cap lands in the spin
registry-wiring commit.
All five CI gates green; `examples/app-demo` tests pass.
Adds a registry-aware dispatch path (`dispatch_with_registries`) that builds per-id `KvRegistry`/`ConfigRegistry`/`SecretRegistry` from baked `Hooks::stores()` + `EDGEZERO__STORES__*`. `run_app` switches to it; the legacy single-handle helpers (`run_app_with_config`, `run_app_with_logging`, the individual `dispatch_with_*` entry points) remain for back-compat. Fastly is `Multi` for all three kinds (§6.6). Per kind: - KV: each declared id opens its own `FastlyKvStore` via the platform name from `EDGEZERO__STORES__KV__<ID>__NAME` (default = id). The per-id open is required — declaring `[stores.kv]` means failure to open is a runtime error, not silent degradation. - Config: each declared id opens its own `FastlyConfigStore` via `EDGEZERO__STORES__CONFIG__<ID>__NAME`; missing stores log a one-time warning and the id is dropped from the registry. - Secrets: `FastlySecretStore` is stateless (it opens the named store per call), so one shared `SecretHandle` is registered under every declared id for now. Per-id platform-name binding lands when Task 2.7 reshapes `BoundSecretStore` to capture the name. All five CI gates green; `examples/app-demo` tests pass.
…ys_page Wires Spin to the registry model and replaces the unconditional listing error with real client-side paging. Per §6.6 / §6.7 Spin capability map: - KV (Multi): each declared id opens its own `SpinKvStore` under the Spin label resolved from `EDGEZERO__STORES__KV__<ID>__NAME`. The `max_list_keys` paging cap is per-id, read from `EDGEZERO__STORES__KV__<ID>__MAX_LIST_KEYS` and defaulting to 1000. Required: a `[stores.kv]` id failing to open is a runtime error. - Config (Single): the one shared `SpinConfigStore` is registered under every declared id. Capability validation that catches `ids.len() > 1` on Spin lands in `config validate` (§10). - Secrets (Single): the one shared `SpinSecretStore` is registered under every declared id. `SpinKvStore::list_keys_page` materialises `Store::get_keys()`, filters by prefix, sorts, applies the `max_list_keys` cap, and slices the page via the new pure `crate::kv_pagination::paginate_keys` helper. Pagination invariants are unit-tested on the host (8 tests covering: empty prefix, prefix filter + sort, page-smaller-than-match cursor, cursor advance, final page, cap exceeded → `LimitExceeded`, cap=0 disables, cursor past end). The wasm-only `SpinKvStore` is the production consumer. `run_app` switches from the legacy `SpinStoreSettings` settings path to `request::dispatch_with_registries`. The now-dead `SpinStoreSettings` + `resolve_store_settings` + `dispatch_with_store_settings` slice is removed; the old single-handle tests in `lib.rs` are superseded by the upcoming registry-aware contract tests (Task 2.6 final). All five CI gates green; `examples/app-demo` tests pass.
…nfigStore [vars] → KV async Replaces the pre-rewrite JSON-string config store with a Cloudflare KV namespace backing (§6.6) and wires per-id registries through dispatch. CloudflareConfigStore rewrite: - Old: `[vars]` string binding whose value is a JSON object; parse-and-cache at construction with O(1) map reads. - New: a Cloudflare KV namespace binding opened at construction (`env.kv(binding_name)`); `get(key)` reads asynchronously via `worker::kv::KvStore::get(key).text().await`. - `[vars]` bindings are restricted to JavaScript identifier syntax — arbitrary dotted keys (e.g. `feature.checkout`) used to require JSON packing; the KV backing has no such restriction. - The whole `lookup_cached` / `CONFIG_CACHE_LIMIT` parse cache and the `try_new` / `new_or_empty` / `empty` constructors are gone. - The module now compiles on the host (with a `#[cfg(test)] InMemory` backend) so the shared contract-test macro can run there, matching the `SpinConfigStore` pattern. Per §6.6 Cloudflare capability map: - KV (Multi): each declared id opens its own KV namespace via `EDGEZERO__STORES__KV__<ID>__NAME`. Required: failure is a runtime error rather than a silent skip. - Config (Multi): each declared id opens its own KV namespace via `EDGEZERO__STORES__CONFIG__<ID>__NAME`. Missing bindings log a one-time warning (`warn_missing_config_binding_once`) and the id is dropped from the registry. - Secrets (Single): one shared `CloudflareSecretStore` is registered under every declared id. `run_app` switches to the new `dispatch_with_registries`. The legacy `dispatch_with_config` / `dispatch_with_bindings` entry points stay for back-compat; their config-binding semantics are updated to open KV namespaces (with a one-time `warn` on missing bindings, mirroring the old quiet-skip behaviour). All five CI gates green; `examples/app-demo` tests pass.
Adds two registry-aware service tests proving the new wiring works
through the full dispatch path:
- `with_kv_registry_resolves_named_and_default`: builds a two-id
`KvRegistry` (`sessions` + `cache`, default = `sessions`) backed by
two distinct `PersistentKvStore`s, attaches it via
`EdgeZeroAxumService::with_kv_registry`, and exercises three routes
that call `ctx.kv_store("sessions")`, `ctx.kv_store("cache")`, and
`ctx.kv_store_default()`. Each route must hit the correct backing
store — verified by writing distinct values to each before dispatch.
- `kv_registry_lookup_is_strict_for_unknown_ids`: confirms the
strict-lookup contract from §6.9 — when a registry is wired,
`ctx.kv_store("missing")` yields `None` rather than falling back to
the default.
These tests cover the registry path end-to-end on the most-used adapter.
Per-adapter registry construction in fastly/spin/cloudflare is
exercised at compile time + via the shared core `StoreRegistry` tests
(Task 2.5) and the per-adapter `config_store_contract_tests` (running
against the InMemory backends). Wasm-only contract tests for the spin
TTL → `Unsupported` and listing-cap paths live alongside the existing
`tests/contract.rs` wasm bundle and are exercised when CI runs the
spin wasm32 build.
All five CI gates green; `examples/app-demo` tests pass.
…med() shape
Reshapes the per-request store extractors to wrap registries instead of
single handles (§6.9). Handler code now picks a bound store by id at the
call site.
- `Kv` / `Secrets` are no longer tuple structs wrapping a handle. Each
now wraps the matching `*Registry` and exposes:
* `default() -> Option<Bound*Store>` — the registered default id
* `named(id: &str) -> Option<Bound*Store>` — strict lookup; unknown
ids yield `None`
* `registry() -> &*Registry` — escape hatch for advanced use
- New `Config` extractor follows the same shape; rounds out the trio
promised by spec §6.9.
- Each `FromRequest` impl reads the matching registry from extensions
when present, else falls back to the legacy single handle, wrapping
it in a synthetic one-id registry under the conventional `"default"`
id (`single_id_registry`). This keeps adapters that have not yet
wired registries working through Task 2.7's transition.
Migrations in this commit (all that consumed the old destructure
pattern):
- `examples/app-demo/crates/app-demo-core/src/handlers.rs`:
`kv_counter`, `kv_note_put`, `kv_note_get`, `kv_note_delete`,
`secrets_echo` now call `<extractor>.default()` and bubble a
`service_unavailable` error when no store is registered.
- `crates/edgezero-adapter-axum/src/dev_server.rs::secret_value_handler`
(demo-only) likewise.
Tests: replaced the legacy `Kv` / `Secrets` extractor tests with
registry-aware variants. New coverage:
- `Kv` falls back to legacy single handle when no registry is wired.
- `Kv` prefers a wired `KvRegistry` over a coexisting legacy handle,
with strict `named` lookups.
- `Config` resolves named and default ids from a wired `ConfigRegistry`
and falls back to a legacy `ConfigStoreHandle`; missing both yields a
500 with the expected guidance string.
All five CI gates green; `examples/app-demo` tests pass.
Closes Stage 2 of the CLI-extensions work. The portable manifest +
EDGEZERO__* env-config + per-id store registries + id-keyed extractors
are in. This commit ships the user-facing docs that explain the new
shape and run the full gate one last time as the ship-gate.
Task 2.8 docs:
- New `docs/guide/manifest-store-migration.md`: the page the loader's
hard-error message points at. TL;DR; field-by-field old → new;
capability matrix; runtime env-var table; handler-code migration;
the `[stores.config.defaults]` → `.edgezero/local-config-<id>.json`
story; the Cloudflare config `[vars]` → KV namespace migration.
- `docs/guide/kv.md`: store declaration uses portable `ids` schema;
`Kv` examples switch to `default()` / `named()`; Spin TTL writes
return `KvError::Unsupported`; Spin listing-cap pagination via
`EDGEZERO__STORES__KV__<ID>__MAX_LIST_KEYS` is described.
- `docs/guide/configuration.md`: `[stores.secrets]` and `[stores.config]`
sections rewritten to portable schema with the per-adapter capability
matrix; `ManifestLoader` summary updated for `Hooks::stores()`; one
inline secret-store example uses `ids = ["default"]`.
- `docs/guide/adapters/cloudflare.md`: config-store section rewritten —
`[vars]` JSON binding replaced with KV namespace binding + async
reads. Links to the migration guide.
- `docs/guide/adapters/axum.md`: config-store section explains the
`.edgezero/local-config-<id>.json` flow and shows the `Config`
extractor.
- `docs/guide/adapters/overview.md`: "Config Store Resolution" replaced
with "Store Registry Resolution" — covers all three kinds and points
at the id-keyed extractors / `RequestContext` accessors.
- `docs/guide/adapters/fastly.md`: config-store id-keyed example +
`Config` extractor; migration-guide link.
- `docs/guide/architecture.md`: Prettier reformat of the features table
(table-column widening only).
Task 2.9 ship gate:
- `cargo fmt --all -- --check` ✅
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` ✅
- `cargo test --workspace --all-targets` ✅ (335 core, 138 axum, 31 spin,
68 macros, 0 failures across all suites)
- `cargo check --workspace --all-targets --features "fastly cloudflare spin"` ✅
- `cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin` ✅
- `cd examples/app-demo && cargo test` ✅
- `cd docs && npm run lint && npm run format` ✅
An adapter binary built with no `edgezero.toml` and no `EDGEZERO__*`
env vars runs against the §6.6 defaults: `App::build_app()` produces a
`Hooks::stores()` with all three kinds `None`; the registry-builder
helpers in each adapter return `None`, the request context exposes no
store registries (and the legacy single-handle accessors return
`None` too), and bind host/port/logging fall back to `127.0.0.1:8787`
/ `info`. The four generated entrypoint templates and `examples/app-demo`
already drop `include_str!("edgezero.toml")`.
…fault scaffold; doc + warning fixes
P1 (correctness) — generated/app-demo non-axum runtimes were broken
after the portable store rewrite. Each adapter built a `KvRegistry`
keyed by the declared logical ids, but the platform manifests still
declared the legacy single-store bindings, so `spin up`, `wrangler dev`,
and `fastly compute serve` would error before any handler ran.
- Scaffold (`templates/root/edgezero.toml.hbs`): drop the default
`[stores.kv]`, `[stores.config]`, `[stores.secrets]` blocks. The
scaffold handlers don't read any store, so declaring them forced
every new project to provision matching `wrangler.toml` /
`spin.toml` / `fastly.toml` bindings (which the scaffold templates
for those files do not generate). They're now commented out with a
pointer to the migration guide so users opt in when they need them.
- app-demo: provision matching platform bindings for the declared
logical ids (`sessions` + `cache` for KV; `app_config` for config;
`default` for secrets):
- `app-demo-adapter-spin/spin.toml`: `key_value_stores =
["sessions", "cache"]` (was `["default"]`).
- `app-demo-adapter-fastly/fastly.toml`: rename `EDGEZERO_KV` → two
stores `sessions` + `cache`; keep the `app_config` config store
(id already matches); keep the Fastly secret store named
`EDGEZERO_SECRETS` because `BoundSecretStore` does not yet
capture per-id platform names (the handler hardcodes
`SECRET_STORE_NAME = "EDGEZERO_SECRETS"` and passes it as the
`store_name` argument).
- `app-demo-adapter-cloudflare/wrangler.toml`: replace `[vars]
app_config` (dead since Task 2.6E rewrote the config store to KV)
+ the lone `EDGEZERO_KV` namespace with three KV namespace
bindings (`sessions`, `cache`, `app_config`). Local KV seed data
is no longer provided inline; populate via `wrangler kv key put`
when running the cloudflare smoke tests.
P2 — Spin is missing from user-facing CLI docs even though it has
been a default-feature built-in since the workspace ships with
`spin` in `default = […]`.
- `docs/guide/cli-reference.md`: add `spin` to `--adapter` argument
lists for `serve` and `deploy`, the provider-behaviour rundowns
(mapping to `spin up` / `spin deploy`), and the built-in adapters
list under "Adapter Discovery".
- `docs/guide/getting-started.md`: add a Spin prerequisites bullet
(wasm32-wasip1 target + Spin CLI).
P3 — Cloudflare wasm dead-code warning.
- `crates/edgezero-adapter-cloudflare/src/request.rs`: the
`dispatch_with_bindings` helper was orphaned by Task 2.6E's switch
to `dispatch_with_registries`. No callers in-tree (it was
`pub(crate)`); deleted outright. `cargo test -p
edgezero-adapter-cloudflare --target wasm32-unknown-unknown
--features cloudflare --no-run` now compiles warning-free.
Verification: all five workspace gates pass; `examples/app-demo`
tests pass; docs lint + format pass; `cargo test -p edgezero-cli
--test generated_project_builds -- --ignored` passes (51s; the
scaffold-probe workspace compiles cleanly under the trimmed
manifest); the Cloudflare wasm `--no-run` build emits no `dead_code`
warning.
…lare build passthrough, build script rerun, Spin docs P2 — `EDGEZERO_MANIFEST` typos no longer silently fall back to built-ins. - `load_manifest_optional` now distinguishes a missing default `edgezero.toml` (still `Ok(None)` — built-in adapters handle the request) from an explicit `EDGEZERO_MANIFEST` pointing at a missing file (hard error naming the bad path). The previous test that locked in the silent-fallback behaviour is replaced with one that asserts the hard error; added a companion test that proves the default-path case stays permissive. P2 — Cloudflare built-in build now honors `BuildArgs.adapter_args`. - `cli.rs::Adapter::execute` was the only built-in action call site that dropped the passthrough `args` slice. `pub fn build(extra_args: &[String])` now takes the slice and appends it after the canonical cargo flags (mirroring `fastly`/`spin`). The cloudflare adapter registration was the only in-tree consumer; the public signature change is symmetrical with the existing `deploy`/`serve` helpers. P3 — `build.rs` rerun trigger. - `crates/edgezero-cli/build.rs` reads `Cargo.toml` to discover optional `edgezero-adapter-*` deps but only requested re-runs on `build.rs` changes. Added `cargo:rerun-if-changed=Cargo.toml` so the generated `linked_adapters.rs` stays in sync when adapters are added/removed or flip `optional`. P3 — Spin in CLI build docs. - `docs/guide/cli-reference.md`: add `spin` to `edgezero build`'s `--adapter` argument list and examples; extend the wasm-target troubleshooting hint (`wasm32-wasip1` for Fastly or Spin); add the Spin CLI install pointer to the provider-CLI section. Verification: all five workspace gates ✅; `examples/app-demo` ✅; docs lint + format ✅; Cloudflare wasm `--no-run` warning-free; new manifest tests pass (`load_manifest_optional_hard_errors_when_explicit_env_path_missing`, `load_manifest_optional_returns_none_when_default_missing`).
P3 — Two stale documentation surfaces flagged in review.
`crates/edgezero-cli/src/templates/root/README.md.hbs` (generated
project README): the scaffold now writes a `crates/{{proj_cli}}` crate
(Stage 1's main user-visible change), but the workspace-summary list
in the generated README only mentioned the core crate plus per-adapter
crates. Added a line for the downstream CLI crate that explains it is
the intended customization point and is built on the reusable
`edgezero-cli` library.
`crates/edgezero-cli/README.md`: was a leftover from before Stage 1.
It still said build/deploy were "soon" and only documented
Fastly-specific subcommands. Rewritten to cover the actual surface:
- the binary-plus-library shape;
- the four built-in subcommands across `fastly` / `cloudflare` /
`spin` / `axum`, with the manifest-vs-built-in fallback noted;
- the `(<Cmd>Args, run_<cmd>)` library API and the
generated-project pattern;
- the link-time adapter discovery via `build.rs`;
- the contributor pointer to the opt-in `generated_project_builds`
scaffold test.
Adds a cross-link to `docs/guide/cli-reference.md` so the user-facing
reference is the source of truth and the README stays a crate-level
overview.
Verification: `cargo test --workspace --all-targets` ✅; clippy ✅;
fmt ✅; `cargo test -p edgezero-cli --test generated_project_builds
-- --ignored` ✅ (50s, scaffold-probe workspace compiles cleanly with
the new README line); docs lint + format ✅.
…, Spin adapter doc
P2 — `crates/edgezero-cli/README.md` distributable-build command.
- Old `cargo build -p edgezero-cli --no-default-features --features cli`
dropped every `edgezero-adapter-*` feature dep (which the default
set brings in), so the resulting binary had no built-in adapter
helpers and `edgezero new` would scaffold nothing. The features
table now lists each adapter feature individually (all four enabled
by default); the "distributable build" command uses `--release`
alone; a separate "slim build" example shows the
`--no-default-features --features "cli edgezero-adapter-axum"`
pattern with an explicit warning that dropping default features
also drops every adapter.
P3 — `crates/edgezero-cli/README.md` library API sample.
- Imports were wrong: the args structs live under `edgezero_cli::args::*`,
not the crate root. Aligned with the canonical sample in
`docs/guide/cli-reference.md`.
- The previous `BuildArgs { … ..Default::default() }` snippet does
not compile from downstream code because the struct is
`#[non_exhaustive]` (Rust forbids struct-literal expressions across
crates for those types). Replaced with the correct
`let mut args = BuildArgs::default(); args.adapter = …` idiom, with
an explicit note on the constraint.
P3 — Spin missing from `docs/guide/adapters/overview.md` "Available
Adapters" table even though the Spin blueprint has been registered
since Stage 1.
- Added the Spin row (`wasm32-wasip1`, Stable) linking to
`/guide/adapters/spin`.
- Wrote `docs/guide/adapters/spin.md` covering prerequisites, project
setup, the `#[http_component]` entrypoint shape, build / serve /
deploy commands, and Spin-specific store semantics from spec §6.7:
label-backed multi-store KV with `EDGEZERO__STORES__KV__<ID>__NAME`
and the `MAX_LIST_KEYS` cap; `Unsupported`/`LimitExceeded` error
variants; flat-variable single-store Config and Secrets; Spin
variable naming rules + `.`→`__` translation; config/secret
namespace collision check; the component-discovery rules driving
`provision` / `config push`.
- Registered the new page in the VitePress sidebar
(`docs/.vitepress/config.mts`).
Note: `npm run build` still trips on a pre-existing Vue-compiler
parse error in `docs/superpowers/specs/2026-05-19-cli-extensions-design.md`
at line 1069 (`<key> = "{{ <key> }}"` inside inline code — Vue treats
the double braces as an interpolation expression). This is unrelated
to this commit, predates it, and is not in the documented CI gates
(`npm run lint` and `npm run format` are; both pass). Cleaning it up
is a separate follow-up — either escape the braces or wrap that block
in `<span v-pre>…</span>`.
Verification: cargo fmt + clippy + workspace tests + feature-combo
check + spin wasm32 check all green; `examples/app-demo` tests pass;
docs `npm run lint` + `npm run format` pass.
…ress build
P2 — Spin config store now honors the canonical dotted handler-facing
key surface (spec §6.7). Previously `SpinConfigStore::get(key)` passed
the key straight through to `spin_sdk::variables::get`, which rejects
dotted keys (Spin variable names must match `^[a-z][a-z0-9_]*$`). The
documented translation rule (`service.timeout_ms` →
`service__timeout_ms`) was a doc promise the implementation didn't
keep; `scripts/smoke_test_config.sh` had a workaround that skipped
dotted keys for Spin.
- `SpinConfigStore::translate_key(key) -> String` (`pub(crate)`):
every `.` becomes `__`, other characters pass through (case-
preserving; uppercase still hits `InvalidName` at the backend as
the spec says).
- `get` translates before delegating to either backend.
- `from_entries` (test fixture) translates on insert so the InMemory
representation mirrors what the real Spin runtime would store. The
existing contract tests (`store.get("contract.key.a")`) keep
exercising the same translation path as production.
- New unit tests cover the translation directly (dots → `__`, flat
keys pass through, case-preserving) plus a round-trip that asserts
`get("feature.new_checkout")` resolves against the
`feature__new_checkout` storage form.
- `examples/app-demo/crates/app-demo-adapter-spin/spin.toml` now
declares the translated variables (`feature__new_checkout`,
`service__timeout_ms`) and binds them in the component-variables
table, so the existing dotted-key smoke checks light up end-to-end
on Spin.
- `scripts/smoke_test_config.sh` drops the `if [ "$ADAPTER" != "spin" ]`
skip; the spin adapter now goes through the same dotted-key checks
as fastly/cloudflare/axum. Updated the header note to describe the
translation rather than the old skip.
P3 — VitePress build was failing on
`docs/superpowers/specs/2026-05-19-cli-extensions-design.md:1069`,
where the inline-code snippet `<key> = "{{ <key> }}"` was being
Vue-parsed as an interpolation expression. `superpowers/` is the
internal design-doc folder — it sits under `docs/` so the doc
tooling (prettier/eslint) covers it, but it's not in the VitePress
sidebar and is not part of the published site. Added
`srcExclude: ['superpowers/**']` to `.vitepress/config.mts` so Vue
skips parsing those pages. `npm run build` now succeeds (1.4s, full
site rendered) without touching the spec content.
Verification: `cargo fmt --all -- --check` ✅; clippy ✅; workspace
tests ✅ (spin lib tests 31 → 35, +4 new); feature-combo check ✅;
spin wasm32 ✅; `examples/app-demo` tests ✅; docs `npm run lint`,
`npm run format`, **`npm run build`** all ✅.
… config, ManifestAdapter hard-cutoff, BoundSecretStore per-id binding
H2 — app-demo's `/config/<key>` was 503'ing under the Stage 2 run_app
path on every registry-backed adapter. The `config_get` handler still
called `ctx.config_handle()` (legacy single-handle accessor that
ignores `ConfigRegistry`), so once Task 2.6 switched adapters to wire
`ConfigRegistry` instead of `ConfigStoreHandle`, the handler stopped
finding a store. Migrated to `ctx.config_store_default()` (registry-
aware; falls back to the legacy handle when no registry is wired).
Added a registry-path test that proves the new accessor reads the
wired `ConfigRegistry`.
H3 — Hard cutoff for the manifest's `[stores.<kind>]` legacy fields
was in place, but `[adapters.<name>.<sub>]` legacy subtables (the
pre-rewrite `[adapters.spin.stores.kv.default]` /
`[adapters.fastly.stores.config]` shape, plus legacy `runtime`
tuning) were silently deserialized into nothing. Added a
`#[serde(flatten)] legacy: BTreeMap<String, toml::Value>` catch-all on
`ManifestAdapter` plus `validate_manifest_adapter` that yields a
load error pointing at the migration guide when anything other than
`adapter` / `build` / `commands` / `logging` shows up. Four new
fixtures cover the regression.
H1 — Axum's Stage 2 config-file read path was missing.
`build_config_registry` was building empty `AxumConfigStore`s via
`from_env(iter::empty())`, even though the spec / migration guide /
adapter doc all promise `.edgezero/local-config-<id>.json` is read.
Rewrote `AxumConfigStore`:
- New `from_local_file(id)` reads `.edgezero/local-config-<id>.json`
and parses it as a flat `string -> string` JSON object. Missing
file → empty store (permissive); malformed file →
`ConfigStoreError::Unavailable` (surfaces in the dev-server log).
- Dropped the old env-vars-shadow-defaults model — Stage 2's portable
manifest carries no inline defaults and `EDGEZERO__*` does not
layer over config-store keys.
- `dev_server::build_config_registry` now opens one file-backed store
per declared id; a malformed file logs a warning and drops the id
rather than failing startup. `serde_json` added to the axum adapter
deps. New contract tests cover missing file, flat-JSON read,
malformed JSON, and non-string values; the in-tree
`local_path_is_keyed_by_logical_id` test pins the path convention.
M1 — `BoundSecretStore` is now a real type that captures the per-id
platform store name; `EDGEZERO__STORES__SECRETS__<ID>__NAME` actually
binds.
- `BoundSecretStore { handle: SecretHandle, store_name: String }`
exposes `get_bytes(key)` / `require_bytes(key)` / `require_str(key)`
(no `store_name` arg at the call site) plus accessors for the
underlying handle and bound name.
- `SecretRegistry = StoreRegistry<BoundSecretStore>` (was
`StoreRegistry<SecretHandle>`); the four adapter
`build_secret_registry` helpers now construct `BoundSecretStore` per
id from `env.store_name("secrets", id)`. Fastly's `Multi` capability
is now wired end-to-end — each declared id resolves to its own
Fastly secret-store name via the env var (default = the logical id).
- Context's `secret_store(id)` / `secret_store_default()` and the
`Secrets` extractor return the new `BoundSecretStore`. The legacy
single-handle fallback wraps the handle under the conventional
`"default"` platform name so adapters that haven't yet wired a
registry keep working.
- app-demo: dropped the `SECRET_STORE_NAME = "EDGEZERO_SECRETS"`
hardcode; `secrets_echo` calls `store.require_str(¶ms.name)`
with the platform name now coming from the registry binding. Fastly
`fastly.toml` renames the secret store from `EDGEZERO_SECRETS` to
`default` so it matches the logical id without needing an env
override. The axum dev-server `secret_value_handler` test
re-keys its `InMemorySecretStore` fixture under the conventional
`default/` prefix.
- New tests: `secret_store_resolves_named_handle_from_registry`
asserts each id keeps its bound platform name; a complementary
fallback test pins the `"default"` legacy-handle binding; the
`Secrets` extractor gains a per-id-platform-name preservation test.
All five workspace gates + cloudflare wasm + spin wasm + app-demo
tests + docs lint / format / build green.
…in smoke script, hard-cutoff scope, registry default-id invariant H1 — Axum bind config was split across three contracts. The Stage 2 runtime reads only `EDGEZERO__ADAPTER__HOST/PORT` via `EnvConfig`, but the axum CLI wrapper still set the pre-Stage-2 `EDGEZERO_HOST/PORT` on subprocesses (the runtime ignored them), and the generic manifest-command path in `edgezero-cli/src/adapter.rs` never translated `[adapters.<name>.adapter] host`/`port` into anything the runtime would see. Net effect: documented manifest host/port and the old `EDGEZERO_HOST=... cargo run` overrides did not reach the runtime. - `crates/edgezero-adapter-axum/src/cli.rs`: emit `EDGEZERO__ADAPTER__HOST` / `EDGEZERO__ADAPTER__PORT` on the subprocess env (was: legacy `EDGEZERO_HOST/PORT`). Read the parent env in canonical-first order, with `EDGEZERO_HOST/PORT` accepted as a back-compat fallback; warning strings reference the new names. - `crates/edgezero-cli/src/adapter.rs`: extract `[adapters.<name>.adapter] host/port` from the manifest in the shell-dispatch path and inject `EDGEZERO__ADAPTER__HOST/PORT` on the subprocess env. Parent-env values win — if the canonical variable is already set, the manifest value is skipped. - `crates/edgezero-core/src/addr.rs`: warning strings reference `EDGEZERO__ADAPTER__HOST/PORT`. Tests updated. - `docs/guide/configuration.md`: precedence list and the override example use `EDGEZERO__ADAPTER__HOST/PORT`. H2 — `scripts/smoke_test_config.sh axum` was bound to fail. After H1 of the previous review, Stage 2 Axum config reads `.edgezero/local-config-<id>.json` per logical id. No fixture is tracked (`.edgezero/` is gitignored), so the script's `/config/greeting` etc. checks would 404. Added a heredoc seed step that writes `.edgezero/local-config-app_config.json` with the same demo values Fastly's `[local_server.config_stores.app_config.contents]` and Spin's `[variables]` defaults carry; the dotted keys (`feature.new_checkout`, `service.timeout_ms`) align with the allowlist in `app-demo`'s `config_get` handler. M1 — Spec language said "no `[adapters.*]` table", which over-claims relative to the implementation (the table is retained for `crate`/`build`/`commands`/`logging` adapter discovery + shell-command wiring). Narrowed the wording in `docs/superpowers/specs/2026-05-19-cli-extensions-design.md` to "no per-adapter store / runtime tables" and called out explicitly that `[adapters.<name>.adapter] host/port` survives as a hint the CLI translates into `EDGEZERO__ADAPTER__HOST/PORT` env vars. M2 — `StoreRegistry::new` only `debug_assert!`-ed the default-id invariant. In release, adapter builders that skipped a failed-to-open default could call `StoreRegistry::new(by_id, missing_default)`, yielding a silent registry whose `default()` returned `None` despite a declared default — masking the underlying backend / config error. - `crates/edgezero-core/src/store_registry.rs`: `new` now panics in both debug and release if the default id is not in `by_id` (the invariant is part of the public contract; silent violation was a bug). Added a `should_panic` test that pins the new behaviour in release builds too. - Added `StoreRegistry::from_parts(by_id, default_id) -> Option<Self>`: the safe constructor for builders that skip failed backends. It returns `None` when `by_id` is empty OR when the declared default is not registered. All four adapter builders (axum, fastly, cloudflare, spin × kv/config/secret) now call `from_parts` instead of `new`, with a `log::warn!` on the dropped-default path. The axum KV builder's previous "salvage the registry under a different default id" fallback is gone — a silent default swap was the same class of bug, just less subtle. Verification: all five workspace gates + cloudflare wasm + spin wasm + `examples/app-demo` tests + docs lint/format/build green.
… axum doc API drift
P2 — Spin `component` field was documented and spec'd but ignored by
the manifest model. `docs/guide/adapters/spin.md` (and the design spec
section on component discovery) call for `[adapters.spin.adapter]
component = "<id>"` to disambiguate multi-component `spin.toml`s,
but `ManifestAdapterDefinition` had no `component` field — and unknown
adapter-definition fields silently deserialized into nothing.
- `crates/edgezero-core/src/manifest.rs`:
- `ManifestAdapterDefinition` gains `component: Option<String>` with
a length-validator (round-trips, ignored at runtime — read by the
future `provision` / `config push` commands).
- Added a `#[serde(flatten)] legacy` catch-all plus
`validate_manifest_adapter_definition`: any field other than the
five declared (`component`, `crate`, `host`, `manifest`, `port`)
is a hard load error pointing at the migration guide. Closes the
same silent-drop gap H3 closed at the parent
`[adapters.<name>]` level.
- Two new tests pin both behaviours: the Spin component field
round-trips, and an unknown `[adapters.<name>.adapter]` field
yields an `InvalidData` error naming both the offending key and
the migration guide.
P2 — Manifest-provided `EDGEZERO__ADAPTER__PORT` (via
`[environment.variables]`) was being overwritten by the
`[adapters.<name>.adapter] host/port` injection.
- `crates/edgezero-cli/src/adapter.rs::run_shell`: invert the order
so the bind hint is injected first, then `apply_environment` runs
and explicit manifest variables (set with a `value`) win over the
bind hint. The pre-existing parent-env-wins check on the bind
injection remains, so the documented precedence
(parent env > manifest variable > bind hint) holds end-to-end.
P3 — `docs/guide/adapters/axum.md` still showed the pre-Stage-2
`run_app(include_str!("../../../edgezero.toml"))` API. The current
`pub fn run_app<A: Hooks>() -> anyhow::Result<()>` is no-arg
(`crates/edgezero-adapter-axum/src/dev_server.rs::run_app`); the
entrypoint sample is updated, with a pointer to the migration guide
and a note that `EDGEZERO__*` env vars drive runtime config.
Verification: all five workspace gates + cloudflare wasm + spin
wasm + `examples/app-demo` tests + docs lint/format/build green;
core test count 339 → 341 (+1 component round-trip, +1 unknown-field
hard-error), macros 69 → 71 (re-exported `manifest_definitions`
tests pick up the same).
…tLoader rustdoc
P3 — Fastly + Cloudflare adapter docs still showed the pre-Stage-2
`run_app::<App>(include_str!("…/edgezero.toml"), req[, env, ctx])`
API and prose pointing at `edgezero.toml` as the runtime config
source. The current signatures are `run_app::<App>(req)` /
`run_app::<App>(req, env, ctx)` (manifest source dropped in Stage 2);
runtime config flows through `EDGEZERO__*` env vars + the portable
store metadata baked by the `app!` macro.
- `docs/guide/adapters/fastly.md`: entrypoint sample is the no-
manifest `run_app::<App>(req)`. Surrounding prose updated to call
out `EDGEZERO__*` env vars and the per-id `KV`/`Config`/`Secret`
registries; cross-link to the migration guide.
- `docs/guide/adapters/cloudflare.md`: entrypoint sample is the
no-manifest `run_app::<App>(req, env, ctx).await`. Prose explains
the Workers-specific env-config probe (`Env` cannot be enumerated,
so the canonical key set is derived from baked store ids) and the
per-id registries; cross-link to the migration guide.
P3 — `docs/superpowers/plans/2026-05-20-cli-extensions.md` Status
block was misleading. Stage 1 was marked DONE but Stage 2 was
collapsed into "Stages 2-8 pending" — yet Stage 2 has fully shipped
on this branch. Updated the bullet to enumerate the substrate
landed across the commit chain rooted at `f5bd432` (Task 2.1)
through `8942ec2` (Stage 2 review fixes), then narrows the open
work to Stages 3-8 so the next worker starts from a coherent
status.
P3 — `crates/edgezero-core/src/manifest.rs::ManifestLoader::load_from_str`
rustdoc still described it as the "binary-embedded manifest" path,
which contradicts the Stage 2 hard cutoff: `run_app` no longer
consumes a manifest source string at all. Rewrote the doc to
describe the current callers (the `app!` macro at compile time and
test fixtures), kept the rationale for the panic-on-bad-input
behaviour, and adjusted the `#[expect(clippy::panic, …)]` reason
string to match.
Verification: all five workspace gates + cloudflare wasm + spin
wasm + `examples/app-demo` tests + docs lint/format/build green.
P3 — `docs/superpowers/plans/2026-05-20-cli-extensions.md` still
described the pre-Stage-1/2 codebase under "facts this plan relies
on":
- edgezero-cli "is a binary-only crate" — wrong since Stage 1: it's
now a library + binary with `(<Cmd>Args, run_<cmd>)` pairs under
`edgezero_cli::args`.
- ConfigStore::get "is synchronous" — wrong since Task 2.5: the
trait is `#[async_trait(?Send)]` and all four adapter impls are
async (Cloudflare needs it for KV-backed reads).
- RequestContext exposes only singular `config_store()` /
`kv_handle()` / `secret_handle()` — wrong since Task 2.5: the
registry-aware id-keyed `kv_store(id)` / `config_store(id)` /
`secret_store(id)` accessors (plus the `_default()` helpers)
are the primary surface; the singular legacy accessors only
survive as fallbacks for unmigrated adapters.
A Stage 3 implementer who treated those bullets as ground truth
would code against the wrong substrate. Rewrote the section to
enumerate the post-Stage-2 reality:
- edgezero-cli lib+bin shape, args module, link-time adapter
discovery via `build.rs`.
- async `ConfigStore::get` plus the new `KvError` variants
(`Unsupported` → 501, `LimitExceeded` → 503).
- `BoundSecretStore { handle, store_name }` reshape so per-id
platform names actually bind (Fastly multi-secret).
- `StoreRegistry` panics on missing default; safe `from_parts`
constructor for builders that skip failed backends.
- id-keyed `RequestContext` accessors + the legacy-fallback rule.
- `Kv`/`Secrets`/`Config` `.default()` / `.named(id)` extractor shape.
- Portable manifest model + the three hard-cutoff surfaces
(`[stores.<kind>]`, `[adapters.<name>.<sub>]`,
`[adapters.<name>.adapter].<unknown>`).
- `run_app::<A>()` no-`manifest_src` signature on all four adapters,
with the CLI's `EDGEZERO__ADAPTER__HOST/PORT` translation and
documented precedence.
- Per-adapter store shapes: axum KV file convention, axum
`.edgezero/local-config-<id>.json` flow, Spin KV cap +
`Unsupported`/`LimitExceeded` semantics, Cloudflare config
KV-namespace rewrite.
- The opt-in `generated_project_builds` test as a constraint on
any Stage 3 generator-template changes.
Verification: docs `npm run lint` / `format` / `build` green. No
Rust changes; the prior commit's workspace + wasm + app-demo gates
remain the latest authoritative pass.
Stage 3 of the CLI-extensions plan: introduces the typed application config layer (`<name>.toml` → `<Name>Config`) alongside the existing `edgezero.toml` manifest. - `edgezero_core::app_config`: loader (`load_app_config`, `load_app_config_with_options`, `load_app_config_raw[_with_options]`) with a six-variant `AppConfigError`. Reads the `[config]` table only and applies the `<APP_NAME>__<SECTION>__…__<KEY>` env-var overlay (§6.10): existing keys only, sibling-segment ambiguity rejected up front, type coercion driven by the parsed TOML scalar type. - `#[derive(AppConfig)]` in `edgezero_macros`, re-exported as `edgezero_core::AppConfig`. Emits `impl AppConfigMeta` with the `SECRET_FIELDS` array; enforces §6.8 constraints (`#[secret]` only on bare `String`; rejects `serde(flatten|rename|skip*)`). Four trybuild compile-fail fixtures pin the error messages. - Generator: new `core_src_config_rs` and `app_name_toml` templates, plus a `NameUpperCamel` Handlebars key derived from the sanitised project name (`my-app` → `MyApp`, digit-leading → `App` prefix). Seeds the `validator` workspace dep so `<name>-core` builds out of the box. Generator-test asserts the new artifacts; the opt-in `generated_project_builds` smoke test verifies the scaffolded workspace still compiles end to end across host + wasm targets. - `app-demo`: `app-demo.toml` and `AppDemoConfig` (greeting, feature_new_checkout, nested ServiceConfig, `#[secret] api_token`, `#[secret(store_ref)] vault`). Three round-trip tests in `app-demo-core` cover loader, `SECRET_FIELDS` metadata, and the env overlay on a nested value. - Docs: new "Application config" section in `configuration.md` documenting the file, derive, secret annotations, and env-var overlay; `getting-started.md` notes that `edgezero new` now emits `<name>.toml` + `<Name>Config`. Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test --workspace --all-targets / cargo check --features "fastly cloudflare spin" / cargo check -p edgezero-adapter-spin --target wasm32-wasip1 — all green on both the root and `app-demo` workspaces. Opt-in `generated_project_builds` test also passes.
…nfigMeta bound, docs format
Four review findings on the freshly-landed Stage 3:
- P2: `#[derive(AppConfig)]` now rejects container-level
`#[serde(rename_all = ...)]` whenever a `#[secret]` field is
present. Without the guard, a `kebab-case` rename would deserialise
`api-token` while `SECRET_FIELDS` keeps reporting `api_token`,
silently desyncing Stage 4's typed secret validation and the Spin
collision check (spec §6.7/§6.8). New trybuild fixture
`secret_with_serde_container_rename_all` pins the error message.
- P3: `load_app_config_raw[_with_options]` now returns a new
`AppConfigError::ConfigNotATable { actual }` variant when the
top-level `config` key resolves to a scalar / array instead of a
table, so `config validate` (raw) doesn't have to rediscover the
mismatch downstream. Added unit test for `config = "..."` covering
the new variant.
- P3: tightened the typed loader bounds to
`C: DeserializeOwned + Validate + AppConfigMeta`, matching the
spec's published `load_app_config` signature (§4). The bound forces
every downstream typed config to derive `AppConfig`, which Stage 4
already assumes for `SECRET_FIELDS` access. The internal
`FixtureConfig` test struct gains a hand-rolled `AppConfigMeta`
impl with empty `SECRET_FIELDS` rather than invoking the derive
(proc-macro absolute paths don't resolve inside the defining
crate); the public derive remains exercised by the
`edgezero-macros` integration tests.
- P2: ran `prettier --write` on `docs/guide/configuration.md` to
satisfy the docs CI gate; no content changes, only whitespace /
list-marker normalisation that prettier introduced when the
"Application config" section landed.
Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test
--workspace --all-targets / cargo check --features
"fastly cloudflare spin" / cargo check -p edgezero-adapter-spin
--target wasm32-wasip1 — all green on both the root and `app-demo`
workspaces. `prettier --check` on `docs/` now passes.
…ret] fields Closes the last serde-skip variant the §6.8 contract requires the derive to reject. `skip_serializing_if = "..."` conditionally omits the field from serialisation; combined with `#[secret]`, that would make `config push` drop the secret key whenever the predicate fires — desyncing the on-the-wire shape from the SECRET_FIELDS invariant Stage 4 depends on. Matcher now lists the attribute alongside the unconditional skip family, with a trybuild fixture pinning the error message. Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test --workspace --all-targets all green; the six secret_* compile-fail fixtures still match their goldens.
Stage 4 of the CLI-extensions plan: ships `edgezero config validate`
on the default binary (raw flavour) and a typed-validator hook
downstream CLIs use for the full §10 contract.
- `ConfigValidateArgs { manifest, app_config, strict, no_env }` and a
`ConfigCmd` subcommand enum added to `edgezero-cli/src/args.rs`,
wired into the top-level `Command` enum and the default `edgezero`
binary as `Command::Config(ConfigCmd::Validate(a))` →
`run_config_validate` (raw).
- `crates/edgezero-cli/src/config.rs` (new):
- `run_config_validate` (raw) — manifest via `ManifestLoader` +
app-config TOML / `[config]`-table shape + Spin key-syntax (§6.7
check 1) + Spin component discovery (check 3) + `--strict`
capability completeness + handler-path well-formedness.
- `run_config_validate_typed<C>` — typed deserialise into `C`,
`validator::Validate::validate()`, `#[secret]` non-empty /
`#[secret(store_ref)]` ∈ `[stores.secrets].ids` (§6.8) and the
Spin config/secret namespace collision check (§6.7 check 2,
typed-only — needs `AppConfigMeta::SECRET_FIELDS`).
- 26 unit tests cover every failure mode the spec calls out: bad
manifest, missing `[config]`, missing `[app].name`, unknown field,
validator-rule failure, empty secret, store_ref miss, secret w/o
`[stores.secrets]`, Spin uppercase / dashed keys, zero / multi
components without selector, multi components with matching
selector, typed-only collision detection, strict capability
matrix, malformed handler path; plus helper-fn tests.
- `app-demo-cli` adds `app-demo-core = { path = "..." }` and a
`Config(AppDemoConfigCmd::Validate(...))` arm dispatched to
`run_config_validate_typed::<AppDemoConfig>` — the canonical
example of a downstream CLI driving the typed flow.
- `docs/guide/cli-reference.md` documents the subcommand, the
raw-vs-typed split, and the `--strict` / `--no-env` flags.
Capability matrix correction: spec §6.6 lists Spin's KV as Multi
(label-backed) and only Config/Secrets as Single — the strict
checker now matches, so `app-demo-cli config validate --strict`
exits 0 on the real `app-demo` manifest (2 KV ids declared).
Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test
--workspace --all-targets / cargo check --features
"fastly cloudflare spin" / cargo check -p edgezero-adapter-spin
--target wasm32-wasip1 — all green on both the root and `app-demo`
workspaces. Ship gate: both `./target/debug/edgezero config validate
--strict` (raw) and `cargo run -p app-demo-cli -- config validate
--strict` (typed) exit 0 against the in-tree `app-demo` fixture.
docs `prettier --check` passes.
…drift
Three review findings on the Stage 4 landing:
- Important: Spin component validation now rejects an explicit
`[adapters.spin.adapter].component` selector that does not match
any declared `[component.*]` id, even when `spin.toml` declares
exactly one component. The earlier auto-select path returned
before checking the selector, so a typo would slip through
`config validate` and only fail later in `config push` /
`provision`. New regression test
`spin_component_discovery_rejects_bad_selector_against_single_component`.
- Important: the Spin config/secret namespace collision (§6.7
check 2) now considers only plain `#[secret]` field values.
`#[secret(store_ref)]` values are *logical store ids* resolved at
runtime and never enter Spin's flat variable namespace, so they
cannot collide. Filtering by `SecretKind::KeyInDefault` matches
the spec and removes a false-positive that rejected
perfectly-valid configs with a `vault = "default"` plus a
similarly-named config key. New regression test
`spin_config_secret_collision_ignores_store_ref_values`.
- Medium: `app-demo` typed config no longer drifts from the runtime
config-store keys. `feature_new_checkout` is replaced by a nested
`feature: FeatureConfig { new_checkout: bool }`, so the TOML key
becomes `feature.new_checkout` — matching the handler that reads
`feature.new_checkout` from the config store, the seed in
`fastly.toml`, and Spin's `feature__new_checkout` (after `.`→`__`
translation). Stage 7's `config push` will now write the value
the existing route actually reads.
Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test
--workspace --all-targets / cargo check --features
"fastly cloudflare spin" / cargo check -p edgezero-adapter-spin
--target wasm32-wasip1 — all green on both the root and `app-demo`
workspaces. Ship gate: both `app-demo-cli config validate --strict`
(typed) and `edgezero config validate --strict` (raw) exit 0
against the in-tree `app-demo` fixture.
…caffold Without `#[validate(nested)]` on a struct field, `validator`'s outer `validate()` does not recurse into the inner type. The `ServiceConfig::timeout_ms` `range(min = 100, ...)` rule on `AppDemoConfig` was therefore silently a no-op — proven on the prior HEAD by `app-demo-cli config validate --strict --no-env` exiting 0 against `timeout_ms = 50`. The same hole sat in the generator template, so every freshly-scaffolded project would have inherited it. - `AppDemoConfig` now carries `#[validate(nested)]` on both `feature` and `service`, with a comment naming the consequence so the next reader doesn't lose it. - `core/src/config.rs.hbs` (the scaffold template) adds the same attribute on `service` with the equivalent note. The opt-in `generated_project_builds` test confirms the freshly-scaffolded workspace still compiles end-to-end. - New regression test `nested_validator_rules_propagate_to_outer_validate` in `app-demo-core::config`: writes a tempfile fixture with `timeout_ms = 50` and loads it via `load_app_config_with_options::<AppDemoConfig>` with the env overlay disabled, asserting the load fails with a validation error naming `timeout_ms`. The fixture-file approach avoids the process-env race with the sibling `env_overlay_overrides_nested_value` test (both target the same shared `APP_DEMO__SERVICE__TIMEOUT_MS` key under parallel `cargo test`). Adds `tempfile` to `app-demo` workspace deps to support the fixture. - Spec §6.8 and Stage 3 plan now describe the nested `feature: FeatureConfig` shape (was flat `feature_new_checkout`), and the spec snippet shows the required `#[validate(nested)]` attributes — so Stage 7/8 implementation reads the corrected shape. Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test --workspace --all-targets / cargo check --features "fastly cloudflare spin" / cargo check -p edgezero-adapter-spin --target wasm32-wasip1 — all green on both the root and `app-demo` workspaces. Opt-in `generated_project_builds` passes. Docs `prettier --check` passes.
The `[config]` table on every app-config file was an unnecessary
indirection: nothing in Stages 1-4 used sibling tables, the loader
discarded them, and end users had to indent every field a level
deeper for no payoff. The flat shape is what new projects intuit on
first read.
- `load_app_config_raw_with_options` returns the parsed root table
verbatim instead of looking up a `[config]` sub-table; the env
overlay walks the same root.
- `AppConfigError::MissingConfigTable` and
`AppConfigError::ConfigNotATable` are gone — neither variant is
expressible anymore. The remaining error surface (`Io`, `Parse`,
`Deserialize`, `Validation`, `EnvOverlay`) covers every real
failure: TOML always parses to a root table, and an empty or
field-mismatched file surfaces as `Deserialize`. `#[non_exhaustive]`
keeps this a non-breaking change for pattern-matchers.
- `name.toml.hbs` and `config.rs.hbs` updated to emit the flat
shape. The opt-in `generated_project_builds` test confirms the
freshly-scaffolded workspace still compiles end to end.
- `examples/app-demo/app-demo.toml` and the round-trip test fixture
drop the wrapper. Loader, CLI, and app-demo tests all rebuilt
against the new shape; `app-demo-cli config validate --strict` and
`edgezero config validate --strict` still exit 0.
- Small Spin-coupling cleanup in `config.rs`: the
`ValidationContext::has_spin_adapter()` method is gone; each Spin
check (`spin_key_syntax_check`, `spin_component_discovery`,
`spin_config_secret_collision`) now self-gates on
`manifest.adapters.contains_key("spin")`. The context type stays
adapter-agnostic — call sites read as a flat list of contract
checks. A trait-based dispatch over per-adapter check bundles
comes in a follow-up commit.
- Spec §3 (out-of-scope), §6.5, §6.10, §9 and plan Tasks 3.1 / 3.3 /
3.4 / 3.5 / 4.1 updated. `prettier --check` on `docs/` passes.
Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test
--workspace --all-targets / cargo check --features
"fastly cloudflare spin" / cargo check -p edgezero-adapter-spin
--target wasm32-wasip1 — all green on both the root and `app-demo`
workspaces. Opt-in `generated_project_builds` still passes.
Previously the validator string-matched `"spin"` / `"axum"` /
`"cloudflare"` in scattered helpers — `is_single_store_adapter`
was a `matches!` over adapter names, the Spin checks self-gated on
`manifest.adapters.contains_key("spin")`, and the typed flow called
`spin_config_secret_collision` unconditionally. Adding a new
per-adapter rule meant patching every helper.
Replace with an `AdapterCheck` trait + a `const ADAPTER_CHECKS`
registry of unit-struct implementers (`Axum`, `Cloudflare`,
`Spin`). The orchestrator iterates `adapter_checks_for(manifest)`,
which filters the registry by the adapter id the impl advertises
via `adapter_id()` — call sites read as a flat list of contract
checks, and each adapter's policy lives in one place.
- `check_app_config` / `check_manifest` / `check_typed` model the
three call sites (raw-flow body, manifest-side, typed-flow
collision). Defaults are `Ok(())`, so Axum and Cloudflare carry
only `single_store_kinds()` (their `secrets` Single capability).
- `single_store_kinds()` returns the spec §6.6 entries that
`strict_capability_completeness` walks, replacing the hardcoded
`matches!` table.
- Spin's three checks (key syntax, component discovery, collision)
remain as free fns; `Spin::check_*` thin-dispatches into them.
The internal self-gates inside those fns are gone — the registry
filter is the one true gate.
- Fastly is deliberately absent from the registry: it has no
special validation rules, so the default no-op impls would only
add noise.
The only string-match on adapter ids left in the validator is the
single `manifest.adapters.contains_key(check.adapter_id())` inside
`adapter_checks_for` — exactly the boundary where adapter-name
matching belongs.
Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test
--workspace --all-targets / cargo check --features
"fastly cloudflare spin" / cargo check -p edgezero-adapter-spin
--target wasm32-wasip1 — all green on both the root and `app-demo`
workspaces. All 27 `config validate` tests still pass.
…xample
Four small leftovers from the [config]-wrapper removal and the
Stage 2 `Kv` API change:
- Generator scaffold test now parses the generated `<name>.toml`
and asserts the structural contract — no `config` root key, and
`[service]` is at the root — instead of substring-matching on a
doc comment. The prior `app_toml.contains("[config]")` check
passed only because the generated file's *comment* mentions "no
`[config]` wrapper"; a regression that re-introduced
`[config.service]` would have slipped through.
- Public `Kv` extractor examples in `docs/guide/kv.md` and the
`edgezero-core::key_value_store` module rustdoc updated from the
removed `Kv(store): Kv` tuple-destructure form to the Stage 2
registry API (`kv.default()` / `kv.named(id)`). Both surfaces are
read first by anyone learning the KV story.
- Spec §9 ("Generated template vs the `app-demo` example") and the
plan status block updated to reflect the flat `<name>.toml` shape
and Stage 3 / 4 shipped state.
- `docs/guide/configuration.md` env-overlay example replaced. The
earlier text claimed `foo-bar` and `foo_bar` collapse to the
same env segment, but `env_segment` only uppercases — dashes and
underscores stay distinct. The actual collision case (same key
modulo letter case, e.g. `greeting_a` vs `GREETING_A`) is what
the loader rejects, and the docs now say so.
Gates: cargo fmt / clippy --all-features (-D warnings) / cargo test
--workspace --all-targets / cargo check --features
"fastly cloudflare spin" / cargo check -p edgezero-adapter-spin
--target wasm32-wasip1 — all green on both the root and `app-demo`
workspaces. `prettier --check` on `docs/` passes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the portable outbound HTTP and request-lifecycle contracts across core and the Axum, Cloudflare, Fastly, and Spin adapters.
start_batch_until, indexednext, explicitcancel, orderedcollect, and thesend_all_untilconvenience path.CompletedorCutoff. OnlyCutoffpermits unresolved slots in a successful result. Explicit adapter failure, premature driver EOF, and duplicate or out-of-range indices returnOutboundBatchFailure, preserving the precise error and every terminal slot collected before failure.ResponseEgressCompletion, transferred directly throughPreparedIngress,ResponseEgressEnvelope, and the soleResponseEgressAttempt. Terminal state is stored before the response-scoped completion and global observer run, each exactly once and behind independent panic boundaries. No response-extension carrier or envelope extraction bypass remains.ResponseEgressCompletion::joincomposition so independent response-owned resources share one exactly-once terminal report, and hard-cuts detached normalized errors toSend { completion, deadline } | Abortwith request-start-relative bounded deadlines.AdmissionDecision::Abort. Axum closes the owned HTTP/1 connection before body polling; Cloudflare, Fastly, and Spin return a fixed provider error at their strongest available pre-response boundary and remain conservatively classifiedBestEffort.Hooks::configureandHooks::build_app, propagating typed configuration failures before request serving. The demo, generator, templates, adapter entrypoints, and guides all use the new contract.AbortController, Fastly's low-levelstream_to_client, and Spin's WASI response/body/result writers.Downstream acceptance impact
Cloudflare
outbound-deadlinesandstreamed-upload-deadlinesareBestEffort, notNative. Downstream acceptance suites must use best-effort deadline wording and must not claim exact cancellation enforcement until deployed host-observed evidence exists.The response completion, ingress abort, and fallible application-assembly changes are intentional breaking changes. Consumers must provide a completion on every response-producing admission decision and propagate
Hooks::build_app()failures.Evidence boundaries
Capability declarations remain conservative where provider behavior is not proved:
Unsupportedon all adapters.Nativeon Axum andBestEfforton Cloudflare, Fastly, and Spin because finite host-side teardown is not proved or synchronously preemptible.Nativeon Axum's owned HTTP/1 connection andBestEfforton Cloudflare, Fastly, and Spin because provider transport reset/close is not directly observable.BestEfforton every adapter because no boundary proves end-client receipt; provider-specific blocking and teardown limits are documented.Unsupported; provider parsing/materialization, SDK copies, informational responses, trailers, allocator overhead, and other host-owned memory remain outside guest-visible caps.BestEfforton every adapter because provider operations are not proved cancellable within a finite wall-clock interval.BestEffortpending a deployed host-observed cancellation artifact. Fastly and Spin retain their separately documented deadline, phase-budget, and isolation limitations.Validation
scripts/run_tests.shcargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-targetscargo check --workspace --all-targets --features "fastly cloudflare spin"