Add docs.publishDev RPC method #107

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

Parent: #101

What to add

Method: docs.publishDev
Params: { path: string, name: string }
Returns: { job_id: string }
Wraps: DocSite::publish_dev() at lib.rs:135 — rsyncs to dev destination

Implementation steps

  1. Verify hero_docs publish-dev subcommand exists
  2. Add handle_docs_publish_dev(id, params, config) -> RpcResponse
  3. Non-deterministic output — extend prefix list in handle_docs_job_status
  4. Add dispatch arm "docs.publishDev" 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 publish-dev subcommand exists
  • Handler shells out via hero_docs
  • Returns { job_id }
  • spec/impl parity check passes
Parent: #101 ## What to add **Method:** `docs.publishDev` **Params:** `{ path: string, name: string }` **Returns:** `{ job_id: string }` **Wraps:** `DocSite::publish_dev()` at `lib.rs:135` — rsyncs to dev destination ## Implementation steps 1. Verify `hero_docs publish-dev` subcommand exists 2. Add `handle_docs_publish_dev(id, params, config) -> RpcResponse` 3. Non-deterministic output — extend prefix list in `handle_docs_job_status` 4. Add dispatch arm `"docs.publishDev"` 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 publish-dev` subcommand exists - [x] Handler shells out via hero_docs - [x] Returns `{ job_id }` - [x] spec/impl parity check passes
rawdaGastan added this to the ACTIVE project 2026-04-26 14:08:43 +00:00
Member

Implementation Spec for Issue #107

Objective

Expose DocSite::publish_dev() over JSON-RPC as docs.publishDev. Unlike docs.publish (#106) which uses the default production rsync target, docs.publishDev uses the dev destinations defined per-site via the heroscript !!site.publish_dev directive (parsed by parse_publish(playbook, _, true) in crates/hero_books_docusaurus/src/heroscript.rs). This is the only place where dev destinations are actually defined in the codebase, so the handler must load DocSite from heroscript to honour them.

This means path semantics for docs.publishDev differ from #102-#106: it is a heroscript path/URL (matching docs.generate's convention), not a build-directory path. The intentional difference reflects that dev publish destinations are per-site configured (not a single global default), while production publish (#106) has a sensible global fallback.

Requirements

  • docs.publishDev is dispatchable from crates/hero_books_server/src/web/rpc.rs and reaches a new handler handle_docs_publish_dev.
  • Matching entry in openrpc.json and the inline rpc_spec.rs schema. Summary documents the heroscript-based path semantics, the name override, and the no-output_path policy.
  • info.version unchanged at 0.1.6 (one bump per rolling PR).
  • ## Docs doc-header gains docs.publishDev.
  • hero_docs (src/bin/hero_docs.rs) gains a new publish-dev subcommand: hero_docs publish-dev --path <heroscript_path_or_url> --name <name>. It loads DocSite::from_heroscript(&path), overrides docsite.name = name, and calls docsite.publish_dev(). The name override allows callers to pick a site name distinct from the heroscript's site.config.name (typical convention: append _dev).
  • Handler shells out via config.hero_docs_bin, never DocSite in-process.
  • Action name format: docs_publish_dev_<input_hash> where input_hash = calculate_docs_input_hash(&[&path, &name]). The path here is a heroscript path/URL; the hash treats it as opaque text.
  • derive_docs_output_path doc-comment updated to mention docs_publish_dev_* explicitly. No code change in the helper — unknown prefixes already return None (the desired behaviour).
  • Invalid-params: RpcResponse::invalid_params (-32602); internal failures: RpcResponse::error(id, -32000, ...). Both path and name required.
  • INSTRUCTIONS_OPENRPC.md §Verification diff produces empty output.

Files to Modify/Create

  • src/bin/hero_docs.rsPublishDev(PublishDevArgs) variant, args struct, runner that loads DocSite from heroscript and calls publish_dev().
  • crates/hero_books_server/src/web/rpc.rs
    • handle_docs_publish_dev handler (mirrors handle_docs_publish shape).
    • Dispatch arm.
    • Doc-header bullet.
    • derive_docs_output_path doc-comment update — docs_publish_dev_* explicit (no longer "upcoming").
    • New unit test test_docs_publish_dev_missing_params.
    • Existing test_derive_docs_output_path_returns_none_for_install_update_template extended with one more assert!(...is_none()) for docs_publish_dev_xxx.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline docs.publishDev entry.
  • crates/hero_books_server/openrpc.jsondocs.publishDev method entry.

No new files.

Implementation Plan

Step 1 — Add publish-dev subcommand to hero_docs

Files: src/bin/hero_docs.rs

  • Extend the Commands enum with PublishDev(PublishDevArgs). Clap will derive the kebab-case publish-dev from PublishDev.
  • Args struct:
    #[derive(Args)]
    struct PublishDevArgs {
        /// Heroscript path or URL describing the site (must include `!!site.publish_dev` directives for the destinations to be non-default)
        #[arg(long)]
        path: String,
        /// Site name override (used as the rsync destination's site_name)
        #[arg(long)]
        name: String,
    }
    
  • Runner:
    fn run_publish_dev(args: PublishDevArgs) -> Result<(), Box<dyn std::error::Error>> {
        let mut docsite = hero_books_docusaurus::DocSite::from_heroscript(&args.path)?;
        docsite.name = args.name.clone();
        log::info!(
            "Publishing site '{}' (heroscript: {}) to dev rsync destination(s)",
            docsite.name, args.path
        );
        docsite.publish_dev()?;
        log::info!("Published to dev.");
        Ok(())
    }
    
    Note: this calls DocSite::from_heroscript, NOT from_heroscript_full. We don't need the export directory for publish; from_heroscript is the cheaper variant that only parses the heroscript without scanning collections. The publish_dev() method calls build::publish(&self.path_build, &self.site.config.build_dest_dev, &self.name) — the dev destinations come from the heroscript's !!site.publish_dev directives. If the heroscript has none, build::publish falls back to a single default PublishDest using name as the site_name (same fallback as docs.publish).
  • Dispatch from main().

Dependencies: none.

Step 2 — Add handle_docs_publish_dev handler

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

  • Add the function below handle_docs_publish with signature fn handle_docs_publish_dev(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse.
  • Body mirrors handle_docs_publish exactly (validate path + name, hash both, format script, submit) with two changes:
    • Action name prefix: docs_publish_dev_ (instead of docs_publish_).
    • Subcommand: publish-dev (instead of publish).
    • The interpretation of path differs (heroscript vs build dir) — but the handler doesn't validate that distinction; it just passes the string through. The hero_docs runner does the heroscript loading.

Dependencies: Step 1 (subcommand at runtime).

Step 3 — Wire dispatch arm

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

  • Add immediately after docs.publish and before docs.jobStatus:
    "docs.publishDev" => handle_docs_publish_dev(request.id, request.params, config),
    

Dependencies: Step 2.

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

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

  • ## Docs doc-header: insert docs.publishDev after docs.publish:
    //! - `docs.publishDev` - Publish the built site to per-site heroscript-defined dev rsync destinations
    
  • Update derive_docs_output_path's bucket-3 comment so docs_publish_dev_* appears explicitly (no longer "upcoming"):
    ///    `docs_publish_*` (output is a remote rsync push, not a local
    ///    directory), `docs_publish_dev_*` (same — remote rsync push to
    ///    heroscript-defined dev destinations)) — `None`.
    

Dependencies: none.

Step 5 — Add docs.publishDev 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.publish and docs.jobStatus:
    {
      "name": "docs.publishDev",
      "summary": "Submit a hero_proc job that publishes the built Docusaurus site to its heroscript-defined dev rsync destinations. The `path` parameter is a heroscript path or URL (different from docs.publish, which takes a build directory). The handler loads the DocSite from heroscript, overrides the site name with the `name` parameter, and rsyncs to destinations configured via `!!site.publish_dev` directives. If the heroscript has no `!!site.publish_dev` directive, falls back to a single default destination using `name` as the site_name. Reads the rsync password from the RSYNCD_SECRET env var. Returns the hero_proc job id; poll via docs.jobStatus. docs.jobStatus does not surface output_path for this method.",
      "params": [
        { "name": "path", "schema": { "type": "string" }, "required": true },
        { "name": "name", "schema": { "type": "string" }, "required": true }
      ],
      "result": {
        "name": "jobRef",
        "schema": {
          "type": "object",
          "properties": { "job_id": { "type": "string" } }
        }
      }
    }
    
  • info.version unchanged.

Dependencies: Step 3.

Step 6 — Add unit test

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

  • test_docs_publish_dev_missing_params — same shape as test_docs_publish_missing_params. Five assertions covering missing/empty path, missing/empty name (with valid path), and no-params case.
  • Extend test_derive_docs_output_path_returns_none_for_install_update_template with one more line:
    assert!(derive_docs_output_path("docs_publish_dev_zzz", cache).is_none());
    

Dependencies: Step 2.

Step 7 — 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 -- publish-dev --help.
  • Live RPC:
    • rpc.discover lists docs.publishDev with two required params.
    • Validation: missing path and missing name each return -32602.
    • Submit returns {"job_id": "<n>"}.
    • Idempotent dedup with same (path, name) → same id.
    • Different name (or different path) → different id.
    • docs.jobStatus returns no output_path.
    • DB: action_id = docs_publish_dev_<hash>; timeout_ms = 600000.
    • The job will fail without a real heroscript at path — that's expected. We're verifying the RPC chain.

Dependencies: Steps 1-6.

Acceptance Criteria

  • hero_docs publish-dev --help prints usage with --path and --name (both required).
  • crates/hero_books_server/src/web/rpc.rs defines handle_docs_publish_dev.
  • docs.publishDev is wired into the dispatcher between docs.publish and docs.jobStatus.
  • Action name format is docs_publish_dev_<input_hash> where the hash includes both path and name.
  • docs.jobStatus returns no output_path for docs_publish_dev_* jobs (locked by extending the existing test).
  • ## Docs doc-header lists docs.publishDev.
  • crates/hero_books_server/openrpc.json and rpc_spec.rs inline schema both contain docs.publishDev.
  • info.version is unchanged at "0.1.6".
  • INSTRUCTIONS_OPENRPC.md §Verification diff empty.
  • cargo check, cargo clippy, cargo test all pass — including the new test and the extended prefix test.

Notes

  • Path semantics divergence (docs.publish vs docs.publishDev): the two methods take different kinds of path because the underlying behaviour is different. Production publish has a sensible global default rsync target, so build-dir + name is enough (#106). Dev publish destinations are per-site (defined via !!site.publish_dev in heroscript), so heroscript loading is necessary to honour them — there's no global "dev" default in the codebase. This divergence is documented in the OpenRPC summaries and in the ## Docs doc-header.
  • Why from_heroscript, not from_heroscript_full: we don't need to scan and export collections to publish. from_heroscript parses just enough of the heroscript to populate site.config (including build_dest_dev) and path_build. Cheaper, and avoids running collection scans that the publish operation doesn't need.
  • name override: DocSite::from_heroscript sets docsite.name = site.config.name (from !!site.config name:'...' in heroscript). The name RPC param overrides this so callers can publish under a different site name without editing the heroscript. Convention: append _dev to the production site name.
  • Empty build_dest_dev: if the heroscript has no !!site.publish_dev directives, docsite.publish_dev() calls build::publish with an empty destinations slice — which falls back to a single default PublishDest using docsite.name as the site_name (same fallback as production publish). This is the same code path as #106's &[] empty slice.
  • Output_path policy: a remote rsync push has no local "output_path" to surface. derive_docs_output_path returns None for docs_publish_dev_*.
  • info.version: unchanged at 0.1.6. This is the final child of the rolling PR, so the version stays put.
  • Pre-condition for live success: path must point to a valid heroscript file or URL; the resulting build path (docusaurus_config.path_build) must contain <path_build>/build/ from a prior docs.build (or docs.generate / docs.new); RSYNCD_SECRET must be set on the server. Without these, the job lands in state: failed with a clear error from either from_heroscript or build::publish.
  • Backwards compatibility: this PR is strictly additive at every API boundary. No existing methods/subcommands change.
## Implementation Spec for Issue #107 ### Objective Expose `DocSite::publish_dev()` over JSON-RPC as `docs.publishDev`. Unlike `docs.publish` (#106) which uses the default production rsync target, `docs.publishDev` uses the dev destinations defined per-site via the heroscript `!!site.publish_dev` directive (parsed by `parse_publish(playbook, _, true)` in `crates/hero_books_docusaurus/src/heroscript.rs`). This is the only place where dev destinations are actually defined in the codebase, so the handler must load DocSite from heroscript to honour them. This means `path` semantics for `docs.publishDev` differ from #102-#106: it is a heroscript path/URL (matching `docs.generate`'s convention), not a build-directory path. The intentional difference reflects that dev publish destinations are per-site configured (not a single global default), while production publish (#106) has a sensible global fallback. ### Requirements - `docs.publishDev` is dispatchable from `crates/hero_books_server/src/web/rpc.rs` and reaches a new handler `handle_docs_publish_dev`. - Matching entry in `openrpc.json` and the inline `rpc_spec.rs` schema. Summary documents the heroscript-based path semantics, the `name` override, and the no-`output_path` policy. - `info.version` unchanged at `0.1.6` (one bump per rolling PR). - `## Docs` doc-header gains `docs.publishDev`. - `hero_docs` (`src/bin/hero_docs.rs`) gains a new `publish-dev` subcommand: `hero_docs publish-dev --path <heroscript_path_or_url> --name <name>`. It loads `DocSite::from_heroscript(&path)`, overrides `docsite.name = name`, and calls `docsite.publish_dev()`. The `name` override allows callers to pick a site name distinct from the heroscript's site.config.name (typical convention: append `_dev`). - Handler shells out via `config.hero_docs_bin`, never `DocSite` in-process. - Action name format: `docs_publish_dev_<input_hash>` where `input_hash = calculate_docs_input_hash(&[&path, &name])`. The `path` here is a heroscript path/URL; the hash treats it as opaque text. - `derive_docs_output_path` doc-comment updated to mention `docs_publish_dev_*` explicitly. No code change in the helper — unknown prefixes already return `None` (the desired behaviour). - Invalid-params: `RpcResponse::invalid_params` (-32602); internal failures: `RpcResponse::error(id, -32000, ...)`. Both `path` and `name` required. - INSTRUCTIONS_OPENRPC.md §Verification diff produces empty output. ### Files to Modify/Create - `src/bin/hero_docs.rs` — `PublishDev(PublishDevArgs)` variant, args struct, runner that loads DocSite from heroscript and calls `publish_dev()`. - `crates/hero_books_server/src/web/rpc.rs` - `handle_docs_publish_dev` handler (mirrors `handle_docs_publish` shape). - Dispatch arm. - Doc-header bullet. - `derive_docs_output_path` doc-comment update — `docs_publish_dev_*` explicit (no longer "upcoming"). - New unit test `test_docs_publish_dev_missing_params`. - Existing `test_derive_docs_output_path_returns_none_for_install_update_template` extended with one more `assert!(...is_none())` for `docs_publish_dev_xxx`. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline `docs.publishDev` entry. - `crates/hero_books_server/openrpc.json` — `docs.publishDev` method entry. No new files. ### Implementation Plan #### Step 1 — Add `publish-dev` subcommand to `hero_docs` Files: `src/bin/hero_docs.rs` - Extend the `Commands` enum with `PublishDev(PublishDevArgs)`. Clap will derive the kebab-case `publish-dev` from `PublishDev`. - Args struct: ```rust #[derive(Args)] struct PublishDevArgs { /// Heroscript path or URL describing the site (must include `!!site.publish_dev` directives for the destinations to be non-default) #[arg(long)] path: String, /// Site name override (used as the rsync destination's site_name) #[arg(long)] name: String, } ``` - Runner: ```rust fn run_publish_dev(args: PublishDevArgs) -> Result<(), Box<dyn std::error::Error>> { let mut docsite = hero_books_docusaurus::DocSite::from_heroscript(&args.path)?; docsite.name = args.name.clone(); log::info!( "Publishing site '{}' (heroscript: {}) to dev rsync destination(s)", docsite.name, args.path ); docsite.publish_dev()?; log::info!("Published to dev."); Ok(()) } ``` Note: this calls `DocSite::from_heroscript`, NOT `from_heroscript_full`. We don't need the export directory for publish; `from_heroscript` is the cheaper variant that only parses the heroscript without scanning collections. The `publish_dev()` method calls `build::publish(&self.path_build, &self.site.config.build_dest_dev, &self.name)` — the dev destinations come from the heroscript's `!!site.publish_dev` directives. If the heroscript has none, `build::publish` falls back to a single default `PublishDest` using `name` as the site_name (same fallback as `docs.publish`). - Dispatch from `main()`. Dependencies: none. #### Step 2 — Add `handle_docs_publish_dev` handler Files: `crates/hero_books_server/src/web/rpc.rs` - Add the function below `handle_docs_publish` with signature `fn handle_docs_publish_dev(id: Option<Value>, params: Option<Value>, config: &ServerConfig) -> RpcResponse`. - Body mirrors `handle_docs_publish` exactly (validate `path` + `name`, hash both, format script, submit) with two changes: - Action name prefix: `docs_publish_dev_` (instead of `docs_publish_`). - Subcommand: `publish-dev` (instead of `publish`). - The interpretation of `path` differs (heroscript vs build dir) — but the handler doesn't validate that distinction; it just passes the string through. The hero_docs runner does the heroscript loading. Dependencies: Step 1 (subcommand at runtime). #### Step 3 — Wire dispatch arm Files: `crates/hero_books_server/src/web/rpc.rs` - Add immediately after `docs.publish` and before `docs.jobStatus`: ```rust "docs.publishDev" => handle_docs_publish_dev(request.id, request.params, config), ``` Dependencies: Step 2. #### Step 4 — Update doc-header and `derive_docs_output_path` doc-comment Files: `crates/hero_books_server/src/web/rpc.rs` - `## Docs` doc-header: insert `docs.publishDev` after `docs.publish`: ```rust //! - `docs.publishDev` - Publish the built site to per-site heroscript-defined dev rsync destinations ``` - Update `derive_docs_output_path`'s bucket-3 comment so `docs_publish_dev_*` appears explicitly (no longer "upcoming"): ``` /// `docs_publish_*` (output is a remote rsync push, not a local /// directory), `docs_publish_dev_*` (same — remote rsync push to /// heroscript-defined dev destinations)) — `None`. ``` Dependencies: none. #### Step 5 — Add `docs.publishDev` 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.publish` and `docs.jobStatus`: ```json { "name": "docs.publishDev", "summary": "Submit a hero_proc job that publishes the built Docusaurus site to its heroscript-defined dev rsync destinations. The `path` parameter is a heroscript path or URL (different from docs.publish, which takes a build directory). The handler loads the DocSite from heroscript, overrides the site name with the `name` parameter, and rsyncs to destinations configured via `!!site.publish_dev` directives. If the heroscript has no `!!site.publish_dev` directive, falls back to a single default destination using `name` as the site_name. Reads the rsync password from the RSYNCD_SECRET env var. Returns the hero_proc job id; poll via docs.jobStatus. docs.jobStatus does not surface output_path for this method.", "params": [ { "name": "path", "schema": { "type": "string" }, "required": true }, { "name": "name", "schema": { "type": "string" }, "required": true } ], "result": { "name": "jobRef", "schema": { "type": "object", "properties": { "job_id": { "type": "string" } } } } } ``` - `info.version` unchanged. Dependencies: Step 3. #### Step 6 — Add unit test Files: `crates/hero_books_server/src/web/rpc.rs` - `test_docs_publish_dev_missing_params` — same shape as `test_docs_publish_missing_params`. Five assertions covering missing/empty `path`, missing/empty `name` (with valid `path`), and no-`params` case. - Extend `test_derive_docs_output_path_returns_none_for_install_update_template` with one more line: ```rust assert!(derive_docs_output_path("docs_publish_dev_zzz", cache).is_none()); ``` Dependencies: Step 2. #### Step 7 — 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 -- publish-dev --help`. - Live RPC: - `rpc.discover` lists `docs.publishDev` with two required params. - Validation: missing `path` and missing `name` each return `-32602`. - Submit returns `{"job_id": "<n>"}`. - Idempotent dedup with same `(path, name)` → same id. - Different `name` (or different `path`) → different id. - `docs.jobStatus` returns no `output_path`. - DB: `action_id = docs_publish_dev_<hash>`; `timeout_ms = 600000`. - The job will fail without a real heroscript at `path` — that's expected. We're verifying the RPC chain. Dependencies: Steps 1-6. ### Acceptance Criteria - [ ] `hero_docs publish-dev --help` prints usage with `--path` and `--name` (both required). - [ ] `crates/hero_books_server/src/web/rpc.rs` defines `handle_docs_publish_dev`. - [ ] `docs.publishDev` is wired into the dispatcher between `docs.publish` and `docs.jobStatus`. - [ ] Action name format is `docs_publish_dev_<input_hash>` where the hash includes both `path` and `name`. - [ ] `docs.jobStatus` returns no `output_path` for `docs_publish_dev_*` jobs (locked by extending the existing test). - [ ] `## Docs` doc-header lists `docs.publishDev`. - [ ] `crates/hero_books_server/openrpc.json` and `rpc_spec.rs` inline schema both contain `docs.publishDev`. - [ ] `info.version` is unchanged at `"0.1.6"`. - [ ] INSTRUCTIONS_OPENRPC.md §Verification diff empty. - [ ] `cargo check`, `cargo clippy`, `cargo test` all pass — including the new test and the extended prefix test. ### Notes - **Path semantics divergence** (`docs.publish` vs `docs.publishDev`): the two methods take different kinds of `path` because the underlying behaviour is different. Production publish has a sensible global default rsync target, so build-dir + name is enough (#106). Dev publish destinations are per-site (defined via `!!site.publish_dev` in heroscript), so heroscript loading is necessary to honour them — there's no global "dev" default in the codebase. This divergence is documented in the OpenRPC summaries and in the `## Docs` doc-header. - **Why `from_heroscript`, not `from_heroscript_full`**: we don't need to scan and export collections to publish. `from_heroscript` parses just enough of the heroscript to populate `site.config` (including `build_dest_dev`) and `path_build`. Cheaper, and avoids running collection scans that the publish operation doesn't need. - **`name` override**: `DocSite::from_heroscript` sets `docsite.name = site.config.name` (from `!!site.config name:'...'` in heroscript). The `name` RPC param overrides this so callers can publish under a different site name without editing the heroscript. Convention: append `_dev` to the production site name. - **Empty `build_dest_dev`**: if the heroscript has no `!!site.publish_dev` directives, `docsite.publish_dev()` calls `build::publish` with an empty destinations slice — which falls back to a single default `PublishDest` using `docsite.name` as the site_name (same fallback as production publish). This is the same code path as #106's `&[]` empty slice. - **Output_path policy**: a remote rsync push has no local "output_path" to surface. `derive_docs_output_path` returns `None` for `docs_publish_dev_*`. - **`info.version`**: unchanged at `0.1.6`. This is the final child of the rolling PR, so the version stays put. - **Pre-condition for live success**: `path` must point to a valid heroscript file or URL; the resulting build path (`docusaurus_config.path_build`) must contain `<path_build>/build/` from a prior `docs.build` (or `docs.generate` / `docs.new`); `RSYNCD_SECRET` must be set on the server. Without these, the job lands in `state: failed` with a clear error from either `from_heroscript` or `build::publish`. - **Backwards compatibility**: this PR is strictly additive at every API boundary. No existing methods/subcommands change.
Member

Test Results

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

New test (1) and one existing test extended:

  • test_docs_publish_dev_missing_params — covers missing/empty path, missing/empty name (with valid path), and no-params case. Each returns -32602 with the appropriate field-specific message.
  • test_derive_docs_output_path_returns_none_for_install_update_template — extended with assert!(derive_docs_output_path("docs_publish_dev_zzz", cache).is_none()). Locks the no-output-path policy for the publish-dev family.

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
  • cargo clippy --bin hero_docs --no-deps — silent
  • cargo test --release — 26/26

Spec/impl parity

INSTRUCTIONS_OPENRPC.md §Verification diff is empty.

Build infrastructure note

The inline get_openrpc_schema block in crates/hero_books_server/src/web/rpc_spec.rs is one large json! macro expansion. Adding the docs.publishDev entry exceeded Rust's default macro recursion limit (128). Bumped the crate's recursion_limit to 256 in crates/hero_books_server/src/lib.rs with an inline comment explaining why. No runtime impact; compile-time only.

CLI smoke test

$ hero_docs publish-dev --help
Publish the built site to per-site heroscript-defined dev rsync destinations

Usage: hero_docs publish-dev --path <PATH> --name <NAME>

Options:
      --path <PATH>  Heroscript path or URL describing the site (must include `!!site.publish_dev` directives for the destinations to be non-default)
      --name <NAME>  Site name override (used as the rsync destination's site_name)
  -h, --help         Print help

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

Step Result
rpc.discover lists docs.publishDev with path (required) and name (required) pass
Validation: missing path returns -32602 "missing or empty 'path' parameter" pass
Validation: missing name returns -32602 "missing or empty 'name' parameter" pass
Submit returns {"job_id":"115"} for (path=/tmp/x.heroscript, name=my_site_dev) pass
Re-submit with same (path, name) dedupes to job_id: "115" pass
Different name produces a different job_id: "116" pass
docs.jobStatus for the failed job: state: failed, error tail "HeroScript directory not found: /tmp/x.heroscript", and no output_path field pass
DB: action_id = docs_publish_dev_<hash>, timeout_ms = 600000 (default 10-min cap) pass

The error from from_heroscript() propagated correctly through hero_docs publish-dev -> hero_proc -> docs.jobStatus, proving the heroscript-loading path is wired correctly. With a real heroscript file at path, the handler would proceed to override the site name and call docsite.publish_dev(), rsyncing to build_dest_dev destinations.

Live discovery: all 9 docs.* methods exposed

docs.build, docs.dev, docs.generate, docs.installTemplate, docs.jobStatus, docs.new, docs.publish, docs.publishDev, docs.updateTemplate — 3 originals + 6 from the rolling PR (#102-#107).

Backwards compatibility

Public RPC surface and hero_docs CLI: strictly additive. Existing methods/subcommands unchanged. The new recursion_limit attribute is a build-time setting only — no runtime effect.

info.version

Unchanged at 0.1.6 — single bump per rolling PR.

## Test Results **Suite:** `cargo test -p hero_books_server --lib` **Total:** 26 — Passed: 26 — Failed: 0 New test (1) and one existing test extended: - `test_docs_publish_dev_missing_params` — covers missing/empty `path`, missing/empty `name` (with valid `path`), and no-`params` case. Each returns `-32602` with the appropriate field-specific message. - `test_derive_docs_output_path_returns_none_for_install_update_template` — extended with `assert!(derive_docs_output_path("docs_publish_dev_zzz", cache).is_none())`. Locks the no-output-path policy for the publish-dev family. ### 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 - `cargo clippy --bin hero_docs --no-deps` — silent - `cargo test --release` — 26/26 ### Spec/impl parity INSTRUCTIONS_OPENRPC.md §Verification diff is empty. ### Build infrastructure note The inline `get_openrpc_schema` block in `crates/hero_books_server/src/web/rpc_spec.rs` is one large `json!` macro expansion. Adding the `docs.publishDev` entry exceeded Rust's default macro recursion limit (128). Bumped the crate's `recursion_limit` to `256` in `crates/hero_books_server/src/lib.rs` with an inline comment explaining why. No runtime impact; compile-time only. ### CLI smoke test ``` $ hero_docs publish-dev --help Publish the built site to per-site heroscript-defined dev rsync destinations Usage: hero_docs publish-dev --path <PATH> --name <NAME> Options: --path <PATH> Heroscript path or URL describing the site (must include `!!site.publish_dev` directives for the destinations to be non-default) --name <NAME> Site name override (used as the rsync destination's site_name) -h, --help Print help ``` ### Live end-to-end RPC test (against running hero_books_server + hero_proc) | Step | Result | | --- | --- | | `rpc.discover` lists `docs.publishDev` with `path` (required) and `name` (required) | pass | | Validation: missing `path` returns `-32602 "missing or empty 'path' parameter"` | pass | | Validation: missing `name` returns `-32602 "missing or empty 'name' parameter"` | pass | | Submit returns `{"job_id":"115"}` for `(path=/tmp/x.heroscript, name=my_site_dev)` | pass | | Re-submit with same `(path, name)` dedupes to `job_id: "115"` | pass | | Different `name` produces a different `job_id: "116"` | pass | | `docs.jobStatus` for the failed job: `state: failed`, error tail `"HeroScript directory not found: /tmp/x.heroscript"`, **and no `output_path` field** | pass | | DB: `action_id = docs_publish_dev_<hash>`, `timeout_ms = 600000` (default 10-min cap) | pass | The error from `from_heroscript()` propagated correctly through `hero_docs publish-dev` -> `hero_proc` -> `docs.jobStatus`, proving the heroscript-loading path is wired correctly. With a real heroscript file at `path`, the handler would proceed to override the site name and call `docsite.publish_dev()`, rsyncing to `build_dest_dev` destinations. ### Live discovery: all 9 `docs.*` methods exposed `docs.build`, `docs.dev`, `docs.generate`, `docs.installTemplate`, `docs.jobStatus`, `docs.new`, `docs.publish`, `docs.publishDev`, `docs.updateTemplate` — 3 originals + 6 from the rolling PR (#102-#107). ### Backwards compatibility Public RPC surface and `hero_docs` CLI: strictly additive. Existing methods/subcommands unchanged. The new `recursion_limit` attribute is a build-time setting only — no runtime effect. ### `info.version` Unchanged at `0.1.6` — single bump per rolling PR.
Member

Implementation Summary

docs.publishDev is now exposed over JSON-RPC. Unlike docs.publish (#106) which uses the default production rsync target with a build-directory path, docs.publishDev takes a heroscript path/URL and uses the dev destinations defined in heroscript via the !!site.publish_dev directive (parsed by parse_publish(playbook, _, true) in crates/hero_books_docusaurus/src/heroscript.rs).

This is the first child issue with a different path semantic from #102-#106 — and it's necessary because dev publish destinations are per-site configured (no global default exists in the codebase), while production publish has a sensible global fallback.

Files changed (this iteration)

  • src/bin/hero_docs.rs — new PublishDev(PublishDevArgs) subcommand. Runner loads DocSite::from_heroscript(&path), overrides docsite.name, and calls docsite.publish_dev().
  • crates/hero_books_server/src/web/rpc.rs
    • handle_docs_publish_dev handler — same shape as handle_docs_publish, just different action prefix and subcommand name. The path validation is identical at the RPC layer (non-empty string); the heroscript-vs-build-dir distinction is enforced by the runner inside hero_docs.
    • Dispatch arm "docs.publishDev" between docs.publish and docs.jobStatus.
    • ## Docs doc-header lists docs.publishDev.
    • derive_docs_output_path doc-comment finalised — all 9 docs.* action prefixes are now explicit (no more "upcoming" placeholders).
    • New unit test test_docs_publish_dev_missing_params (5 assertions).
    • Existing prefix-recognition test extended with docs_publish_dev_zzz.
  • crates/hero_books_server/src/web/rpc_spec.rs — inline schema gains a docs.publishDev 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.
  • crates/hero_books_server/src/lib.rs — bumped recursion_limit to 256. The inline get_openrpc_schema json! block hit Rust's default 128-recursion limit once docs.publishDev was added. Build-time only; no runtime effect. Inline comment explains why.

Tests

  • 26/26 lib tests pass (debug + release) — 1 new + 1 extended.
  • cargo clippy silent on both crates.
  • Spec/impl parity diff empty.
  • Live RPC test: discovery, both validation paths, submit, idempotent dedup, name-driven distinction, hero_proc dispatch, error-tail surfacing through docs.jobStatus. Job correctly failed with "HeroScript directory not found" when given a non-existent path — proving the heroscript-loading code path is invoked.

Backwards compatibility

  • Public RPC API: only adds docs.publishDev. All existing methods unchanged.
  • hero_docs CLI: only adds publish-dev subcommand. Existing subcommands unchanged.
  • info.version: unchanged at 0.1.6.
  • recursion_limit bump: build-time attribute only; doesn't affect any runtime semantics.

Notes

  • Path semantics divergence (docs.publish vs docs.publishDev): production publish has a global default rsync target so build-dir + name is sufficient. Dev publish destinations are per-site (defined via !!site.publish_dev in heroscript), so heroscript loading is necessary. This divergence is documented in the OpenRPC summaries and the ## Docs doc-header.
  • from_heroscript, not from_heroscript_full: publishing doesn't need collection scanning. from_heroscript is the cheaper variant that only parses heroscript to populate site.config (including build_dest_dev) and path_build.
  • name override: lets callers pick a site_name distinct from site.config.name without editing heroscript. Convention: append _dev (e.g., my_docs -> my_docs_dev).
  • Empty build_dest_dev: if the heroscript has no !!site.publish_dev directives, docsite.publish_dev() falls back to a single default PublishDest (production rsync target) with docsite.name as the site_name. This is the same fallback as docs.publish.
  • Pre-conditions for live success: heroscript at path must exist and parse, <path_build>/build/ must exist (from a prior docs.build or similar), RSYNCD_SECRET must be set on the server.

Rolling PR status

This commit closes the rolling PR for #101: 6 of 6 children done.

  • #102docs.installTemplate
  • #103docs.updateTemplate
  • #104docs.build (with output_path recovery via base64url-encoded action names)
  • #105docs.dev (long-running, no timeout, cancel via job.cancel JSON-RPC)
  • #106docs.publish
  • #107docs.publishDev

PR is mergeable; all 9 docs.* methods discoverable via live rpc.discover.

## Implementation Summary `docs.publishDev` is now exposed over JSON-RPC. Unlike `docs.publish` (#106) which uses the default production rsync target with a build-directory path, `docs.publishDev` takes a **heroscript path/URL** and uses the dev destinations defined in heroscript via the `!!site.publish_dev` directive (parsed by `parse_publish(playbook, _, true)` in `crates/hero_books_docusaurus/src/heroscript.rs`). This is the first child issue with a different `path` semantic from #102-#106 — and it's necessary because dev publish destinations are per-site configured (no global default exists in the codebase), while production publish has a sensible global fallback. ### Files changed (this iteration) - `src/bin/hero_docs.rs` — new `PublishDev(PublishDevArgs)` subcommand. Runner loads `DocSite::from_heroscript(&path)`, overrides `docsite.name`, and calls `docsite.publish_dev()`. - `crates/hero_books_server/src/web/rpc.rs` - `handle_docs_publish_dev` handler — same shape as `handle_docs_publish`, just different action prefix and subcommand name. The `path` validation is identical at the RPC layer (non-empty string); the heroscript-vs-build-dir distinction is enforced by the runner inside `hero_docs`. - Dispatch arm `"docs.publishDev"` between `docs.publish` and `docs.jobStatus`. - `## Docs` doc-header lists `docs.publishDev`. - `derive_docs_output_path` doc-comment finalised — all 9 `docs.*` action prefixes are now explicit (no more "upcoming" placeholders). - New unit test `test_docs_publish_dev_missing_params` (5 assertions). - Existing prefix-recognition test extended with `docs_publish_dev_zzz`. - `crates/hero_books_server/src/web/rpc_spec.rs` — inline schema gains a `docs.publishDev` 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. - `crates/hero_books_server/src/lib.rs` — bumped `recursion_limit` to `256`. The inline `get_openrpc_schema` `json!` block hit Rust's default 128-recursion limit once `docs.publishDev` was added. Build-time only; no runtime effect. Inline comment explains why. ### Tests - 26/26 lib tests pass (debug + release) — 1 new + 1 extended. - `cargo clippy` silent on both crates. - Spec/impl parity diff empty. - Live RPC test: discovery, both validation paths, submit, idempotent dedup, name-driven distinction, hero_proc dispatch, error-tail surfacing through `docs.jobStatus`. Job correctly failed with `"HeroScript directory not found"` when given a non-existent path — proving the heroscript-loading code path is invoked. ### Backwards compatibility - Public RPC API: only adds `docs.publishDev`. All existing methods unchanged. - `hero_docs` CLI: only adds `publish-dev` subcommand. Existing subcommands unchanged. - `info.version`: unchanged at `0.1.6`. - `recursion_limit` bump: build-time attribute only; doesn't affect any runtime semantics. ### Notes - **Path semantics divergence** (`docs.publish` vs `docs.publishDev`): production publish has a global default rsync target so build-dir + name is sufficient. Dev publish destinations are per-site (defined via `!!site.publish_dev` in heroscript), so heroscript loading is necessary. This divergence is documented in the OpenRPC summaries and the `## Docs` doc-header. - **`from_heroscript`, not `from_heroscript_full`**: publishing doesn't need collection scanning. `from_heroscript` is the cheaper variant that only parses heroscript to populate `site.config` (including `build_dest_dev`) and `path_build`. - **`name` override**: lets callers pick a site_name distinct from `site.config.name` without editing heroscript. Convention: append `_dev` (e.g., `my_docs` -> `my_docs_dev`). - **Empty `build_dest_dev`**: if the heroscript has no `!!site.publish_dev` directives, `docsite.publish_dev()` falls back to a single default `PublishDest` (production rsync target) with `docsite.name` as the site_name. This is the same fallback as `docs.publish`. - **Pre-conditions for live success**: heroscript at `path` must exist and parse, `<path_build>/build/` must exist (from a prior `docs.build` or similar), `RSYNCD_SECRET` must be set on the server. ### Rolling PR status This commit closes the rolling PR for #101: 6 of 6 children done. - #102 — `docs.installTemplate` - #103 — `docs.updateTemplate` - #104 — `docs.build` (with `output_path` recovery via base64url-encoded action names) - #105 — `docs.dev` (long-running, no timeout, cancel via `job.cancel` JSON-RPC) - #106 — `docs.publish` - #107 — `docs.publishDev` PR is mergeable; all 9 `docs.*` methods discoverable via live `rpc.discover`.
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#107
No description provided.