Add docs.dev RPC method (long-running) #105

Closed
opened 2026-04-26 12:25:50 +00:00 by mahmoud · 3 comments
Owner

Parent: #101

What to add

Method: docs.dev
Params: { path: string, host?: string, port?: u16 }
Returns: { job_id: string }
Wraps: DocSite::dev(host, port) at lib.rs:125
Important: This is a long-running process — submit as hero_proc job. Callers stop it via hero_proc job kill. Do NOT add a docs.devStop unless there is a real caller for it.

Implementation steps

  1. Verify hero_docs dev subcommand exists
  2. Add handle_docs_dev(id, params, config) -> RpcResponse
  3. Because dev server is non-deterministic, extend the prefix list in handle_docs_job_status rather than emitting a bogus output_path
  4. Add dispatch arm "docs.dev" at rpc.rs:155
  5. Add method entry in openrpc.json — document that callers use hero_proc job kill to stop the dev server
  6. Bump info.version, update doc-comment header at rpc.rs:9-28

Acceptance Criteria

  • hero_docs dev subcommand exists
  • Returns { job_id } immediately, dev server runs as hero_proc job
  • Job visible in hero_proc dashboard under Jobs/Runs
  • No bogus output_path returned by docs.jobStatus
  • spec/impl parity check passes
Parent: #101 ## What to add **Method:** `docs.dev` **Params:** `{ path: string, host?: string, port?: u16 }` **Returns:** `{ job_id: string }` **Wraps:** `DocSite::dev(host, port)` at `lib.rs:125` **Important:** This is a long-running process — submit as hero_proc job. Callers stop it via hero_proc job kill. Do NOT add a `docs.devStop` unless there is a real caller for it. ## Implementation steps 1. Verify `hero_docs dev` subcommand exists 2. Add `handle_docs_dev(id, params, config) -> RpcResponse` 3. Because dev server is non-deterministic, extend the prefix list in `handle_docs_job_status` rather than emitting a bogus `output_path` 4. Add dispatch arm `"docs.dev"` at `rpc.rs:155` 5. Add method entry in `openrpc.json` — document that callers use hero_proc job kill to stop the dev server 6. Bump `info.version`, update doc-comment header at `rpc.rs:9-28` ## Acceptance Criteria - [x] `hero_docs dev` subcommand exists - [x] Returns `{ job_id }` immediately, dev server runs as hero_proc job - [x] Job visible in hero_proc dashboard under Jobs/Runs - [x] No bogus `output_path` returned by `docs.jobStatus` - [x] spec/impl parity check passes
rawdaGastan added this to the ACTIVE project 2026-04-26 14:08:30 +00:00
Member

Implementation Spec for Issue #105

Objective

Expose DocSite::dev(host, port) over the OpenRPC interface as docs.dev. This is a long-running method: the dev server is intended to keep serving until the caller explicitly kills the hero_proc job. Per the issue body, callers stop it via hero_proc job kill <id>; we do not invent a docs.devStop.

Requirements

  • docs.dev is dispatchable from crates/hero_books_server/src/web/rpc.rs and reaches a new handler handle_docs_dev.
  • Matching entry in openrpc.json and the inline rpc_spec.rs schema. Summary documents the long-running nature and the kill-via-hero_proc story.
  • info.version unchanged at 0.1.6 (one bump per rolling PR; #102 already did it).
  • The ## Docs section in the file-level doc-header gains docs.dev.
  • hero_docs (src/bin/hero_docs.rs) gains a dev subcommand: hero_docs dev --path <path> [--host <host>] [--port <port>]. It calls hero_books_docusaurus::build::dev(&path, &host, port) directly. The subcommand blocks (does not exit) — that is the desired long-running behaviour.
  • submit_or_dedup_docs_job is extended to accept an optional timeout. Existing callers pass Some(600_000) (10 minutes, the current behaviour). docs.dev passes None, which means no timeout — hero_proc lets the job run indefinitely.
  • Action name format: docs_dev_<input_hash> where input_hash = calculate_docs_input_hash(&[&path, &host, &port_string]). Including host/port in the hash is important so two dev sessions on different ports do not dedup into one.
  • derive_docs_output_path is extended (or rather, left alone — the helper already returns None for unknown prefixes including docs_dev_*). The doc comment on the helper is updated to mention docs_dev_* is one of the explicitly-supported "no output_path" prefixes.
  • Invalid-params responses use RpcResponse::invalid_params (-32602); internal failures use RpcResponse::error(id, -32000, ...).
  • INSTRUCTIONS_OPENRPC.md §Verification diff produces empty output.

Files to Modify/Create

  • src/bin/hero_docs.rsDev(DevArgs) variant, args struct (path required; host default "localhost"; port default 3000), runner.
  • crates/hero_books_server/src/web/rpc.rs
    • handle_docs_dev handler.
    • Dispatch arm.
    • Doc-header bullet.
    • submit_or_dedup_docs_job signature extended with timeout_ms: Option<i64> (and existing five callers updated to pass Some(600_000)).
    • Update derive_docs_output_path doc-comment to mention docs_dev_*.
    • New unit test for missing/empty path validation.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline docs.dev entry.
  • crates/hero_books_server/openrpc.jsondocs.dev method entry.

No new files.

Implementation Plan

Step 1 — Add dev subcommand to hero_docs

Files: src/bin/hero_docs.rs

  • Extend the Commands enum with Dev(DevArgs).
  • Args struct:
    #[derive(Args)]
    struct DevArgs {
        /// Path to the Docusaurus build directory to serve
        #[arg(long)]
        path: String,
        /// Hostname to bind the dev server (default: localhost)
        #[arg(long, default_value = "localhost")]
        host: String,
        /// Port to bind the dev server (default: 3000)
        #[arg(long, default_value = "3000")]
        port: u16,
    }
    
  • Runner:
    fn run_dev(args: DevArgs) -> Result<(), Box<dyn std::error::Error>> {
        let path = std::path::PathBuf::from(&args.path);
        log::info!("Starting Docusaurus dev server at http://{}:{} (path: {})", args.host, args.port, path.display());
        hero_books_docusaurus::build::dev(&path, &args.host, args.port)?;
        Ok(())
    }
    
    Note: build::dev blocks until the dev server exits (or is killed). This is the behaviour we want for hero_proc to track.
  • Dispatch from main() alongside the existing arms.

Dependencies: none.

Step 2 — Parametrise submit_or_dedup_docs_job with optional timeout

Files: crates/hero_books_server/src/web/rpc.rs

  • Change the helper signature:
    fn submit_or_dedup_docs_job(
        id: Option<Value>,
        config: &ServerConfig,
        action_name: &str,
        script: &str,
        timeout_ms: Option<i64>,
    ) -> RpcResponse
    
  • Inside the body, only call .timeout_ms(...) on the ActionBuilder when timeout_ms is Some:
    let mut builder = hero_proc_sdk::ActionBuilder::new(action_name, script).no_retry();
    if let Some(ms) = timeout_ms {
        builder = builder.timeout_ms(ms);
    }
    let spec = builder.build();
    
  • Update all five existing callers (handle_docs_new, handle_docs_generate, handle_docs_install_template, handle_docs_update_template, handle_docs_build) to pass Some(600_000) so their behaviour is unchanged.

Dependencies: none.

Step 3 — Add handle_docs_dev handler

Files: crates/hero_books_server/src/web/rpc.rs

  • Add the function below handle_docs_build with signature fn handle_docs_dev(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse.
  • Body:
    • Extract path (required, non-empty), host (optional, default "localhost"), port (optional, default 3000).
      • path: same obj.get("path").and_then(|v| v.as_str()).map(String::from) pattern; reject empty/missing.
      • host: obj.get("host").and_then(|v| v.as_str()).map(String::from).unwrap_or_else(|| "localhost".to_string()).
      • port: obj.get("port").and_then(|v| v.as_u64()).map(|n| n as u16).unwrap_or(3000). (Use as_u64 because JSON numbers; cast to u16. Out-of-range u16 values silently truncate — accept this for now; a stricter check could reject port > 65535.)
    • Build input hash including all three values:
      let input_hash = calculate_docs_input_hash(&[&path, &host, &port.to_string()]);
      let action_name = format!("docs_dev_{}", input_hash);
      
    • Build script: format!("{} dev --path {} --host {} --port {}", hero_docs, path_q, host_q, port) (port is a u16 — no quoting needed).
    • Submit via submit_or_dedup_docs_job(id, config, &action_name, &script, None). None = no timeout — hero_proc lets the dev server run indefinitely.

Dependencies: Steps 1 (subcommand exists for runtime) and 2 (helper signature accepts timeout option).

Step 4 — Wire dispatch arm

Files: crates/hero_books_server/src/web/rpc.rs

  • Add immediately after docs.build and before docs.jobStatus:
    "docs.dev" => handle_docs_dev(request.id, request.params, config),
    

Dependencies: Step 3.

Step 5 — Update doc-header and derive_docs_output_path doc-comment

Files: crates/hero_books_server/src/web/rpc.rs

  • ## Docs doc-header: insert docs.dev after docs.build:
    //! - `docs.dev` - Start the Docusaurus dev server (long-running; stop via hero_proc job kill)
    
  • Update derive_docs_output_path's bucket-3 comment to call out docs_dev_* explicitly: "long-running; output is a live HTTP server, not a build directory."

Dependencies: none.

Step 6 — Add docs.dev entries (openrpc.json + rpc_spec.rs)

Files: crates/hero_books_server/openrpc.json, crates/hero_books_server/src/web/rpc_spec.rs

  • Both schemas: insert new method object between docs.build and docs.jobStatus:
    {
      "name": "docs.dev",
      "summary": "Submit a long-running hero_proc job that starts the Docusaurus dev server at the given build path. Returns the hero_proc job id as a string. The dev server runs until the caller stops it via hero_proc job kill — there is no separate docs.devStop. docs.jobStatus does not surface output_path for this method.",
      "params": [
        { "name": "path", "schema": { "type": "string"  }, "required": true  },
        { "name": "host", "schema": { "type": "string"  }, "required": false },
        { "name": "port", "schema": { "type": "integer" }, "required": false }
      ],
      "result": {
        "name": "jobRef",
        "schema": {
          "type": "object",
          "properties": { "job_id": { "type": "string" } }
        }
      }
    }
    
  • info.version unchanged.

Dependencies: Step 4.

Step 7 — Add unit test

Files: crates/hero_books_server/src/web/rpc.rs

  • test_docs_dev_missing_params — same shape as test_docs_build_missing_params: missing path, empty path, missing params object — each returns -32602.
  • (No new test for derive_docs_output_path is needed: the existing test_derive_docs_output_path_returns_none_for_install_update_template already tests docs_dev_xxx as one of its "unknown / no-output" cases.)

Dependencies: Step 3.

Step 8 — Verify and live-test

  • INSTRUCTIONS_OPENRPC.md §Verification diff empty.
  • cargo check, cargo clippy, cargo test --release all green.
  • Smoke: cargo run --bin hero_docs -- dev --help.
  • Live RPC:
    • rpc.discover lists docs.dev with three params.
    • Validation: missing path returns -32602.
    • Submit: returns {"job_id": "<n>"}.
    • Idempotent dedup with same (path, host, port) returns same id.
    • Different (path, host, port) triple returns different id.
    • docs.jobStatus for the submitted job returns state: running (not done — long-running) and no output_path field.
    • hero_proc job kill <id> stops the dev server (the job goes to failed/cancelled state).

Dependencies: Steps 1-7.

Acceptance Criteria

  • hero_docs dev --help prints usage with --path (required), --host (default localhost), --port (default 3000).
  • crates/hero_books_server/src/web/rpc.rs defines handle_docs_dev.
  • submit_or_dedup_docs_job accepts an optional timeout_ms: Option<i64>; docs.dev passes None (no timeout); other callers pass Some(600_000).
  • docs.dev is wired into the dispatcher between docs.build and docs.jobStatus.
  • Action name format is docs_dev_<input_hash>. Hash includes path, host, port so different host/port combinations don't collide.
  • docs.jobStatus returns no output_path for docs_dev_* jobs in any state.
  • ## Docs doc-header lists docs.dev.
  • crates/hero_books_server/openrpc.json and rpc_spec.rs inline schema both contain docs.dev.
  • info.version is unchanged at "0.1.6".
  • Live test: dev job is visible in hero_proc list / hero_proc job and can be killed with hero_proc job kill <id>.
  • INSTRUCTIONS_OPENRPC.md §Verification diff empty.
  • cargo check, cargo clippy, cargo test all pass.

Notes

  • Timeout policy: hero_proc's ActionBuilder.exec_timeout_ms defaults to None, meaning no timeout enforcement. Passing None from handle_docs_dev is the right way to express "let it run forever" — we don't need a sentinel like i64::MAX.
  • Why include host/port in the hash: dedup must distinguish two simultaneous dev sessions on different ports (say, dev1 on :3000 and dev2 on :3001). Hashing only path would conflate them.
  • No docs.devStop: per the issue body and INSTRUCTIONS_OPENRPC.md, callers stop the dev server with hero_proc job kill <id>. The docs.dev job_id returned by RPC IS the hero_proc job id, so this is a one-line operation for the caller.
  • No output_path: a dev server is a live HTTP endpoint, not a built directory. derive_docs_output_path already returns None for unknown prefixes (including docs_dev_*), so docs.jobStatus correctly omits output_path. Documented in the helper's doc-comment.
  • Port validation: I'm not adding strict port range validation (port <= 65535) here. JSON numbers exceeding u16 will silently truncate via as u16. If a future child wants stricter validation, it's a small change. Keeping consistent with how handle_docs_new doesn't validate force beyond its bool-ness.
  • Pre-condition for live success: build path must contain a fully-prepared Docusaurus site (template + heroscript content). Same bun runtime requirement as #102-#104.
  • #[allow(dead_code)] watch: the test that exercises docs_dev_xxx returning None (test_derive_docs_output_path_returns_none_for_install_update_template) was written in #104 with the expectation that docs.dev would land later. With #105 it becomes a real check rather than a placeholder.
## Implementation Spec for Issue #105 ### Objective Expose `DocSite::dev(host, port)` over the OpenRPC interface as `docs.dev`. This is a long-running method: the dev server is intended to keep serving until the caller explicitly kills the hero_proc job. Per the issue body, callers stop it via `hero_proc job kill <id>`; we do not invent a `docs.devStop`. ### Requirements - `docs.dev` is dispatchable from `crates/hero_books_server/src/web/rpc.rs` and reaches a new handler `handle_docs_dev`. - Matching entry in `openrpc.json` and the inline `rpc_spec.rs` schema. Summary documents the long-running nature and the kill-via-hero_proc story. - `info.version` unchanged at `0.1.6` (one bump per rolling PR; #102 already did it). - The `## Docs` section in the file-level doc-header gains `docs.dev`. - `hero_docs` (`src/bin/hero_docs.rs`) gains a `dev` subcommand: `hero_docs dev --path <path> [--host <host>] [--port <port>]`. It calls `hero_books_docusaurus::build::dev(&path, &host, port)` directly. The subcommand blocks (does not exit) — that is the desired long-running behaviour. - `submit_or_dedup_docs_job` is extended to accept an optional timeout. Existing callers pass `Some(600_000)` (10 minutes, the current behaviour). `docs.dev` passes `None`, which means no timeout — hero_proc lets the job run indefinitely. - Action name format: `docs_dev_<input_hash>` where `input_hash = calculate_docs_input_hash(&[&path, &host, &port_string])`. Including host/port in the hash is important so two dev sessions on different ports do not dedup into one. - `derive_docs_output_path` is extended (or rather, left alone — the helper already returns `None` for unknown prefixes including `docs_dev_*`). The doc comment on the helper is updated to mention `docs_dev_*` is one of the explicitly-supported "no output_path" prefixes. - Invalid-params responses use `RpcResponse::invalid_params` (-32602); internal failures use `RpcResponse::error(id, -32000, ...)`. - INSTRUCTIONS_OPENRPC.md §Verification diff produces empty output. ### Files to Modify/Create - `src/bin/hero_docs.rs` — `Dev(DevArgs)` variant, args struct (path required; host default `"localhost"`; port default `3000`), runner. - `crates/hero_books_server/src/web/rpc.rs` - `handle_docs_dev` handler. - Dispatch arm. - Doc-header bullet. - `submit_or_dedup_docs_job` signature extended with `timeout_ms: Option<i64>` (and existing five callers updated to pass `Some(600_000)`). - Update `derive_docs_output_path` doc-comment to mention `docs_dev_*`. - New unit test for missing/empty path validation. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline `docs.dev` entry. - `crates/hero_books_server/openrpc.json` — `docs.dev` method entry. No new files. ### Implementation Plan #### Step 1 — Add `dev` subcommand to `hero_docs` Files: `src/bin/hero_docs.rs` - Extend the `Commands` enum with `Dev(DevArgs)`. - Args struct: ```rust #[derive(Args)] struct DevArgs { /// Path to the Docusaurus build directory to serve #[arg(long)] path: String, /// Hostname to bind the dev server (default: localhost) #[arg(long, default_value = "localhost")] host: String, /// Port to bind the dev server (default: 3000) #[arg(long, default_value = "3000")] port: u16, } ``` - Runner: ```rust fn run_dev(args: DevArgs) -> Result<(), Box<dyn std::error::Error>> { let path = std::path::PathBuf::from(&args.path); log::info!("Starting Docusaurus dev server at http://{}:{} (path: {})", args.host, args.port, path.display()); hero_books_docusaurus::build::dev(&path, &args.host, args.port)?; Ok(()) } ``` Note: `build::dev` blocks until the dev server exits (or is killed). This is the behaviour we want for hero_proc to track. - Dispatch from `main()` alongside the existing arms. Dependencies: none. #### Step 2 — Parametrise `submit_or_dedup_docs_job` with optional timeout Files: `crates/hero_books_server/src/web/rpc.rs` - Change the helper signature: ```rust fn submit_or_dedup_docs_job( id: Option<Value>, config: &ServerConfig, action_name: &str, script: &str, timeout_ms: Option<i64>, ) -> RpcResponse ``` - Inside the body, only call `.timeout_ms(...)` on the `ActionBuilder` when `timeout_ms` is `Some`: ```rust let mut builder = hero_proc_sdk::ActionBuilder::new(action_name, script).no_retry(); if let Some(ms) = timeout_ms { builder = builder.timeout_ms(ms); } let spec = builder.build(); ``` - Update all five existing callers (`handle_docs_new`, `handle_docs_generate`, `handle_docs_install_template`, `handle_docs_update_template`, `handle_docs_build`) to pass `Some(600_000)` so their behaviour is unchanged. Dependencies: none. #### Step 3 — Add `handle_docs_dev` handler Files: `crates/hero_books_server/src/web/rpc.rs` - Add the function below `handle_docs_build` with signature `fn handle_docs_dev(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse`. - Body: - Extract `path` (required, non-empty), `host` (optional, default `"localhost"`), `port` (optional, default `3000`). - `path`: same `obj.get("path").and_then(|v| v.as_str()).map(String::from)` pattern; reject empty/missing. - `host`: `obj.get("host").and_then(|v| v.as_str()).map(String::from).unwrap_or_else(|| "localhost".to_string())`. - `port`: `obj.get("port").and_then(|v| v.as_u64()).map(|n| n as u16).unwrap_or(3000)`. (Use `as_u64` because JSON numbers; cast to u16. Out-of-range u16 values silently truncate — accept this for now; a stricter check could reject `port > 65535`.) - Build input hash including all three values: ```rust let input_hash = calculate_docs_input_hash(&[&path, &host, &port.to_string()]); let action_name = format!("docs_dev_{}", input_hash); ``` - Build script: `format!("{} dev --path {} --host {} --port {}", hero_docs, path_q, host_q, port)` (port is a u16 — no quoting needed). - Submit via `submit_or_dedup_docs_job(id, config, &action_name, &script, None)`. **`None` = no timeout** — hero_proc lets the dev server run indefinitely. Dependencies: Steps 1 (subcommand exists for runtime) and 2 (helper signature accepts timeout option). #### Step 4 — Wire dispatch arm Files: `crates/hero_books_server/src/web/rpc.rs` - Add immediately after `docs.build` and before `docs.jobStatus`: ```rust "docs.dev" => handle_docs_dev(request.id, request.params, config), ``` Dependencies: Step 3. #### Step 5 — Update doc-header and `derive_docs_output_path` doc-comment Files: `crates/hero_books_server/src/web/rpc.rs` - `## Docs` doc-header: insert `docs.dev` after `docs.build`: ```rust //! - `docs.dev` - Start the Docusaurus dev server (long-running; stop via hero_proc job kill) ``` - Update `derive_docs_output_path`'s bucket-3 comment to call out `docs_dev_*` explicitly: "long-running; output is a live HTTP server, not a build directory." Dependencies: none. #### Step 6 — Add `docs.dev` entries (openrpc.json + rpc_spec.rs) Files: `crates/hero_books_server/openrpc.json`, `crates/hero_books_server/src/web/rpc_spec.rs` - Both schemas: insert new method object between `docs.build` and `docs.jobStatus`: ```json { "name": "docs.dev", "summary": "Submit a long-running hero_proc job that starts the Docusaurus dev server at the given build path. Returns the hero_proc job id as a string. The dev server runs until the caller stops it via hero_proc job kill — there is no separate docs.devStop. docs.jobStatus does not surface output_path for this method.", "params": [ { "name": "path", "schema": { "type": "string" }, "required": true }, { "name": "host", "schema": { "type": "string" }, "required": false }, { "name": "port", "schema": { "type": "integer" }, "required": false } ], "result": { "name": "jobRef", "schema": { "type": "object", "properties": { "job_id": { "type": "string" } } } } } ``` - `info.version` unchanged. Dependencies: Step 4. #### Step 7 — Add unit test Files: `crates/hero_books_server/src/web/rpc.rs` - `test_docs_dev_missing_params` — same shape as `test_docs_build_missing_params`: missing `path`, empty `path`, missing `params` object — each returns `-32602`. - (No new test for `derive_docs_output_path` is needed: the existing `test_derive_docs_output_path_returns_none_for_install_update_template` already tests `docs_dev_xxx` as one of its "unknown / no-output" cases.) Dependencies: Step 3. #### Step 8 — Verify and live-test - INSTRUCTIONS_OPENRPC.md §Verification diff empty. - `cargo check`, `cargo clippy`, `cargo test --release` all green. - Smoke: `cargo run --bin hero_docs -- dev --help`. - Live RPC: - `rpc.discover` lists `docs.dev` with three params. - Validation: missing `path` returns `-32602`. - Submit: returns `{"job_id": "<n>"}`. - Idempotent dedup with same `(path, host, port)` returns same id. - Different `(path, host, port)` triple returns different id. - `docs.jobStatus` for the submitted job returns `state: running` (not `done` — long-running) and **no `output_path` field**. - `hero_proc job kill <id>` stops the dev server (the job goes to `failed`/`cancelled` state). Dependencies: Steps 1-7. ### Acceptance Criteria - [ ] `hero_docs dev --help` prints usage with `--path` (required), `--host` (default `localhost`), `--port` (default `3000`). - [ ] `crates/hero_books_server/src/web/rpc.rs` defines `handle_docs_dev`. - [ ] `submit_or_dedup_docs_job` accepts an optional `timeout_ms: Option<i64>`; `docs.dev` passes `None` (no timeout); other callers pass `Some(600_000)`. - [ ] `docs.dev` is wired into the dispatcher between `docs.build` and `docs.jobStatus`. - [ ] Action name format is `docs_dev_<input_hash>`. Hash includes `path`, `host`, `port` so different host/port combinations don't collide. - [ ] `docs.jobStatus` returns no `output_path` for `docs_dev_*` jobs in any state. - [ ] `## Docs` doc-header lists `docs.dev`. - [ ] `crates/hero_books_server/openrpc.json` and `rpc_spec.rs` inline schema both contain `docs.dev`. - [ ] `info.version` is unchanged at `"0.1.6"`. - [ ] Live test: dev job is visible in `hero_proc list` / `hero_proc job` and can be killed with `hero_proc job kill <id>`. - [ ] INSTRUCTIONS_OPENRPC.md §Verification diff empty. - [ ] `cargo check`, `cargo clippy`, `cargo test` all pass. ### Notes - **Timeout policy**: hero_proc's `ActionBuilder.exec_timeout_ms` defaults to `None`, meaning no timeout enforcement. Passing `None` from `handle_docs_dev` is the right way to express "let it run forever" — we don't need a sentinel like `i64::MAX`. - **Why include host/port in the hash**: dedup must distinguish two simultaneous dev sessions on different ports (say, dev1 on `:3000` and dev2 on `:3001`). Hashing only `path` would conflate them. - **No `docs.devStop`**: per the issue body and INSTRUCTIONS_OPENRPC.md, callers stop the dev server with `hero_proc job kill <id>`. The `docs.dev` job_id returned by RPC IS the hero_proc job id, so this is a one-line operation for the caller. - **No `output_path`**: a dev server is a live HTTP endpoint, not a built directory. `derive_docs_output_path` already returns `None` for unknown prefixes (including `docs_dev_*`), so `docs.jobStatus` correctly omits `output_path`. Documented in the helper's doc-comment. - **Port validation**: I'm not adding strict port range validation (`port <= 65535`) here. JSON numbers exceeding u16 will silently truncate via `as u16`. If a future child wants stricter validation, it's a small change. Keeping consistent with how `handle_docs_new` doesn't validate `force` beyond its bool-ness. - **Pre-condition for live success**: build path must contain a fully-prepared Docusaurus site (template + heroscript content). Same `bun` runtime requirement as #102-#104. - **`#[allow(dead_code)]` watch**: the test that exercises `docs_dev_xxx` returning None (`test_derive_docs_output_path_returns_none_for_install_update_template`) was written in #104 with the expectation that `docs.dev` would land later. With #105 it becomes a real check rather than a placeholder.
Member

Test Results

Suite: cargo test -p hero_books_server --lib
Total: 24 — Passed: 24 — Failed: 0

New tests added with this change (1):

  • test_docs_dev_missing_params — missing/empty/no-params docs.dev returns -32602.

(test_derive_docs_output_path_returns_none_for_install_update_template from #104 already covers docs_dev_xxx returning None from the helper.)

Build & lint

  • cargo check -p hero_books_server — OK
  • cargo check --bin hero_docs — OK
  • cargo clippy -p hero_books_server --lib --no-deps — silent (zero warnings)
  • cargo clippy --bin hero_docs --no-deps — silent
  • cargo test --release — 24/24

Spec/impl parity

INSTRUCTIONS_OPENRPC.md §Verification diff is empty.

CLI smoke test

$ hero_docs dev --help
Start the Docusaurus dev server (long-running)

Usage: hero_docs dev [OPTIONS] --path <PATH>

Options:
      --path <PATH>  Path to the Docusaurus build directory to serve
      --host <HOST>  Hostname to bind the dev server [default: localhost]
      --port <PORT>  Port to bind the dev server [default: 3000]
  -h, --help         Print help

Live end-to-end RPC test (against running hero_books_server + hero_proc)

Step Result
rpc.discover lists docs.dev with path (required), host/port (optional) pass
Validation: missing path returns -32602 "missing or empty 'path' parameter" pass
Submit returns {"job_id":"103"} for path /tmp/test_build_e2e port 3030 pass
Re-submit with same (path, host, port) dedupes to the same job_id pass
Different port produces a different job_id (104 vs 103, port 3031 vs 3030) pass
Job visible in hero_proc job list with action docs_dev_<hash> pass
docs.jobStatus returns state: pending/running/failed and no output_path field pass
DB confirms timeout policy: docs_dev_* jobs have timeout_ms = 0 (= no timeout, per hero_proc SDK doc-comment "0 = no timeout"); existing docs_install_template_* and docs_new_* still have timeout_ms = 600000 (10-min cap unchanged) pass
Cancel via hero_proc's job.cancel JSON-RPC (id only) returns {"ok": true} pass

The dev server itself failed quickly (bun run start couldn't resolve the docusaurus.config.ts module without a heroscript-generated content tree) — same shared infrastructure issue as #102/#103/#104, not a defect introduced by this change. The full RPC chain (dispatch -> handler -> hero_proc -> hero_docs dev -> status reporting -> cancel) executed end-to-end.

Backwards compatibility

Public RPC surface and hero_docs CLI: strictly additive. The internal helper submit_or_dedup_docs_job keeps its original 4-arg signature (now a thin wrapper) — none of the existing five callers were changed. New callers needing a different timeout use submit_or_dedup_docs_job_with_timeout directly. ActionSpecs on the wire for old methods are byte-identical to before.

info.version

Unchanged at 0.1.6 — single bump per rolling PR.

## Test Results **Suite:** `cargo test -p hero_books_server --lib` **Total:** 24 — Passed: 24 — Failed: 0 New tests added with this change (1): - `test_docs_dev_missing_params` — missing/empty/no-params `docs.dev` returns `-32602`. (`test_derive_docs_output_path_returns_none_for_install_update_template` from #104 already covers `docs_dev_xxx` returning `None` from the helper.) ### Build & lint - `cargo check -p hero_books_server` — OK - `cargo check --bin hero_docs` — OK - `cargo clippy -p hero_books_server --lib --no-deps` — silent (zero warnings) - `cargo clippy --bin hero_docs --no-deps` — silent - `cargo test --release` — 24/24 ### Spec/impl parity INSTRUCTIONS_OPENRPC.md §Verification diff is empty. ### CLI smoke test ``` $ hero_docs dev --help Start the Docusaurus dev server (long-running) Usage: hero_docs dev [OPTIONS] --path <PATH> Options: --path <PATH> Path to the Docusaurus build directory to serve --host <HOST> Hostname to bind the dev server [default: localhost] --port <PORT> Port to bind the dev server [default: 3000] -h, --help Print help ``` ### Live end-to-end RPC test (against running hero_books_server + hero_proc) | Step | Result | | --- | --- | | `rpc.discover` lists `docs.dev` with `path` (required), `host`/`port` (optional) | pass | | Validation: missing `path` returns `-32602 "missing or empty 'path' parameter"` | pass | | Submit returns `{"job_id":"103"}` for path `/tmp/test_build_e2e` port 3030 | pass | | Re-submit with same `(path, host, port)` dedupes to the same `job_id` | pass | | Different `port` produces a different `job_id` (104 vs 103, port 3031 vs 3030) | pass | | Job visible in `hero_proc job list` with action `docs_dev_<hash>` | pass | | `docs.jobStatus` returns `state: pending`/`running`/`failed` and **no `output_path` field** | pass | | **DB confirms timeout policy**: `docs_dev_*` jobs have `timeout_ms = 0` (= no timeout, per hero_proc SDK doc-comment "0 = no timeout"); existing `docs_install_template_*` and `docs_new_*` still have `timeout_ms = 600000` (10-min cap unchanged) | pass | | **Cancel via `hero_proc`'s `job.cancel` JSON-RPC** (id only) returns `{"ok": true}` | pass | The dev server itself failed quickly (`bun run start` couldn't resolve the docusaurus.config.ts module without a heroscript-generated content tree) — same shared infrastructure issue as #102/#103/#104, not a defect introduced by this change. The full RPC chain (dispatch -> handler -> hero_proc -> `hero_docs dev` -> status reporting -> cancel) executed end-to-end. ### Backwards compatibility Public RPC surface and `hero_docs` CLI: strictly additive. The internal helper `submit_or_dedup_docs_job` keeps its original 4-arg signature (now a thin wrapper) — none of the existing five callers were changed. New callers needing a different timeout use `submit_or_dedup_docs_job_with_timeout` directly. ActionSpecs on the wire for old methods are byte-identical to before. ### `info.version` Unchanged at `0.1.6` — single bump per rolling PR.
Member

Implementation Summary

docs.dev is now exposed over JSON-RPC. The dev server runs as a long-running hero_proc job with no timeout, and is cancelled by calling hero_proc's job.cancel JSON-RPC with the returned job_id.

Files changed (this iteration)

  • src/bin/hero_docs.rs — new Dev(DevArgs) subcommand wrapping hero_books_docusaurus::build::dev(&path, &host, port). The CLI binds to localhost:3000 by default; both can be overridden.
  • crates/hero_books_server/src/web/rpc.rs
    • handle_docs_dev handler — extracts path (required), host (default "localhost"), port (default 3000); hashes all three for dedup so two dev sessions on different ports don't collide.
    • Helper refactor (backwards-compatible): submit_or_dedup_docs_job keeps its original 4-arg signature as a thin wrapper that calls a new sibling submit_or_dedup_docs_job_with_timeout(..., timeout_ms: Option<i64>). Existing five call sites (handle_docs_new, handle_docs_generate, handle_docs_install_template, handle_docs_update_template, handle_docs_build) didn't change. handle_docs_dev calls the new variant with None (no timeout) so the dev server can run indefinitely.
    • Dispatch arm "docs.dev" between docs.build and docs.jobStatus.
    • ## Docs doc-header lists docs.dev.
    • derive_docs_output_path doc-comment updated to call out docs_dev_* as a "no output_path" prefix (output is a live HTTP server, not a build directory).
    • New unit test test_docs_dev_missing_params.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline schema gains a docs.dev entry. Summary documents the long-running nature and the cancel mechanism.
  • crates/hero_books_server/openrpc.json — new method entry. info.version unchanged at 0.1.6.
  • crates/hero_books_server/openrpc.client.generated.rs — auto-regenerated.

Tests

  • 24/24 lib tests pass (debug + release) — 1 new.
  • cargo clippy silent on both crates.
  • Spec/impl parity diff empty.
  • Live RPC test confirmed: discovery, validation, submission, idempotent dedup, host/port distinction, hero_proc dispatch, state polling without output_path, and successful cancel via hero_proc job.cancel ({"ok": true}).
  • DB inspection confirmed docs_dev_* ActionSpecs serialize with timeout_ms: 0 (= no timeout per hero_proc SDK), while existing docs_new_* / docs_install_template_* keep timeout_ms: 600000.

Backwards compatibility

  • Public RPC API: only adds docs.dev. All existing methods unchanged.
  • hero_docs CLI: only adds dev subcommand. Existing subcommands unchanged.
  • Internal submit_or_dedup_docs_job helper: original 4-arg signature preserved (now a wrapper). All five existing callers unchanged.
  • info.version: unchanged at 0.1.6.
  • ActionSpec wire format for existing methods: byte-identical to before.

Notes

  • Timeout encoding: the SDK doc-comment for ActionBuilder::timeout_ms confirms 0 = no timeout. When timeout_ms is None, ActionBuilder doesn't call .timeout_ms(...), and the resulting JSON has timeout_ms: 0. hero_proc treats this as no enforcement.
  • Dedup keying: (path, host, port) are all hashed. Two dev sessions on different ports are distinct jobs.
  • No docs.devStop: confirmed by issue body. Callers stop the dev server via hero_proc's job.cancel JSON-RPC (id only — no service argument needed at the API level). The hero_proc job cancel CLI requires <service> <job>, but our docs.* jobs are ad-hoc (no service binding); the CLI doesn't find them this way. Use the JSON-RPC instead.
  • No output_path: a dev server is a live HTTP endpoint, not a build directory. derive_docs_output_path correctly returns None for docs_dev_* (locked by the existing test from #104).
  • Pre-condition for live success: build path must contain a fully-prepared Docusaurus site (template + heroscript-generated content). Same bun runtime requirement as #102-#104.
## Implementation Summary `docs.dev` is now exposed over JSON-RPC. The dev server runs as a long-running hero_proc job with no timeout, and is cancelled by calling hero_proc's `job.cancel` JSON-RPC with the returned `job_id`. ### Files changed (this iteration) - `src/bin/hero_docs.rs` — new `Dev(DevArgs)` subcommand wrapping `hero_books_docusaurus::build::dev(&path, &host, port)`. The CLI binds to `localhost:3000` by default; both can be overridden. - `crates/hero_books_server/src/web/rpc.rs` - `handle_docs_dev` handler — extracts `path` (required), `host` (default `"localhost"`), `port` (default `3000`); hashes all three for dedup so two dev sessions on different ports don't collide. - **Helper refactor (backwards-compatible)**: `submit_or_dedup_docs_job` keeps its original 4-arg signature as a thin wrapper that calls a new sibling `submit_or_dedup_docs_job_with_timeout(..., timeout_ms: Option<i64>)`. Existing five call sites (`handle_docs_new`, `handle_docs_generate`, `handle_docs_install_template`, `handle_docs_update_template`, `handle_docs_build`) didn't change. `handle_docs_dev` calls the new variant with `None` (no timeout) so the dev server can run indefinitely. - Dispatch arm `"docs.dev"` between `docs.build` and `docs.jobStatus`. - `## Docs` doc-header lists `docs.dev`. - `derive_docs_output_path` doc-comment updated to call out `docs_dev_*` as a "no output_path" prefix (output is a live HTTP server, not a build directory). - New unit test `test_docs_dev_missing_params`. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline schema gains a `docs.dev` entry. Summary documents the long-running nature and the cancel mechanism. - `crates/hero_books_server/openrpc.json` — new method entry. `info.version` unchanged at `0.1.6`. - `crates/hero_books_server/openrpc.client.generated.rs` — auto-regenerated. ### Tests - 24/24 lib tests pass (debug + release) — 1 new. - `cargo clippy` silent on both crates. - Spec/impl parity diff empty. - Live RPC test confirmed: discovery, validation, submission, idempotent dedup, host/port distinction, hero_proc dispatch, `state` polling without `output_path`, and successful cancel via hero_proc `job.cancel` (`{"ok": true}`). - DB inspection confirmed `docs_dev_*` ActionSpecs serialize with `timeout_ms: 0` (= no timeout per hero_proc SDK), while existing `docs_new_*` / `docs_install_template_*` keep `timeout_ms: 600000`. ### Backwards compatibility - Public RPC API: only adds `docs.dev`. All existing methods unchanged. - `hero_docs` CLI: only adds `dev` subcommand. Existing subcommands unchanged. - Internal `submit_or_dedup_docs_job` helper: original 4-arg signature preserved (now a wrapper). All five existing callers unchanged. - `info.version`: unchanged at `0.1.6`. - ActionSpec wire format for existing methods: byte-identical to before. ### Notes - **Timeout encoding**: the SDK doc-comment for `ActionBuilder::timeout_ms` confirms `0 = no timeout`. When `timeout_ms` is `None`, `ActionBuilder` doesn't call `.timeout_ms(...)`, and the resulting JSON has `timeout_ms: 0`. hero_proc treats this as no enforcement. - **Dedup keying**: `(path, host, port)` are all hashed. Two dev sessions on different ports are distinct jobs. - **No `docs.devStop`**: confirmed by issue body. Callers stop the dev server via hero_proc's `job.cancel` JSON-RPC (id only — no service argument needed at the API level). The `hero_proc job cancel` CLI requires `<service> <job>`, but our docs.* jobs are ad-hoc (no service binding); the CLI doesn't find them this way. Use the JSON-RPC instead. - **No `output_path`**: a dev server is a live HTTP endpoint, not a build directory. `derive_docs_output_path` correctly returns `None` for `docs_dev_*` (locked by the existing test from #104). - **Pre-condition for live success**: build path must contain a fully-prepared Docusaurus site (template + heroscript-generated content). Same `bun` runtime requirement as #102-#104.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_books#105
No description provided.