Add docs.build RPC method #104

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

Parent: #101

What to add

Method: docs.build
Params: { path: string }
Returns: { job_id: string }
Wraps: DocSite::build() at lib.rs:120 — directly, without the
new/generate prelude
Note: docs.jobStatus should report output_path pointing at <path>/build

Implementation steps

  1. Verify hero_docs build subcommand exists
  2. Add handle_docs_build(id, params, config) -> RpcResponse in crates/hero_books_server/src/web/rpc.rs
  3. Action key: docs_build_<input_hash> so handle_docs_job_status prefix-strip logic at rpc.rs:1508-1519 can recover output_path
  4. Add dispatch arm "docs.build" at rpc.rs:155
  5. Add method entry in openrpc.json, bump info.version
  6. Update doc-comment header at rpc.rs:9-28

Acceptance Criteria

  • hero_docs build subcommand exists
  • Handler shells out via hero_docs
  • docs.jobStatus returns correct output_path = <path>/build
  • spec/impl parity check passes
Parent: #101 ## What to add **Method:** `docs.build` **Params:** `{ path: string }` **Returns:** `{ job_id: string }` **Wraps:** `DocSite::build()` at `lib.rs:120` — directly, without the `new`/`generate` prelude **Note:** `docs.jobStatus` should report `output_path` pointing at `<path>/build` ## Implementation steps 1. Verify `hero_docs build` subcommand exists 2. Add `handle_docs_build(id, params, config) -> RpcResponse` in `crates/hero_books_server/src/web/rpc.rs` 3. Action key: `docs_build_<input_hash>` so `handle_docs_job_status` prefix-strip logic at `rpc.rs:1508-1519` can recover `output_path` 4. Add dispatch arm `"docs.build"` at `rpc.rs:155` 5. Add method entry in `openrpc.json`, bump `info.version` 6. Update doc-comment header at `rpc.rs:9-28` ## Acceptance Criteria - [x] `hero_docs build` subcommand exists - [x] Handler shells out via hero_docs - [x] `docs.jobStatus` returns correct `output_path` = `<path>/build` - [x] spec/impl parity check passes
rawdaGastan added this to the ACTIVE project 2026-04-26 14:08:55 +00:00
Member

Implementation Spec for Issue #104

Objective

Expose DocSite::build() directly over the OpenRPC interface as docs.build, without the new / generate prelude. Unlike docs.installTemplate / docs.updateTemplate, the issue requires docs.jobStatus to return output_path = <path>/build — so the user-supplied path must be recoverable when polling status. We achieve this by encoding the path into the action name with URL-safe base64 (no padding) instead of a one-way hash, while preserving the dedup property of submit_or_dedup_docs_job (same path produces the same encoded action name).

Requirements

  • docs.build is dispatchable from crates/hero_books_server/src/web/rpc.rs and reaches a new handler handle_docs_build.
  • Matching entry in crates/hero_books_server/openrpc.json and the inline schema in rpc_spec.rs (no examples).
  • info.version in openrpc.json is not bumped — single bump per rolling PR (already done in #102).
  • The ## Docs section in the rpc.rs file-level doc-header gains a docs.build bullet.
  • hero_docs (src/bin/hero_docs.rs) gains a new build subcommand: hero_docs build --path <path>. It calls hero_books_docusaurus::build::build(&path) directly — no heroscript loading.
  • Handler shells out via config.hero_docs_bin, never DocSite in-process.
  • Action name format: docs_build_<base64url(path)> using base64::engine::general_purpose::URL_SAFE_NO_PAD. No separate input hash — the encoded path itself serves as the dedup key. Same path always produces the same name; different paths produce different names.
  • handle_docs_job_status is extended to strip the docs_build_ prefix, base64url-decode the suffix, and surface output_path = <decoded_path>/build when the job is in done state.
  • Invalid-params responses use RpcResponse::invalid_params (-32602); internal failures use RpcResponse::error(id, -32000, ...).
  • Verification diff (INSTRUCTIONS_OPENRPC.md §Verification) produces empty output.

Files to Modify/Create

  • src/bin/hero_docs.rs — new Build(BuildArgs) variant, args struct, runner.
  • crates/hero_books_server/src/web/rpc.rshandle_docs_build, dispatch arm, handle_docs_job_status extended for docs_build_ prefix → output_path, doc-header bullet, prefix-recognition comment update, two unit tests.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline docs.build entry.
  • crates/hero_books_server/openrpc.jsondocs.build method entry.

No new files.

Implementation Plan

Step 1 — Add build subcommand to hero_docs

Files: src/bin/hero_docs.rs

  • Extend the Commands enum with Build(BuildArgs).
  • Args struct:
    #[derive(Args)]
    struct BuildArgs {
        /// Path to the Docusaurus build directory to build (the directory the
        /// template was previously installed into)
        #[arg(long)]
        path: String,
    }
    
  • Runner:
    fn run_build(args: BuildArgs) -> Result<(), Box<dyn std::error::Error>> {
        let path = std::path::PathBuf::from(&args.path);
        hero_books_docusaurus::build::build(&path)?;
        log::info!("Site built at {}", path.display());
        Ok(())
    }
    
  • Dispatch from main() alongside the existing arms.

Dependencies: none.

Step 2 — Add handle_docs_build handler

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

  • Add the function below handle_docs_update_template with signature fn handle_docs_build(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse.
  • Body:
    • Extract path (required, non-empty). Reject empty / missing with RpcResponse::invalid_params(id, "missing or empty 'path' parameter").
    • Encode action name:
      use base64::Engine;
      let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(path.as_bytes());
      let action_name = format!("docs_build_{}", encoded);
      
    • Build script: format!("{} build --path {}", hero_docs, path_q) using shell_quote.
    • Submit via submit_or_dedup_docs_job(id, config, &action_name, &script).

Dependencies: Step 1 (subcommand must exist for runtime; not required for compilation).

Step 3 — Wire dispatch arm

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

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

Dependencies: Step 2.

Step 4 — Extend handle_docs_job_status to surface output_path for docs_build_*

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

  • The current if mapped_state == "done" block strips docs_new_ / docs_generate_ and joins with <cache>/<hash>/build. Extend it:
    • First, try the existing docs_new_ / docs_generate_ strip-list (unchanged).
    • If neither matches, try docs_build_:
      if let Some(encoded) = name.strip_prefix("docs_build_") {
          if let Ok(bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded) {
              if let Ok(path) = std::str::from_utf8(&bytes) {
                  let output = std::path::Path::new(path).join("build");
                  out.insert(
                      "output_path".to_string(),
                      Value::String(output.to_string_lossy().to_string()),
                  );
              }
          }
      }
      
    • On any decode failure (malformed base64 or non-UTF-8 bytes): silently skip emitting output_path. The state and any error tail still flow through.
  • Update the comment block above so it accurately describes the three buckets:
    • docs_new_* / docs_generate_*<cache>/<hash>/build
    • docs_build_*<base64url-decoded path>/build
    • docs_install_template_* / docs_update_template_* → no output_path (path not encoded)
    • upcoming docs_dev_* / docs_publish_* / docs_publish_dev_* from #105-#107 → no output_path

Dependencies: Step 2.

Step 5 — Update doc-header

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

  • Inside the ## Docs section, insert docs.build after docs.updateTemplate:
    //! - `docs.build` - Build the Docusaurus static site at a build path
    

Dependencies: none.

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

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

  • openrpc.json: insert new method object between docs.updateTemplate and docs.jobStatus:
    {
      "name": "docs.build",
      "summary": "Submit a hero_proc job that builds the Docusaurus static site at the given build path. Returns the hero_proc job id as a string; poll via docs.jobStatus. On completion, docs.jobStatus surfaces output_path = <path>/build.",
      "params": [
        { "name": "path", "schema": { "type": "string" }, "required": true }
      ],
      "result": {
        "name": "jobRef",
        "schema": {
          "type": "object",
          "properties": { "job_id": { "type": "string" } }
        }
      }
    }
    
  • rpc_spec.rs: insert the same entry into the inline schema between docs.updateTemplate and docs.jobStatus.
  • Do not bump info.version (already at 0.1.6 for this rolling PR).

Dependencies: Step 3.

Step 7 — Add unit tests

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

  • test_docs_build_missing_params — same shape as test_docs_update_template_missing_params: missing/empty/no-params all return -32602.
  • test_docs_build_action_name_round_trip — encode a sample path with URL_SAFE_NO_PAD, prepend docs_build_, then strip the prefix and decode, asserting the original path comes back. This locks the encoding choice and guards against future drift.

Dependencies: Step 2.

Step 8 — Verify and live-test

Files: none (read-only check + cargo + curl).

  • Run INSTRUCTIONS_OPENRPC.md §Verification snippet — diff must be empty.
  • cargo check -p hero_books_server and cargo check --bin hero_docs succeed.
  • cargo test -p hero_books_server --lib — 20 passing (two new), no regressions.
  • Smoke test: cargo run --bin hero_docs -- build --help.
  • Live RPC: discovery, validation, submission, idempotent dedup with same path, output_path returns <path>/build once state == "done" (or expected failure tail if bun not installed).

Dependencies: Steps 1-7.

Acceptance Criteria

  • hero_docs build --help prints usage with --path (required).
  • crates/hero_books_server/src/web/rpc.rs defines handle_docs_build.
  • docs.build is wired into the dispatcher between docs.updateTemplate and docs.jobStatus.
  • Action name format is docs_build_<base64url_no_pad(path)>; same path produces the same action name (idempotent dedup).
  • handle_docs_job_status returns output_path = <path>/build for docs_build_* jobs in done state, by strip-then-base64-decode.
  • On malformed action name (decode failure), handle_docs_job_status silently omits output_path rather than emitting a bogus value.
  • ## Docs doc-header in rpc.rs lists docs.build.
  • crates/hero_books_server/openrpc.json and rpc_spec.rs inline schema both contain the docs.build method entry.
  • info.version in openrpc.json is unchanged at "0.1.6".
  • INSTRUCTIONS_OPENRPC.md §Verification diff is empty.
  • cargo check -p hero_books_server and cargo check --bin hero_docs succeed.
  • cargo test -p hero_books_server --lib passes (with two new tests).

Notes

  • Action-name length: URL-safe base64 (no padding) of a UTF-8 path uses ~1.33× the path's byte length. A 200-byte path encodes to ~270 bytes. Linux filesystem path limit is 4096 bytes; hero_proc's action_id field is a sqlite TEXT column with no fixed limit, so even pathological cases comfortably fit. Documented for future maintainers.
  • Why no input_hash: dedup is naturally stable on the encoded path. Adding a hash on top would force two callers with the same path to compute the same hash anyway, so there's no benefit.
  • Path normalisation: not done in this PR — /tmp/x and /tmp/x/ are treated as distinct paths and produce different output_path values (/tmp/x/build vs /tmp/x//build). If we later want canonicalisation, we'd canonicalise before encoding so the dedup key changes accordingly. Out of scope for #104.
  • Handler infrastructure note: the prefix-strip block in handle_docs_job_status now grows a third arm (after docs_new_ / docs_generate_ and before the silent fall-throughs). Future #105-#107 may add similar arms (or stay silent) — keep the comment block accurate as each child lands.
  • Pre-condition for live success: the build path must already contain an installed template with content (i.e. docs.installTemplate or docs.generate has run there first). bun runtime required, same as #102/#103.
## Implementation Spec for Issue #104 ### Objective Expose `DocSite::build()` directly over the OpenRPC interface as `docs.build`, without the `new` / `generate` prelude. Unlike `docs.installTemplate` / `docs.updateTemplate`, the issue requires `docs.jobStatus` to return `output_path` = `<path>/build` — so the user-supplied `path` must be recoverable when polling status. We achieve this by encoding the path into the action name with URL-safe base64 (no padding) instead of a one-way hash, while preserving the dedup property of `submit_or_dedup_docs_job` (same path produces the same encoded action name). ### Requirements - `docs.build` is dispatchable from `crates/hero_books_server/src/web/rpc.rs` and reaches a new handler `handle_docs_build`. - Matching entry in `crates/hero_books_server/openrpc.json` and the inline schema in `rpc_spec.rs` (no `examples`). - `info.version` in `openrpc.json` is **not** bumped — single bump per rolling PR (already done in #102). - The `## Docs` section in the `rpc.rs` file-level doc-header gains a `docs.build` bullet. - `hero_docs` (`src/bin/hero_docs.rs`) gains a new `build` subcommand: `hero_docs build --path <path>`. It calls `hero_books_docusaurus::build::build(&path)` directly — no heroscript loading. - Handler shells out via `config.hero_docs_bin`, never `DocSite` in-process. - Action name format: `docs_build_<base64url(path)>` using `base64::engine::general_purpose::URL_SAFE_NO_PAD`. **No** separate input hash — the encoded path itself serves as the dedup key. Same path always produces the same name; different paths produce different names. - `handle_docs_job_status` is extended to strip the `docs_build_` prefix, base64url-decode the suffix, and surface `output_path` = `<decoded_path>/build` when the job is in `done` state. - Invalid-params responses use `RpcResponse::invalid_params` (-32602); internal failures use `RpcResponse::error(id, -32000, ...)`. - Verification diff (INSTRUCTIONS_OPENRPC.md §Verification) produces empty output. ### Files to Modify/Create - `src/bin/hero_docs.rs` — new `Build(BuildArgs)` variant, args struct, runner. - `crates/hero_books_server/src/web/rpc.rs` — `handle_docs_build`, dispatch arm, `handle_docs_job_status` extended for `docs_build_` prefix → output_path, doc-header bullet, prefix-recognition comment update, two unit tests. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline `docs.build` entry. - `crates/hero_books_server/openrpc.json` — `docs.build` method entry. No new files. ### Implementation Plan #### Step 1 — Add `build` subcommand to `hero_docs` Files: `src/bin/hero_docs.rs` - Extend the `Commands` enum with `Build(BuildArgs)`. - Args struct: ```rust #[derive(Args)] struct BuildArgs { /// Path to the Docusaurus build directory to build (the directory the /// template was previously installed into) #[arg(long)] path: String, } ``` - Runner: ```rust fn run_build(args: BuildArgs) -> Result<(), Box<dyn std::error::Error>> { let path = std::path::PathBuf::from(&args.path); hero_books_docusaurus::build::build(&path)?; log::info!("Site built at {}", path.display()); Ok(()) } ``` - Dispatch from `main()` alongside the existing arms. Dependencies: none. #### Step 2 — Add `handle_docs_build` handler Files: `crates/hero_books_server/src/web/rpc.rs` - Add the function below `handle_docs_update_template` with signature `fn handle_docs_build(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse`. - Body: - Extract `path` (required, non-empty). Reject empty / missing with `RpcResponse::invalid_params(id, "missing or empty 'path' parameter")`. - Encode action name: ```rust use base64::Engine; let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(path.as_bytes()); let action_name = format!("docs_build_{}", encoded); ``` - Build script: `format!("{} build --path {}", hero_docs, path_q)` using `shell_quote`. - Submit via `submit_or_dedup_docs_job(id, config, &action_name, &script)`. Dependencies: Step 1 (subcommand must exist for runtime; not required for compilation). #### Step 3 — Wire dispatch arm Files: `crates/hero_books_server/src/web/rpc.rs` - Add immediately after `docs.updateTemplate` and before `docs.jobStatus`: ```rust "docs.build" => handle_docs_build(request.id, request.params, config), ``` Dependencies: Step 2. #### Step 4 — Extend `handle_docs_job_status` to surface `output_path` for `docs_build_*` Files: `crates/hero_books_server/src/web/rpc.rs` - The current `if mapped_state == "done"` block strips `docs_new_` / `docs_generate_` and joins with `<cache>/<hash>/build`. Extend it: - First, try the existing `docs_new_` / `docs_generate_` strip-list (unchanged). - If neither matches, try `docs_build_`: ```rust if let Some(encoded) = name.strip_prefix("docs_build_") { if let Ok(bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded) { if let Ok(path) = std::str::from_utf8(&bytes) { let output = std::path::Path::new(path).join("build"); out.insert( "output_path".to_string(), Value::String(output.to_string_lossy().to_string()), ); } } } ``` - On any decode failure (malformed base64 or non-UTF-8 bytes): silently skip emitting `output_path`. The state and any error tail still flow through. - Update the comment block above so it accurately describes the three buckets: - `docs_new_*` / `docs_generate_*` → `<cache>/<hash>/build` - `docs_build_*` → `<base64url-decoded path>/build` - `docs_install_template_*` / `docs_update_template_*` → no `output_path` (path not encoded) - upcoming `docs_dev_*` / `docs_publish_*` / `docs_publish_dev_*` from #105-#107 → no `output_path` Dependencies: Step 2. #### Step 5 — Update doc-header Files: `crates/hero_books_server/src/web/rpc.rs` - Inside the `## Docs` section, insert `docs.build` after `docs.updateTemplate`: ```rust //! - `docs.build` - Build the Docusaurus static site at a build path ``` Dependencies: none. #### Step 6 — Add `docs.build` entries (openrpc.json + rpc_spec.rs) Files: `crates/hero_books_server/openrpc.json`, `crates/hero_books_server/src/web/rpc_spec.rs` - `openrpc.json`: insert new method object between `docs.updateTemplate` and `docs.jobStatus`: ```json { "name": "docs.build", "summary": "Submit a hero_proc job that builds the Docusaurus static site at the given build path. Returns the hero_proc job id as a string; poll via docs.jobStatus. On completion, docs.jobStatus surfaces output_path = <path>/build.", "params": [ { "name": "path", "schema": { "type": "string" }, "required": true } ], "result": { "name": "jobRef", "schema": { "type": "object", "properties": { "job_id": { "type": "string" } } } } } ``` - `rpc_spec.rs`: insert the same entry into the inline schema between `docs.updateTemplate` and `docs.jobStatus`. - Do **not** bump `info.version` (already at `0.1.6` for this rolling PR). Dependencies: Step 3. #### Step 7 — Add unit tests Files: `crates/hero_books_server/src/web/rpc.rs` - `test_docs_build_missing_params` — same shape as `test_docs_update_template_missing_params`: missing/empty/no-params all return `-32602`. - `test_docs_build_action_name_round_trip` — encode a sample path with `URL_SAFE_NO_PAD`, prepend `docs_build_`, then strip the prefix and decode, asserting the original path comes back. This locks the encoding choice and guards against future drift. Dependencies: Step 2. #### Step 8 — Verify and live-test Files: none (read-only check + cargo + curl). - Run INSTRUCTIONS_OPENRPC.md §Verification snippet — diff must be empty. - `cargo check -p hero_books_server` and `cargo check --bin hero_docs` succeed. - `cargo test -p hero_books_server --lib` — 20 passing (two new), no regressions. - Smoke test: `cargo run --bin hero_docs -- build --help`. - Live RPC: discovery, validation, submission, idempotent dedup with same path, `output_path` returns `<path>/build` once `state == "done"` (or expected failure tail if `bun` not installed). Dependencies: Steps 1-7. ### Acceptance Criteria - [ ] `hero_docs build --help` prints usage with `--path` (required). - [ ] `crates/hero_books_server/src/web/rpc.rs` defines `handle_docs_build`. - [ ] `docs.build` is wired into the dispatcher between `docs.updateTemplate` and `docs.jobStatus`. - [ ] Action name format is `docs_build_<base64url_no_pad(path)>`; same path produces the same action name (idempotent dedup). - [ ] `handle_docs_job_status` returns `output_path` = `<path>/build` for `docs_build_*` jobs in `done` state, by strip-then-base64-decode. - [ ] On malformed action name (decode failure), `handle_docs_job_status` silently omits `output_path` rather than emitting a bogus value. - [ ] `## Docs` doc-header in `rpc.rs` lists `docs.build`. - [ ] `crates/hero_books_server/openrpc.json` and `rpc_spec.rs` inline schema both contain the `docs.build` method entry. - [ ] `info.version` in `openrpc.json` is **unchanged** at `"0.1.6"`. - [ ] INSTRUCTIONS_OPENRPC.md §Verification diff is empty. - [ ] `cargo check -p hero_books_server` and `cargo check --bin hero_docs` succeed. - [ ] `cargo test -p hero_books_server --lib` passes (with two new tests). ### Notes - **Action-name length**: URL-safe base64 (no padding) of a UTF-8 path uses ~1.33× the path's byte length. A 200-byte path encodes to ~270 bytes. Linux filesystem path limit is 4096 bytes; hero_proc's `action_id` field is a sqlite TEXT column with no fixed limit, so even pathological cases comfortably fit. Documented for future maintainers. - **Why no `input_hash`**: dedup is naturally stable on the encoded path. Adding a hash on top would force two callers with the same path to compute the same hash anyway, so there's no benefit. - **Path normalisation**: not done in this PR — `/tmp/x` and `/tmp/x/` are treated as distinct paths and produce different `output_path` values (`/tmp/x/build` vs `/tmp/x//build`). If we later want canonicalisation, we'd canonicalise *before* encoding so the dedup key changes accordingly. Out of scope for #104. - **Handler infrastructure note**: the prefix-strip block in `handle_docs_job_status` now grows a third arm (after `docs_new_` / `docs_generate_` and before the silent fall-throughs). Future #105-#107 may add similar arms (or stay silent) — keep the comment block accurate as each child lands. - **Pre-condition for live success**: the build path must already contain an installed template with content (i.e. `docs.installTemplate` or `docs.generate` has run there first). `bun` runtime required, same as #102/#103.
Member

Test Results

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

New tests added with this change (4):

  • test_docs_build_missing_params — missing/empty/no-params docs.build returns -32602.
  • test_derive_docs_output_path_for_docs_build — for docs_build_<base64url(path)> action names, the helper returns <path>/build. Tested with three paths including one containing spaces and special characters.
  • test_derive_docs_output_path_for_docs_new_and_generate — preserves the existing contract: docs_new_<hash> / docs_generate_<hash><cache>/<hash>/build.
  • test_derive_docs_output_path_returns_none_for_install_update_template — install/update template, dev, and unknown prefixes return None.
  • test_derive_docs_output_path_returns_none_for_malformed_docs_build — invalid base64 or valid base64 of non-UTF-8 bytes returns None. Decoder fails closed.

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 — 23/23

Spec/impl parity

INSTRUCTIONS_OPENRPC.md §Verification diff is empty — openrpc.json and rpc.rs dispatcher agree, with docs.build listed in both.

CLI smoke test

$ hero_docs build --help
Build the Docusaurus static site at a build directory

Usage: hero_docs build --path <PATH>

Options:
      --path <PATH>  Path to the Docusaurus build directory to build
  -h, --help         Print help

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

Step Result
rpc.discover lists docs.build with required path pass
Validation: missing path returns -32602 "missing or empty 'path' parameter" pass
Submit docs.build for /tmp/test_build_rpc returns {"job_id":"89"} pass
Re-submit same path dedupes to job_id: "89" pass
Different path produces different job_id: "90" pass
hero_proc DB stores action_id = docs_build_L3RtcC90ZXN0X2J1aWxkX3JwYw (decodes back to /tmp/test_build_rpc) pass
Live output_path integration confirmed via docs.new: returned state: done with output_path: /home/rawda/hero/var/books/.docusaurus_cache/c3cf0e37cf92889c/build — same derive_docs_output_path helper code path that serves docs.build. pass
output_path correctly omitted when state == failed (guarded by if mapped_state == "done") pass

A direct end-to-end docs.build to state == done would require a fully-prepared Docusaurus site (config.ts that resolves all modules), which needs heroscript content via docs.generate first — circular for this verification. Combined unit-level proof of derive_docs_output_path (the only logic specific to this change) plus live integration proof via docs.new (same handler call site) covers the entire output_path code path.

info.version

Unchanged at 0.1.6 — single bump per rolling PR (#102 already did it).

## Test Results **Suite:** `cargo test -p hero_books_server --lib` **Total:** 23 — Passed: 23 — Failed: 0 New tests added with this change (4): - `test_docs_build_missing_params` — missing/empty/no-params `docs.build` returns `-32602`. - `test_derive_docs_output_path_for_docs_build` — for `docs_build_<base64url(path)>` action names, the helper returns `<path>/build`. Tested with three paths including one containing spaces and special characters. - `test_derive_docs_output_path_for_docs_new_and_generate` — preserves the existing contract: `docs_new_<hash>` / `docs_generate_<hash>` → `<cache>/<hash>/build`. - `test_derive_docs_output_path_returns_none_for_install_update_template` — install/update template, dev, and unknown prefixes return `None`. - `test_derive_docs_output_path_returns_none_for_malformed_docs_build` — invalid base64 or valid base64 of non-UTF-8 bytes returns `None`. Decoder fails closed. ### 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` — 23/23 ### Spec/impl parity INSTRUCTIONS_OPENRPC.md §Verification diff is empty — `openrpc.json` and `rpc.rs` dispatcher agree, with `docs.build` listed in both. ### CLI smoke test ``` $ hero_docs build --help Build the Docusaurus static site at a build directory Usage: hero_docs build --path <PATH> Options: --path <PATH> Path to the Docusaurus build directory to build -h, --help Print help ``` ### Live end-to-end RPC test (against running hero_books_server + hero_proc) | Step | Result | | --- | --- | | `rpc.discover` lists `docs.build` with required `path` | pass | | Validation: missing `path` returns `-32602 "missing or empty 'path' parameter"` | pass | | Submit `docs.build` for `/tmp/test_build_rpc` returns `{"job_id":"89"}` | pass | | Re-submit same path dedupes to `job_id: "89"` | pass | | Different path produces different `job_id: "90"` | pass | | hero_proc DB stores `action_id = docs_build_L3RtcC90ZXN0X2J1aWxkX3JwYw` (decodes back to `/tmp/test_build_rpc`) | pass | | **Live `output_path` integration confirmed via `docs.new`**: returned `state: done` with `output_path: /home/rawda/hero/var/books/.docusaurus_cache/c3cf0e37cf92889c/build` — same `derive_docs_output_path` helper code path that serves `docs.build`. | pass | | `output_path` correctly omitted when `state == failed` (guarded by `if mapped_state == "done"`) | pass | A direct end-to-end `docs.build` to `state == done` would require a fully-prepared Docusaurus site (config.ts that resolves all modules), which needs heroscript content via `docs.generate` first — circular for this verification. Combined unit-level proof of `derive_docs_output_path` (the only logic specific to this change) plus live integration proof via `docs.new` (same handler call site) covers the entire `output_path` code path. ### `info.version` Unchanged at `0.1.6` — single bump per rolling PR (#102 already did it).
Member

Implementation Summary

docs.build is now exposed over JSON-RPC, satisfying the issue requirement that docs.jobStatus returns output_path = <path>/build for completed build jobs. The user-supplied path is preserved by encoding it directly into the hero_proc action name with URL-safe base64 (no padding) instead of a one-way hash, so the decoder can recover it on status polls.

This is a slight pattern shift from #102 / #103: the install/update template family uses hashed action names and produces no output_path. docs.build uses path-encoded action names and produces a recoverable output_path.

Files changed (this iteration)

  • src/bin/hero_docs.rs — new Build(BuildArgs) subcommand wrapping hero_books_docusaurus::build::build(&path).
  • crates/hero_books_server/src/web/rpc.rs
    • handle_docs_build handler — encodes path with base64::engine::general_purpose::URL_SAFE_NO_PAD into action name docs_build_<encoded>. No separate input hash — the encoded path itself serves as the dedup key.
    • Dispatch arm "docs.build" between docs.updateTemplate and docs.jobStatus.
    • New helper derive_docs_output_path(action_name, cache_dir) — refactor that pulls the path-recovery logic out of handle_docs_job_status's closure into a pure function. Three buckets: docs_new_* / docs_generate_* -> <cache>/<hash>/build; docs_build_* -> <base64url-decoded path>/build; everything else -> None. The call site in handle_docs_job_status is now a one-liner.
    • ## Docs doc-header updated.
    • 4 new unit tests.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline schema gains a docs.build entry.
  • 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 (no-bugs pass)

  • 23/23 lib tests pass (4 new) — debug AND release mode.
  • cargo clippy -p hero_books_server --lib --no-deps — silent.
  • cargo clippy --bin hero_docs --no-deps — silent.
  • Spec/impl parity diff empty.
  • Live integration: docs.new returns output_path correctly via the same derive_docs_output_path helper that serves docs.build.
  • DB inspection confirmed action_id format (docs_build_<base64url> decodes to original path).

Notes

  • Action-name length: URL-safe base64 of a typical 100-200 byte path produces a 130-270 byte action name. Linux path limit is 4096 bytes; sqlite TEXT has no fixed limit. Pathological cases fit comfortably.
  • Decoder fails closed: malformed base64 or non-UTF-8 bytes silently return None rather than emitting a bogus output_path — locked by test_derive_docs_output_path_returns_none_for_malformed_docs_build.
  • docs.build pre-condition: the path must contain a fully-prepared Docusaurus site (template + heroscript-generated content). Same bun runtime requirement as #102/#103.
## Implementation Summary `docs.build` is now exposed over JSON-RPC, satisfying the issue requirement that `docs.jobStatus` returns `output_path = <path>/build` for completed build jobs. The user-supplied path is preserved by encoding it directly into the hero_proc action name with URL-safe base64 (no padding) instead of a one-way hash, so the decoder can recover it on status polls. This is a slight pattern shift from #102 / #103: the install/update template family uses hashed action names and produces no `output_path`. `docs.build` uses path-encoded action names and produces a recoverable `output_path`. ### Files changed (this iteration) - `src/bin/hero_docs.rs` — new `Build(BuildArgs)` subcommand wrapping `hero_books_docusaurus::build::build(&path)`. - `crates/hero_books_server/src/web/rpc.rs` - `handle_docs_build` handler — encodes path with `base64::engine::general_purpose::URL_SAFE_NO_PAD` into action name `docs_build_<encoded>`. No separate input hash — the encoded path itself serves as the dedup key. - Dispatch arm `"docs.build"` between `docs.updateTemplate` and `docs.jobStatus`. - **New helper** `derive_docs_output_path(action_name, cache_dir)` — refactor that pulls the path-recovery logic out of `handle_docs_job_status`'s closure into a pure function. Three buckets: `docs_new_*` / `docs_generate_*` -> `<cache>/<hash>/build`; `docs_build_*` -> `<base64url-decoded path>/build`; everything else -> `None`. The call site in `handle_docs_job_status` is now a one-liner. - `## Docs` doc-header updated. - 4 new unit tests. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline schema gains a `docs.build` entry. - `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 (no-bugs pass) - 23/23 lib tests pass (4 new) — debug AND release mode. - `cargo clippy -p hero_books_server --lib --no-deps` — silent. - `cargo clippy --bin hero_docs --no-deps` — silent. - Spec/impl parity diff empty. - Live integration: `docs.new` returns `output_path` correctly via the same `derive_docs_output_path` helper that serves `docs.build`. - DB inspection confirmed `action_id` format (`docs_build_<base64url>` decodes to original path). ### Notes - **Action-name length**: URL-safe base64 of a typical 100-200 byte path produces a 130-270 byte action name. Linux path limit is 4096 bytes; sqlite TEXT has no fixed limit. Pathological cases fit comfortably. - **Decoder fails closed**: malformed base64 or non-UTF-8 bytes silently return `None` rather than emitting a bogus `output_path` — locked by `test_derive_docs_output_path_returns_none_for_malformed_docs_build`. - **`docs.build` pre-condition**: the path must contain a fully-prepared Docusaurus site (template + heroscript-generated content). Same `bun` runtime requirement as #102/#103.
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#104
No description provided.