Skip to content

EdgeZero CLI Extensions: extensible CLI, multi-store manifest, auth/provision/config#269

Open
aram356 wants to merge 241 commits into
mainfrom
feature/extensible-cli
Open

EdgeZero CLI Extensions: extensible CLI, multi-store manifest, auth/provision/config#269
aram356 wants to merge 241 commits into
mainfrom
feature/extensible-cli

Conversation

@aram356

@aram356 aram356 commented May 20, 2026

Copy link
Copy Markdown
Contributor

Epic: #268

Summary

Turns edgezero-cli into an extensible library, rewrites the manifest store schema and runtime to a multi-store model, adds auth / provision / config validate / config push commands, replaces the dev subcommand with a contributor-only demo, and updates app-demo to exercise everything across axum / cloudflare / fastly / spin. Plus two integrations completed in-branch: Spin 6.0 (WASI Preview 2) and chore/strict-clippy (PR #257).

Delivered as one PR, eight sequential stages (one per sub-issue of #268), with additional hardening from multi-round self-review and two integration cycles on top.

Stages (sub-issues of #268)

# Sub-issue Stage Status
1 #260 Extensible edgezero-cli library + generator + app-demo-cli skeleton ✅ Landed
2 #261 Manifest + runtime rewrite (atomic, all four adapters) ✅ Landed
3 #262 App-config schema, derive macro, env-overlay loader ✅ Landed
4 #263 config validate command ✅ Landed
5 #264 auth command + CommandRunner infrastructure ✅ Landed
6 #265 provision command ✅ Landed
7 #266 config push command ✅ Landed
8 #267 app-demo integration polish + docs audit ✅ Landed

In-branch integrations on top of the eight stages

Spin 6.0 (WASI Preview 2) — 487ac5f

  • spin-sdk = "6" workspace + app-demo + generator default
  • wasm32-wasip1wasm32-wasip2 for the Spin adapter only; Fastly stays on wasip1. CI matrix entry + CLAUDE.md + docs + scripts + scaffold template + app-demo manifests all updated.
  • 6.0 API rewrite: IncomingRequestRequest; req.into_parts() + IncomingBodyExt::bytes(); deleted the 10-arm Method enum match (Method is now a re-export of http::Method); Request::builder()…build().body(FullBody::new(Bytes::from(...)))?; async Store::open/get/set/delete/exists/get_keys; async variables::get; #[http_component]#[http_service].
  • Response type now Response<FullBody<Bytes>>; contract tests use http_body_util::BodyExt::collect().

chore/strict-clippy integration — 611064e

  • Merged origin/chore/strict-clippy so the workspace-wide strict-clippy gate (pedantic warn + restriction deny, 13-item allow-list) sits underneath everything in this PR.
  • Inherited the per-target adapter-wasm-clippy CI matrix added in Enable strict clippy with documented allow-list and defensive-coding pass #257; switched its spin entry from wasip1 → wasip2 to match the Spin 6 migration.
  • Conflict resolution: for the spin adapter (incoming was 5.2-era) and the cloudflare adapter (incoming pre-dated the Service builder), our 6.0 / Service-builder code wins semantically. Contract tests likewise stay on the Service builder surface. See merge commit body for the full per-file rationale.

Strict-clippy pass on wasm32 adapter targets — 910739d

  • Brings the inherited adapter-wasm-clippy matrix to green: cloudflare (~44 errors), fastly (~14), spin (~115).
  • Patterns applied (no #[allow] sprinkles): pub use mod::Foopub mod foo; absolute_pathsuse imports; min_ident_chars → renames (|e||err|, |c|char::is_control); #[inline] + # Errors on every public fn; arbitrary_source_item_ordering reordering; tests_outside_test_module + expect_used in tests fixed by wrapping in #[cfg(test)] mod tests { ... }; used_underscore_itemsconst _: fn() = assert_provider_impl::<T>;; let_underscore_must_usedrop(init_logger()); type alias SpinFullResponse = Response<FullBody<Bytes>> for the recurring complex type; HeaderValue::from_static("spin") to kill expect_used in proxy.rs; .saturating_add() for arithmetic_side_effects.

Self-review followups closed in this PR

Two close-out commits address findings from the post-merge self-reviews:

  • 16898f8 — Store id validation (rejects blank / whitespace / control / duplicate logical ids with store_id_blank / store_id_duplicate errors); docs ESLint ignore for .vitepress/.temp so the docs lint gate is build-state-independent; removed the misleading singular StoreDeclaration::config_store_name(&self, _adapter) helper.
  • 4928cdc — Spin wasm contract CI fix (tests insert *Registry::single_id(...) instead of bare handles so they match the dispatch boundary's synthesise_store_registries); generated_project_builds now invokes the generated typed CLI's config validate --strict (catches AppConfig drift the raw validator can't); docs build fix (multi-line inline backticks in cli-walkthrough.md push bullets converted to fenced shell blocks — VitePress was parsing the leaked <tempfile>/<id> as unterminated HTML tags); provision + push docs now describe the <platform-name> resolution (EDGEZERO__STORES__<KIND>__<ID>__NAME → logical id) for cloudflare/fastly instead of the wrong "matched by <store_id>" wording; Spin docs corrected on secret translation (config keys translate .→__; #[secret] values are only lowercased — matches SpinSecretStore::get_bytes + the cli validator exactly); stale plan doc references updated (app-demo IS in CI; Spin target IS wasip2); axum secret store rustdoc no longer references the removed cargo edgezero dev command.

Design + plan

Committed under docs/superpowers/ (force-added — that path is currently in .gitignore; maintainers can decide whether to formally track it):

  • docs/superpowers/specs/2026-05-19-cli-extensions-design.md
  • docs/superpowers/plans/2026-05-20-cli-extensions.md

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings (host strict-clippy)
  • cargo test --workspace --all-targets (1057+ tests pass)
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
  • cargo check -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly
  • cargo check -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare
  • cargo clippy -p edgezero-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown --all-targets -- -D warnings
  • cargo clippy -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --all-targets -- -D warnings
  • cargo clippy -p edgezero-adapter-spin --features spin --target wasm32-wasip2 --all-targets -- -D warnings
  • cd examples/app-demo && cargo test --workspace --all-targets
  • cd examples/app-demo && cargo fmt --check && cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo check -p app-demo-adapter-spin --target wasm32-wasip2
  • cargo test -p edgezero-cli --test generated_project_builds -- --ignored (now also runs the generated typed CLI's config validate --strict)
  • cd docs && npm run lint && npm run format && npm run build
  • Spin wasm contract suite under wasmtime run (12/12 — reviewer-verified locally; CI matrix runs the same)

aram356 added 30 commits April 25, 2026 14:13
Turns on `pedantic` (warn) and `restriction` (deny) workspace-wide and adds
`[lints] workspace = true` to every crate so the policy actually applies.

Captures a baseline allow-list in `Cargo.toml`, organized by category
(Documentation, Style/formatting, Defensive coding, API design, Imports/paths,
Output/diagnostics, Tests, Attributes) with per-lint counts and rationales —
each entry is a TODO unless explicitly marked intentional.

Defensive-coding pass:
- New `clippy.toml` with `allow-{unwrap,expect,panic,indexing-slicing}-in-tests`
  so test code keeps its conventional idioms; production code is denied.
- Production unwraps factored out: `current_dir()`/`init_logger()` now
  propagate via `?`; `writeln!` to a `String` rewritten as `push_str(&format!)`
  so there's no `Result` to discard; bundled-template registration and other
  genuine compile-time invariants use `.expect("...")` as documented assertions.
- Other small wins: `inefficient_to_string` fixed, `match_same_arms` collapsed,
  `manual_assert` swapped, `cast_lossless`+truncation replaced with bound-checked
  `u16::try_from` in adapter-axum CLI, `unreachable!()` in `#[action]` macro
  replaced with a proper `syn::Error::compile_error`.

Lints kept allowed in the workspace are annotated with `(intentional)` where
they conflict with idiomatic Rust (`implicit_return`, `question_mark_used`,
`pattern_type_mismatch`, `default_numeric_fallback`, `arithmetic_side_effects`,
`as_conversions`, `string_slice`) or have no per-test config option
(`assertions_on_result_states`).

`cargo clippy --workspace --all-targets --all-features -- -D warnings`,
`cargo fmt`, and `cargo test --workspace --all-targets` all pass.
Drives the API-design lint group from 18 allows down to 8 (kept as intentional
with rationale comments in `Cargo.toml`).

Factored out:
- `return_self_not_must_use` (18): added `#[must_use]` to all `RouterBuilder`
  builder methods. Catches "I forgot to call `.build()`" bugs.
- `impl_trait_in_params` (26): converted `fn f(x: impl Into<String>)` →
  explicit generics on `EdgeError::*`, `ConfigStoreError::*`, `RouteInfo::new`,
  `InMemorySecretStore::new`, `AxumConfigStore::{new,from_env,from_lookup}`.
  Makes turbofish callable.
- `rc_buffer` (4): `Arc<Vec<RouteInfo>>` → `Arc<[RouteInfo]>` in `RouterInner`
  and the builder. Saves an indirection.
- `unnecessary_wraps` (4): `build_fastly_request` and `convert_response` no
  longer wrap an always-Ok value in `Result`. Cleaner call sites.
- `mutex_atomic` (1): `Arc<Mutex<bool>>` → `Arc<AtomicBool>` in the
  `middleware_fn` test.
- `ref_patterns` (11): `if let Some(ref x) = ...` → `if let Some(x) = &...`
  across env-override `Drop` impls, router builder, response builder, body
  matchers.
- `wildcard_enum_match_arm` (7): `args.rs` tests now use `let-else` instead of
  catch-all wildcard match arms; `EdgeError::source` now lists each non-Internal
  variant explicitly; `cli/build.rs` switched to `if let Value::Table(_) = ...`;
  the one site that genuinely matches an external enum (`fastly::config_store::
  LookupError`) keeps a localized `#[allow(..., reason = "external enum")]`.
- `clone_on_ref_ptr` (1): `store.clone()` → `Arc::clone(&store)` in the axum
  service test (with explicit `Arc<dyn KvStore>` annotation so `Arc::clone`
  picks the right type).
- `renamed_function_params` (4): renamed `request: Request` → `req: Request`
  in `Service::call` impls to match the trait signature.
- `same_name_method` (2): `EdgeError::source` deliberately shadows
  `std::error::Error::source` (typed `&AnyError` vs trait-object `&dyn Error`).
  Documented at the call site with a `#[allow(..., reason = "...")]`.

Kept allowed (with `(intentional: ...)` comments in `Cargo.toml`):
- `exhaustive_structs` (108) and `exhaustive_enums` (18): blanket
  `#[non_exhaustive]` would break user pattern matching and field-syntax
  construction. Apply per-type only when genuinely planned.
- `must_use_candidate` (117): most flagged sites are getters returning
  `&str`/`&Path` — ignoring is impossible, the lint adds noise.
- `missing_trait_methods` (20): relying on default trait methods is fine.
- `needless_pass_by_value` (16): most flagged sites are deliberate ownership
  transfers — error transformers, proc-macro signatures, builders.
- `field_scoped_visibility_modifiers`, `partial_pub_fields`,
  `trivially_copy_pass_by_ref`: deliberate API design choices.

Final clippy + workspace tests pass.
Following pushback that the prior passes were papering over lints rather
than addressing them, this commit revisits each lint that was previously
allowed with hand-wavy reasoning and either (a) factors it out for real,
(b) applies it selectively where the fix matters, or (c) replaces the
rationale with a per-site audit finding.

Real fixes:

- `Body::as_bytes` and `Body::into_bytes` no longer panic on streaming
  bodies — they return `Option`. This eliminates two production panic
  sites the previous pass left as `panic = "allow"`. The internal
  `into_bytes_bounded` site is correctly gated by `is_stream()`; all
  other callers are tests that *intentionally* assert the body is
  buffered, now with `.expect("buffered")`.

- `assertions_on_result_states` is no longer allowed. All 13 sites
  converted from `assert!(r.is_ok())` / `assert!(r.is_err())` to
  `r.expect("...")` / `r.expect_err("...")` — these print the value or
  error on failure instead of just `assertion failed: false`.

- `#[non_exhaustive]` applied to all 4 error enums (`EdgeError`,
  `KvError`, `SecretError`, `ConfigStoreError`) and the 3 manifest
  enums (`HttpMethod`, `BodyMode`, `LogLevel`) — this is the idiomatic
  Rust pattern for error/config enums (see `std::io::ErrorKind`,
  `serde::de::Error`). Also applied to 19 deserialize-only manifest
  structs (`Manifest*`, `ResolvedEnvironment*`-where-not-constructed-
  externally).

- `needless_pass_by_value` real fix in `run_app_with_stores`:
  `FastlyLogging` and `StoreRequirements` are now passed by reference
  since the function only reads from them.

Lints kept allowed but with audited per-site rationales (replacing the
previous one-line hand-waves):

- `pattern_type_mismatch`: every flagged site uses Rust 2018
  match-ergonomics. The "fix" reverts to manual `ref` patterns or
  explicit `&Variant(...)` arms, both worse.
- `arithmetic_side_effects`: every site is bounded by domain invariants
  (TTL+now, path component counts, byte offsets after `len()` checks).
- `as_conversions`: dominated by trait-object coercions (`Arc::new(x)
  as BoxMiddleware`) which cannot be expressed as `From`/`Into` in
  stable Rust.
- `string_slice`: every flagged site indexes ASCII-only data (env var
  names, header names, `matchit` path components).
- `expect_used`: 62 production sites audited — bundled-template
  registration, AsyncRead-contract slice access, lock-poisoning
  unrecoverable, build-script panics. None benefit from `?`
  propagation.
- `panic`: route-registration `unwrap_or_else(|err| panic!(...))` and
  proc-macro expansion failures. Both build/setup-time programmer
  errors, not runtime conditions.
- `cast_possible_truncation` / `cast_sign_loss`: narrowing/sign casts
  always preceded by range checks.
- `exhaustive_structs` / `exhaustive_enums`: applied selectively above;
  remaining sites are tuple-struct extractors users *destructure*,
  unit structs, externally-constructed scaffold blueprints, request-
  context types used in integration tests, and small enums (`Body`,
  `AdapterAction`) where adding `#[non_exhaustive]` would force 12+
  adapter sites to add never-firing wildcard arms.

Workspace clippy + tests still pass with `-D warnings`.
Removes 22 mechanical-fix allow entries from `Cargo.toml` after fixing the
underlying call sites:

Auto-fixed (`cargo clippy --fix` + manual cleanup):
- `uninlined_format_args` (180), `redundant_closure_for_method_calls` (25),
  `map_unwrap_or` (29), `explicit_iter_loop` (14),
  `unseparated_literal_suffix` (24, separated form chosen),
  `implicit_clone` (2), `pathbuf_init_then_push` (3), `string_add` (3),
  `unreadable_literal` (4), `manual_let_else` (2), `else_if_without_else`
  (2 — the Fastly-vs-other-adapter logging branch refactored to a
  pre-computed `Option<endpoint>`), `return_and_then` (2), `ip_constant`
  (2), `manual_string_new` (1), `redundant_type_annotations` (1),
  `needless_raw_strings` (1), `needless_raw_string_hashes` (1),
  `elidable_lifetime_names` (2), `redundant_test_prefix` (1),
  `if_then_some_else_none` (6), `deref_by_slicing` (5), `shadow_same` (4),
  `match_wildcard_for_single_variants` (5), `pub_with_shorthand` (30),
  `decimal_literal_representation` (1).

Real fixes (manual):
- `key_value_store.rs`: replaced bare scoping blocks `{ ...?; }` with
  explicit `drop(table)` so neither `semicolon_inside_block` nor
  `semicolon_outside_block` fires (the lint pair is mutually exclusive
  and one always fires). Same treatment for `decompress.rs` and
  `proxy.rs` brotli-test compressor scopes.
- `middleware.rs`: collapsed the `Mutex` lock+await pattern into a
  single `self.log.lock().unwrap().push(...)` statement so the lock
  guard drops immediately (was previously triggering
  `await_holding_lock` after I removed the scoping block).
- `dev_server.rs`: `let service = service` (shadow_same) refactored
  into a `let service = { mut service = ...; ...; service }` block
  expression that yields the configured value.
- `response.rs`: dropped redundant `let stream = stream` shadow.
- `request.rs`: renamed `test_is_json_content_type` →
  `json_content_type_detection` (the redundant `test_` prefix).
- `proxy.rs` test panics: `_ => panic!(...)` → `Body::Stream(_) =>
  panic!(...)` so the match stays exhaustive when `Body` grows.
- `cli.rs`: `0xFFFF` instead of `65535` for the u16-MAX boundary.
- `dev_server.rs::stable_store_name_hash`: split FNV-1a magic numbers
  with `_` separators.

The Style section in `Cargo.toml` is rewritten as a tight allow-list
(no narrative, no historical commit log inside the manifest). Each
remaining entry has a one-line rationale grouped by category:
- Idiomatic Rust (8 lints): `implicit_return`, `min_ident_chars`,
  `single_call_fn`, `single_char_lifetime_names`, `pub_use`,
  `str_to_string`, `question_mark_used` (was duplicated; consolidated
  in Defensive section).
- Mutually-exclusive pairs we picked one side of: `separated_literal_suffix`,
  `pub_with_shorthand`.
- Held-by-choice (5 lints): `format_push_string`, `shadow_reuse`,
  `shadow_unrelated`, `similar_names`, `non_ascii_literal`,
  `too_many_lines`, `arbitrary_source_item_ordering`,
  `module_name_repetitions`.

Allow-list went from ~80 entries to 57 across all categories.
`cargo clippy --workspace --all-targets --all-features -- -D warnings`
and `cargo test --workspace --all-targets` both pass.
`#[action]` requires the user-written fn to be `async fn` because the
generated outer fn `.await`s it. When a handler body has no awaits of
its own, `clippy::unused_async` fires on the user's source — but the
user has no choice; the macro forces `async`.

Inject the allow into the inner fn's attribute list inside the macro
expansion so handler authors don't have to know about the lint.
Imports/paths track:
- `non_std_lazy_statics` (6 sites): `once_cell::Lazy` → `std::sync::LazyLock`
  in `crates/edgezero-adapter/src/{registry,scaffold}.rs`. Drops `once_cell`
  from `crates/edgezero-adapter/Cargo.toml`. (Workspace dep stays — example
  app still uses it.)
- `unused_trait_names` (37 sites): `use Foo;` → `use Foo as _;` for traits
  imported only for their methods (`StreamExt`, `Write`, `Read`, `Hooks`,
  `IntoHandler`, `Spanned`, etc.) across both library and proc-macro crates.
- `iter_over_hash_type` (1 site): the only flagged production iteration is
  in `RouterInner::dispatch` (collecting allowed methods for a 405 response).
  Refactored from a `for ... { allowed.insert(...) }` loop into
  `.iter().filter().map().collect::<HashSet<_>>()`. The result is a `HashSet`
  whose order doesn't matter (`EdgeError::method_not_allowed` sorts on render).

Attributes track:
- `allow_attributes` (3 sites): `#[allow(...)]` → `#[expect(..., reason)]` on
  the genuine deliberate-shadowing/wildcard-match-arm sites in
  `error.rs::EdgeError::source` and `config_store.rs::map_lookup_error`. The
  CLI build script (`build.rs`) now emits `#[expect(unused_imports, reason)]`
  on every generated `pub(crate) use` re-export.
- `allow_attributes_without_reason` (5 sites): every existing `#[allow(...)]`
  now has a `, reason = "..."` and (where stable-`expect` applies) is migrated
  to `#[expect(...)]`. Sites: `cli_support.rs` and `decompress.rs` top-of-file
  `#![expect(dead_code, ...)]`; the four test-only `Deserialize` field structs
  in `context.rs` and `params.rs`; the macro's `manifest_definitions` shim;
  the two fastly `deprecated` re-exports.

Also kept allowed (real audits in `Cargo.toml` rationales):
- `absolute_paths` (200+ sites): one-shot `std::env::var()` / `std::fmt::Display`
  uses; adding `use` statements wouldn't improve readability for single-use.
- `std_instead_of_alloc` / `std_instead_of_core`: not targeting `no_std`.
- `tests_outside_test_module`: lint matches plain `#[cfg(test)] mod tests`
  only — doesn't recognize `#[cfg(all(test, feature = "..."))]` or
  integration-test files in `tests/`.
- `print_stderr` / `print_stdout`: kept in CLI top-level error reporters and
  status output (`[edgezero] creating project at ...`).

Allow-list now at 51 entries.
…c / doc_markdown / missing_fields_in_debug

Adds public-API docs across every flagged site:

- `missing_panics_doc` (28 sites): added `# Panics` sections describing
  each panic condition. Most are documented invariants (lock poisoning,
  AsyncRead-contract slice access, builder pre-validated headers); a few
  are caller-controlled (`enable_route_listing_at` asserts on path shape,
  `RouterBuilder::build` panics on duplicate route, `load_from_str` panics
  on invalid embedded TOML — the docs note safer alternatives).
- `missing_errors_doc` (62 unique pub fns, 124 lints with re-exports):
  added `# Errors` sections describing the concrete error variants
  returned. Dispatched via batch script with per-fn descriptions covering
  every site (KV / secret / config-store / manifest / proxy / extractor /
  body / responder / middleware / adapter dispatch APIs).
- `missing_fields_in_debug` (2 unique sites — 4 with re-exports):
  `ProxyRequest`/`ProxyResponse` `Debug` impls now use `finish_non_exhaustive()`
  to acknowledge the deliberately-skipped `body` and `extensions` fields.
- `doc_markdown` (17 sites): backticked `EdgeZero`, `SystemTime`, `Axum`,
  `SecretStore`, etc. in doc comments.

Lints kept allowed (with rationale comments in `Cargo.toml`):
- `missing_docs_in_private_items` (275 sites): private docs aren't
  load-bearing for users — industry-standard "kept allowed".
- `missing_inline_in_public_items`: `#[inline]` is a perf hint; rustc/LLVM
  make better decisions than blanket-marking every cross-crate public item.

Allow-list: 51 → 47 entries.
…t_stdout allows

The CLI binary now initializes a `simple_logger` with no timestamps and no
level prefixes (so the user-facing UX is unchanged: `[edgezero] creating
project at ...` still prints exactly that), and all `println!` /
`eprintln!` sites are converted to `log::info!` / `log::error!` /
`log::warn!`.

Sites converted (24 total):
- `crates/edgezero-cli/src/main.rs`: top-level error reporters (`new`,
  `build`, `deploy`, `serve`, `dev`) + status output for store-binding
  warnings.
- `crates/edgezero-cli/src/generator.rs`: 9 status messages and 2 git
  warnings now go through the logger.
- `crates/edgezero-cli/src/dev_server.rs`, `adapter.rs`: dev manifest /
  command-failure reporting.
- `crates/edgezero-adapter-{axum,cloudflare,fastly,spin}/src/cli.rs`:
  one build-artifact-path message each.

Allow-list: 47 → 45 entries (`print_stderr` + `print_stdout` removed).
Real renames + restructuring (no inline allow attrs):

- `non_ascii_literal` (3 sites): replaced the Japanese KV-key test literal
  with `\u{...}` escapes (same runtime bytes, ASCII source) instead of
  `#[expect]`-ing the lint. Replaced `→` arrow in a CLI test message with
  `->`.
- `similar_names` (2 sites): renamed `decoded` → `output` in
  `crates/edgezero-adapter-spin/src/decompress.rs` to break the
  `decoded`/`decoder` prefix-share that the lint flags.
- `too_many_lines` (1 site): split `collect_adapter_data` in
  `crates/edgezero-cli/src/generator.rs` into three helpers
  (`blueprint_data_entries`, `render_manifest_section`,
  `append_readme_entries`).
- `shadow_unrelated` (~14 sites): renamed every flagged inner binding
  to be specific to its purpose:
  - `serve_with_stores`: `let router = Router::new()...` →
    `axum_router`; `let server = server.with_graceful_shutdown(...)` →
    `graceful_server`; `let shutdown = ...` → `shutdown_signal`.
  - `store_name_slug`: `Some(ch)` → `Some(lower_ch)` (was shadowing
    outer `ch`).
  - dev_server tests: `let url = ...` reused per-step → `write_url`,
    `read_url`, `check_url`, `delete_url`, `save_url`, `load_url`;
    `let resp = ...` → `write_response`/`read_response`/`save_resp`/
    `load_resp`/`exists_before`/`exists_after`.
  - `axum::key_value_store::get_bytes`: inner write-txn `table` →
    `write_table`, `entry` → `fresh_entry`.
  - `list_keys_page` cursor match: inner `Some(cursor)` → `Some(scan_from)`.
  - `data_persists_across_reopens` test: second `let store = ...` →
    `reopened`.
  - `axum::response::into_axum_response` error path: `body` →
    `error_body`, `response` → `error_response`. Test: `stream` →
    `body_stream`.
  - `fastly::key_value_store::list_keys_page`: inner `cursor` →
    `next_cursor`.
  - `fastly::proxy` test: collapsed two pairs of `body`/`collected` reuse
    into named bindings (`plain_body`, `gzip_body`).
  - `spin::decompress` test: `let result = ...` reused per-encoding →
    `none_encoding`, `identity_encoding`.
  - `core::body::from_stream_maps_errors` test: `stream` →
    `source`/`chunks`.
  - `core::key_value_store` tests: `let val = ...` reused → `after_first`/
    `after_second`/`int_val`/`str_val`/`single_dot_err`/`double_dot_err`.
  - `axum::cli::read_axum_project`: `Some(value)` → `Some(port_value)`
    (was shadowing outer `value` from `toml::from_str`).

Allow-list: 45 → 41 entries.
…quest path

Real fixes (not just docs) for every production-code .expect() that could
fire under upstream contract change or misconfigured input:

- `IntoResponse::into_response` now returns `Result<Response, EdgeError>`
  workspace-wide (breaking change). Cascades through `Responder`,
  `EdgeError::into_response`, `RouterService::oneshot`, the handler future
  in `core/handler.rs`, and the route-listing builder.
- `ProxyResponse::into_response` and `core::response::response_with_body`
  now return `Result<Response, EdgeError>` and propagate `http::Builder`
  failures via `map_err(EdgeError::internal)?` instead of `.expect()`.
- `core::body::Body::into_bytes_bounded` rewritten as a `match self {
  Once | Stream }` so the unreachable `is_stream()`-guarded `.expect()`
  pair is gone — the compiler proves exhaustiveness.
- `core/compression.rs` decoder slice access now propagates as
  `io::Error::other(...)` instead of `.expect("AsyncRead contract")`,
  so a malicious or buggy upstream stream fails the request rather than
  crashing the worker.
- `axum/response.rs::into_axum_response` error path no longer uses
  `Response::builder().expect(...)`; constructs the 500 response
  directly via `Response::new` + `status_mut` + `headers_mut().insert`,
  every step infallible by `http`-crate contract.
- `axum/proxy.rs` replaced `Default` (which panicked on TLS init) with
  fallible `AxumProxyClient::try_new() -> Result<_, reqwest::Error>`.
  Production caller in `request.rs::into_core_request` propagates as a
  `String` error (matches the fn's existing return type).
- `fastly/logger.rs::init_logger` now returns
  `Result<(), InitLoggerError>` (a typed enum wrapping the underlying
  build error and `log::SetLoggerError`) instead of `.expect("non-empty
  Fastly logger endpoint")`. `lib.rs::init_logger` re-exports the wider
  return type.
- `cli/generator.rs::render_templates` propagates the previously-
  `.expect("adapter context dir has a file name")` invariant as
  `io::Error::other` since the surrounding fn already returns
  `io::Result<()>`.

`axum/service.rs::call` (the tower `Service` impl) bridges the new
`Result<Response, EdgeError>` from `RouterService::oneshot` into a
`Response<AxumBody>` by mapping the error to a hard-coded 500 with a
plain-text body — `Service::call` returns `Result<Response, Infallible>`
so we cannot propagate further up the stack here.

`adapter-fastly` adds `thiserror` as a direct dependency for
`InitLoggerError`. All 557 workspace tests still pass.
Replaces the previous \`std::io::Result<()>\` / \`io::Error::other(format!(...))\`
shape across the \`edgezero new\` code path with two domain-specific error
types:

- \`crate::scaffold::ScaffoldError\` (variants \`Io { path, source }\` and
  \`Render { name, message }\`) wraps every Handlebars failure and every
  filesystem op inside template rendering with the offending path/template
  name attached.
- \`crate::generator::GeneratorError\` (variants \`OutputDirExists\`,
  \`AdapterDirMissingFileName\`, \`Io { path, source }\`, and
  \`Scaffold(#[from] ScaffoldError)\`) replaces the workspace-construction
  io::Error stringification.

\`generate_new\`, \`ProjectLayout::new\`, \`collect_adapter_data\`, and
\`render_templates\` all return \`Result<_, GeneratorError>\`.

\`adapter-cli\` and \`scaffold\` now depend on \`thiserror\` directly. All
557 workspace tests still pass.
The `IntoResponse::into_response` change in 1506738 turned the trait into
`-> Result<Response, EdgeError>` workspace-wide. The demo app
(`examples/app-demo/`) is excluded from the main `Cargo.toml` workspace,
so it didn't get rebuilt by the workspace clippy/test gate and silently
broke. This propagates the same fix to the demo:

- Every `block_on(handler(ctx)).expect("handler ok").into_response()` in
  `crates/app-demo-core/src/handlers.rs` test code now appends
  `.expect("response")` to unwrap the response result.
- Every `into_body().into_bytes()` test path now appends
  `.expect("buffered")` since `Body::into_bytes()` returns
  `Option<Bytes>` (changed in the defensive-coding pass).

`cd examples/app-demo && cargo test --workspace --all-targets` passes
all 21 demo handler tests; `cargo clippy --workspace -- -D warnings`
also clean.
Inherit pedantic+restriction lints in the demo workspace and each demo
crate. Fix the lints that flagged real issues in the demo handlers
(`as _` trait imports, inlined format args, fast-path `to_string`,
renamed shadowed bindings, separated literal suffix). The demo's
allow-list is intentionally narrower than the library's — only entries
the demo actually trips. New allows can be added lazily as future
failures surface.
Add a clippy.toml mirroring the parent (allow expect/unwrap/panic/
indexing-slicing in tests). Then refactor away the workspace allows
that were genuine wins:

- shadow_reuse: rename `chunk` and `cursor` shadows
- absolute_paths: import std::env, std::time::Duration, std::process,
  and use already-imported Arc instead of std::sync::Arc
- default_numeric_fallback: add type suffixes (1_u64, 0_i32..3_i32, 1_i64)
- pattern_type_mismatch: implicitly fixed by str_to_owned changes
- missing_trait_methods: implement KvStore::exists on the test MockKv
- expect_used in production code: stream() now propagates the response
  builder error via EdgeError::internal

The remaining allow-list keeps only entries the demo actually trips
that match main's philosophical stance — std (not core/alloc) for
binaries, idiomatic `?` over match, terse closure idents, and the
single exhaustive_structs site that comes from the `app!` macro.
- str_to_string (21 sites): `.to_string()` → `.to_owned()` on `&str`
- arithmetic_side_effects: counter `n + 1` → `n.wrapping_add(1)`
- min_ident_chars + pattern_type_mismatch: rename closure
  destructures `|(k, v)|` → `|&(name, value)|`/`|&(key, value)|`
- pub_with_shorthand + field_scoped_visibility_modifiers:
  drop `pub(crate)` shorthand on the demo's DTOs and handlers — the
  `mod handlers;` declaration is already private, so plain `pub` is
  crate-private at the boundary
- print_stderr: axum main returns `anyhow::Result<()>` and lets the
  Termination impl render errors; fastly/cloudflare host stubs keep
  `eprintln!` behind a localized `#[expect]` with reason since
  they only run on the wrong target

Workspace allow-list now keeps only the entries that match main's
philosophical stance (idiomatic `?`, `pub` shorthand handled per-call
site, etc.) plus the single `exhaustive_structs` site from the `app!`
macro.
Drop the `arbitrary_source_item_ordering` allow in favor of the
canonical clippy-restriction layout:

- Top of `handlers.rs`: consts (alphabetical), then structs
  (alphabetical: ConfigParams, EchoBody, EchoParams, NoteIdPath,
  ProxyPath), then handler fns
- Test mod: uses, then structs (alphabetical), then impls grouped
  with their self-types, then helper + test fns interleaved in
  alphabetical order
- `impl KvStore for MockKv` methods alphabetical (delete, exists,
  get_bytes, list_keys_page, put_bytes, put_bytes_with_ttl)
- Hoisted the late `use edgezero_core::secret_store::...` up to
  the test mod's use block

No behavior changes — pure reordering. Demo workspace allow-list
drops to 8 entries.
The `edgezero new` generator now scaffolds the same lint policy
EdgeZero itself uses:

- Root `Cargo.toml` carries `[workspace.lints.clippy]` (pedantic warn
  + restriction deny) with the same demo-tested allow-list
- Root `clippy.toml` exempts tests from `unwrap`/`expect`/`panic`/
  indexing-slicing restriction lints
- Each generated crate's Cargo.toml inherits via `[lints] workspace = true`

Generated projects are clippy-clean against the strict gate out of the box.
Both adapters were calling `from_core_response` directly on the router's
return value, but `oneshot` now yields `Result<Response, EdgeError>`
since the response builder errors propagate through the router. Extract
the response with `?` first so the wasm32 builds (`--target
wasm32-unknown-unknown` for cloudflare, `--target wasm32-wasip1` for
spin) compile again.
… per-site

Real fixes (allows now justified by audit, not laziness):
- build.rs returns `Result<(), Box<dyn Error>>` instead of expect-panicking
- adapter registry / blueprint registry recover from poisoned RwLocks via
  `unwrap_or_else(PoisonError::into_inner)` rather than expect-panicking
- ManifestLoader gains `try_load_from_str` returning `io::Result`; adapter
  `run_app` paths propagate via `?`. The non-fallible `load_from_str` keeps
  its panic-on-bad-input contract for compile-time-embedded manifests, with
  a documented per-fn `#[expect(clippy::panic, reason = ...)]`
- `expand_app` macro emits `compile_error!()` instead of panicking on bad
  `edgezero.toml` (rustc surfaces a clean build error)
- `parse_handler_path` keeps a panic with a clear reason — proc-macro
  expansion errors *are* build failures
- `partial_pub_fields` on `Manifest`: privatized `root` and
  `logging_resolved`, kept the deserialized fields `pub` for the public
  API. Localized `#[expect]` documents the deliberate split
- `must_use_candidate` fixed on cli_support helpers via `#[must_use]`
- `missing_inline` fixed on adapter/scaffold registry functions
- `pub_use`, `format_push_string`, `arithmetic_side_effects`,
  `default_numeric_fallback`, `pattern_type_mismatch`, `min_ident_chars`,
  `str_to_string`, `absolute_paths`, `module_name_repetitions`,
  `shadow_reuse`: all kept as workspace allows but with concise
  rationales replacing the prior verbose audit notes

Each remaining workspace allow now has a one-line reason. The list is
shorter than before but explicitly accepts the lints whose "fix" would
universally make the code worse (match-ergonomics destructures, std-only
binary entrypoints, idiomatic `?`/return).
…space-wide

54 sites across 23 files. Fixed places where my bulk replace had wrongly
converted Display::to_string() calls (anyhow::Error, io::Error, i32 etc.)
back to .to_string(). The lint allow is dropped from the workspace.
23 sites across extractor.rs, key_value_store.rs, middleware.rs, proxy.rs,
adapter-axum dev_server/key_value_store, adapter-spin decompress.

Validator length(min=N) gets _u64; range(min=N, max=N) gets matching
type suffix; loop-bound and assertion literals get explicit i32.
Core crate: replaced 60+ `std::collections::HashMap`,
`std::sync::Arc`, `std::ops::Deref/DerefMut`, `crate::error::EdgeError`,
`futures::executor::block_on`, `std::task::*`, `std::string::String::*`
absolute paths with explicit `use` statements.

Axum proxy.rs: imported the various `axum::http::*` and `axum::routing::*`
types used in test functions.

The lint stays allowed at the workspace level for adapter test modules where
one-shot uses of framework types like `axum::http::HeaderMap` and
`fastly::kv_store::KVStore` are clearer inline.
Real fixes (workspace allows dropped, code refactored):
- AdapterAction marked #[non_exhaustive] with wildcard arms in adapter cli
  match sites — drops a workspace exhaustive_enums concession
- Adapter crate exposes `pub mod registry` instead of pub-using items at
  the crate root — drops the workspace pub_use concession
- expand_action_impl made private (no longer pub(crate)) — drops the
  workspace pub_with_shorthand concession on this site
- ManifestLoader, Manifest, ManifestApp/HttpTrigger/Environment/Binding/
  ResolvedEnvironment*, ManifestAdapterBuild/Commands,
  ManifestConfigStoreConfig, ManifestLoggingConfig, ResolvedLoggingConfig,
  ManifestKvConfig, ManifestSecretsConfig, HttpMethod, LogLevel — all
  reordered to match canonical clippy item ordering (consts first, then
  structs, impls, fns; alphabetical within each group)
- Manifest impl methods sorted alphabetically; Manifest fields sorted
- match-ergonomics destructures rewritten as let-else for clarity
- HttpMethod gained Copy; LogLevel/HttpMethod take `self` (drops
  trivially_copy_pass_by_ref)
- partial_pub_fields fixed via consistent pub on Stores in fastly request
- needless_pass_by_value: run_app_with_config / run_app_with_logging take
  `&FastlyLogging`; map_edge_error / map_lookup_error take by ref;
  build_fastly_request takes `&HeaderMap`; generate_new takes `&NewArgs`
- expect_used localized on register_templates with rationale
- ManifestLoader::load_from_str / parse_handler_path keep panic-on-bad-
  build-input contract documented per-fn
- Router: route-listing duplicate-path panic + add_route panic both
  documented per-fn (build-time programmer error)
- spin contract test uses #[allow] for expect/tests-outside per file
- separate manifest_definitions.rs in macros crate (drops mod-after-use)

Workspace allows that survived (most match audited rationales):
implicit_return, question_mark_used, single_call_fn, separated_literal_suffix,
pub_with_shorthand (rustfmt-enforced), pub_use, min_ident_chars,
single_char_lifetime_names, shadow_reuse, module_name_repetitions,
format_push_string, pattern_type_mismatch, arithmetic_side_effects,
float_arithmetic, as_conversions, exhaustive_structs, exhaustive_enums,
missing_trait_methods, absolute_paths, std_instead_of_alloc/core,
missing_inline_in_public_items, tests_outside_test_module,
arbitrary_source_item_ordering (core-crate files outside manifest.rs).

Tests pass, strict clippy clean across workspace + demo.
Override KvStore::exists in 4 production impls (axum/fastly/cloudflare +
NoopKvStore) and the in-test MockStore. Override configure/name/
config_store/build_app in the two Hooks test impls. Update the #[app]
macro to emit configure, build_app, and a None-returning config_store
when [stores.config] is absent so generated user apps still pass clippy.
Add explicit clone_from to RouteEntry's Clone impl.
Delete config_store, key_value_store, and secret_store crate-root
re-exports — items remain reachable via the `pub mod` paths. Update the
two short-path callers (axum service.rs / secret_store.rs) to use full
module paths. Keep `pub use edgezero_macros::{action, app}` and the
`http` facade re-exports — these are the only surviving sites and the
lint is module-scoped so it cannot be silenced per-item. Workspace
allow rationale updated to point to those two patterns.
The previous comment framed `push_str(&format!(...))` as a stylistic
preference. It is actually the only call-site form that satisfies the
full restriction-deny gate: `write!(s, ...)` returns a `Result` which
trips `let_underscore_must_use` under `let _ =`, `unwrap_used` under
`.unwrap()`, and `expect_used` under `.expect()`.
Switch generator.rs from `push_str(&format!(...))` to `writeln!(...)?`
which writes directly into the buffer (no temp String allocation) and
propagates `std::fmt::Error` rather than silencing it. Add
`GeneratorError::Format(#[from] std::fmt::Error)` and bubble the result
through `render_manifest_section` and `append_readme_entries`. Drop the
workspace allow.
Rename 'a → 'mw on Next, 'a → 'route on RouteMatch,
'a → 'manifest on manifest_command, and 'a → 'blueprint on
AdapterContext. Drop the workspace allow.
Eliminate let-rebinding shadows across core, fastly, axum, and cli
crates. The recurring patterns:
- `while let Some(chunk) = stream.next().await { let chunk = chunk?; }`
  → rename outer to `result`, keep inner `chunk`
- `if let Some(cursor) = cursor.filter(...)` → rename outer/inner to
  distinct names
- `let path = path.into()` (Into-paramter idiom) → rename to
  destination-specific name
- closure params shadowing outer captures → rename closure param

All renames preserve semantics; tests + workspace clippy + wasm
target checks all pass.
Split `#[cfg(all(test, feature = "..."))]` on test modules into
two separate cfg attributes (`#[cfg(test)] #[cfg(feature = "...")]`)
which the lint recognizes correctly. Affects edgezero-adapter-fastly
lib.rs and edgezero-cli main.rs.
aram356 added 3 commits June 6, 2026 15:54
Addresses the post-f1179df PR #257 review.

1. Medium — Spin Cloud pushes are non-atomic across chunks (one
   `spin cloud key-value set` shellout per ≤96 KiB argv chunk), but
   the pre-fix error only named the failing chunk's size + exit
   status. If chunk 1 committed and chunk 2 failed, the operator
   was left with partially-updated live cloud state and no resume
   boundary. Fastly already produces a committed / failed /
   not-attempted diagnostic (cli.rs::push_entries_with_committer).

   Mirror Fastly's shape in `push_cloud::write_batch`: track a
   cursor through `entries` as committed-chunks accumulate, and on
   failure emit a structured error naming
   - committed keys (already on Fermyon Cloud, safe to skip),
   - failed-chunk keys (the cloud API is atomic per shellout, so a
     non-zero exit means none of these landed),
   - not-attempted keys (subsequent chunks, fully re-push needed),
   - a resume hint pointing out that `set` is idempotent so
     re-running with the full set is also safe.

   Regression covered by
   `write_batch_partial_failure_reports_committed_failed_and_not_attempted_keys`,
   which stands up a fake `spin` that succeeds on invocation 1 and
   fails on invocation 2, feeds 7 × ~30 KiB entries (>=3 chunks),
   and asserts the diagnostic names all three buckets plus the
   upstream stderr.

2. Medium — `docs/guide/manifest-store-migration.md`'s capability
   table still listed spin's Config as `Single (flat variables)`,
   but the Spin runtime is KV-backed and Multi for Config (one
   `spin_sdk::key_value::Store` per declared `[stores.config].id`).
   This page is linked from the loader's hard-error message when
   it encounters a pre-rewrite manifest, so the stale entry can
   actively mislead migrations. Changed to `Multi (KV label)`.

3. Low — Axum config-push docs claimed it was "future Stage 7
   work" and told users to populate `.edgezero/local-config-<id>.json`
   directly. `config push --adapter axum` ships and writes the same
   file the runtime reads. Updated:
   - `docs/guide/configuration.md` — bullet now points operators
     at `edgezero config push --adapter axum`.
   - `docs/guide/adapters/axum.md` — same bullet under the Config
     Store section; also calls out the typed flow from a
     downstream `<your-cli>` for `#[secret]` stripping.

Verified (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
- (cd docs && npx prettier --check guide/manifest-store-migration.md guide/configuration.md guide/adapters/axum.md)
The June-07 review caught a clippy regression in the freshly-added
`write_batch` partial-failure diagnostic: under
`cargo clippy --workspace --all-targets --all-features -- -D warnings`
the direct slice indexing (`entries[..cursor]`, `entries[cursor..chunk_end]`,
`entries[chunk_end..]`) tripped `clippy::indexing_slicing`, and the
single-letter destructured binding `(k, _)` tripped
`clippy::min_ident_chars`. Both are part of this repo's strict-clippy
restriction set (Stage 8 chore/strict-clippy merge), so the previous
form failed CI on `cargo clippy` even though `cargo test` and
`cargo fmt` were clean.

Refactor: replace the three slice indices with `.get(..)` calls that
fall through to `&[]` if the in-bounds invariant is ever violated
(it can't be — `cursor` and `chunk_end` are produced from
`entries.len()` and `chunk_entries`'s structure) — and rename the
destructured binding to `(key, _value)`. Pure refactor: the
partial-failure error string and the
`write_batch_partial_failure_reports_committed_failed_and_not_attempted_keys`
regression test are unchanged. All 16 push_cloud tests still pass.

Verified (clean):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test -p edgezero-adapter-spin --features cli push_cloud
The June-07 review caught `npx prettier --check .` failing on
`docs/guide/cli-walkthrough.md` lines 190 / 202-204. Prettier doesn't
preserve indentation on continuation lines inside backtick-wrapped
inline-code spans (`KEY=VALUE`, `SET <key> <value>`, `azure_cosmos`),
so the de-indent it wants is purely a source-format change — the
rendered VitePress output is unchanged because the surrounding
backticks group the whole multi-line span as one inline code block.

Pure formatting; no content edits.

Verified: `(cd docs && npx prettier --check .)` is clean.
@aram356 aram356 marked this pull request as ready for review June 7, 2026 07:23
@aram356 aram356 mentioned this pull request Jun 8, 2026
5 tasks

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Requesting changes for four blocking issues found in the extensible CLI/store work. Inline comments cover the diff-addressable findings; the second location for the secret-store issue is listed below because it is not commentable on the current PR diff.

Body-level finding

  • 🔧 with_secrets() issue also affects Fastly (crates/edgezero-adapter-fastly/src/request.rs:483): resolve_secret_handle(false) returns None, so .with_secrets() without .require_secrets() does not inject a Fastly SecretRegistry. Apply the same fix as the Cloudflare comment: construct the secret handle whenever SecretSource::On is selected, and add a regression test for .with_secrets() without .require_secrets().

CI Status

  • fmt: PASS
  • clippy: NOT VERIFIED locally — local rustc 1.91.1 is below the repo-pinned 1.95.0 / spin-sdk 6.0.0 requirement
  • tests: PASS

Comment thread crates/edgezero-adapter-cloudflare/src/request.rs Outdated
Comment thread crates/edgezero-adapter-cloudflare/src/cli.rs
Comment thread crates/edgezero-adapter-fastly/src/cli.rs Outdated
Comment thread crates/edgezero-core/src/manifest.rs
aram356 added 3 commits June 10, 2026 13:29
Five reviewer follow-ups from the chore/strict-clippy PR:

- fastly/dispatch_with_kv: # Errors doc now distinguishes the
  kv_required=true (errors) and kv_required=false (logs + skips) paths.
- core/KvPage: doc clarifies termination is cursor.is_none(), not
  keys.is_empty(); empty keys with Some(cursor) is a valid intermediate
  page when the scan-cap skips a run of expired entries.
- core/EdgeError: rename intrinsic source() to inner() and drop the
  same_name_method expect — the name conflict with Error::source is gone.
- macros/#[app]: comment the emitted Hooks impl to flag drift risk for
  configure/build_app vs the trait defaults under missing_trait_methods.
- core/tests: collapse DefaultHooks/TestHooks Hooks impls to the methods
  they actually override; add a targeted #[expect(missing_trait_methods)]
  on each stub with the rationale.
Pulls in c5652d3 — chore/strict-clippy's second round of PR #257
review-feedback fixes — on top of f374351. Conflicts resolved in
three files; the other two touched files auto-merged cleanly:

- crates/edgezero-core/src/error.rs
  Upstream renamed the typed `EdgeError::source(&self) -> Option<&AnyError>`
  helper to `inner()` to drop the same-name shadow of `Error::source`
  (and dropped the `#[expect(clippy::same_name_method)]` it needed).
  Git auto-merged the new `inner()` insertion near the top of the
  impl, but couldn't tell HEAD's old `source()` block (further down)
  was the same logic — that landed as a conflict.
  Resolution: keep upstream's `inner()` rename, drop the duplicate
  `source()` block, and add `EdgeError::NotImplemented` to `inner()`'s
  None-arm match (that variant exists only on this branch — upstream
  hadn't seen it, so its match was missing the arm).

- crates/edgezero-core/src/app.rs
  Upstream collapsed the `DefaultHooks` / `TestHooks` test stubs to
  only the methods they actually override (the rest now use trait
  defaults under `#[expect(missing_trait_methods)]`).
  Conflict 1 (DefaultHooks): HEAD still spelled out `build_app`,
  `configure`, `name` overrides that just delegated to the trait
  default. Kept upstream's slim form (only `routes` + `stores`).
  Conflict 2 (TestHooks): HEAD had a `build_app` override that
  delegated to the trait default; upstream had a `config_store()`
  override targeting a method removed from the `Hooks` trait on
  this branch (post-rewrite `stores()` carries config metadata).
  Kept only the methods that still override real trait behavior:
  `configure`, `name`, `routes`, `stores`.

- crates/edgezero-adapter-fastly/src/request.rs
  Upstream improved the `# Errors` doc on a `pub fn dispatch_with_kv`
  that no longer exists on this branch — the post-rewrite signature
  is `pub(crate) fn dispatch_with_registries(..., config_meta,
  kv_meta, secret_meta, env)` (three-store, not single-kv). The
  `kv_required` distinction upstream documented now lives inside
  `resolve_kv_handle`.
  Resolution: kept HEAD's signature + brief doc, with a pointer to
  `resolve_kv_handle` for the `kv_required=true/false` escalation
  semantics upstream wanted documented. No `# Errors` section needed
  since the function is `pub(crate)`, outside the
  `missing_errors_doc` lint's scope.

Auto-merged (no conflict):
- crates/edgezero-core/src/key_value_store.rs
  (upstream `KvPage` doc clarification on termination = cursor.is_none())
- crates/edgezero-macros/src/app.rs
  (upstream comment on the emitted Hooks impl to flag drift risk
  for configure/build_app vs trait defaults under
  missing_trait_methods)

Verified on this branch (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
… leak, tighten store-id validation

Addresses the PR #269 review (5 actionable findings — 1 body-level,
4 inline).

1. **`.with_secrets()` silently dropped the secret store**
   (cloudflare + fastly — `request.rs`)
   `SecretSource::On { required }` calls `resolve_secret_handle(..,
   required)`, and that helper short-circuited to `None` whenever
   `!required`. `.with_secrets()` sets `required: false`, so the
   documented `.with_secrets().dispatch(...)` path ran handlers
   without any `SecretRegistry`. Users who never called
   `.require_secrets()` saw `ctx.secret_store_default()` return
   `None` even though they'd opted in.

   Fix: always construct the handle when `SecretSource::On` is
   selected — the per-platform constructors (`CloudflareSecretStore::
   from_env`, `FastlySecretStore`) can't fail at construction time,
   so there's nothing for `required` to gate at the
   handle-building step. The bool stays in the signature for
   symmetry with `resolve_kv_handle` and as a hook for future
   per-secret-lookup availability policy.

   Regression: two new `synthesis_tests` in fastly assert
   `resolve_secret_handle(false).is_some()` AND
   `resolve_secret_handle(true).is_some()`. The cloudflare path
   takes a `worker::Env` (wasm-only, not constructible on host), so
   the fix is documented in-source at the `dispatch` call site and
   on the function itself; the shared logic is covered by fastly's
   host regression.

2. **Cloudflare provision could orphan a namespace**
   (`adapter-cloudflare/src/cli.rs`)
   `read_namespace_id` accepts both `[[kv_namespaces]]` (array-of-
   tables) AND `kv_namespaces = [{ binding, id }]` (inline-array)
   forms. An inline-array entry with no `id` returned `None`
   ("not yet provisioned"), so provision shelled `wrangler kv
   namespace create` successfully, then `upsert_kv_namespace`'s
   `as_array_of_tables_mut()` rejected the inline shape and the
   upsert errored — leaving the freshly-created namespace orphaned
   on Cloudflare with no local entry to track it.

   Fix: new `check_kv_namespaces_writeback_shape` pre-flight runs
   BEFORE `create_kv_namespace`. Missing or array-of-tables form is
   OK; inline-array form fails fast with an actionable message
   pointing at the conversion AND naming the orphan hazard so the
   operator knows the risk. Four regression tests in cloudflare's
   `cli.rs::tests` cover missing-file, no-kv_namespaces, valid
   array-of-tables, and the inline-array rejection.

3. **Fastly config push leaked values through argv**
   (`adapter-fastly/src/cli.rs::create_config_store_entry`)
   `--value=<value>` exposed every entry's bytes in `ps`/
   `/proc/<pid>/cmdline` AND was bounded by the host's `ARG_MAX`
   (4 KiB–256 KiB depending on platform — easy to trip with a JSON
   blob).

   Fix: switch to `--stdin`, pipe the value through `child.stdin`.
   Verified `fastly config-store-entry create --help` on the
   pinned Fastly CLI 15.x lists `--stdin` as an "OPTIONAL FLAGS"
   entry that, when set, makes `--value` ignored. Value bytes no
   longer appear in argv; effective size cap lifts to whatever the
   OS pipe buffer + CLI accept (megabytes in practice).

4. **Store logical ids were under-validated**
   (`edgezero-core/src/manifest.rs::validate_store_declaration`)
   Pre-fix only blanks, control characters, exact duplicates, and
   default-not-in-ids were rejected. Values like `foo/bar` (escapes
   the local-config dir at `.edgezero/local-config-<id>.json`),
   `foo__bar` (collides with the `EDGEZERO__STORES__<KIND>__<ID>__
   NAME` env-var segment separator), and case-only duplicates
   (`foo`/`FOO` collide under upper-case env-var lookup AND
   lower-cased segment paths) made it through and broke at runtime.

   Fix: two new validators —
   - `store_id_format`: only `[A-Za-z0-9_-]` allowed, `__` reserved
     as the env-var separator;
   - `store_id_case_duplicate`: ASCII case-insensitive dedup so
     `foo` and `FOO` no longer alias at runtime.

   Four regression tests cover the new error paths (path-separator
   rejected, double-underscore rejected, case-only dup rejected)
   plus a positive case asserting the canonical scaffold shapes
   (`app_config`, `feature-flags`, `appConfig`, `v1`+`v2`) still
   validate cleanly.

Architectural review feedback (shell-out vs. tight API integration)
is acknowledged but left for follow-up — that's a scope-level
tradeoff, not a blocker on this PR. Documented as such in the
inline reply.

Verified (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
@aram356

aram356 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Review-feedback summary — all 5 findings addressed in e1d7753

Replies posted on each inline thread; this is a summary so the body-level Fastly secrets mirror is also visible.

# Finding Resolution
1 with_secrets() no-op on Cloudflare (request.rs:338) resolve_secret_handle always builds the handle when SecretSource::On; _required is reserved for future per-lookup policy
1b Body-level: same bug on Fastly (fastly/request.rs:483) Structurally identical fix; two host-runnable regression tests in synthesis_tests
2 CF provision orphans namespace for inline kv_namespaces (cloudflare/cli.rs:693) New check_kv_namespaces_writeback_shape pre-flight; fails fast BEFORE any Cloud-side mutation. 4 regression tests.
3 Fastly argv leak via --value=... (fastly/cli.rs:647) Switched to --stdin + piped value; bytes no longer in ps/cmdline, ARG_MAX cap lifted
4 Store-id validation gaps (manifest.rs:801) New store_id_format ([A-Za-z0-9_-], __ reserved) + store_id_case_duplicate validators; 4 regression tests
5 Spec-doc: shell-out vs native API tradeoff Acknowledged as deliberate scope decision; reply explains the rationale and offers to open a follow-up tracking issue

Gates verified clean on the merged tree (host fmt + clippy + workspace tests + 3 wasm clippy targets + app-demo workspace tests).

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Requesting changes for several correctness issues in the new provision/config-push paths.

😃 The overall direction looks solid: portable logical stores, env-resolved platform names, adapter-owned provision/push, and typed config metadata all feel like the right seams.

I kept the inline comments focused on high-confidence cases where a command can mutate the wrong platform state, report success without making the runtime able to open the resource, or leave generated/local flows broken. These should be addressed before this lands.

Folded here because GitHub would not resolve these as inline comments in the current PR diff:

  • 🔧 Generated Cloudflare commands still use wrangler build, which current Wrangler docs list as removed. Because manifest commands take precedence over the adapter's built-in build implementation, generated projects/app-demo get a broken edgezero build --adapter cloudflare. Please switch scaffold/docs to a supported build flow and keep project-context handling consistent with the other Wrangler paths.
  • 🔧 ManifestStores should reject unknown [stores.*] subtables. Without deny_unknown_fields or a flatten catch-all, typos like [stores.configs] / [stores.secret] are silently ignored. With the hard-cutoff schema posture, these should be schema errors rather than 'no store declared'.

Comment thread crates/edgezero-adapter-cloudflare/src/cli.rs Outdated
Comment thread crates/edgezero-adapter-cloudflare/src/cli.rs Outdated
Comment thread crates/edgezero-adapter-cloudflare/src/cli.rs
Comment thread crates/edgezero-adapter-fastly/src/cli.rs Outdated
Comment thread crates/edgezero-adapter-fastly/src/cli.rs
Comment thread crates/edgezero-adapter-fastly/src/cli.rs Outdated
Comment thread crates/edgezero-adapter-axum/src/cli.rs
@ChristianPavilonis

ChristianPavilonis commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🌱 Architecture follow-up, non-blocking for the correctness review I just submitted: two design questions from my architecture pass that I intentionally did not include as request-changes inline findings.

  1. 🤔 Should config validate --strict fail closed when the manifest declares an adapter that is not registered in the current CLI build? Otherwise strict validation strength depends on which adapter features this binary linked, so a manifest can appear strictly valid while adapter-specific checks were skipped.

  2. 🌱 The Adapter trait is accumulating static capabilities plus auth/provision/config-push/validation operations. Before this becomes the long-term extension surface, I think it would be worth considering one of these shapes:

    • Descriptor + operation traits: keep static facts in an AdapterCapabilities / descriptor (id, display name, single-store kinds, merged-id kinds, supported operations, scaffold metadata), and put verbs behind narrower traits like AdapterAuth, AdapterProvision, AdapterConfigPush, and AdapterValidation. The registry can expose the descriptor for validation/planning without implying every adapter must implement every verb.
    • Versionable context structs: if one trait remains simpler for now, prefer method inputs like ProvisionContext<'_> / ConfigPushContext<'_> / ValidationContext<'_> over increasingly wide positional method signatures. That gives future fields somewhere to land without repeatedly changing every adapter implementation.
    • Two-phase dispatch: have the CLI first inspect capabilities/descriptors to decide whether a command is supported and what preflight is required, then invoke the narrow operation implementation. This keeps declarative compatibility checks separate from side-effecting platform mutations.

    This is not a blocker for this PR, but the current trait feels like the place future adapter features will keep accreting, so drawing this boundary early may save churn for downstream adapter authors.

aram356 added 2 commits June 10, 2026 16:32
Two real fixes from PR review feedback (round 3):

- cloudflare scaffold + demo: replace wildcard `use worker::*` with
  explicit imports, add `# Errors` doc and `#[inline]`. Generated
  Cloudflare projects now pass strict wasm32 clippy.
- docs/guide/handlers.md: update the IntoResponse example to return
  `Result<Response, EdgeError>` (the trait signature changed in this PR);
  the old `-> Response` example would fail to compile.

The reviewer's matching suggestions for the fastly and spin scaffolds
required suppressions on macro-expanded code (`#[fastly::main]`,
`#[http_component]`) we do not own. Those are deliberately declined; see
the PR thread replies for the rationale.
…lision, fastly --upsert/--stdin + resource-link + valid setup tables, axum project-root discovery

Addresses the 7 inline findings from the June-10 23:10 review round on PR #269.

1. CF non-local push needs --remote + correct cwd (cloudflare/cli.rs)
   Wrangler v4 defaults `kv bulk put` to LOCAL Miniflare storage when
   the command supports both, so the old `wrangler kv bulk put
   <file> --namespace-id=<id>` invocation could silently populate
   `.wrangler/state` while reporting success and leaving the live
   Cloudflare namespace empty. Add explicit `--remote`. Also set
   `current_dir(wrangler_path.parent())` so wrangler picks up the
   project's `account_id` / `--env` / persistence settings the same
   way `wrangler dev` / `wrangler deploy` do.

2. CF --local should target the binding, not a remote namespace id
   (cloudflare/cli.rs)
   The scaffold ships with `local-dev-placeholder` ids, so the old
   `find_namespace_id(...)?` call would block `--local` push until
   the operator had also run `edgezero provision` against the live
   Cloudflare account. Switch to wrangler's documented local KV
   shape: `wrangler kv bulk put <file> --binding <BINDING> --local`
   (binding-keyed, no remote id resolution). `current_dir` is set
   to the wrangler project so the binding resolves under the same
   `wrangler.toml` `wrangler dev --local` uses.

3. CF kv/config merged-id collision check (cloudflare/cli.rs)
   Implement `merged_id_kinds() -> &["kv", "config"]` (mirrors the
   Spin adapter's same return). Both `[stores.kv]` and
   `[stores.config]` resolve to the same `[[kv_namespaces]] binding
   = <platform-name>` entry in wrangler.toml. Declaring the same
   logical id under both kinds (e.g. `kv.ids = ["x"]` AND
   `config.ids = ["x"]`) would resolve to ONE KV namespace at
   runtime with silent kv/config write collisions, and provision
   would skip the second binding as "already provisioned". `config
   validate` now catches this before any wrangler shell-out.

4. Fastly remote push is now repeatable (fastly/cli.rs)
   Switched `create_config_store_entry` from
   `config-store-entry create --value=...` to
   `config-store-entry update --upsert --stdin`. `update --upsert`
   matches the convergent semantic the other adapters' config-push
   paths already have (axum overwrites JSON, cloudflare `kv bulk
   put` overwrites, spin `cloud key-value set` overwrites). The
   prior `create` form erred on every existing key, making the
   first push succeed but every follow-up push fail at the first
   unchanged key.

5. Fastly provision warns when service is already deployed
   (fastly/cli.rs)
   Fastly's `[setup.<kind>_stores.<name>]` is consumed ONLY when
   `fastly compute deploy` is creating a new service. If
   `service_id` is already present in fastly.toml the service has
   been deployed at least once and subsequent deploys skip
   `[setup]` entirely — the store gets created in the account but
   no resource link ties it to a service version, so the running
   Compute service can't open it. Provision now reads `service_id`
   via the new `read_fastly_service_id` helper and, when an
   existing service is detected, EMITS the exact
   `fastly resource-link create --service-id=<id>
   --resource-id=<STORE-ID> --version=latest --autoclone
   --name=<name>` one-shot the operator needs to run (plus the
   `fastly <kind>-store list --json` lookup for `<STORE-ID>`). We
   deliberately don't auto-run the link command because
   `--autoclone` mutates the active service version and silently
   doing that is surprising; the instruction lets the operator
   audit before committing.

6. Fastly [local_server.*_stores.<id>] no longer written by
   provision (fastly/cli.rs)
   Pre-fix `append_fastly_setup` wrote an empty
   `[local_server.<kind>_stores.<id>]` alongside
   `[setup.<kind>_stores.<id>]`. The empty stanza doesn't satisfy
   fastly's local-server schema: config-stores need
   `format = "inline-toml"` plus a contents table, kv/secret
   stores need a JSON `file = "..."` or `{key, data}` array. The
   empty form made `fastly compute serve` either error at startup
   or silently skip the declared store. `provision` now writes
   ONLY the `[setup]` half — local-server seeding is
   `edgezero config push --adapter fastly --local`'s job (already
   producing the valid `format` + `contents` shape for
   config-stores; kv/secret stores stay operator-managed for
   local-server until equivalent writers land). `setup_block_present`
   matches: only checks `[setup.*]`. Updated 4 existing tests +
   added a regression for the post-fix empty-table avoidance.

7. Axum push/runtime cwd mismatch (axum/config_store.rs)
   Push wrote to `manifest_root/.edgezero/local-config-<id>.json`,
   but the runtime's `from_local_file` resolved the same name
   against the process cwd. Cargo `cargo run -p
   my-app-adapter-axum` from a crate dir made the runtime read
   `crates/<adapter>/.edgezero/...` while push wrote to
   `<project>/.edgezero/...` — empty store, no warning.

   `local_path` now walks up from cwd looking for an ancestor
   containing `edgezero.toml` (the same convention cargo uses for
   `Cargo.toml`). Found → rooted under the ancestor; absent →
   falls back to cwd-relative (preserves the deployment shape
   where the binary ships without `edgezero.toml` alongside it).
   3 regression tests in `config_store::tests` (no-match,
   single-match, nearest-of-multiple).

Verified (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
@aram356

aram356 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Round-2 review summary — all 7 findings addressed in 4079cde

# Finding Resolution
F1 CF non-local push needs --remote (wrangler v4 local-default) + correct cwd Push runs wrangler kv bulk put --remote with current_dir = wrangler.toml's parent dir
F2 CF --local blocked by missing remote namespace-id Switched to wrangler kv bulk put <file> --binding <BINDING> --local (binding-keyed, no remote lookup)
F3 CF kv/config merged-id collision Adapter::merged_id_kinds() -> &["kv", "config"] mirrors Spin; config validate rejects before any shell-out
F4 Fastly remote push errors on existing keys Switched to config-store-entry update --upsert --stdin; convergent like the other adapters
F5 Fastly provision can't link to existing service New read_fastly_service_id detects deployed services and emits an actionable fastly resource-link create --service-id=<svc> --resource-id=<STORE-ID> --version=latest --autoclone instruction (operator audits + runs; we don't silently autoclone)
F6 Empty [local_server.*] stanzas don't satisfy fastly's schema Dropped from append_fastly_setup; local-server seeding lives in config push --local, which already produces the valid format + contents shape for config-stores. 4 tests updated, 1 regression added.
F7 Axum push/runtime cwd mismatch AxumConfigStore::local_path walks up for edgezero.toml (cargo-style); push & runtime now agree regardless of launch cwd. Falls back to cwd-relative for deployed-binary shape. 3 regression tests.

Gates verified clean on the merged tree (host fmt + clippy + workspace tests + 3 wasm clippy targets + app-demo workspace tests).

…i + strict-clippy fixes

Pulls in 453a44b — chore/strict-clippy's third review round —
on top of 4079cde. Two of the three files (the cloudflare scaffold
and the app-demo cloudflare adapter) auto-merged cleanly: git took
upstream's explicit `use worker::{event, Context, Env, Request,
Response, Result}` import + `# Errors` doc + `#[inline]`, while
keeping the post-fix 3-arg `run_app::<App>(req, env, ctx)` call
this branch shipped in f1179df. `docs/guide/handlers.md` had no
conflict (single-hunk doc fix on a section this branch didn't
touch).

Strict-clippy passes on the merged tree EXCEPT for five lints
already pending in the codebase pre-merge that strict-clippy hit
on a clean build. Fixed in this commit:

- `clippy::arbitrary_source_item_ordering` on
  `axum/config_store.rs`: my F7 fix in 4079cde inserted
  `find_project_root_dir{,_from}` free functions BETWEEN
  `impl AxumConfigStore` and `impl ConfigStore for AxumConfigStore`,
  splitting two impl blocks across a free-function definition.
  Reordered so both impls sit together first, then the free
  functions.

- `clippy::absolute_paths` on `axum/config_store.rs`:
  `&std::env::current_dir().ok()?` → `use std::env;` + `&env::...`.

- `clippy::min_ident_chars` on `fastly/cli.rs:504`:
  `.filter(|s| !s.is_empty())` → `.filter(|svc_id| !svc_id.is_empty())`.

- `clippy::unused_trait_names` on `fastly/cli.rs:3`: `use std::io::
  {ErrorKind, Write};` → `Write as _` since `Write` is only used
  anonymously (via `stdin.write_all`).

- `clippy::semicolon_outside_block` on `fastly/cli.rs:743`: the
  bare scoping block around the `stdin.write_all` was there to
  drop the `&mut` borrow before `child.wait_with_output()`
  consumes child. Refactored to `child.stdin.take()` + `drop(stdin)`
  which gives the same drop-before-consume semantics without the
  bare block.

- `clippy::unnecessary_wraps` on `fastly/request.rs:484` AND
  `cloudflare/request.rs:357`: post-fix `resolve_secret_handle`
  unconditionally builds `Some(handle)`. Returning `SecretHandle`
  directly (and wrapping `Some(...)` at the single
  `SecretSource::On` call site) drops the redundant `Option`.
  Updated the two host-runnable Fastly regression tests to match.

Auto-merged (no conflicts):
- crates/edgezero-adapter-cloudflare/src/templates/src/lib.rs.hbs
- examples/app-demo/crates/app-demo-adapter-cloudflare/src/lib.rs
- docs/guide/handlers.md

Verified (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
@aram356 aram356 self-assigned this Jun 11, 2026
aram356 added 3 commits June 10, 2026 22:38
Newly scaffolded EdgeZero projects now get a `.tool-versions` file
at the project root, so `asdf install` picks up matching toolchain
pins automatically. The file's contents adapt to the adapter set
the operator selected at `edgezero new` time:

- `rust 1.95.0` — always (every generated project is a Rust workspace).
- `nodejs 24.12.0` — added when the cloudflare adapter is in the
  project (wrangler is a Node binary).
- `fastly 15.1.0` + `viceroy 0.17.0` — added when the fastly
  adapter is in the project (the CLI we shell out to for
  provision/config push, and what `fastly compute serve` uses for
  local emulation).
- `spin` — no asdf pin; instead a trailing header comment points
  the operator at https://spinframework.dev/install. The Spin CLI
  is install-flow-managed across our toolchain (no consistent
  asdf plugin we pin to), so writing a brittle pin we can't
  honour would mislead more than it would help.
- `axum` — no extra pin (uses the host Rust toolchain only).

Version numbers track the repo's own `.tool-versions`. When we
bump those, we bump these.

Implementation:

- New `templates/root/tool-versions.hbs` is a one-liner
  (`{{{tool_versions_contents}}}`) — the file body is computed in
  Rust so the per-adapter conditional is type-checkable instead
  of living in handlebars templating.
- New `build_tool_versions(adapter_ids)` in `generator.rs` returns
  the file body. Pin lines are sorted + deduped so the output is
  stable regardless of declaration order.
- Registered alongside the other root templates in
  `scaffold.rs::register_templates`, written via `write_tmpl` in
  `generator.rs`. To stay under the project's clippy
  `too_many_lines` cap on `render_templates`, the six root files
  are now written through a new `write_root_files` helper.

Regression coverage:

- `register_templates_registers_all_known_templates` extended to
  cover `root_tool_versions`.
- `assert_scaffold_files` (used by both end-to-end scaffold tests)
  asserts the file exists in the generated project.
- Six new unit tests on `build_tool_versions`:
  - rust-only fallback with no adapters,
  - nodejs added only for cloudflare,
  - fastly+viceroy added only for fastly,
  - axum-only matches the no-adapter shape,
  - spin gets the install-hint comment instead of an asdf pin,
  - the all-four composite case (no duplicates, sorted output).

Verified (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo test -p edgezero-cli --test generated_project_builds -- --ignored
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
…ync) into feature/extensible-cli

Pulls in two new commits on chore/strict-clippy since the last sync
at 453a44b:

- dbb7fa3 Bump redb 4.0 → 4.1.0 (#255) — minor bump within the
  existing 4.x major. No API or on-disk format changes for the
  axum adapter's KV store; the workspace lock already resolved
  HEAD's `redb = "4.1"` to 4.1.0, so the merged Cargo.lock is
  unchanged in practice.
- c666132 Merge remote-tracking branch 'origin/main' into
  chore/strict-clippy — the main-into-branch sync carrying #255.

Conflicts resolved in two files:

- Cargo.toml: HEAD's `redb = "4.1"` vs. upstream's
  `redb = "4.1.0"` are semver-equivalent — kept upstream's more
  specific pin to track the explicit dependency intent from #255.
  HEAD's reqwest feature set (`["rustls", "blocking", "json"]`)
  is kept verbatim: upstream's set (`["rustls"]`) is from before
  this branch added the `blocking` + `json` features that
  `edgezero-adapter-fastly`'s CLI shell-out path depends on.
  HEAD's `rusqlite` block (introduced for the Spin local KV
  writer's vendored schema) is preserved — upstream didn't have
  it because the redb bump landed before that work merged.

- Cargo.lock: 15 conflict regions. Took HEAD's lock entirely
  (`git checkout --ours`) — HEAD's lock already pins
  `redb = "4.1.0"` because `4.1` resolves to the same version, so
  no manual conflict surgery was needed. Verified via
  `cargo update -p redb` (zero packages updated, no drift).

Verified on the merged tree (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cd examples/app-demo && cargo test --workspace --all-targets

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😃 Approving — I did one more correctness-focused pass and did not find any merge-blocking regressions. The main paths I checked are covered by passing workspace/app-demo/generated-project validation, and the remaining items look appropriate as non-blocking follow-ups.

📌 I left a handful of follow-up notes inline using the review emoji guide.

📝 One extra follow-up I did not inline: cargo test -p edgezero-cli --no-default-features --features cli --lib currently fails because adapter-dependent tests still run when no adapter features are linked. That does not appear to be part of the current CI gate, but it is worth cleaning up separately if the slim cli feature combo is intended to be testable.

Comment thread crates/edgezero-cli/src/lib.rs Outdated
Comment thread crates/edgezero-core/src/manifest.rs
Comment thread crates/edgezero-core/src/app_config.rs
Comment thread crates/edgezero-cli/src/args.rs Outdated
Comment thread scripts/smoke_test_config.sh Outdated
aram356 added 2 commits June 11, 2026 13:47
Three real fixes from PR review feedback:

- .github/workflows/test.yml: switch wasmtime version verification from
  `grep -qF "wasmtime $version"` (substring match — accepts `44.0.10`
  when pinned to `44.0.1`) to exact comparison via `awk '{print $2}'`.
- crates/edgezero-cli/src/main.rs: replace SimpleLogger with a tiny
  custom log::Log impl that prints only `record.args()` — `info`/`debug`
  /`trace` to stdout, `warn`/`error` to stderr. SimpleLogger always
  emitted `INFO [edgezero_cli::xxx] ...` prefixes even with
  `without_timestamps()`, regressing the user-facing CLI UX the
  surrounding comment promised.
- docs/guide/handlers.md: simplify the IntoResponse example to
  `.map_err(EdgeError::internal)` — drops the undeclared `anyhow`
  dependency the previous snippet required.

The reviewer's fourth comment (body.rs as_bytes/into_bytes signature
changes — compat shims or migration docs) is intentionally declined.
The crate is pre-1.0; the project rejects backward-compat shims, and
the new `Option<&[u8]>` / `Option<Bytes>` signatures are the safer ones.
…ields on [stores], reject `__` in app-config keys, args Default fix, Spin smoke runtime-config

Addresses the 5 non-blocking follow-ups from the June-11 17:09Z
review round on PR #269.

1. Case-insensitive `[adapters.<name>]` lookup + load-time dup
   rejection (`core/manifest.rs`, `cli/{adapter,config,lib,provision}.rs`)
   Pre-fix, the manifest stored `[adapters.fastly]` and CLI
   lookups used exact-case `adapters.get(name)` /
   `contains_key(name)`. The adapter registry treats names
   case-insensitively, so `--adapter Fastly` against
   `[adapters.fastly]` failed at the CLI's pre-check before
   reaching the registry.

   New `Manifest::adapter_entry(name)` performs the
   case-insensitive lookup and returns the manifest's canonical
   spelling so error messages surface the operator's exact text.
   `validate_manifest_adapter_keys_case_unique` rejects
   case-fold duplicate keys (e.g. both `[adapters.fastly]` AND
   `[adapters.Fastly]`) at manifest load so the lookup is never
   ambiguous. All five CLI lookup sites switched to
   `adapter_entry`.

2. `deny_unknown_fields` on `ManifestStores` (`core/manifest.rs`)
   `[stores.secret]` (typo for `secrets`) was silently ignored —
   validation passed with no secrets metadata wired. Adding
   `#[serde(deny_unknown_fields)]` on the parent table catches
   the typo at parse time, mirroring the rejection rules
   `StoreDeclaration` already has for legacy fields below it.

3. Reject `__` in app-config keys (`core/app_config.rs`)
   `foo__bar` (scalar) and `[foo] bar` (nested) both built env
   path `<APP>__FOO__BAR` — a single env var would ambiguously
   override both. The walker now rejects keys containing `__`
   at every level with an actionable error explaining the
   collision. Mirrors the manifest-side `store_id_format` rule
   shipped in PR #269 round 1.

4. Manual `Default` impls on the three args structs
   (`cli/args.rs`)
   `ProvisionArgs`, `ConfigPushArgs`, `ConfigValidateArgs` all
   derive `clap::Args` with `#[arg(default_value = "edgezero.toml")]`
   on `manifest`. Pre-fix they also derived `Default`, which set
   `manifest = PathBuf::new()` — library callers using
   `..Default::default()` hit a confusing "no such file" error
   when `ManifestLoader::from_path("")` ran. Manual `Default`
   impls now set `manifest = PathBuf::from("edgezero.toml")`
   matching what clap's CLI parser writes; the value is
   centralised in a `default_manifest_path()` helper so the
   clap literal and the `Default` impls stay in sync.

5. Spin smoke scripts pass `--runtime-config-file` + seed
   (`scripts/smoke_test_{config,kv,secrets}.sh`)
   The demo's `spin.toml` declares non-`default` KV labels
   (`app_config`, `sessions`, `cache`) that Spin's runtime only
   honours when `--runtime-config-file runtime-config.toml` is
   passed to `spin up`. Pre-fix, all three Spin smoke paths
   started without the flag and could fail with
   `unknown key_value_stores label <name>` before readiness.
   The config smoke ALSO seeds the local Spin KV via
   `app-demo-cli config push --adapter spin --local --no-env`
   before `spin up` so the per-key checks observe seeded
   values, not defaults.

Regression coverage (synthetic adapter names — `xenon`/`krypton`
— keep these manifest-level tests independent of any specific
adapter crate's identity):

- `manifest::tests::manifest_rejects_case_fold_duplicate_adapter_keys`
- `manifest::tests::manifest_adapter_entry_matches_case_insensitively_returning_canonical_key`
- `manifest::tests::manifest_stores_rejects_unknown_kind_at_parse_time`
- `app_config::tests::env_overlay_rejects_scalar_key_containing_double_underscore`
- `app_config::tests::env_overlay_rejects_double_underscore_in_nested_table_key_too`
- `args::tests::provision_args_default_manifest_matches_clap_default`
- `args::tests::config_push_args_default_manifest_matches_clap_default`
- `args::tests::config_validate_args_default_manifest_matches_clap_default`
- `args::tests::default_manifest_path_matches_clap_literal`
- Updated `args::tests::config_validate_args_defaults` to expect
  the post-fix `"edgezero.toml"` instead of the pre-fix
  `PathBuf::new()`.

Verified (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
@aram356

aram356 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Round-4 review summary — all 5 non-blocking follow-ups addressed in a9ae306

# Finding Resolution
F1 Adapter names are case-insensitive in the registry but exact-case in manifest lookups New Manifest::adapter_entry(name) performs case-insensitive lookup returning the canonical key; validate_manifest_adapter_keys_case_unique rejects case-fold dups at load. All 5 CLI lookup sites switched.
F2 [stores] table silently ignored unknown kinds (e.g. [stores.secret] typo) #[serde(deny_unknown_fields)] on ManifestStores; typo now fails at parse time.
F3 __ in app-config keys collides with the env-overlay segment separator The overlay walker rejects keys containing __ at every level with an actionable error explaining the collision. Mirrors the manifest-side store_id_format rule.
F4 ..Default::default() produced empty manifest: PathBuf::new() instead of "edgezero.toml" Manual Default impls on ProvisionArgs/ConfigPushArgs/ConfigValidateArgs matching clap's literal, centralised in default_manifest_path().
F5 Spin smoke scripts started without --runtime-config-file; config smoke didn't seed All three smoke scripts pass --runtime-config-file runtime-config.toml; config smoke also seeds via app-demo-cli config push --adapter spin --local --no-env before spin up.

Regression coverage: 9 new tests, plus 1 updated to expect the new Default value. Manifest-level tests use synthetic adapter names (xenon/krypton) so they don't couple a generic core invariant to any specific adapter crate.

Gates clean (fmt + host clippy + workspace tests + 3 wasm clippy targets + app-demo workspace tests).

…ndlers.md) into feature/extensible-cli

Pulls in dfb00b3 — the third strict-clippy round-of-review-fixes
commit since the last sync at c666132:

- .github/workflows/test.yml: switch wasmtime version verification
  from `grep -qF "wasmtime $version"` (substring match — accepts
  `44.0.10` when pinned to `44.0.1`) to exact comparison via
  `awk '{print $2}'`. Auto-merged cleanly.
- docs/guide/handlers.md: simplify the IntoResponse example to
  `.map_err(EdgeError::internal)` — drops the undeclared `anyhow`
  dependency the previous snippet required. Auto-merged cleanly.
- crates/edgezero-cli/src/main.rs: upstream introduced a custom
  `CliLogger` (`log::Log` impl that prints `record.args()` verbatim
  with no `INFO [edgezero_cli::xxx]` prefixes) to replace the
  previous `SimpleLogger`-based init. The fix is real:
  `SimpleLogger::without_timestamps()` still emits the
  `INFO [module]` prefix, regressing the user-facing CLI UX the
  surrounding doc promised. Upstream put the logger code in the
  binary (`main.rs`). On this branch, the logger lives in the
  library (`lib.rs::init_cli_logger`), called by the binary, so a
  downstream CLI built on `edgezero-cli` can reuse it.

  Conflict resolution: kept HEAD's thin `main.rs` (no logger code
  in the binary) AND ported the upstream `CliLogger` improvement
  INTO `lib.rs`, replacing the `SimpleLogger`-based body of
  `init_cli_logger` with the routing rules upstream wrote
  (info/debug/trace → stdout, warn/error → stderr). That gives us
  the actual UX fix on this branch's architecture.

`simple_logger` stays in the dep graph for the non-`cli`-feature
`main()` branch (still uses it to print the "rebuild with
--features cli" hint).

Verified on the merged tree (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cd examples/app-demo && cargo test --workspace --all-targets
Base automatically changed from chore/strict-clippy to main June 12, 2026 23:26
… feature/extensible-cli

PR #257 (chore/strict-clippy) was squash-merged into main as
7ec2ad1, so the same content this branch had been pulling in
piecewise via repeated `origin/chore/strict-clippy` merges
(c666132, 1b7342f, 3082391, a04814d, 9175cff, e526e6b) now arrives
as a single tree-level diff against main.

Tree-level reconciliation produced 54 conflicts because the squash
commit ships the pre-rewrite/pre-PR-#269 shape of each affected
file (old `Command::{ Build {…}, Deploy {…}, Serve {…}, Dev }`
enum variants, `handle_build/deploy/serve` routing in `main.rs`,
old `SimpleLogger` init, missing `Auth`/`Config`/`Provision`
subcommands, old store/manifest types, etc.), while HEAD has the
fully-rewritten state that PR #269 has been delivering and
reviewing for the last several rounds.

Resolution: take HEAD for all 54 conflicts. The squash commit
adds nothing this branch doesn't already have via the prior
piecewise merges — every conflict region's upstream side is
content this branch explicitly rewrote. Confirmed by walking the
conflict markers and matching each upstream hunk to a
corresponding rewrite already on HEAD (case-insensitive adapter
lookup, manual `Default` impls, `[stores]` `deny_unknown_fields`,
`__` rejection in app-config keys, F1-F7 fastly/cloudflare/spin
fixes, `.tool-versions` scaffold template, `CliLogger` in lib.rs,
etc.).

One additional new-file delete:
`crates/edgezero-cli/src/dev_server.rs` (added by the squash) is
the pre-rewrite location of the dev server; HEAD has moved it to
`crates/edgezero-adapter-axum/src/dev_server.rs`. Removed the
stray upstream-added file.

Plus one real round-5 review-feedback fix carried over from
upstream's `75a01ec strict-clippy: round-5 review feedback (cli
args + logger)`. That commit had two fixes:

1. `args.rs`: drop the unused `--local-core` flag from `NewArgs`.
   This branch already doesn't have it (we dropped it as part of
   earlier rewrites), so it's already in the right state.

2. `lib.rs::CliLogger`: filter `debug`/`trace` explicitly rather
   than routing them to stdout alongside `info`. Pre-fix the
   match arm wrote `info|debug|trace => println!(...)` and the
   doc claimed all three went to stdout, but `enabled()` already
   capped at `<= Info` and `LevelFilter::Info` was set globally
   so debug/trace never reached `log()` — the comment promised
   behaviour the impl couldn't deliver. Split the match arm so
   `info => println!`, `debug | trace => {}`, and the doc now
   says "info to stdout, warn/error to stderr; debug/trace
   filtered out (no verbosity flag yet)".

   Applied here on lib.rs since this branch already moved
   `CliLogger` out of `main.rs` (where upstream put it) into the
   library so downstream CLIs built on `edgezero-cli` reuse the
   same prefix-less output.

Verified on the merged tree (all gates green):
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo clippy -p edgezero-adapter-spin --target wasm32-wasip2 --features spin --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-fastly --target wasm32-wasip1 --features fastly --all-targets -- -D warnings
- cargo clippy -p edgezero-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare --all-targets -- -D warnings
- cd examples/app-demo && cargo test --workspace --all-targets
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants