diff --git a/.agents/skills/docs-driven-development/SKILL.md b/.agents/skills/docs-driven-development/SKILL.md
index f6af715..5f80268 100644
--- a/.agents/skills/docs-driven-development/SKILL.md
+++ b/.agents/skills/docs-driven-development/SKILL.md
@@ -153,8 +153,10 @@ it does not establish cross-renderer pixel equivalence. Use `just test`
for the full native suite and `just test-metal` for focused capture checks.
For skill or hand-authored contributor-doc changes alone, validate instructions
and links; unchanged app/media output does not require regeneration.
-For portable framework changes, `just doc-framework-test` provides focused SDK
-and consumer checks; N3 adapter changes still require native replay and `just verify`.
+Portable framework changes belong in the
+[lgtmd repository](https://github.com/lgtmd/lgtmd) and use its SDK and
+consumer gates. N3 adapter or dependency revision changes require native replay
+and `just verify` here.
## Evolve the harness when the user story needs it
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4d567be..3c10599 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,6 +34,8 @@ jobs:
path: |
.cache/ci/linux-amd64/cargo/registry/cache
.cache/ci/linux-amd64/cargo/registry/index
+ .cache/ci/linux-amd64/cargo/git/db
+ .cache/ci/linux-amd64/cargo/git/checkouts
.cache/ci/linux-amd64/target/debug/deps
.cache/ci/linux-amd64/target/debug/build
.cache/ci/linux-amd64/target/debug/.fingerprint
@@ -51,6 +53,8 @@ jobs:
path: |
.cache/ci/linux-amd64/cargo/registry/cache
.cache/ci/linux-amd64/cargo/registry/index
+ .cache/ci/linux-amd64/cargo/git/db
+ .cache/ci/linux-amd64/cargo/git/checkouts
.cache/ci/linux-amd64/target/debug/deps
.cache/ci/linux-amd64/target/debug/build
.cache/ci/linux-amd64/target/debug/.fingerprint
@@ -90,6 +94,8 @@ jobs:
path: |
.cache/ci/linux-amd64/cargo/registry/cache
.cache/ci/linux-amd64/cargo/registry/index
+ .cache/ci/linux-amd64/cargo/git/db
+ .cache/ci/linux-amd64/cargo/git/checkouts
.cache/ci/linux-amd64/target/debug/deps
.cache/ci/linux-amd64/target/debug/build
.cache/ci/linux-amd64/target/debug/.fingerprint
@@ -139,6 +145,8 @@ jobs:
path: |
.cache/cargo/registry/cache
.cache/cargo/registry/index
+ .cache/cargo/git/db
+ .cache/cargo/git/checkouts
target/debug/deps
target/debug/build
target/debug/.fingerprint
@@ -164,6 +172,8 @@ jobs:
path: |
.cache/cargo/registry/cache
.cache/cargo/registry/index
+ .cache/cargo/git/db
+ .cache/cargo/git/checkouts
target/debug/deps
target/debug/build
target/debug/.fingerprint
@@ -208,6 +218,8 @@ jobs:
path: |
.cache/cargo/registry/cache
.cache/cargo/registry/index
+ .cache/cargo/git/db
+ .cache/cargo/git/checkouts
target/debug/deps
target/debug/build
target/debug/.fingerprint
@@ -259,6 +271,8 @@ jobs:
path: |
.cache/cargo/registry/cache
.cache/cargo/registry/index
+ .cache/cargo/git/db
+ .cache/cargo/git/checkouts
target/debug/deps
target/debug/build
target/debug/.fingerprint
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
index f3f81eb..c9935d6 100644
--- a/.oxfmtrc.json
+++ b/.oxfmtrc.json
@@ -6,8 +6,6 @@
"/TODO.md",
"/Cargo.lock",
"/docs/guide/**",
- "/crates/doc-example-config/baseline/**",
- "/crates/doc-harness/docs/guide/**",
"/docs/preview/vendor/**",
"/vendor/**",
"/examples/**",
diff --git a/AGENTS.md b/AGENTS.md
index e872502..e974640 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -69,12 +69,14 @@ boundaries. The current Editor still depends on egui and is not a public kernel
## Code organization
-Keep N3 as one application crate. The workspace also maintains the portable
-`executable-docs` SDK in `crates/doc-harness` and its consumer examples in
-`crates/doc-example-config`; N3's documentation adapter is a real SDK consumer.
-This extraction is justified by a tested boundary between application replay and
-document composition/publication. It does not make the editor a public kernel.
-Prefer focused modules over further speculative crates or APIs. See
+Keep N3 as one application crate. Its documentation adapter consumes the portable
+[lgtmd SDK](https://github.com/lgtmd/lgtmd), maintained in its own repository.
+N3 pins an exact Git revision until a registry release is ready. The tested
+boundary separates application replay from document composition and publication;
+it does not make the editor a public kernel. Keep N3's replay, renderer, scenarios,
+fixtures, and guide outputs here. SDK authoring, compiler/store changes, portable
+Rust/Node examples, and SDK verification belong in lgtmd. Prefer focused modules
+over further speculative crates or APIs. See
[executable documents](docs/architecture/executable-documents.md) for this boundary.
Read [platform boundaries and performance](docs/architecture/platform-boundaries.md)
@@ -83,21 +85,19 @@ semantics, keep target selection at module boundaries, and preserve each host's
execution policy. Browser constraints must not lower the primary native target's
capabilities, performance policy, or verification baseline.
-| Location | Responsibility |
-| ---------------------------- | ----------------------------------------------------------------------------- |
-| `src/model/` | Authored documents, units, native text codec, geometry evaluation |
-| `src/asset_io/` | Format adapters, resource resolution, native document persistence |
-| `src/scene/` | Internal imported scene model, validation and pose evaluation |
-| `src/scene_view.rs` | Transient asset inspection and playback; separate from edit history |
-| `src/editor/` | Selection, edits, sessions, history, transform policies |
-| `src/input/` | Key mapping, semantic actions, pointer and navigation routing |
-| `src/render/` | Camera, wgpu rendering, derived visual feedback |
-| `src/ui/` | Panels, controls, gizmo, rulers |
-| `src/native.rs` | Window lifecycle, native events, dialogs, filesystem effects |
-| `src/settings/` | Typed global preferences, validation, merge policy, storage interface |
-| `src/documentation/` | Headless replay, virtual input, annotations, capture, scenarios |
-| `crates/doc-harness/` | Portable document composition, evidence, audience exports, artifact lifecycle |
-| `crates/doc-example-config/` | Standalone CLI and library consumers of the documentation SDK |
+| Location | Responsibility |
+| -------------------- | --------------------------------------------------------------------- |
+| `src/model/` | Authored documents, units, native text codec, geometry evaluation |
+| `src/asset_io/` | Format adapters, resource resolution, native document persistence |
+| `src/scene/` | Internal imported scene model, validation and pose evaluation |
+| `src/scene_view.rs` | Transient asset inspection and playback; separate from edit history |
+| `src/editor/` | Selection, edits, sessions, history, transform policies |
+| `src/input/` | Key mapping, semantic actions, pointer and navigation routing |
+| `src/render/` | Camera, wgpu rendering, derived visual feedback |
+| `src/ui/` | Panels, controls, gizmo, rulers |
+| `src/native.rs` | Window lifecycle, native events, dialogs, filesystem effects |
+| `src/settings/` | Typed global preferences, validation, merge policy, storage interface |
+| `src/documentation/` | Headless replay, virtual input, annotations, capture, scenarios |
Keep code, identifiers, comments, tests, logs, and authored UI strings in English.
Explain non-obvious behavior and platform limitations near the owning code.
@@ -206,6 +206,12 @@ For changes limited to hand-authored documentation such as this file or the
README, check links and factual claims; unchanged application/media output does
not require rebuilding the renderer or regenerating the guide.
+Verify SDK or web adapter changes in the
+[lgtmd repository](https://github.com/lgtmd/lgtmd). An N3 dependency
+revision update still requires native `just verify`, including real replay and
+exact guide checks. Keep dependency updates explicit; do not add local path
+fallbacks or discover a sibling SDK checkout automatically.
+
Use the root `justfile` for development commands (`just` 1.46 is the verified
version). Useful commands from the repository root:
diff --git a/Cargo.lock b/Cargo.lock
index cc91d0d..d295f8f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -553,16 +553,6 @@ dependencies = [
"libloading",
]
-[[package]]
-name = "doc-example-config"
-version = "0.0.0"
-dependencies = [
- "executable-docs",
- "serde",
- "serde_json",
- "tempfile",
-]
-
[[package]]
name = "document-features"
version = "0.2.12"
@@ -720,7 +710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -738,18 +728,6 @@ dependencies = [
"num-traits",
]
-[[package]]
-name = "executable-docs"
-version = "0.0.0"
-dependencies = [
- "pulldown-cmark",
- "same-file",
- "serde",
- "serde_json",
- "sha2",
- "tempfile",
-]
-
[[package]]
name = "fastrand"
version = "2.5.0"
@@ -1375,6 +1353,18 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+[[package]]
+name = "lgtmd"
+version = "0.0.0"
+source = "git+https://github.com/lgtmd/lgtmd?rev=8828c41eaf7fbb002044aea9f8bfd4a06ba76168#8828c41eaf7fbb002044aea9f8bfd4a06ba76168"
+dependencies = [
+ "pulldown-cmark",
+ "same-file",
+ "serde",
+ "serde_json",
+ "sha2",
+]
+
[[package]]
name = "libc"
version = "0.2.189"
@@ -1495,7 +1485,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -1520,11 +1510,11 @@ dependencies = [
"egui",
"egui-wgpu",
"egui-winit",
- "executable-docs",
"glam",
"gltf",
"image",
"js-sys",
+ "lgtmd",
"libc",
"percent-encoding",
"pollster 1.0.1",
@@ -2399,7 +2389,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -2730,19 +2720,6 @@ dependencies = [
"syn 3.0.6",
]
-[[package]]
-name = "tempfile"
-version = "3.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
-dependencies = [
- "fastrand",
- "getrandom 0.4.3",
- "once_cell",
- "rustix 1.1.5",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -3469,7 +3446,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 682681d..093f62e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,11 +5,6 @@ edition = "2024"
rust-version = "1.95"
publish = false
-[workspace]
-members = ["crates/doc-harness", "crates/doc-example-config"]
-default-members = ["."]
-resolver = "3"
-
[features]
# Contributor-only instrumentation; normal builds contain no clocks or recorder.
viewport-measure = []
@@ -53,7 +48,7 @@ wgpu = "30.0.1"
winit = "=0.30.13"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-executable-docs = { path = "crates/doc-harness" }
+lgtmd = { git = "https://github.com/lgtmd/lgtmd", rev = "8828c41eaf7fbb002044aea9f8bfd4a06ba76168" }
alacritty_terminal = "=0.26.0"
egui-winit = { version = "=0.36.2", default-features = false, features = [
"clipboard",
diff --git a/TODO.md b/TODO.md
index ac259f5..7bf4c25 100644
--- a/TODO.md
+++ b/TODO.md
@@ -18,7 +18,7 @@ and document the implemented behavior in the executable user guide.
| Paired selection in 2D | **Deferred:** consider verified front/back vertex pairing in aligned views; exact geometry does not prove selection intent. See [RFC 0001](rfcs/0001-paired-selection-in-2d.md). |
| Grouping and hierarchy entities | Add an explicit document entity for organizing objects; evaluate groups, collections, or folders, including nesting, selection, and shared transform semantics. |
| Multiple authored scenes | **Draft; implementation deferred.** Independent N3 scenes in one document/editor. Revisit [RFC 0002](rfcs/0002-authored-scenes.md), starting with a small native-scene proof of switching, history/recovery, and persistence before broader import work. |
-| Reusable executable documentation framework | **Local implementation complete:** unpublished SDK, shared N3 lifecycle, Rust-authored hand-tool/navigation guides, and CLI/serde_json consumers. Next: independent developer adoption and authoring ergonomics; 26 N3 narratives still use templates. See [RFC 0003](rfcs/0003-executable-documentation-framework.md). |
+| Executable guide authoring | lgtmd has moved to its [own repository](https://github.com/lgtmd/lgtmd); N3 consumes an exact Git revision. Migrate the remaining 26 N3 templates through the existing adapter when scheduled, and switch to crates.io after an explicitly authorized SDK release. See [the boundary](docs/architecture/executable-documents.md) and historical [RFC 0004](rfcs/0004-lgtmd-web-incubation.md). |
| 3D format I/O | Build on read-only glTF/GLB viewing: scene export/authoring, OBJ export, compression and advanced extensions; evaluate OpenUSD composition/schema support. See the [viewer milestone](docs/milestones/modern-scene-viewer.md). |
| Materials and PBR | Extend imported PBR viewing into editable material assignment; HDR environment import, shadows, advanced materials and color management. See the [viewer milestone](docs/milestones/modern-scene-viewer.md). |
| Node Canvas UI Kit | Establish reusable UI primitives for node blocks, ports, connections, selection, dragging, pan, and zoom. Validate interactions in isolated workbench examples so future editors can adopt them confidently when a concrete consumer exists. Scope this as UI foundation work; shader/modifier semantics and evaluation remain separate. |
diff --git a/crates/doc-example-config/.gitignore b/crates/doc-example-config/.gitignore
deleted file mode 100644
index ace0ac9..0000000
--- a/crates/doc-example-config/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Disposable Reader/Contributor review candidates.
-/.cache/
-/target/
diff --git a/crates/doc-example-config/Cargo.toml b/crates/doc-example-config/Cargo.toml
deleted file mode 100644
index 586e57a..0000000
--- a/crates/doc-example-config/Cargo.toml
+++ /dev/null
@@ -1,24 +0,0 @@
-[package]
-name = "doc-example-config"
-version = "0.0.0"
-edition = "2024"
-rust-version = "1.95"
-publish = false
-license = "MIT"
-description = "Executable documentation consumers for a configuration CLI and serde_json"
-readme = "README.md"
-
-[lib]
-name = "config_demo"
-
-[[bin]]
-name = "config-demo"
-path = "src/main.rs"
-
-[dependencies]
-serde = { version = "=1.0.229", features = ["derive"] }
-serde_json = "=1.0.151"
-
-[dev-dependencies]
-executable-docs = { path = "../doc-harness" }
-tempfile = "=3.27.0"
diff --git a/crates/doc-example-config/LICENSE b/crates/doc-example-config/LICENSE
deleted file mode 100644
index 943658a..0000000
--- a/crates/doc-example-config/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2026 Grida
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/crates/doc-example-config/README.md b/crates/doc-example-config/README.md
deleted file mode 100644
index 1f7e4aa..0000000
--- a/crates/doc-example-config/README.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# Executable documentation consumers
-
-This package exercises the documentation SDK as a development dependency through
-two concrete consumers:
-
-- A small configuration library and its real `config-demo` executable. Its guide
- demonstrates saving valid input, rejecting an invalid replacement without
- changing the output, and recovering after correction.
-- The independently existing `serde_json` library. Its guide calls public APIs
- to parse a typed list, inspect a type mismatch, correct the input, and serialize
- the recovered values.
-
-The second consumer is our integration of an existing library. It does not
-represent adoption or approval by the `serde_json` maintainers, and it changes no
-upstream repository.
-
-Both guides are ordinary Rust programs containing multiline Markdown, typed
-checked values, assertions, and captured resources. Each execution produces one
-frozen document that can render into reader and contributor views. The contributor
-view adds notes and private evidence to the same authored narrative.
-
-## Run the examples
-
-Use Rust 1.95 or later; the portable standalone gate verifies Rust 1.95.0. Keep
-this package next to the SDK so the relative development dependency resolves:
-
-```text
-crates/
- doc-harness/
- doc-example-config/
-```
-
-No application renderer, graphics dependencies, service, or provider account is
-required. From the `doc-example-config` directory:
-
-```sh
-cargo build --bin config-demo
-cargo run --example docs -- check --audience both
-cargo run --example serde_json -- check --audience both
-cargo test --all-targets
-cargo clippy --all-targets -- -D warnings
-```
-
-The config guide finds `config-demo` beside the built example. For a separately
-built executable, set `CONFIG_DEMO_BIN` to its path. Fixture files and subprocess
-working directories are isolated temporary directories. The serde_json guide
-needs no subprocess or fixture directory.
-The configuration guide resolves the supplied CLI path before creating fixtures
-or changing the subprocess directory. A missing executable fails with a useful
-error even on emulated hosts that otherwise report only a child exit status.
-
-When a Cargo workspace supplies the lockfile, add `--locked` to these Cargo
-commands. After dependencies are cached, `--offline` can verify both consumers
-without network access.
-
-For the combined SDK and consumer development gate, run
-`python3 ../doc-harness/dev/verify.py`. Its standalone mode copies both packages
-and these retained baselines into a temporary workspace, independently resolving
-only their dependencies. See the SDK's
-[development instructions](../doc-harness/README.md#develop-and-verify-the-portable-packages)
-for the minimum-toolchain check, caches, and formatting scope.
-
-## Review intentional documentation changes
-
-Generate a candidate into a new output directory, read both views and their
-resources, then update the retained baseline only when the changes are intended:
-
-```sh
-cargo run --example docs -- build --out .cache/config-review --audience both
-cargo run --example serde_json -- build --out .cache/json-review --audience both
-
-cargo run --example docs -- update --audience both
-cargo run --example serde_json -- update --audience both
-
-cargo run --example docs -- check --audience both
-cargo run --example serde_json -- check --audience both
-```
-
-Choose a new candidate directory for each review; `build` preserves an existing
-candidate by refusing to overwrite it. `check` is read-only. Each guide has its
-own package-local baseline:
-
-| Guide | Authored source | Baseline |
-| ----------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
-| Configuration CLI | [Runner](examples/docs.rs) and [complete narrative and helpers](examples/support/mod.rs) | [Reader](baseline/config/reader/configure-a-port.md), [contributor](baseline/config/contributor/configure-a-port.md) |
-| Typed JSON list | [Single example source](examples/serde_json.rs) | [Reader](baseline/serde-json/reader/parse-a-typed-list.md), [contributor](baseline/serde-json/contributor/parse-a-typed-list.md) |
-
-The configuration baseline was carried forward from the reviewed implementation
-without changing its document or resource bytes. The tests check both retained
-views for both consumers, repeat generation for determinism, and ensure private
-contributor evidence stays out of the reader output.
-
-## Authoring cost and boundaries
-
-Measured after Rust formatting, including blank lines and comments:
-
-| Consumer | Complete executable authoring | Tests kept in the example |
-| ----------------- | ----------------------------------------------------------------------------------: | -----------------------------: |
-| Configuration CLI | 241 lines: 19 runner + 222 narrative, fixture, subprocess, and capture helper lines | 0; separate integration tests |
-| serde_json | 147 lines: runner, narrative, real API calls, checks, and capture helpers | 32 additional lines; 179 total |
-
-There are no separate Markdown templates or hidden scenario files. The complete
-example source total is 420 lines including the serde_json example's tests. The
-configuration application's implementation and its separate tests are additional
-code, not part of those authoring counts.
-
-The CLI consumer has more setup because it runs an executable and checks actual
-filesystem effects. The library consumer calls existing APIs directly and needs
-less infrastructure. This comparison demonstrates two integration shapes; it is
-not a study of outside developers' onboarding effort.
-
-`Doc::resource_code` combines resource registration and placement when they occur
-together. Reused resources keep a typed handle. Captured library output retains
-its original bytes and producer profile; input fixtures use a distinct producer.
-The diagnostic record created by `Doc::json` belongs only to contributor notes.
-
-The framework checks the encoded claims and retained output. These examples do
-not prove that every possible input works or that every sentence is automatically
-verified. Review the explanation as well as the assertions and resource changes.
-
-## Library provenance
-
-The serde_json example uses the pinned `serde_json` 1.0.151 dependency through
-its public API. Its behavior is grounded in the library's official documentation:
-
-- [`from_str`](https://docs.rs/serde_json/1.0.151/serde_json/fn.from_str.html)
- deserializes text into the requested Rust type and reports incompatible input.
-- [`Error`](https://docs.rs/serde_json/1.0.151/serde_json/struct.Error.html)
- exposes error categories and source locations. The guide observes a `Data`
- error from a string where `u32` is expected.
-- [`to_string_pretty`](https://docs.rs/serde_json/1.0.151/serde_json/fn.to_string_pretty.html)
- serializes the recovered vector into formatted JSON. The guide reparses that
- actual output and checks that the values are preserved.
-
-All narrative and integration code in these examples is authored here. The guide
-captures the library's actual error message and formatted result; no upstream
-guide or example source was copied.
-
-This package's code and documentation use the repository's MIT license, copied
-verbatim into [LICENSE](LICENSE) with the original `Copyright (c) 2026 Grida`
-notice. Dependency code is not vendored, and upstream dependency terms remain
-unchanged.
diff --git a/crates/doc-example-config/baseline/config/contributor/.ownership.json b/crates/doc-example-config/baseline/config/contributor/.ownership.json
deleted file mode 100644
index 620396e..0000000
--- a/crates/doc-example-config/baseline/config/contributor/.ownership.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "schema_version": 1,
- "files": [
- "assets/configure-a-port/execution-checks.json",
- "assets/configure-a-port/invalid-input.json",
- "assets/configure-a-port/recovered-command.txt",
- "assets/configure-a-port/rejected-command.txt",
- "assets/configure-a-port/saved-configuration.json",
- "assets/configure-a-port/successful-command.txt",
- "assets/configure-a-port/valid-input.json",
- "configure-a-port.md",
- "manifest.json"
- ]
-}
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/execution-checks.json b/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/execution-checks.json
deleted file mode 100644
index d303a7f..0000000
--- a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/execution-checks.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "driver": "actual config-demo subprocess",
- "recovered_port": 8080,
- "recovery_exit": 0,
- "rejected_exit": 2,
- "rejected_output_unchanged": true,
- "saved_port": 9090,
- "success_exit": 0
-}
diff --git a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/invalid-input.json b/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/invalid-input.json
deleted file mode 100644
index 2e086da..0000000
--- a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/invalid-input.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "port": 0
-}
diff --git a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/recovered-command.txt b/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/recovered-command.txt
deleted file mode 100644
index a688bc2..0000000
--- a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/recovered-command.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 8080) to settings.json.
-[exit 0]
diff --git a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/rejected-command.txt b/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/rejected-command.txt
deleted file mode 100644
index 41058b6..0000000
--- a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/rejected-command.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-$ config-demo input.json --output settings.json
-Configuration rejected: port must be between 1 and 65535 (received 0).
-[exit 2]
diff --git a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/saved-configuration.json b/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/saved-configuration.json
deleted file mode 100644
index a13d52b..0000000
--- a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/saved-configuration.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "port": 9090
-}
diff --git a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/successful-command.txt b/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/successful-command.txt
deleted file mode 100644
index d3e999d..0000000
--- a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/successful-command.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 9090) to settings.json.
-[exit 0]
diff --git a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/valid-input.json b/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/valid-input.json
deleted file mode 100644
index a13d52b..0000000
--- a/crates/doc-example-config/baseline/config/contributor/assets/configure-a-port/valid-input.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "port": 9090
-}
diff --git a/crates/doc-example-config/baseline/config/contributor/configure-a-port.md b/crates/doc-example-config/baseline/config/contributor/configure-a-port.md
deleted file mode 100644
index b746d63..0000000
--- a/crates/doc-example-config/baseline/config/contributor/configure-a-port.md
+++ /dev/null
@@ -1,129 +0,0 @@
-# Save a valid configuration
-
-
-
-Use **config-demo** to validate a JSON configuration before saving it.
-The command reports a useful error when a value is invalid and keeps the last
-valid output available while you correct the input.
-
-This walkthrough shows how to:
-
-- Save a valid configuration.
-- Reject an invalid replacement without changing the saved file.
-- Correct the input and retry successfully.
-
-Run the commands from a scratch directory so that `input.json` and
-`settings.json` stay together.
-
-
-
-The configuration contains one setting: port\. Choose an integer from 1 to 65535\. The Rust API's default configuration uses port 8080\. The command line reads an explicit configuration file; it does not fill in a missing port\.
-
-
-
-> **Contributor note:** This guide exercises the public Config API and the built config\-demo executable in an isolated temporary directory\. No server is started; the example checks configuration behavior\.
-
-
-
-## Save a configuration
-
-
-
-Create input\.json with this content:
-
-
-
-```json
-{
- "port": 9090
-}
-```
-
-
-
-Pass the input file and an output path\. A successful command saves the validated configuration:
-
-
-
-```text
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 9090) to settings.json.
-[exit 0]
-```
-
-
-
-The saved settings\.json now contains port 9090\. Its contents are shown below, and you can also open the [saved\-configuration](assets/configure-a-port/saved-configuration.json) file directly\.
-
-
-
-```json
-{
- "port": 9090
-}
-```
-
-
-
-## Reject an invalid replacement
-
-
-
-A rejected input leaves an existing output unchanged\. To see this, replace input\.json with an invalid port:
-
-
-
-```json
-{
- "port": 0
-}
-```
-
-
-
-Run the same command again\. The error explains which value needs correction:
-
-
-
-```text
-$ config-demo input.json --output settings.json
-Configuration rejected: port must be between 1 and 65535 (received 0).
-[exit 2]
-```
-
-
-
-The command exits unsuccessfully and preserves every byte of settings\.json\. The [saved\-configuration](assets/configure-a-port/saved-configuration.json) from the first command is still the current configuration\.
-
-
-
-> **Contributor note on `rejected-exit`:** The CLI's rejection status is 2\. The scenario also checks that rejected input produces no success output\.
-
-
-
-> **Contributor note on `rejected-output-unchanged`:** The preservation check compares the complete output bytes before and after the rejected subprocess invocation\.
-
-
-
-## Correct the input and retry
-
-
-
-Change the port in input\.json to 8080 and rerun the command\. The same output path can now be replaced with a valid configuration:
-
-
-
-```text
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 8080) to settings.json.
-[exit 0]
-```
-
-
-
-The output now uses port 8080\. When validation fails, correct the input and retry; the last valid output remains available until the correction succeeds\.
-
-
-
-> **Contributor note on [rejected\-command](assets/configure-a-port/rejected-command.txt):** The transcript comes from the actual command's stdout, stderr, and exit status\. Review the [execution\-checks](assets/configure-a-port/execution-checks.json) for the recorded outcomes\. This contributor evidence is attached to the same reader artifact\.
-
diff --git a/crates/doc-example-config/baseline/config/contributor/manifest.json b/crates/doc-example-config/baseline/config/contributor/manifest.json
deleted file mode 100644
index a525e98..0000000
--- a/crates/doc-example-config/baseline/config/contributor/manifest.json
+++ /dev/null
@@ -1,380 +0,0 @@
-{
- "artifacts": [
- {
- "bytes": 198,
- "document": "configure-a-port",
- "id": "execution-checks",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/configure-a-port/execution-checks.json",
- "producer": "executable-docs/json",
- "profile": "json-pretty-v1",
- "sha256": "92bf03ab28d9bb016c92d37c2164782c148c639ff1e2853f7b3e9e460a6b245a"
- },
- {
- "bytes": 16,
- "document": "configure-a-port",
- "id": "invalid-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/configure-a-port/invalid-input.json",
- "producer": "config-demo",
- "profile": "json-v1",
- "sha256": "4f52f2c12102a1a7ee628a1c571d542080f9042ad41fe10a15073b4c67c9f6af"
- },
- {
- "bytes": 117,
- "document": "configure-a-port",
- "id": "recovered-command",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/configure-a-port/recovered-command.txt",
- "producer": "config-demo-cli",
- "profile": "transcript-v1",
- "sha256": "60d4e34cb43759aededaee90a39a39f9758f9658670ba0f965bfb29f8aad9829"
- },
- {
- "bytes": 128,
- "document": "configure-a-port",
- "id": "rejected-command",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/configure-a-port/rejected-command.txt",
- "producer": "config-demo-cli",
- "profile": "transcript-v1",
- "sha256": "b7e21baceed98258b963c4c72ad1874476a01890501d94c6c92b6a4ae7c93dc3"
- },
- {
- "bytes": 19,
- "document": "configure-a-port",
- "id": "saved-configuration",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/configure-a-port/saved-configuration.json",
- "producer": "config-demo",
- "profile": "json-v1",
- "sha256": "bbcbff1eebf2fbca8dd0f1a4023d4953f70488a4a2d54a5e8d229e3727618ff2"
- },
- {
- "bytes": 117,
- "document": "configure-a-port",
- "id": "successful-command",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/configure-a-port/successful-command.txt",
- "producer": "config-demo-cli",
- "profile": "transcript-v1",
- "sha256": "a98fc8b82913db7ed8c07955e1dcdcad55697afa2d1ae1605349c435e5553eb5"
- },
- {
- "bytes": 19,
- "document": "configure-a-port",
- "id": "valid-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/configure-a-port/valid-input.json",
- "producer": "config-demo",
- "profile": "json-v1",
- "sha256": "bbcbff1eebf2fbca8dd0f1a4023d4953f70488a4a2d54a5e8d229e3727618ff2"
- },
- {
- "bytes": 3796,
- "document": "configure-a-port",
- "kind": "page",
- "mime": "text/markdown",
- "path": "configure-a-port.md",
- "sha256": "eec0ab6442e553848b7a4f7b87edc8ed7db1eeeb1c605cb7fc1a3de011b233f4"
- }
- ],
- "audience": "contributor",
- "documents": [
- {
- "blocks": [
- {
- "id": "block-0001",
- "kind": "markdown",
- "references": []
- },
- {
- "id": "block-0002",
- "kind": "paragraph",
- "references": [
- {
- "id": "default-port",
- "kind": "value"
- }
- ]
- },
- {
- "id": "note-0001",
- "kind": "note",
- "references": []
- },
- {
- "id": "block-0003",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0004",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0005",
- "kind": "code",
- "references": [
- {
- "id": "valid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0006",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0007",
- "kind": "code",
- "references": [
- {
- "id": "successful-command",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0008",
- "kind": "paragraph",
- "references": [
- {
- "id": "saved-port",
- "kind": "value"
- },
- {
- "id": "saved-configuration",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0009",
- "kind": "code",
- "references": [
- {
- "id": "saved-configuration",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0010",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0011",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0012",
- "kind": "code",
- "references": [
- {
- "id": "invalid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0013",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0014",
- "kind": "code",
- "references": [
- {
- "id": "rejected-command",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0015",
- "kind": "paragraph",
- "references": [
- {
- "id": "saved-configuration",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "note-0002",
- "kind": "note",
- "references": [
- {
- "id": "rejected-exit",
- "kind": "value"
- }
- ]
- },
- {
- "id": "note-0003",
- "kind": "note",
- "references": [
- {
- "id": "rejected-output-unchanged",
- "kind": "claim"
- }
- ]
- },
- {
- "id": "block-0016",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0017",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0018",
- "kind": "code",
- "references": [
- {
- "id": "recovered-command",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0019",
- "kind": "paragraph",
- "references": [
- {
- "id": "recovered-port",
- "kind": "value"
- }
- ]
- },
- {
- "id": "note-0004",
- "kind": "note",
- "references": [
- {
- "id": "execution-checks",
- "kind": "resource"
- },
- {
- "id": "rejected-command",
- "kind": "resource"
- }
- ]
- }
- ],
- "checks": [
- {
- "actual": 8080,
- "expected": 8080,
- "id": "default-port",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 8080,
- "expected": 8080,
- "id": "recovered-port",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "recovery-exit",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "recovery-stderr-empty",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": 2,
- "expected": 2,
- "id": "rejected-exit",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "rejected-output-unchanged",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "rejected-stdout-empty",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "rejection-explains-port-range",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": 9090,
- "expected": 9090,
- "id": "saved-port",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "successful-exit",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "successful-stderr-empty",
- "kind": "predicate",
- "passed": true
- }
- ],
- "id": "configure-a-port",
- "title": "Save a valid configuration"
- }
- ],
- "profiles": [
- {
- "producer": "config-demo",
- "profile": "json-v1"
- },
- {
- "producer": "config-demo-cli",
- "profile": "transcript-v1"
- },
- {
- "producer": "executable-docs/json",
- "profile": "json-pretty-v1"
- }
- ],
- "schema_version": 1
-}
diff --git a/crates/doc-example-config/baseline/config/reader/.ownership.json b/crates/doc-example-config/baseline/config/reader/.ownership.json
deleted file mode 100644
index 61c1c53..0000000
--- a/crates/doc-example-config/baseline/config/reader/.ownership.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "schema_version": 1,
- "files": [
- "assets/configure-a-port/invalid-input.json",
- "assets/configure-a-port/recovered-command.txt",
- "assets/configure-a-port/rejected-command.txt",
- "assets/configure-a-port/saved-configuration.json",
- "assets/configure-a-port/successful-command.txt",
- "assets/configure-a-port/valid-input.json",
- "configure-a-port.md",
- "manifest.json"
- ]
-}
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/invalid-input.json b/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/invalid-input.json
deleted file mode 100644
index 2e086da..0000000
--- a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/invalid-input.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "port": 0
-}
diff --git a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/recovered-command.txt b/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/recovered-command.txt
deleted file mode 100644
index a688bc2..0000000
--- a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/recovered-command.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 8080) to settings.json.
-[exit 0]
diff --git a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/rejected-command.txt b/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/rejected-command.txt
deleted file mode 100644
index 41058b6..0000000
--- a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/rejected-command.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-$ config-demo input.json --output settings.json
-Configuration rejected: port must be between 1 and 65535 (received 0).
-[exit 2]
diff --git a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/saved-configuration.json b/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/saved-configuration.json
deleted file mode 100644
index a13d52b..0000000
--- a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/saved-configuration.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "port": 9090
-}
diff --git a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/successful-command.txt b/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/successful-command.txt
deleted file mode 100644
index d3e999d..0000000
--- a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/successful-command.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 9090) to settings.json.
-[exit 0]
diff --git a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/valid-input.json b/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/valid-input.json
deleted file mode 100644
index a13d52b..0000000
--- a/crates/doc-example-config/baseline/config/reader/assets/configure-a-port/valid-input.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "port": 9090
-}
diff --git a/crates/doc-example-config/baseline/config/reader/configure-a-port.md b/crates/doc-example-config/baseline/config/reader/configure-a-port.md
deleted file mode 100644
index 98276d6..0000000
--- a/crates/doc-example-config/baseline/config/reader/configure-a-port.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Save a valid configuration
-
-
-
-Use **config-demo** to validate a JSON configuration before saving it.
-The command reports a useful error when a value is invalid and keeps the last
-valid output available while you correct the input.
-
-This walkthrough shows how to:
-
-- Save a valid configuration.
-- Reject an invalid replacement without changing the saved file.
-- Correct the input and retry successfully.
-
-Run the commands from a scratch directory so that `input.json` and
-`settings.json` stay together.
-
-
-
-The configuration contains one setting: port\. Choose an integer from 1 to 65535\. The Rust API's default configuration uses port 8080\. The command line reads an explicit configuration file; it does not fill in a missing port\.
-
-
-
-## Save a configuration
-
-
-
-Create input\.json with this content:
-
-
-
-```json
-{
- "port": 9090
-}
-```
-
-
-
-Pass the input file and an output path\. A successful command saves the validated configuration:
-
-
-
-```text
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 9090) to settings.json.
-[exit 0]
-```
-
-
-
-The saved settings\.json now contains port 9090\. Its contents are shown below, and you can also open the [saved\-configuration](assets/configure-a-port/saved-configuration.json) file directly\.
-
-
-
-```json
-{
- "port": 9090
-}
-```
-
-
-
-## Reject an invalid replacement
-
-
-
-A rejected input leaves an existing output unchanged\. To see this, replace input\.json with an invalid port:
-
-
-
-```json
-{
- "port": 0
-}
-```
-
-
-
-Run the same command again\. The error explains which value needs correction:
-
-
-
-```text
-$ config-demo input.json --output settings.json
-Configuration rejected: port must be between 1 and 65535 (received 0).
-[exit 2]
-```
-
-
-
-The command exits unsuccessfully and preserves every byte of settings\.json\. The [saved\-configuration](assets/configure-a-port/saved-configuration.json) from the first command is still the current configuration\.
-
-
-
-## Correct the input and retry
-
-
-
-Change the port in input\.json to 8080 and rerun the command\. The same output path can now be replaced with a valid configuration:
-
-
-
-```text
-$ config-demo input.json --output settings.json
-Saved validated configuration (port 8080) to settings.json.
-[exit 0]
-```
-
-
-
-The output now uses port 8080\. When validation fails, correct the input and retry; the last valid output remains available until the correction succeeds\.
-
diff --git a/crates/doc-example-config/baseline/config/reader/manifest.json b/crates/doc-example-config/baseline/config/reader/manifest.json
deleted file mode 100644
index 33d30df..0000000
--- a/crates/doc-example-config/baseline/config/reader/manifest.json
+++ /dev/null
@@ -1,310 +0,0 @@
-{
- "artifacts": [
- {
- "bytes": 16,
- "document": "configure-a-port",
- "id": "invalid-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/configure-a-port/invalid-input.json",
- "producer": "config-demo",
- "profile": "json-v1",
- "sha256": "4f52f2c12102a1a7ee628a1c571d542080f9042ad41fe10a15073b4c67c9f6af"
- },
- {
- "bytes": 117,
- "document": "configure-a-port",
- "id": "recovered-command",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/configure-a-port/recovered-command.txt",
- "producer": "config-demo-cli",
- "profile": "transcript-v1",
- "sha256": "60d4e34cb43759aededaee90a39a39f9758f9658670ba0f965bfb29f8aad9829"
- },
- {
- "bytes": 128,
- "document": "configure-a-port",
- "id": "rejected-command",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/configure-a-port/rejected-command.txt",
- "producer": "config-demo-cli",
- "profile": "transcript-v1",
- "sha256": "b7e21baceed98258b963c4c72ad1874476a01890501d94c6c92b6a4ae7c93dc3"
- },
- {
- "bytes": 19,
- "document": "configure-a-port",
- "id": "saved-configuration",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/configure-a-port/saved-configuration.json",
- "producer": "config-demo",
- "profile": "json-v1",
- "sha256": "bbcbff1eebf2fbca8dd0f1a4023d4953f70488a4a2d54a5e8d229e3727618ff2"
- },
- {
- "bytes": 117,
- "document": "configure-a-port",
- "id": "successful-command",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/configure-a-port/successful-command.txt",
- "producer": "config-demo-cli",
- "profile": "transcript-v1",
- "sha256": "a98fc8b82913db7ed8c07955e1dcdcad55697afa2d1ae1605349c435e5553eb5"
- },
- {
- "bytes": 19,
- "document": "configure-a-port",
- "id": "valid-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/configure-a-port/valid-input.json",
- "producer": "config-demo",
- "profile": "json-v1",
- "sha256": "bbcbff1eebf2fbca8dd0f1a4023d4953f70488a4a2d54a5e8d229e3727618ff2"
- },
- {
- "bytes": 2822,
- "document": "configure-a-port",
- "kind": "page",
- "mime": "text/markdown",
- "path": "configure-a-port.md",
- "sha256": "7ce900dc5b90d729cd3808d32ae17c4d032de4e2c250ffa50a4a7f3d6a8a1a8b"
- }
- ],
- "audience": "reader",
- "documents": [
- {
- "blocks": [
- {
- "id": "block-0001",
- "kind": "markdown",
- "references": []
- },
- {
- "id": "block-0002",
- "kind": "paragraph",
- "references": [
- {
- "id": "default-port",
- "kind": "value"
- }
- ]
- },
- {
- "id": "block-0003",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0004",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0005",
- "kind": "code",
- "references": [
- {
- "id": "valid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0006",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0007",
- "kind": "code",
- "references": [
- {
- "id": "successful-command",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0008",
- "kind": "paragraph",
- "references": [
- {
- "id": "saved-port",
- "kind": "value"
- },
- {
- "id": "saved-configuration",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0009",
- "kind": "code",
- "references": [
- {
- "id": "saved-configuration",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0010",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0011",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0012",
- "kind": "code",
- "references": [
- {
- "id": "invalid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0013",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0014",
- "kind": "code",
- "references": [
- {
- "id": "rejected-command",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0015",
- "kind": "paragraph",
- "references": [
- {
- "id": "saved-configuration",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0016",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0017",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0018",
- "kind": "code",
- "references": [
- {
- "id": "recovered-command",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0019",
- "kind": "paragraph",
- "references": [
- {
- "id": "recovered-port",
- "kind": "value"
- }
- ]
- }
- ],
- "checks": [
- {
- "actual": 8080,
- "expected": 8080,
- "id": "default-port",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 8080,
- "expected": 8080,
- "id": "recovered-port",
- "kind": "equality",
- "passed": true
- },
- {
- "id": "recovery-exit",
- "kind": "predicate",
- "passed": true
- },
- {
- "id": "recovery-stderr-empty",
- "kind": "predicate",
- "passed": true
- },
- {
- "id": "rejected-exit",
- "kind": "equality",
- "passed": true
- },
- {
- "id": "rejected-output-unchanged",
- "kind": "predicate",
- "passed": true
- },
- {
- "id": "rejected-stdout-empty",
- "kind": "predicate",
- "passed": true
- },
- {
- "id": "rejection-explains-port-range",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": 9090,
- "expected": 9090,
- "id": "saved-port",
- "kind": "equality",
- "passed": true
- },
- {
- "id": "successful-exit",
- "kind": "predicate",
- "passed": true
- },
- {
- "id": "successful-stderr-empty",
- "kind": "predicate",
- "passed": true
- }
- ],
- "id": "configure-a-port",
- "title": "Save a valid configuration"
- }
- ],
- "profiles": [
- {
- "producer": "config-demo",
- "profile": "json-v1"
- },
- {
- "producer": "config-demo-cli",
- "profile": "transcript-v1"
- }
- ],
- "schema_version": 1
-}
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/.ownership.json b/crates/doc-example-config/baseline/serde-json/contributor/.ownership.json
deleted file mode 100644
index a2c5e90..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/.ownership.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "schema_version": 1,
- "files": [
- "assets/parse-a-typed-list/error-observation.json",
- "assets/parse-a-typed-list/formatted-list.json",
- "assets/parse-a-typed-list/mismatched-input.json",
- "assets/parse-a-typed-list/type-error.txt",
- "assets/parse-a-typed-list/valid-input.json",
- "manifest.json",
- "parse-a-typed-list.md"
- ]
-}
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/error-observation.json b/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/error-observation.json
deleted file mode 100644
index 67baec7..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/error-observation.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "category": "Data",
- "column": 11,
- "line": 1,
- "message": "invalid type: string \"three\", expected u32 at line 1 column 11"
-}
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/formatted-list.json b/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/formatted-list.json
deleted file mode 100644
index aa50563..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/formatted-list.json
+++ /dev/null
@@ -1,5 +0,0 @@
-[
- 2,
- 3,
- 5
-]
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/mismatched-input.json b/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/mismatched-input.json
deleted file mode 100644
index a47bcf6..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/mismatched-input.json
+++ /dev/null
@@ -1 +0,0 @@
-[2, "three", 5]
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/type-error.txt b/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/type-error.txt
deleted file mode 100644
index 8da8dd3..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/type-error.txt
+++ /dev/null
@@ -1 +0,0 @@
-invalid type: string "three", expected u32 at line 1 column 11
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/valid-input.json b/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/valid-input.json
deleted file mode 100644
index 3ec03bc..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/assets/parse-a-typed-list/valid-input.json
+++ /dev/null
@@ -1 +0,0 @@
-[2, 3, 5]
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/manifest.json b/crates/doc-example-config/baseline/serde-json/contributor/manifest.json
deleted file mode 100644
index 8cb137a..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/manifest.json
+++ /dev/null
@@ -1,314 +0,0 @@
-{
- "artifacts": [
- {
- "bytes": 135,
- "document": "parse-a-typed-list",
- "id": "error-observation",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/parse-a-typed-list/error-observation.json",
- "producer": "executable-docs/json",
- "profile": "json-pretty-v1",
- "sha256": "ceb55f83bc7cfa0f01ca0bf120ec95581583c60eb741b388b83f80e2740b85fa"
- },
- {
- "bytes": 17,
- "document": "parse-a-typed-list",
- "id": "formatted-list",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/parse-a-typed-list/formatted-list.json",
- "producer": "serde_json",
- "profile": "serde-json-1.0.151",
- "sha256": "80c83a54da142957b5a2b7b8ec8f8396d6cdb2daee1848129b19dba82c88c909"
- },
- {
- "bytes": 15,
- "document": "parse-a-typed-list",
- "id": "mismatched-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/parse-a-typed-list/mismatched-input.json",
- "producer": "serde-json-guide",
- "profile": "json-fixture-v1",
- "sha256": "e14fac803c516c4f81b9af6ac11db65e751802c0c495db102f14aa9df774458b"
- },
- {
- "bytes": 62,
- "document": "parse-a-typed-list",
- "id": "type-error",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/parse-a-typed-list/type-error.txt",
- "producer": "serde_json",
- "profile": "serde-json-1.0.151",
- "sha256": "755ae51b66058706677805b97a062f14ecd1895103bd1dc942782a097aa7acd6"
- },
- {
- "bytes": 9,
- "document": "parse-a-typed-list",
- "id": "valid-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/parse-a-typed-list/valid-input.json",
- "producer": "serde-json-guide",
- "profile": "json-fixture-v1",
- "sha256": "40d28069123e2a7d89dd143fd210b66f8ed7fb19ebfc23032c051f4f6aed0381"
- },
- {
- "bytes": 2904,
- "document": "parse-a-typed-list",
- "kind": "page",
- "mime": "text/markdown",
- "path": "parse-a-typed-list.md",
- "sha256": "1d31773366db1e8979454d03c15026a3abdee6a5d32f22a6cc5234a14fc82802"
- }
- ],
- "audience": "contributor",
- "documents": [
- {
- "blocks": [
- {
- "id": "block-0001",
- "kind": "markdown",
- "references": []
- },
- {
- "id": "block-0002",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0003",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0004",
- "kind": "code",
- "references": [
- {
- "id": "valid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0005",
- "kind": "paragraph",
- "references": [
- {
- "id": "parsed-length",
- "kind": "value"
- },
- {
- "id": "parsed-sum",
- "kind": "value"
- }
- ]
- },
- {
- "id": "block-0006",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0007",
- "kind": "markdown",
- "references": []
- },
- {
- "id": "block-0008",
- "kind": "code",
- "references": [
- {
- "id": "mismatched-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0009",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0010",
- "kind": "code",
- "references": [
- {
- "id": "type-error",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0011",
- "kind": "paragraph",
- "references": [
- {
- "id": "error-line",
- "kind": "value"
- },
- {
- "id": "error-column",
- "kind": "value"
- }
- ]
- },
- {
- "id": "note-0001",
- "kind": "note",
- "references": [
- {
- "id": "error-observation",
- "kind": "resource"
- },
- {
- "id": "type-error",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0012",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0013",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0014",
- "kind": "code",
- "references": [
- {
- "id": "valid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0015",
- "kind": "paragraph",
- "references": [
- {
- "id": "corrected-values",
- "kind": "value"
- }
- ]
- },
- {
- "id": "block-0016",
- "kind": "code",
- "references": [
- {
- "id": "formatted-list",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0017",
- "kind": "paragraph",
- "references": [
- {
- "id": "formatted-list",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "note-0002",
- "kind": "note",
- "references": []
- }
- ],
- "checks": [
- {
- "actual": [
- 2,
- 3,
- 5
- ],
- "expected": [
- 2,
- 3,
- 5
- ],
- "id": "corrected-values",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 11,
- "expected": 11,
- "id": "error-column",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 1,
- "expected": 1,
- "id": "error-line",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "mismatched-input-has-valid-json-syntax",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": 3,
- "expected": 3,
- "id": "parsed-length",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 10,
- "expected": 10,
- "id": "parsed-sum",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "pretty-json-preserves-values",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": true,
- "expected": true,
- "id": "type-mismatch-is-data-error",
- "kind": "predicate",
- "passed": true
- }
- ],
- "id": "parse-a-typed-list",
- "title": "Parse a typed JSON list"
- }
- ],
- "profiles": [
- {
- "producer": "executable-docs/json",
- "profile": "json-pretty-v1"
- },
- {
- "producer": "serde-json-guide",
- "profile": "json-fixture-v1"
- },
- {
- "producer": "serde_json",
- "profile": "serde-json-1.0.151"
- }
- ],
- "schema_version": 1
-}
diff --git a/crates/doc-example-config/baseline/serde-json/contributor/parse-a-typed-list.md b/crates/doc-example-config/baseline/serde-json/contributor/parse-a-typed-list.md
deleted file mode 100644
index 3cbc9bf..0000000
--- a/crates/doc-example-config/baseline/serde-json/contributor/parse-a-typed-list.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Parse a typed JSON list
-
-
-
-Use `serde_json::from_str::>()` to read a JSON array of
-unsigned integers into a Rust vector. The Rust type supplies a concrete
-expectation: a string inside the array is a type mismatch, even if the JSON
-syntax is valid.
-
-This walkthrough parses a valid list, inspects an error, and retries with
-corrected input. It calls the public `serde_json` API directly.
-
-The library documents [deserializing from a string](https://docs.rs/serde_json/1.0.151/serde_json/fn.from_str.html)
-and [classifying errors](https://docs.rs/serde_json/1.0.151/serde_json/struct.Error.html).
-
-
-
-## Parse a valid list
-
-
-
-Start with this JSON array:
-
-
-
-```json
-[2, 3, 5]
-```
-
-
-
-Parsing succeeds with 3 integers\. Their sum is 10\. The values are ordinary Rust integers after deserialization\.
-
-
-
-## Inspect a type mismatch
-
-
-
-Replace the second number with a string. This input is still valid JSON,
-but it does not match `Vec`:
-
-
-
-```json
-[2, "three", 5]
-```
-
-
-
-The failed parse returns an error\. Its display text identifies the incompatible value and the expected Rust type:
-
-
-
-```text
-invalid type: string "three", expected u32 at line 1 column 11
-```
-
-
-
-The error is classified as Data and is reported at line 1, column 11\. Use the category to distinguish a type mismatch from invalid JSON syntax\.
-
-
-
-> **Contributor note on [type\-error](assets/parse-a-typed-list/type-error.txt):** The error text is captured from serde\_json 1\.0\.151\. A dependency update must review both the asserted category and the captured message\. The [error\-observation](assets/parse-a-typed-list/error-observation.json) records the observed diagnostic fields for contributors\.
-
-
-
-## Correct the input and serialize the result
-
-
-
-Replace the string with the number 3 and parse the corrected source again\. The corrected input is the same valid array shown above:
-
-
-
-```json
-[2, 3, 5]
-```
-
-
-
-The corrected parse returns \[2,3,5\]\. Pass that vector to serde\_json::to\_string\_pretty to obtain formatted JSON:
-
-
-
-```json
-[
- 2,
- 3,
- 5
-]
-```
-
-
-
-The [formatted\-list](assets/parse-a-typed-list/formatted-list.json) can be parsed back into the same integers\. Formatting changes the presentation while preserving these values\.
-
-
-
-> **Contributor note:** This integration is maintained here to test the documentation framework against an independently existing Rust library\. It is not upstream serde\_json documentation or evidence of adoption by its maintainers\.
-
diff --git a/crates/doc-example-config/baseline/serde-json/reader/.ownership.json b/crates/doc-example-config/baseline/serde-json/reader/.ownership.json
deleted file mode 100644
index a493293..0000000
--- a/crates/doc-example-config/baseline/serde-json/reader/.ownership.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "schema_version": 1,
- "files": [
- "assets/parse-a-typed-list/formatted-list.json",
- "assets/parse-a-typed-list/mismatched-input.json",
- "assets/parse-a-typed-list/type-error.txt",
- "assets/parse-a-typed-list/valid-input.json",
- "manifest.json",
- "parse-a-typed-list.md"
- ]
-}
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/formatted-list.json b/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/formatted-list.json
deleted file mode 100644
index aa50563..0000000
--- a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/formatted-list.json
+++ /dev/null
@@ -1,5 +0,0 @@
-[
- 2,
- 3,
- 5
-]
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/mismatched-input.json b/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/mismatched-input.json
deleted file mode 100644
index a47bcf6..0000000
--- a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/mismatched-input.json
+++ /dev/null
@@ -1 +0,0 @@
-[2, "three", 5]
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/type-error.txt b/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/type-error.txt
deleted file mode 100644
index 8da8dd3..0000000
--- a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/type-error.txt
+++ /dev/null
@@ -1 +0,0 @@
-invalid type: string "three", expected u32 at line 1 column 11
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/valid-input.json b/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/valid-input.json
deleted file mode 100644
index 3ec03bc..0000000
--- a/crates/doc-example-config/baseline/serde-json/reader/assets/parse-a-typed-list/valid-input.json
+++ /dev/null
@@ -1 +0,0 @@
-[2, 3, 5]
\ No newline at end of file
diff --git a/crates/doc-example-config/baseline/serde-json/reader/manifest.json b/crates/doc-example-config/baseline/serde-json/reader/manifest.json
deleted file mode 100644
index cc722f6..0000000
--- a/crates/doc-example-config/baseline/serde-json/reader/manifest.json
+++ /dev/null
@@ -1,274 +0,0 @@
-{
- "artifacts": [
- {
- "bytes": 17,
- "document": "parse-a-typed-list",
- "id": "formatted-list",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/parse-a-typed-list/formatted-list.json",
- "producer": "serde_json",
- "profile": "serde-json-1.0.151",
- "sha256": "80c83a54da142957b5a2b7b8ec8f8396d6cdb2daee1848129b19dba82c88c909"
- },
- {
- "bytes": 15,
- "document": "parse-a-typed-list",
- "id": "mismatched-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/parse-a-typed-list/mismatched-input.json",
- "producer": "serde-json-guide",
- "profile": "json-fixture-v1",
- "sha256": "e14fac803c516c4f81b9af6ac11db65e751802c0c495db102f14aa9df774458b"
- },
- {
- "bytes": 62,
- "document": "parse-a-typed-list",
- "id": "type-error",
- "kind": "resource",
- "mime": "text/plain",
- "path": "assets/parse-a-typed-list/type-error.txt",
- "producer": "serde_json",
- "profile": "serde-json-1.0.151",
- "sha256": "755ae51b66058706677805b97a062f14ecd1895103bd1dc942782a097aa7acd6"
- },
- {
- "bytes": 9,
- "document": "parse-a-typed-list",
- "id": "valid-input",
- "kind": "resource",
- "mime": "application/json",
- "path": "assets/parse-a-typed-list/valid-input.json",
- "producer": "serde-json-guide",
- "profile": "json-fixture-v1",
- "sha256": "40d28069123e2a7d89dd143fd210b66f8ed7fb19ebfc23032c051f4f6aed0381"
- },
- {
- "bytes": 2261,
- "document": "parse-a-typed-list",
- "kind": "page",
- "mime": "text/markdown",
- "path": "parse-a-typed-list.md",
- "sha256": "92f2e8c2837c2111f329fbb6f532e14c2bb4b09d4910c9ced0e624f143e6bee7"
- }
- ],
- "audience": "reader",
- "documents": [
- {
- "blocks": [
- {
- "id": "block-0001",
- "kind": "markdown",
- "references": []
- },
- {
- "id": "block-0002",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0003",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0004",
- "kind": "code",
- "references": [
- {
- "id": "valid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0005",
- "kind": "paragraph",
- "references": [
- {
- "id": "parsed-length",
- "kind": "value"
- },
- {
- "id": "parsed-sum",
- "kind": "value"
- }
- ]
- },
- {
- "id": "block-0006",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0007",
- "kind": "markdown",
- "references": []
- },
- {
- "id": "block-0008",
- "kind": "code",
- "references": [
- {
- "id": "mismatched-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0009",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0010",
- "kind": "code",
- "references": [
- {
- "id": "type-error",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0011",
- "kind": "paragraph",
- "references": [
- {
- "id": "error-line",
- "kind": "value"
- },
- {
- "id": "error-column",
- "kind": "value"
- }
- ]
- },
- {
- "id": "block-0012",
- "kind": "heading",
- "references": []
- },
- {
- "id": "block-0013",
- "kind": "paragraph",
- "references": []
- },
- {
- "id": "block-0014",
- "kind": "code",
- "references": [
- {
- "id": "valid-input",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0015",
- "kind": "paragraph",
- "references": [
- {
- "id": "corrected-values",
- "kind": "value"
- }
- ]
- },
- {
- "id": "block-0016",
- "kind": "code",
- "references": [
- {
- "id": "formatted-list",
- "kind": "resource"
- }
- ]
- },
- {
- "id": "block-0017",
- "kind": "paragraph",
- "references": [
- {
- "id": "formatted-list",
- "kind": "resource"
- }
- ]
- }
- ],
- "checks": [
- {
- "actual": [
- 2,
- 3,
- 5
- ],
- "expected": [
- 2,
- 3,
- 5
- ],
- "id": "corrected-values",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 11,
- "expected": 11,
- "id": "error-column",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 1,
- "expected": 1,
- "id": "error-line",
- "kind": "equality",
- "passed": true
- },
- {
- "id": "mismatched-input-has-valid-json-syntax",
- "kind": "predicate",
- "passed": true
- },
- {
- "actual": 3,
- "expected": 3,
- "id": "parsed-length",
- "kind": "equality",
- "passed": true
- },
- {
- "actual": 10,
- "expected": 10,
- "id": "parsed-sum",
- "kind": "equality",
- "passed": true
- },
- {
- "id": "pretty-json-preserves-values",
- "kind": "predicate",
- "passed": true
- },
- {
- "id": "type-mismatch-is-data-error",
- "kind": "predicate",
- "passed": true
- }
- ],
- "id": "parse-a-typed-list",
- "title": "Parse a typed JSON list"
- }
- ],
- "profiles": [
- {
- "producer": "serde-json-guide",
- "profile": "json-fixture-v1"
- },
- {
- "producer": "serde_json",
- "profile": "serde-json-1.0.151"
- }
- ],
- "schema_version": 1
-}
diff --git a/crates/doc-example-config/baseline/serde-json/reader/parse-a-typed-list.md b/crates/doc-example-config/baseline/serde-json/reader/parse-a-typed-list.md
deleted file mode 100644
index dae6fbc..0000000
--- a/crates/doc-example-config/baseline/serde-json/reader/parse-a-typed-list.md
+++ /dev/null
@@ -1,94 +0,0 @@
-# Parse a typed JSON list
-
-
-
-Use `serde_json::from_str::>()` to read a JSON array of
-unsigned integers into a Rust vector. The Rust type supplies a concrete
-expectation: a string inside the array is a type mismatch, even if the JSON
-syntax is valid.
-
-This walkthrough parses a valid list, inspects an error, and retries with
-corrected input. It calls the public `serde_json` API directly.
-
-The library documents [deserializing from a string](https://docs.rs/serde_json/1.0.151/serde_json/fn.from_str.html)
-and [classifying errors](https://docs.rs/serde_json/1.0.151/serde_json/struct.Error.html).
-
-
-
-## Parse a valid list
-
-
-
-Start with this JSON array:
-
-
-
-```json
-[2, 3, 5]
-```
-
-
-
-Parsing succeeds with 3 integers\. Their sum is 10\. The values are ordinary Rust integers after deserialization\.
-
-
-
-## Inspect a type mismatch
-
-
-
-Replace the second number with a string. This input is still valid JSON,
-but it does not match `Vec`:
-
-
-
-```json
-[2, "three", 5]
-```
-
-
-
-The failed parse returns an error\. Its display text identifies the incompatible value and the expected Rust type:
-
-
-
-```text
-invalid type: string "three", expected u32 at line 1 column 11
-```
-
-
-
-The error is classified as Data and is reported at line 1, column 11\. Use the category to distinguish a type mismatch from invalid JSON syntax\.
-
-
-
-## Correct the input and serialize the result
-
-
-
-Replace the string with the number 3 and parse the corrected source again\. The corrected input is the same valid array shown above:
-
-
-
-```json
-[2, 3, 5]
-```
-
-
-
-The corrected parse returns \[2,3,5\]\. Pass that vector to serde\_json::to\_string\_pretty to obtain formatted JSON:
-
-
-
-```json
-[
- 2,
- 3,
- 5
-]
-```
-
-
-
-The [formatted\-list](assets/parse-a-typed-list/formatted-list.json) can be parsed back into the same integers\. Formatting changes the presentation while preserving these values\.
-
diff --git a/crates/doc-example-config/examples/docs.rs b/crates/doc-example-config/examples/docs.rs
deleted file mode 100644
index 73cab64..0000000
--- a/crates/doc-example-config/examples/docs.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-//! This example is both the authored guide and its executable evidence producer.
-//! Build `config-demo` first so the guide can exercise the actual CLI.
-
-mod support;
-
-use executable_docs::runner::run_cli;
-use std::path::Path;
-
-fn main() {
- let baseline = Path::new(env!("CARGO_MANIFEST_DIR")).join("baseline/config");
- let result = run_cli(std::env::args().skip(1), &baseline, || {
- let binary = support::locate_cli()?;
- support::guide(&binary)
- });
- if let Err(error) = result {
- eprintln!("{error}");
- std::process::exit(1);
- }
-}
diff --git a/crates/doc-example-config/examples/serde_json.rs b/crates/doc-example-config/examples/serde_json.rs
deleted file mode 100644
index 634fce5..0000000
--- a/crates/doc-example-config/examples/serde_json.rs
+++ /dev/null
@@ -1,179 +0,0 @@
-//! An executable user explanation built against the existing serde_json crate.
-//! This is our integration example, not documentation adopted by its maintainers.
-
-use executable_docs::runner::run_cli;
-use executable_docs::{Artifact, Doc, Document, Result};
-use serde_json::error::Category;
-use std::path::Path;
-
-fn main() {
- let baseline = Path::new(env!("CARGO_MANIFEST_DIR")).join("baseline/serde-json");
- if let Err(error) = run_cli(std::env::args().skip(1), &baseline, guide) {
- eprintln!("{error}");
- std::process::exit(1);
- }
-}
-
-fn guide() -> Result> {
- let mut doc = Doc::new("parse-a-typed-list", "Parse a typed JSON list")?;
- doc.markdown(
- r#"Use `serde_json::from_str::>()` to read a JSON array of
-unsigned integers into a Rust vector. The Rust type supplies a concrete
-expectation: a string inside the array is a type mismatch, even if the JSON
-syntax is valid.
-
-This walkthrough parses a valid list, inspects an error, and retries with
-corrected input. It calls the public `serde_json` API directly.
-
-The library documents [deserializing from a string](https://docs.rs/serde_json/1.0.151/serde_json/fn.from_str.html)
-and [classifying errors](https://docs.rs/serde_json/1.0.151/serde_json/struct.Error.html)."#,
- )?;
-
- doc.heading(2, "Parse a valid list")?;
- let source = "[2, 3, 5]";
- doc.paragraph("Start with this JSON array:")?;
- let input = doc.resource_code("valid-input", input_json(source)?, "json")?;
- let values: Vec = serde_json::from_str(source).map_err(|error| error.to_string())?;
- let count = doc.expect_eq("parsed-length", values.len(), 3)?;
- let total = doc.expect_eq("parsed-sum", values.iter().sum::(), 10)?;
- doc.paragraph((
- "Parsing succeeds with ",
- &count,
- " integers. Their sum is ",
- &total,
- ". The values are ordinary Rust integers after deserialization.",
- ))?;
-
- doc.heading(2, "Inspect a type mismatch")?;
- doc.markdown(
- r#"Replace the second number with a string. This input is still valid JSON,
-but it does not match `Vec`:"#,
- )?;
- let invalid = r#"[2, "three", 5]"#;
- doc.resource_code("mismatched-input", input_json(invalid)?, "json")?;
- doc.require(
- "mismatched-input-has-valid-json-syntax",
- serde_json::from_str::(invalid).is_ok(),
- )?;
- let error = match serde_json::from_str::>(invalid) {
- Ok(_) => return Err("Expected the string element to fail typed deserialization".into()),
- Err(error) => error,
- };
- doc.require(
- "type-mismatch-is-data-error",
- error.classify() == Category::Data,
- )?;
- let line = doc.expect_eq("error-line", error.line(), 1)?;
- let column = doc.expect_eq("error-column", error.column(), 11)?;
- doc.paragraph("The failed parse returns an error. Its display text identifies the incompatible value and the expected Rust type:")?;
- let diagnostic = doc.resource_code(
- "type-error",
- captured(&error.to_string(), "text/plain", "txt")?,
- "text",
- )?;
- doc.paragraph((
- "The error is classified as Data and is reported at line ",
- &line,
- ", column ",
- &column,
- ". Use the category to distinguish a type mismatch from invalid JSON syntax.",
- ))?;
- let details = doc.json(
- "error-observation",
- &serde_json::json!({
- "category": format!("{:?}", error.classify()),
- "line": error.line(),
- "column": error.column(),
- "message": error.to_string(),
- }),
- )?;
- doc.note_on(
- &diagnostic,
- (
- "The error text is captured from serde_json 1.0.151. A dependency update must review both the asserted category and the captured message. The ",
- &details,
- " records the observed diagnostic fields for contributors.",
- ),
- )?;
-
- doc.heading(2, "Correct the input and serialize the result")?;
- doc.paragraph("Replace the string with the number 3 and parse the corrected source again. The corrected input is the same valid array shown above:")?;
- doc.code(&input, "json")?;
- let corrected = invalid.replace("\"three\"", "3");
- let recovered: Vec =
- serde_json::from_str(&corrected).map_err(|error| error.to_string())?;
- let recovered_values = doc.expect_eq("corrected-values", &recovered, &values)?;
- doc.paragraph((
- "The corrected parse returns ",
- &recovered_values,
- ". Pass that vector to serde_json::to_string_pretty to obtain formatted JSON:",
- ))?;
- let pretty = serde_json::to_string_pretty(&recovered).map_err(|error| error.to_string())?;
- let roundtrip: Vec = serde_json::from_str(&pretty).map_err(|error| error.to_string())?;
- doc.require("pretty-json-preserves-values", roundtrip == recovered)?;
- let formatted = doc.resource_code(
- "formatted-list",
- captured(&pretty, "application/json", "json")?,
- "json",
- )?;
- doc.paragraph((
- "The ",
- &formatted,
- " can be parsed back into the same integers. Formatting changes the presentation while preserving these values.",
- ))?;
- doc.note("This integration is maintained here to test the documentation framework against an independently existing Rust library. It is not upstream serde_json documentation or evidence of adoption by its maintainers.")?;
- Ok(vec![doc.finish()?])
-}
-
-fn captured(text: &str, mime: &str, extension: &str) -> Result {
- Artifact::new(
- text.as_bytes(),
- mime,
- extension,
- "serde_json",
- "serde-json-1.0.151",
- )
-}
-
-fn input_json(text: &str) -> Result {
- Artifact::new(
- text.as_bytes(),
- "application/json",
- "json",
- "serde-json-guide",
- "json-fixture-v1",
- )
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use executable_docs::{Audience, store};
-
- #[test]
- fn both_serde_json_views_match_the_retained_contract() {
- let documents = guide().unwrap();
- let baseline = Path::new(env!("CARGO_MANIFEST_DIR")).join("baseline/serde-json");
- for (audience, directory) in [
- (Audience::Reader, "reader"),
- (Audience::Contributor, "contributor"),
- ] {
- let files = Document::render_many(&documents, audience).unwrap();
- store::check(&baseline.join(directory), &files).unwrap();
- }
- }
-
- #[test]
- fn library_guide_repeats_exactly_without_exposing_contributor_diagnostics() {
- let first = Document::render_many(&guide().unwrap(), Audience::Reader).unwrap();
- let second = Document::render_many(&guide().unwrap(), Audience::Reader).unwrap();
- assert_eq!(first, second);
- assert!(!first.keys().any(|path| path.contains("error-observation")));
- let reader_text = first
- .values()
- .map(|bytes| String::from_utf8_lossy(bytes))
- .collect::>()
- .join("\n");
- assert!(!reader_text.contains("observed diagnostic fields for contributors"));
- }
-}
diff --git a/crates/doc-example-config/examples/support/mod.rs b/crates/doc-example-config/examples/support/mod.rs
deleted file mode 100644
index 577830a..0000000
--- a/crates/doc-example-config/examples/support/mod.rs
+++ /dev/null
@@ -1,222 +0,0 @@
-use config_demo::Config;
-use executable_docs::{Artifact, Doc, Document, Result};
-use std::fs;
-use std::path::{Path, PathBuf};
-use std::process::{Command, Output};
-
-/// Resolve a built CLI without knowing the repository or target directory.
-pub fn locate_cli() -> Result {
- if let Some(binary) = std::env::var_os("CONFIG_DEMO_BIN") {
- return fs::canonicalize(binary)
- .map_err(|error| format!("CONFIG_DEMO_BIN could not be resolved: {error}"));
- }
- let example = std::env::current_exe().map_err(|error| error.to_string())?;
- let directory = example
- .parent()
- .and_then(Path::parent)
- .ok_or("could not locate the example's build directory")?;
- let binary = directory.join(format!("config-demo{}", std::env::consts::EXE_SUFFIX));
- if !binary.is_file() {
- return Err("Build the actual CLI with `cargo build -p doc-example-config --bin config-demo`, or set CONFIG_DEMO_BIN to its path.".into());
- }
- Ok(binary)
-}
-
-/// Author prose beside the operations and checks which support its promises.
-pub fn guide(binary: &Path) -> Result> {
- // Resolve before changing the child's directory. Some emulated hosts report
- // a missing executable as child exit 127 instead of a process-spawn error.
- let binary = fs::canonicalize(binary).map_err(|error| {
- format!(
- "could not run the config-demo CLI at {}: {error}",
- binary.display()
- )
- })?;
- let directory = tempfile::tempdir().map_err(|error| error.to_string())?;
- let output_path = directory.path().join("settings.json");
- let mut doc = Doc::new("configure-a-port", "Save a valid configuration")?;
-
- doc.markdown(
- r#"Use **config-demo** to validate a JSON configuration before saving it.
-The command reports a useful error when a value is invalid and keeps the last
-valid output available while you correct the input.
-
-This walkthrough shows how to:
-
-- Save a valid configuration.
-- Reject an invalid replacement without changing the saved file.
-- Correct the input and retry successfully.
-
-Run the commands from a scratch directory so that `input.json` and
-`settings.json` stay together."#,
- )?;
- let default_port = doc.expect_eq("default-port", Config::default().port, 8080)?;
- doc.paragraph((
- "The configuration contains one setting: port. Choose an integer from 1 to 65535. The Rust API's default configuration uses port ",
- &default_port,
- ". The command line reads an explicit configuration file; it does not fill in a missing port.",
- ))?;
- doc.note("This guide exercises the public Config API and the built config-demo executable in an isolated temporary directory. No server is started; the example checks configuration behavior.")?;
-
- doc.heading(2, "Save a configuration")?;
- let valid_input = b"{\n \"port\": 9090\n}\n";
- write_fixture(directory.path(), "input.json", valid_input)?;
- let valid_source = doc.resource("valid-input", json_artifact(valid_input.to_vec())?)?;
- doc.paragraph("Create input.json with this content:")?;
- doc.code(&valid_source, "json")?;
-
- let successful = invoke(&binary, directory.path(), "input.json")?;
- doc.require("successful-exit", successful.output.status.success())?;
- doc.require(
- "successful-stderr-empty",
- successful.output.stderr.is_empty(),
- )?;
- let success_transcript = doc.resource(
- "successful-command",
- text_artifact(successful.transcript.into_bytes())?,
- )?;
- doc.paragraph("Pass the input file and an output path. A successful command saves the validated configuration:")?;
- doc.code(&success_transcript, "text")?;
-
- let saved_bytes = fs::read(&output_path).map_err(|error| error.to_string())?;
- let saved = Config::load(&output_path).map_err(|error| error.to_string())?;
- let saved_port = doc.expect_eq("saved-port", saved.port, 9090)?;
- let saved_artifact =
- doc.resource("saved-configuration", json_artifact(saved_bytes.clone())?)?;
- doc.paragraph((
- "The saved settings.json now contains port ",
- &saved_port,
- ". Its contents are shown below, and you can also open the ",
- &saved_artifact,
- " file directly.",
- ))?;
- doc.code(&saved_artifact, "json")?;
-
- doc.heading(2, "Reject an invalid replacement")?;
- doc.paragraph("A rejected input leaves an existing output unchanged. To see this, replace input.json with an invalid port:")?;
- let invalid_input = b"{\n \"port\": 0\n}\n";
- write_fixture(directory.path(), "input.json", invalid_input)?;
- let invalid_source = doc.resource("invalid-input", json_artifact(invalid_input.to_vec())?)?;
- doc.code(&invalid_source, "json")?;
-
- let rejected = invoke(&binary, directory.path(), "input.json")?;
- let rejection_code = doc.expect_eq("rejected-exit", rejected.exit_code, 2)?;
- doc.require("rejected-stdout-empty", rejected.output.stdout.is_empty())?;
- doc.require(
- "rejection-explains-port-range",
- String::from_utf8_lossy(&rejected.output.stderr)
- .contains("port must be between 1 and 65535 (received 0)"),
- )?;
- let after_rejection = fs::read(&output_path).map_err(|error| error.to_string())?;
- let preservation = doc.require("rejected-output-unchanged", after_rejection == saved_bytes)?;
- let rejection_transcript = doc.resource(
- "rejected-command",
- text_artifact(rejected.transcript.into_bytes())?,
- )?;
- doc.paragraph("Run the same command again. The error explains which value needs correction:")?;
- doc.code(&rejection_transcript, "text")?;
- doc.paragraph((
- "The command exits unsuccessfully and preserves every byte of settings.json. The ",
- &saved_artifact,
- " from the first command is still the current configuration.",
- ))?;
- doc.note_on(
- &rejection_code,
- "The CLI's rejection status is 2. The scenario also checks that rejected input produces no success output.",
- )?;
- doc.note_on(
- &preservation,
- "The preservation check compares the complete output bytes before and after the rejected subprocess invocation.",
- )?;
-
- doc.heading(2, "Correct the input and retry")?;
- doc.paragraph("Change the port in input.json to 8080 and rerun the command. The same output path can now be replaced with a valid configuration:")?;
- let recovery_input = b"{\n \"port\": 8080\n}\n";
- write_fixture(directory.path(), "input.json", recovery_input)?;
- let recovered = invoke(&binary, directory.path(), "input.json")?;
- doc.require("recovery-exit", recovered.output.status.success())?;
- doc.require("recovery-stderr-empty", recovered.output.stderr.is_empty())?;
- let recovery_config = Config::load(&output_path).map_err(|error| error.to_string())?;
- let recovered_port = doc.expect_eq("recovered-port", recovery_config.port, 8080)?;
- let recovery_transcript = doc.resource(
- "recovered-command",
- text_artifact(recovered.transcript.into_bytes())?,
- )?;
- doc.code(&recovery_transcript, "text")?;
- doc.paragraph((
- "The output now uses port ",
- &recovered_port,
- ". When validation fails, correct the input and retry; the last valid output remains available until the correction succeeds.",
- ))?;
-
- let diagnostics = serde_json::json!({
- "driver": "actual config-demo subprocess",
- "success_exit": successful.output.status.code(),
- "rejected_exit": rejected.output.status.code(),
- "recovery_exit": recovered.output.status.code(),
- "rejected_output_unchanged": after_rejection == saved_bytes,
- "saved_port": saved.port,
- "recovered_port": recovery_config.port,
- });
- let diagnostic_resource = doc.json("execution-checks", &diagnostics)?;
- doc.note_on(
- &rejection_transcript,
- (
- "The transcript comes from the actual command's stdout, stderr, and exit status. Review the ",
- &diagnostic_resource,
- " for the recorded outcomes. This contributor evidence is attached to the same reader artifact.",
- ),
- )?;
-
- Ok(vec![doc.finish()?])
-}
-
-fn write_fixture(directory: &Path, name: &str, bytes: &[u8]) -> Result<()> {
- fs::write(directory.join(name), bytes).map_err(|error| error.to_string())
-}
-
-struct Invocation {
- output: Output,
- exit_code: i32,
- transcript: String,
-}
-
-fn invoke(binary: &Path, directory: &Path, input: &str) -> Result {
- let arguments = [input, "--output", "settings.json"];
- let output = Command::new(binary)
- .current_dir(directory)
- .args(arguments)
- .output()
- .map_err(|error| format!("could not run the config-demo CLI: {error}"))?;
- let stdout = std::str::from_utf8(&output.stdout).map_err(|error| error.to_string())?;
- let stderr = std::str::from_utf8(&output.stderr).map_err(|error| error.to_string())?;
- let status = output
- .status
- .code()
- .ok_or("config-demo terminated without an exit code")?;
- // Only the executable's display name differs from the actual invocation.
- // Arguments and working-directory-relative paths are preserved verbatim.
- let transcript = format!(
- "$ config-demo {}\n{stdout}{stderr}[exit {status}]\n",
- arguments.join(" ")
- );
- Ok(Invocation {
- output,
- exit_code: status,
- transcript,
- })
-}
-
-fn json_artifact(bytes: Vec) -> Result {
- Artifact::new(bytes, "application/json", "json", "config-demo", "json-v1")
-}
-
-fn text_artifact(bytes: Vec) -> Result {
- Artifact::new(
- bytes,
- "text/plain",
- "txt",
- "config-demo-cli",
- "transcript-v1",
- )
-}
diff --git a/crates/doc-example-config/src/lib.rs b/crates/doc-example-config/src/lib.rs
deleted file mode 100644
index edacb25..0000000
--- a/crates/doc-example-config/src/lib.rs
+++ /dev/null
@@ -1,210 +0,0 @@
-//! A small configuration API used to exercise executable user documentation.
-
-use serde::{Deserialize, Serialize};
-use std::fmt;
-use std::fs::{self, OpenOptions};
-use std::io::{self, Write};
-use std::path::{Path, PathBuf};
-use std::sync::atomic::{AtomicU64, Ordering};
-
-/// The application configuration. Values are validated before saving.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct Config {
- pub port: u32,
-}
-
-impl Default for Config {
- fn default() -> Self {
- Self { port: 8080 }
- }
-}
-
-impl Config {
- pub fn validate(&self) -> Result<(), ConfigError> {
- if (1..=65535).contains(&self.port) {
- Ok(())
- } else {
- Err(ConfigError::InvalidPort(self.port))
- }
- }
-
- pub fn from_json(source: &str) -> Result {
- let config: Self = serde_json::from_str(source).map_err(ConfigError::Json)?;
- config.validate()?;
- Ok(config)
- }
-
- pub fn load(path: &Path) -> Result {
- let source = fs::read_to_string(path).map_err(|error| ConfigError::Io {
- operation: "read",
- path: path.to_owned(),
- error,
- })?;
- Self::from_json(&source)
- }
-
- /// Validate first, then replace the output with a complete adjacent file.
- /// A rejected configuration never creates or changes the output file.
- pub fn save(&self, output: &Path) -> Result<(), ConfigError> {
- self.validate()?;
- let mut bytes = serde_json::to_vec_pretty(self).map_err(ConfigError::Json)?;
- bytes.push(b'\n');
- write_atomic(output, &bytes).map_err(|error| ConfigError::Io {
- operation: "save",
- path: output.to_owned(),
- error,
- })
- }
-}
-
-#[derive(Debug)]
-pub enum ConfigError {
- InvalidPort(u32),
- Json(serde_json::Error),
- Io {
- operation: &'static str,
- path: PathBuf,
- error: io::Error,
- },
-}
-
-impl fmt::Display for ConfigError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::InvalidPort(port) => write!(
- formatter,
- "port must be between 1 and 65535 (received {port})."
- ),
- Self::Json(error) => write!(formatter, "invalid configuration JSON: {error}"),
- Self::Io {
- operation,
- path,
- error,
- } => write!(
- formatter,
- "could not {operation} {}: {error}",
- path.display()
- ),
- }
- }
-}
-
-impl std::error::Error for ConfigError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Self::Json(error) => Some(error),
- Self::Io { error, .. } => Some(error),
- Self::InvalidPort(_) => None,
- }
- }
-}
-
-static NEXT_TEMP_FILE: AtomicU64 = AtomicU64::new(0);
-
-struct PendingFile(PathBuf);
-
-impl Drop for PendingFile {
- fn drop(&mut self) {
- let _ = fs::remove_file(&self.0);
- }
-}
-
-fn write_atomic(output: &Path, bytes: &[u8]) -> io::Result<()> {
- let parent = output
- .parent()
- .filter(|path| !path.as_os_str().is_empty())
- .unwrap_or_else(|| Path::new("."));
- // Adjacent temporary files keep the final rename on one filesystem. Exclusive
- // creation preserves unrelated files even if a previous process left one behind.
- for _ in 0..128 {
- let sequence = NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed);
- let pending = parent.join(format!(
- ".config-demo-{}-{sequence}.tmp",
- std::process::id()
- ));
- let mut file = match OpenOptions::new()
- .write(true)
- .create_new(true)
- .open(&pending)
- {
- Ok(file) => file,
- Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
- Err(error) => return Err(error),
- };
- let pending = PendingFile(pending);
- file.write_all(bytes)?;
- file.sync_all()?;
- drop(file);
- fs::rename(&pending.0, output)?;
- return Ok(());
- }
- Err(io::Error::new(
- io::ErrorKind::AlreadyExists,
- "could not allocate an adjacent temporary file",
- ))
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn accepts_the_documented_port_bounds() {
- for port in [1, 8080, 65535] {
- assert!(Config { port }.validate().is_ok());
- }
- for port in [0, 65536, u32::MAX] {
- assert!(matches!(
- Config { port }.validate(),
- Err(ConfigError::InvalidPort(actual)) if actual == port
- ));
- }
- }
-
- #[test]
- fn parsing_rejects_unknown_fields_and_non_integer_ports() {
- assert!(Config::from_json(r#"{"port":8080,"prot":9090}"#).is_err());
- assert!(Config::from_json(r#"{"port":80.5}"#).is_err());
- assert!(Config::from_json(r#"{"port":-1}"#).is_err());
- assert!(Config::from_json("{}").is_err());
- }
-
- #[test]
- fn rejected_save_preserves_original_bytes_and_leaves_no_temporary_file() {
- let directory = tempfile::tempdir().unwrap();
- let output = directory.path().join("settings.json");
- let original = b"{\"port\":8080}\n";
- fs::write(&output, original).unwrap();
-
- assert!(Config { port: 0 }.save(&output).is_err());
- assert_eq!(fs::read(&output).unwrap(), original);
- assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 1);
- }
-
- #[test]
- fn valid_save_replaces_the_complete_document() {
- let directory = tempfile::tempdir().unwrap();
- let output = directory.path().join("settings.json");
- fs::write(&output, b"previous output").unwrap();
-
- Config { port: 9090 }.save(&output).unwrap();
- assert_eq!(Config::load(&output).unwrap(), Config { port: 9090 });
- assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 1);
- }
-
- #[test]
- fn failed_replacement_cleans_up_its_temporary_file() {
- let directory = tempfile::tempdir().unwrap();
- let output = directory.path().join("settings.json");
- fs::create_dir(&output).unwrap();
- fs::write(output.join("keep.txt"), "existing data").unwrap();
-
- assert!(Config { port: 9090 }.save(&output).is_err());
- assert_eq!(
- fs::read_to_string(output.join("keep.txt")).unwrap(),
- "existing data"
- );
- assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 1);
- }
-}
diff --git a/crates/doc-example-config/src/main.rs b/crates/doc-example-config/src/main.rs
deleted file mode 100644
index 75d21bf..0000000
--- a/crates/doc-example-config/src/main.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-use config_demo::Config;
-use std::ffi::OsString;
-use std::path::PathBuf;
-use std::process::ExitCode;
-
-const USAGE: &str = "Usage: config-demo INPUT.json --output OUTPUT.json";
-
-fn main() -> ExitCode {
- match run(std::env::args_os().skip(1).collect()) {
- Ok(()) => ExitCode::SUCCESS,
- Err(error) => {
- eprintln!("{error}");
- ExitCode::from(2)
- }
- }
-}
-
-fn run(arguments: Vec) -> Result<(), String> {
- if arguments.len() == 1 && arguments[0] == "--help" {
- println!("{USAGE}");
- println!("Validate a configuration and save it only when its port is valid.");
- return Ok(());
- }
- if arguments.len() != 3 || arguments[1] != "--output" {
- return Err(USAGE.to_owned());
- }
- let input = PathBuf::from(&arguments[0]);
- let output = PathBuf::from(&arguments[2]);
- let config =
- Config::load(&input).map_err(|error| format!("Configuration rejected: {error}"))?;
- config
- .save(&output)
- .map_err(|error| format!("Configuration not saved: {error}"))?;
- println!(
- "Saved validated configuration (port {}) to {}.",
- config.port,
- output.display()
- );
- Ok(())
-}
diff --git a/crates/doc-example-config/tests/cli.rs b/crates/doc-example-config/tests/cli.rs
deleted file mode 100644
index 5915e74..0000000
--- a/crates/doc-example-config/tests/cli.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use config_demo::Config;
-use std::fs;
-use std::process::{Command, Output};
-
-fn run(directory: &std::path::Path, input: &str) -> Output {
- Command::new(env!("CARGO_BIN_EXE_config-demo"))
- .current_dir(directory)
- .args([input, "--output", "settings.json"])
- .output()
- .unwrap()
-}
-
-#[test]
-fn actual_cli_rejects_without_replacing_and_recovers_after_correction() {
- let directory = tempfile::tempdir().unwrap();
- let input = directory.path().join("input.json");
- let output = directory.path().join("settings.json");
- fs::write(&input, r#"{"port":9090}"#).unwrap();
-
- let success = run(directory.path(), "input.json");
- assert!(success.status.success());
- assert_eq!(Config::load(&output).unwrap().port, 9090);
- let original = fs::read(&output).unwrap();
-
- fs::write(&input, r#"{"port":0}"#).unwrap();
- let rejected = run(directory.path(), "input.json");
- assert_eq!(rejected.status.code(), Some(2));
- assert!(rejected.stdout.is_empty());
- assert!(
- String::from_utf8(rejected.stderr)
- .unwrap()
- .contains("port must be between 1 and 65535")
- );
- assert_eq!(fs::read(&output).unwrap(), original);
-
- fs::write(&input, r#"{"port":8080}"#).unwrap();
- let recovered = run(directory.path(), "input.json");
- assert!(recovered.status.success());
- assert_eq!(Config::load(&output).unwrap().port, 8080);
- assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 2);
-}
-
-#[test]
-fn malformed_input_does_not_create_output() {
- let directory = tempfile::tempdir().unwrap();
- fs::write(directory.path().join("input.json"), "{unfinished").unwrap();
- let rejected = run(directory.path(), "input.json");
- assert_eq!(rejected.status.code(), Some(2));
- assert!(!directory.path().join("settings.json").exists());
-}
diff --git a/crates/doc-example-config/tests/guide.rs b/crates/doc-example-config/tests/guide.rs
deleted file mode 100644
index c77a9dc..0000000
--- a/crates/doc-example-config/tests/guide.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-#[path = "../examples/support/mod.rs"]
-#[allow(dead_code)]
-mod support;
-
-use executable_docs::{Audience, Document, store};
-use std::path::Path;
-
-#[test]
-fn guide_is_reproducible_and_keeps_contributor_evidence_out_of_reader_output() {
- let binary = Path::new(env!("CARGO_BIN_EXE_config-demo"));
- let first = support::guide(binary).unwrap();
- let second = support::guide(binary).unwrap();
- let reader = Document::render_many(&first, Audience::Reader).unwrap();
- let repeated = Document::render_many(&second, Audience::Reader).unwrap();
- assert_eq!(reader, repeated);
-
- let reader_text = reader
- .iter()
- .filter(|(path, _)| path.ends_with(".md"))
- .map(|(_, bytes)| std::str::from_utf8(bytes).unwrap())
- .collect::>()
- .join("\n");
- assert!(reader_text.contains("preserves every byte"));
- assert!(reader_text.contains("[exit 2]"));
- assert!(!reader_text.contains("contributor evidence"));
- assert!(!reader.keys().any(|path| path.contains("execution-checks")));
-
- let contributor = Document::render_many(&first, Audience::Contributor).unwrap();
- assert!(
- contributor
- .keys()
- .any(|path| path.contains("execution-checks"))
- );
- let contributor_text = contributor
- .iter()
- .filter(|(path, _)| path.ends_with(".md"))
- .map(|(_, bytes)| std::str::from_utf8(bytes).unwrap())
- .collect::>()
- .join("\n");
- assert!(contributor_text.contains("contributor evidence"));
-}
-
-#[test]
-fn guide_fails_if_the_actual_cli_cannot_run() {
- let directory = tempfile::tempdir().unwrap();
- let error = support::guide(&directory.path().join("missing-config-demo")).unwrap_err();
- assert!(error.contains("could not run"), "Unexpected error: {error}");
-}
-
-#[test]
-fn both_config_views_match_the_retained_contract() {
- let binary = Path::new(env!("CARGO_BIN_EXE_config-demo"));
- let documents = support::guide(binary).unwrap();
- let baseline = Path::new(env!("CARGO_MANIFEST_DIR")).join("baseline/config");
- for (audience, directory) in [
- (Audience::Reader, "reader"),
- (Audience::Contributor, "contributor"),
- ] {
- let files = Document::render_many(&documents, audience).unwrap();
- store::check(&baseline.join(directory), &files).unwrap();
- }
-}
diff --git a/crates/doc-harness/.gitignore b/crates/doc-harness/.gitignore
deleted file mode 100644
index bd30896..0000000
--- a/crates/doc-harness/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-# Disposable standalone verification output and local review candidates.
-/.cache/
-/target/
-__pycache__/
diff --git a/crates/doc-harness/Cargo.toml b/crates/doc-harness/Cargo.toml
deleted file mode 100644
index 75874e2..0000000
--- a/crates/doc-harness/Cargo.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[package]
-name = "executable-docs"
-version = "0.0.0"
-edition = "2024"
-rust-version = "1.95"
-publish = false
-license = "MIT"
-description = "Rust-authored executable documents with checked observations and owned artifacts"
-readme = "README.md"
-
-[dependencies]
-serde = { version = "=1.0.229", features = ["derive"] }
-serde_json = "=1.0.151"
-sha2 = "=0.10.9"
-same-file = "=1.0.6"
-pulldown-cmark = { version = "=0.13.0", default-features = false, features = [
- "html",
-] }
-
-[dev-dependencies]
-tempfile = "=3.27.0"
diff --git a/crates/doc-harness/LICENSE b/crates/doc-harness/LICENSE
deleted file mode 100644
index 943658a..0000000
--- a/crates/doc-harness/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2026 Grida
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/crates/doc-harness/README.md b/crates/doc-harness/README.md
deleted file mode 100644
index ce4e280..0000000
--- a/crates/doc-harness/README.md
+++ /dev/null
@@ -1,380 +0,0 @@
-# Executable documents
-
-Author a document while executing the behavior it explains. Ordinary Rust code
-supplies the actions and checks; the document collects prose, frozen observations,
-resources, and contributor notes. The same completed document can produce Reader
-and Contributor exports without executing the application again.
-
-This package is maintained in the N3 repository, with local consumers. Its
-API and manifest are experimental, and `publish = false` prevents registry
-publication. It does not depend on N3, egui, wgpu, a renderer, an agent skill, or a
-documentation website.
-
-## Start with an executable paragraph
-
-The example below is also a Rust documentation test. It needs only this crate:
-
-```rust
-use executable_docs::{Audience, Doc, Document};
-
-let mut doc = Doc::new("trim-text", "Trim surrounding whitespace")?;
-doc.heading(2, "Normalize a string")?;
-
-let normalized = " hello\n".trim();
-let result = doc.expect_eq("trimmed-value", normalized, "hello")?;
-doc.paragraph(("Trimming the input produces ", &result, "."))?;
-
-let output = doc.text_code("output", normalized, "text")?;
-doc.note_on(&output, "The fixture includes spaces and a trailing newline.")?;
-
-let document = doc.finish()?;
-let reader = Document::render_many(std::slice::from_ref(&document), Audience::Reader)?;
-let contributor = Document::render_many(&[document], Audience::Contributor)?;
-assert_eq!(reader["assets/trim-text/output.txt"], b"hello");
-assert!(!String::from_utf8_lossy(&reader["trim-text.md"]).contains("fixture"));
-assert!(String::from_utf8_lossy(&contributor["trim-text.md"]).contains("fixture"));
-# Ok::<(), String>(())
-```
-
-Add this package as a development dependency through its local path. A standalone
-copy of this directory retains explicit Cargo dependency versions, this README,
-tests, and its license; no workspace-inherited package fields are required.
-
-## Authoring and evidence
-
-- `heading`, `paragraph`, and `markdown` append document blocks. Paragraph tuples
- combine literal text with typed references without a separate template or
- placeholder registry. Plain paragraph text is escaped, while `markdown` accepts
- CommonMark with tables and strikethrough.
-- `heading_with_id(id, level, text)` assigns a stable section anchor when that
- section needs durable references. `Block::link(label)` gives those references
- reader-facing labels; the returned block also accepts attached notes. Named
- sections share the checked evidence namespace, so collisions fail explicitly.
-- `markdown_parts` combines authored Markdown with typed references and preserves
- the supplied whitespace, including the absence of a trailing newline. Use it
- when an existing document needs exact composition. `binding` freezes a host's
- observed text, code label, or key sequence; its typed handle emits escaped text,
- ``, or `` markup. The host must validate control witnesses and
- canonical key mappings. A binding is not a behavioral assertion.
-- `expect_eq(id, actual, expected)` executes an equality check and freezes its
- serialized observation. Its returned value handle can appear in prose.
- `require(id, condition)` records a predicate check. Meaningful checks remain the
- author's responsibility; the framework cannot prove arbitrary prose.
-- `resource(id, artifact)` registers owned bytes. `code` or `embed` places an
- existing resource. `resource_code` registers and places text while retaining its
- custom MIME, producer and comparison profile. The same handle can be reused in
- prose, placed again, or annotated without rerunning its producer.
- `Resource::image(alt)` and `Resource::link(label)` provide inline views;
- image views require a declared image MIME type. `resource_at` preserves an
- existing safe artifact path, while complete exports reject duplicate owners
- across documents and generated pages.
-- `text`, `json`, `text_code`, and `json_code` are conveniences for the built-in
- UTF-8 and deterministic JSON encoders. Their producer labels identify those
- encoders. Use `Artifact::new` or `Artifact::from_file` when application or external
- producer attribution matters. File bytes are read immediately, not at export.
-- `note` appends contributor commentary; `note_on` attaches it to a block, check,
- checked value, or resource. Notes have their own block counter, so adding notes
- cannot renumber reader blocks. Reader exports omit notes, their exclusive
- resources, and their relations and producer profiles.
-
-Handles belong to one document run. Duplicate evidence IDs, foreign handles,
-wrong text types, unreferenced resources, and failed checks fail explicitly.
-Ignoring a failed builder operation does not make the document valid: it remains
-tainted and `finish()` rejects it. A completed document needs at least one
-successful check. Public block IDs are currently positional; inserting public
-content can change later block anchors. Check and resource IDs are author-owned.
-
-`finish()` freezes and validates the authored document independently of its future
-file layout. Export then validates resource use, page and artifact paths, local
-links, and the complete selected audience. A finished document alone is not a
-publishable bundle; every export performs those remaining checks before returning
-bytes, and the runner prepares every requested export before publication begins.
-
-For readable inline sentences, the optional named form of `prose!` keeps the
-text together and inserts typed handles. It returns a result because missing,
-duplicate, unused, or malformed bindings fail rather than silently dropping text
-or evidence. Use `{{` and `}}` for literal braces; format specifiers are not
-supported. Each binding expression runs once, even when its name appears more
-than once. This composition lives in the scenario, without a separate template.
-
-```rust
-use executable_docs::{prose, Doc};
-
-let mut doc = Doc::new("retry", "Retry an operation")?;
-let section = doc.heading_with_id("retry-limit", 2, "Choose a retry limit")?;
-let count = doc.expect_eq("attempts", 3, 3)?;
-doc.paragraph(prose!("The operation takes {count} attempts.", count = &count)?)?;
-doc.paragraph(("See ", section.link("the retry limit"), "."))?;
-doc.note_on(§ion, "Keep the section ID when moving this explanation.")?;
-let document = doc.finish()?;
-# Ok::<(), String>(())
-```
-
-The expression-list form of `prose!` accepts any number of ordinary Rust
-expressions and works with `paragraph`, `markdown_parts`, and notes. Tuples remain
-convenient for shorter sequences. Prefer ordinary blocks for new documents;
-`markdown_parts` preserves exact Markdown bytes for an existing host format.
-
-```rust
-use executable_docs::{prose, Doc};
-
-let mut doc = Doc::new("retry-limit", "Retry limit")?;
-let count = doc.expect_eq("attempts", 3, 3)?;
-doc.markdown_parts(prose!["The operation takes **", &count, "** attempts.\n"])?;
-let document = doc.finish()?;
-# Ok::<(), String>(())
-```
-
-Markdown references are parsed with `pulldown-cmark`, including reference-style
-links, escaping, inline code and fenced code. Definitions resolve within each
-`markdown` or `markdown_parts` block. Literal raw HTML and unresolved references
-fail; typed code/key bindings generate only their escaped, predefined HTML.
-The completed page is parsed again: generated tags must remain tags, authored
-HTML must remain excluded, and code fences or other containers must not swallow
-later document blocks. Place captured code with `code`/`resource_code`; do not
-wrap typed bindings in Markdown code delimiters. Typed artifact destinations are
-URL-encoded without changing their owned filesystem names.
-Local links must resolve
-within the actual audience's exported bundle; fragments refer to generated block
-IDs, not downstream website heading slugs. HTTP(S) and `mailto:` links are allowed
-without checking remote availability. An artifact's declared MIME type does not
-validate its binary contents, and producer metadata is attribution rather than
-cryptographic proof of origin.
-
-JSON resources and frozen equality observations recursively sort object keys,
-including when a consumer enables `serde_json/preserve_order`. Array order is
-preserved. Final manifests use the same canonical object ordering.
-
-## Exports, comparison and publication
-
-`Document::render_many` returns a sorted map of relative paths to bytes: Markdown
-pages, resources under `assets//.`, and
-`manifest.json`. It freezes no additional application state and performs no
-filesystem publication. The package's small `runner` module accepts a generation
-callback and exposes `check`, `build --out PATH`, and `update`, with Reader,
-Contributor, or both audiences. The `store` module owns those filesystem effects.
-
-### Configure your project's layout
-
-The SDK does not discover a repository, read Cargo metadata, or prescribe where
-document functions, fixtures, skills, or application binaries live. Your callback
-supplies completed documents. Configure export paths separately from authoring:
-
-```rust,no_run
-use executable_docs::{Audience, Doc, ExportLayout, runner::RunnerConfig};
-# let project_root = std::env::current_dir().unwrap();
-
-let layout = ExportLayout::default()
- .pages_under("reference")?
- .resources_under("downloads")?
- .page("setup", "manual/getting-started/index.md")?;
-
-let config = RunnerConfig::new(&project_root)?
- .output(Audience::Reader, "website/generated", "public")?
- .output(Audience::Contributor, "review/evidence", "internal")?
- .layout(layout);
-
-executable_docs::runner::run_cli_with(std::env::args().skip(1), &config, || {
- let mut doc = Doc::new("setup", "Set up the application")?;
- let value = doc.expect_eq("trimmed", " ready ".trim(), "ready")?;
- doc.paragraph(("The normalized value is ", &value, "."))?;
- Ok(vec![doc.finish()?])
-})?;
-# Ok::<(), String>(())
-```
-
-`project_root` is a path chosen by your host. `RunnerConfig::new` captures that
-base once; relative baseline paths and CLI `--out` paths resolve against it.
-Absolute destinations may be outside the project. The base need not contain a
-Cargo manifest or Git repository, and the runner never changes the working
-directory. Fixture paths and process working directories remain the callback's
-responsibility.
-
-Each `output` selects an audience, its retained baseline, and its relative
-directory inside a fresh build destination. In this example, `update` writes
-`website/generated` and `review/evidence` beneath the chosen base;
-`build --out candidate` writes `candidate/public` and `candidate/internal`.
-An empty build directory name exports a single selected audience directly at
-the build root. Configured CLI runs select all configured audiences by default;
-`default_audiences` or `--audience` can narrow that selection. Call
-`runner::run(&config, RunMode::Check, &[Audience::Reader], generate)` to integrate
-with a test, build tool, or your own CLI parser. The convenience `run_cli` keeps
-its original `reader`/`contributor` directories and reader-only default.
-
-`pages_under` and `resources_under` accept an empty prefix for the bundle root.
-Per-document `page` overrides are exact bundle-relative paths; they do not inherit
-the page prefix. `Doc::resource_at` paths are also exact and do not inherit the
-resource prefix. Page contents remain Markdown regardless of the chosen filename.
-Typed resource links are calculated relative to the actual page location.
-Authored Markdown links use that same page-relative convention: `..` may reach
-another location within the bundle but cannot escape it. Missing targets, unknown
-page overrides, duplicate paths, and file/directory collisions fail, including
-collisions with contributor-only resources.
-
-For in-memory integration, use `Document::render_many_with(documents, audience,
-&layout)` or `document.render_with(audience, &layout)`. No filesystem runner is
-required. The configurable layout belongs to full exports; fragment adapters keep
-their existing explicit resource paths and host-owned page assembly.
-
-The [configured example](examples/configured.rs) demonstrates host-owned fixtures,
-external baselines, nested pages, renamed audience directories, and private
-resources. Its positional arguments are `PROJECT_ROOT BASELINE_ROOT`, followed by
-the standard SDK commands. The fixture convention in that example belongs to its
-host, not the framework.
-
-Configuration does not weaken the evidence contract. The manifest filename
-`manifest.json`, store metadata `.ownership.json`, audience rules, exact comparison,
-and path-safety checks remain fixed. Retained audience trees must be separate, and
-build output must not overlap any configured baseline. Moving an owned page is an
-explicit retirement of its previous path, not silent deletion.
-Each output also reserves adjacent `.NAME.lock`, `.NAME.staging`, and
-`.NAME.backup` paths. The runner rejects configurations that place another output
-inside those transaction paths before generation, including fresh build roots.
-Artifact inventory keys always use `/`, independently of host path separators.
-
-Before creating audience trees, the runner also rejects paths that could alias on
-a case-insensitive or Unicode-normalizing filesystem. Existing directories are
-compared by filesystem identity. Where separation depends on missing directory
-names, use distinct ASCII names such as `public` and `internal`; case alone is not
-a distinction. Unicode roots and suffixes are supported, but if Unicode names are
-the only distinction, create the separate directories first so their identities
-can be checked. Missing names ending in dots or spaces, or containing `~`, also
-cannot establish separation because some filesystems treat them as aliases.
-This conservative preflight is read-only and does not guess the
-filesystem's normalization policy. It builds on `lifecycle::ensure_separate_paths`,
-which checks ancestry and existing directory identities. These checks assume a
-cooperative filesystem; they do not reserve paths against another process changing
-them after preflight.
-
-Checking compares the complete owned tree without writing. Building requires a
-fresh destination separate from the retained baseline. Updating is explicit and
-stages the complete candidate before replacing a compatible owned destination.
-Ordinary publication failures preserve or restore a backup; this is not a
-crash-atomic transaction. Multiple audiences publish independently. A successful
-update records candidate bytes; it does not establish human review or approve a
-release. Sites, navigation, styling, review policy, and application drivers belong
-to each consumer.
-
-An existing host can use `Document::render_fragment` to retain its page title,
-artifact paths and formatting. A fragment adds no title or block anchors, and
-rejects typed block links that would point to omitted anchors. The host owns
-whole-bundle link validation for that export. Handle ownership, image kinds,
-resource accounting, and audience exclusions still apply. This adapter seam
-does not weaken ordinary `render_many` bundle checking.
-`Document::resource_files(audience)` returns only that audience's frozen resource
-paths and bytes, without a page or manifest. Fragment adapters must apply the same
-audience to both text and copied files so contributor-only resources stay private.
-
-The lower-level `lifecycle` module shares preparation, complete tree comparison,
-path validation, staging, publication, and recovery with hosts that retain their
-own manifest format. `Ownership::Dedicated` adds no metadata to a host-owned tree
-and refuses unowned preexisting files; `Ownership::Recorded` retains explicit
-ownership metadata for SDK exports. Host compatibility rules are explicit
-callbacks. The structured `store` wrapper validates the SDK schema, identities,
-references, complete artifact inventory, byte lengths and hashes, audience, and
-producer/profile metadata before publication. Checks and recovery also validate
-the retained tree. Updating permits an explicitly removed obsolete artifact only
-when the complete candidate also omits that path; retained bytes must still match
-their receipt. The generic lifecycle does not interpret application evidence or
-claim that every byte is reproducible across different environments.
-
-Manifest schema version 1 records:
-
-| Field | Meaning |
-| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `schema_version`, `audience` | Interpretation and visibility policy for this export. |
-| `profiles` | Sorted producer/profile pairs for exported resources. |
-| `documents` | Document IDs, titles, visible block IDs and reference relations, executed checks, and any visible typed bindings. |
-| `artifacts` | Every exported page and resource except the manifest itself, including byte length and SHA-256; resources also identify owner, MIME, producer and profile. |
-
-Check records contain their ID, kind, and passed status. Contributor records also
-contain observed and expected values. Reader records include those payloads only
-when a public typed reference uses the value; check IDs and status are still
-public. Treat evidence IDs as public metadata. Profiles describe the producer's
-comparison conditions and must be chosen honestly by the adapter; they are not a
-complete environment fingerprint. The manifest contains no run timestamp,
-absolute source path, or process ID. Full tree comparison checks the manifest's
-bytes as well as artifact bytes; a manifest cannot contain its own final hash.
-
-The initial schema is not a cross-language execution protocol or a promised
-long-term compatibility standard. See the source and tests for its current
-failure semantics. No crate release, registry publication, or repository split is
-implied by this maintained local package.
-
-### Review exported documents
-
-The `preview` example renders a valid SDK export into a new disposable HTML tree.
-It validates the source's complete receipt and ownership, then uses artifact
-identities to convert pages, resolve page links, and preserve download bytes.
-Custom page extensions work; a Markdown resource remains a download. Tables and
-strikethrough use the same options as compilation. Page/resource output collisions
-fail before writing. Reader previews contain only Reader-exported resources.
-
-After generating the [consumer example's candidate](../doc-example-config/README.md#review-intentional-documentation-changes),
-run from this package directory:
-
-```sh
-cargo run --example preview -- ../doc-example-config/.cache/config-review/reader .cache/config-reader-preview
-cargo run --example preview -- ../doc-example-config/.cache/config-review/contributor .cache/config-contributor-preview
-```
-
-Open the resulting HTML pages in a browser and inspect both audiences and their
-resources before explicitly accepting a baseline. The preview requires a new
-destination and omits receipt metadata because its HTML differs from the exported
-Markdown. It does not update the export or accept a baseline.
-
-## Develop and verify the portable packages
-
-From this package directory, run the package-owned GPU-free gate:
-
-```sh
-python3 dev/verify.py
-python3 dev/verify.py --standalone --toolchain 1.95.0 --offline
-```
-
-The first command uses the containing workspace's existing lockfile and host
-toolchain. It checks Rust formatting for only the SDK and consumer packages,
-warnings-denied Clippy, all targets including examples and exact Reader/Contributor
-baselines, both packages' doctests, and warnings-denied API documentation. It also
-runs the development harness's Python regressions. Python 3 and Rust with rustfmt
-and Clippy are required; no N3 setup command or graphics adapter is involved.
-N3's `just doc-framework-test` and its CI invoke this same gate.
-
-The standalone command copies both sibling packages, tests, examples, baselines,
-and license notices into a disposable minimal workspace. It resolves a temporary
-lockfile independently of N3's lockfile, patches, assets, and dependencies, then
-runs the same gate on the explicitly selected installed toolchain. Rust 1.95.0
-is the declared and verified minimum. `just doc-framework-smoke --offline` runs
-this check from N3. Omit `--offline` to allow dependency resolution when caches
-are cold. Temporary resolution can select different compatible transitive
-versions from N3's lockfile; its lockfile is deleted with the temporary workspace.
-No second permanent workspace or lockfile is maintained here.
-
-Explicit `CARGO_HOME`, `CARGO_TARGET_DIR`, and offline environment settings are
-preserved. Relative cache paths retain their meaning from the invocation directory
-when checks change their working directory; absolute paths and empty overrides
-are passed through. Without a target override, standalone builds reuse the ignored
-package-local `.cache/standalone-target` directory. Omitting `--toolchain` uses
-the installed Cargo toolchain selected in the temporary workspace; it does not
-inherit N3's toolchain pin.
-
-Authored documentation consists of the two package READMEs and Rust documentation
-comments. Rust formatting belongs to this gate. In N3, the authored READMEs use
-`just fmt-docs`; the portable gate adds no Node.js requirement. Generated example
-baselines, guide pages, manifests, and resources are excluded from prose
-formatting and change only through explicit candidate review and baseline update.
-Independent repository extraction will carry this development harness and both
-packages together.
-
-## License and provenance
-
-The package originates in N3's executable documentation harness and its local
-framework experiment. It uses the repository's existing MIT license, copied
-verbatim into the packaged `LICENSE` file so a standalone copy preserves its notice.
-The notice retains `Copyright (c) 2026 Grida`. Dependency licenses remain those of
-their respective packages; no dependency code is vendored here.
-The pinned direct dependency manifests declare MIT for `pulldown-cmark`,
-`Unlicense/MIT` for `same-file`, and
-MIT OR Apache-2.0 for `serde`, `serde_json`, `sha2`, and the `tempfile` development
-dependency. This records the current source metadata rather than changing any
-upstream terms.
diff --git a/crates/doc-harness/dev/tests/test_verify.py b/crates/doc-harness/dev/tests/test_verify.py
deleted file mode 100644
index 0466136..0000000
--- a/crates/doc-harness/dev/tests/test_verify.py
+++ /dev/null
@@ -1,133 +0,0 @@
-import contextlib
-import importlib.util
-import io
-import os
-from pathlib import Path
-import tempfile
-import unittest
-from unittest.mock import patch
-
-
-SPEC = importlib.util.spec_from_file_location("portable_verify", Path(__file__).resolve().parents[1] / "verify.py")
-verify = importlib.util.module_from_spec(SPEC)
-SPEC.loader.exec_module(verify)
-
-
-class PortableVerificationTests(unittest.TestCase):
- def setUp(self):
- temporary = tempfile.TemporaryDirectory()
- self.addCleanup(temporary.cleanup)
- self.root = Path(temporary.name) / "host with spaces $(not-a-command)"
- self.sdk = self.root / "crates/doc-harness"
- self.example = self.root / "crates/doc-example-config"
- for package in (self.sdk, self.example):
- (package / "src").mkdir(parents=True)
- (package / "src/lib.rs").write_text("// Portable source.\n")
- (package / "Cargo.toml").write_text('[package]\nlicense = "MIT"\n')
- (package / "README.md").write_text("# Package\n")
- (package / "LICENSE").write_text("MIT License\nCopyright notice\n")
-
- def test_copy_preserves_baselines_and_license_without_host_dependencies_or_output(self):
- (self.root / "Cargo.toml").write_text("host patches and graphics dependencies\n")
- (self.root / "Cargo.lock").write_text("host lockfile\n")
- (self.root / "rust-toolchain.toml").write_text("host toolchain pin\n")
- baseline = self.example / "baseline/reader"
- baseline.mkdir(parents=True)
- (baseline / "manifest.json").write_bytes(b'{"exact":"baseline"}\n')
- (baseline / ".ownership.json").write_bytes(b'{"owned":"receipt"}\n')
- download = baseline / "assets/target/Cargo.lock"
- download.parent.mkdir(parents=True)
- download.write_bytes(b"An owned download, not a build lockfile.\n")
- private_download = baseline / "assets/.cache/__pycache__/receipt.txt"
- private_download.parent.mkdir(parents=True)
- private_download.write_bytes(b"Owned bytes also survive cache-like names.\n")
- for excluded in (".cache", "target", "__pycache__"):
- (self.sdk / excluded).mkdir()
- (self.sdk / excluded / "disposable").write_text("ignored\n")
- (self.sdk / "Cargo.lock").write_text("package lockfile is not portable input\n")
- destination = self.root.parent / "isolated"
- copied = verify.payload(self.sdk, destination)
- self.assertEqual(copied, destination / "crates/doc-harness")
- self.assertNotIn("host", (destination / "Cargo.toml").read_text())
- self.assertFalse((destination / "Cargo.lock").exists())
- self.assertFalse((destination / "rust-toolchain.toml").exists())
- for excluded in (".cache", "target", "__pycache__", "Cargo.lock"):
- self.assertFalse((copied / excluded).exists())
- for package in ("doc-harness", "doc-example-config"):
- self.assertEqual((destination / "crates" / package / "LICENSE").read_bytes(), b"MIT License\nCopyright notice\n")
- copied_baseline = destination / "crates/doc-example-config/baseline/reader"
- for name in ("manifest.json", ".ownership.json"):
- self.assertEqual((copied_baseline / name).read_bytes(), (baseline / name).read_bytes())
- self.assertEqual((copied_baseline / "assets/target/Cargo.lock").read_bytes(), download.read_bytes())
- self.assertEqual((copied_baseline / "assets/.cache/__pycache__/receipt.txt").read_bytes(), private_download.read_bytes())
-
- def test_copy_fails_if_provenance_is_missing(self):
- (self.example / "LICENSE").unlink()
- with self.assertRaisesRegex(OSError, "LICENSE"):
- verify.payload(self.sdk, self.root.parent / "isolated")
-
- def test_standalone_resolves_a_temporary_lock_and_failure_stops_the_gate(self):
- calls = []
-
- def fail_resolution(command, *, cwd, environment):
- calls.append((command, cwd, environment))
- self.assertTrue((cwd.parent.parent / "Cargo.toml").exists())
- self.assertFalse((cwd.parent.parent / "Cargo.lock").exists())
- return 17
-
- with patch.object(verify, "run", side_effect=fail_resolution):
- self.assertEqual(verify.main(["--standalone", "--toolchain", "1.95.0", "--offline"], sdk=self.sdk), 17)
- self.assertEqual(calls[0][0], ["cargo", "+1.95.0", "generate-lockfile", "--offline"])
- self.assertEqual(len(calls), 1)
- self.assertFalse(calls[0][1].exists())
- self.assertFalse((self.root / "Cargo.lock").exists())
-
- def test_failing_consumer_checks_stop_before_doctests_and_docs(self):
- calls = []
-
- def fail_tests(command, *, cwd, environment):
- calls.append(command)
- return 19 if "--all-targets" in command and "test" in command else 0
-
- with patch.object(verify, "run", side_effect=fail_tests):
- self.assertEqual(verify.main([], sdk=self.sdk), 19)
- self.assertEqual(len(calls), 4)
- self.assertFalse(any("doc" in command or "--doc" in command for command in calls))
-
- def test_environment_keeps_explicit_caches_and_doc_flags_without_mutating_caller(self):
- environment = {"CARGO_TARGET_DIR": "/explicit target", "CARGO_HOME": "/explicit cargo", "RUSTDOCFLAGS": "--cfg custom"}
- with patch.dict(os.environ, environment, clear=True):
- prepared = verify.environment_for(self.sdk, True)
- self.assertEqual(prepared["CARGO_TARGET_DIR"], "/explicit target")
- self.assertEqual(prepared["CARGO_HOME"], "/explicit cargo")
- self.assertNotIn("PYTHONDONTWRITEBYTECODE", os.environ)
- with patch.object(verify, "run", return_value=0) as run:
- self.assertEqual(verify.checks(self.sdk, ["cargo"], prepared, False), 0)
- docs = run.call_args.kwargs["environment"]
- self.assertEqual(docs["RUSTDOCFLAGS"], "--cfg custom -D warnings")
- self.assertEqual(prepared["RUSTDOCFLAGS"], "--cfg custom")
-
- def test_relative_caches_keep_the_invocation_location_when_check_cwd_changes(self):
- for standalone in (False, True):
- with self.subTest(standalone=standalone), patch.dict(os.environ, {
- "CARGO_HOME": "cache with spaces $(not-a-command)",
- "CARGO_TARGET_DIR": "../shared target",
- }, clear=True), patch.object(verify.Path, "cwd", return_value=self.root):
- prepared = verify.environment_for(self.sdk, standalone)
- self.assertEqual(prepared["CARGO_HOME"], str(self.root / "cache with spaces $(not-a-command)"))
- self.assertEqual(prepared["CARGO_TARGET_DIR"], str(self.root / "../shared target"))
- self.assertEqual(os.environ["CARGO_HOME"], "cache with spaces $(not-a-command)")
- with patch.dict(os.environ, {"CARGO_HOME": "", "CARGO_TARGET_DIR": ""}, clear=True):
- prepared = verify.environment_for(self.sdk, True)
- self.assertEqual(prepared["CARGO_HOME"], "")
- self.assertEqual(prepared["CARGO_TARGET_DIR"], "")
-
- def test_missing_cargo_fails_visibly(self):
- output = io.StringIO()
- with contextlib.redirect_stderr(output), patch.object(verify, "run", side_effect=FileNotFoundError("cargo unavailable")):
- self.assertEqual(verify.main([], sdk=self.sdk), 1)
- self.assertIn("cargo unavailable", output.getvalue())
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/crates/doc-harness/dev/verify.py b/crates/doc-harness/dev/verify.py
deleted file mode 100644
index 8523521..0000000
--- a/crates/doc-harness/dev/verify.py
+++ /dev/null
@@ -1,123 +0,0 @@
-"""Verify the portable Rust SDK and its real consumers without application replay."""
-
-import argparse
-import os
-from pathlib import Path
-import shutil
-import subprocess
-import sys
-import tempfile
-
-
-SDK = Path(__file__).resolve().parents[1]
-PACKAGES = ("executable-docs", "doc-example-config")
-WORKSPACE = """[workspace]
-members = ["crates/doc-harness", "crates/doc-example-config"]
-resolver = "3"
-"""
-
-
-def status(returncode):
- return returncode if returncode >= 0 else 128 - returncode
-
-
-def run(command, *, cwd, environment):
- # Argument lists preserve paths and toolchain names as data, including spaces.
- print("Portable check: " + " ".join(command), flush=True)
- return status(subprocess.run(command, cwd=cwd, env=environment, check=False).returncode)
-
-
-def payload(sdk, destination):
- """Copy the maintained packages, without the host workspace or disposable output."""
- for source in (sdk, sdk.parent / "doc-example-config"):
- for required in ("Cargo.toml", "README.md", "LICENSE", "src"):
- if not (source / required).exists():
- raise OSError(f"Portable payload is missing {source / required}")
- def disposable(directory, names):
- directory = Path(directory)
- excluded = set()
- if directory == source:
- excluded.update((".cache", "target", "Cargo.lock", "__pycache__"))
- if directory == source / "dev" or source / "dev" in directory.parents:
- excluded.add("__pycache__")
- return excluded.intersection(names)
-
- shutil.copytree(
- source,
- destination / "crates" / source.name,
- ignore=disposable,
- )
- (destination / "Cargo.toml").write_text(WORKSPACE, encoding="utf-8")
- return destination / "crates/doc-harness"
-
-
-def environment_for(sdk, standalone):
- environment = os.environ.copy()
- environment["PYTHONDONTWRITEBYTECODE"] = "1"
- invocation = Path.cwd()
- for variable in ("CARGO_HOME", "CARGO_TARGET_DIR"):
- value = environment.get(variable)
- if value and not Path(value).is_absolute():
- # Cargo interprets these against its working directory. Capture the
- # caller's location before native/standalone checks change that cwd.
- environment[variable] = str(invocation / value)
- if standalone:
- # Keep rebuilds cheap while preserving explicit native/container overrides.
- environment.setdefault("CARGO_TARGET_DIR", str(sdk / ".cache/standalone-target"))
- return environment
-
-
-def checks(sdk, cargo, environment, offline):
- selection = [part for package in PACKAGES for part in ("-p", package)]
- locked = ["--locked", *(["--offline"] if offline else [])]
- commands = [
- [sys.executable, "-m", "unittest", "discover", "-s", str(sdk / "dev/tests"), "-p", "test_*.py"],
- [*cargo, "fmt", *selection, "--check"],
- [*cargo, "clippy", *locked, *selection, "--all-targets", "--", "-D", "warnings"],
- [*cargo, "test", *locked, *selection, "--all-targets"],
- [*cargo, "test", *locked, *selection, "--doc"],
- [*cargo, "doc", *locked, *selection, "--no-deps"],
- ]
- for command in commands:
- command_environment = environment
- if command[:len(cargo) + 1] == [*cargo, "doc"]:
- command_environment = environment.copy()
- command_environment["RUSTDOCFLAGS"] = environment.get("RUSTDOCFLAGS", "") + " -D warnings"
- result = run(command, cwd=sdk, environment=command_environment)
- if result:
- return result
- return 0
-
-
-def main(arguments=None, *, sdk=SDK):
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--standalone", action="store_true", help="copy both packages to a disposable minimal workspace")
- parser.add_argument("--toolchain", help="select an installed rustup toolchain, e.g. 1.95.0")
- parser.add_argument("--offline", action="store_true", help="resolve and verify using cached Cargo dependencies")
- options = parser.parse_args(arguments)
- cargo = ["cargo", *([f"+{options.toolchain}"] if options.toolchain else [])]
- try:
- sdk = Path(sdk).resolve()
- environment = environment_for(sdk, options.standalone)
- if not options.standalone:
- return checks(sdk, cargo, environment, options.offline)
- with tempfile.TemporaryDirectory(prefix="executable-docs-") as temporary:
- isolated = payload(sdk, Path(temporary))
- # Resolve only portable dependencies into a temporary lockfile. Never
- # copy or modify the application's lockfile, patches, or toolchain pin.
- result = run(
- [*cargo, "generate-lockfile", *(["--offline"] if options.offline else [])],
- cwd=isolated, environment=environment,
- )
- if result:
- return result
- return checks(isolated, cargo, environment, options.offline)
- except OSError as error:
- print(f"Portable verification failed: {error}", file=sys.stderr)
- return 1
- except KeyboardInterrupt:
- return 130
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/crates/doc-harness/examples/configured.rs b/crates/doc-harness/examples/configured.rs
deleted file mode 100644
index ae13898..0000000
--- a/crates/doc-harness/examples/configured.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-//! The host owns its project root, fixtures, audience destinations and export layout.
-//! Run: configured PROJECT_ROOT BASELINE_ROOT check|update|build [SDK options]
-//! PROJECT_ROOT contains fixtures/ports.txt with two nonzero ports, one per line.
-use executable_docs::{
- Audience, Doc, Document, ExportLayout, Result, prose,
- runner::{RunnerConfig, run_cli_with},
-};
-use std::path::Path;
-
-fn main() {
- if let Err(error) = execute() {
- eprintln!("{error}");
- std::process::exit(1);
- }
-}
-
-fn execute() -> Result<()> {
- let mut args = std::env::args().skip(1);
- let usage = "Usage: configured PROJECT_ROOT BASELINE_ROOT check|update|build [SDK options]";
- let project = args.next().ok_or(usage)?;
- let baseline = args.next().ok_or(usage)?;
- let project = Path::new(&project)
- .canonicalize()
- .map_err(|error| format!("Cannot resolve the project root: {error}"))?;
- let config = configuration(&project, Path::new(&baseline))?;
- run_cli_with(args, &config, || guide(&project))
-}
-
-fn configuration(project: &Path, baseline: &Path) -> Result {
- let layout = ExportLayout::default()
- .pages_under("handbook")?
- .resources_under("downloads/evidence")?
- .page("ports", "manual/network/index.md")?;
- Ok(RunnerConfig::new(project)?
- .output(Audience::Reader, baseline.join("published"), "website")?
- .output(
- Audience::Contributor,
- baseline.join("review-notes"),
- "maintainers",
- )?
- .layout(layout))
-}
-
-fn guide(project: &Path) -> Result> {
- let text = std::fs::read_to_string(project.join("fixtures/ports.txt"))
- .map_err(|error| format!("Cannot read the project's ports fixture: {error}"))?;
- let ports: Vec = text
- .lines()
- .map(|line| line.parse::().map_err(|error| error.to_string()))
- .collect::>()?;
- let mut doc = Doc::new("ports", "Read configured ports")?;
- let count = doc.expect_eq("fixture-port-count", ports.len(), 2)?;
- doc.require("ports-are-nonzero", ports.iter().all(|port| *port != 0))?;
- doc.paragraph(prose!(
- "The fixture supplies {count} nonzero ports.",
- count = &count
- )?)?;
- let input = doc.text_code("input", text, "text")?;
- doc.paragraph(("Download ", &input, " to reuse the fixture."))?;
- let observation = doc.json("private-observation", &ports)?;
- doc.note_on(
- &input,
- (
- "The private parsed-port observation is ",
- &observation,
- ". It is included only in contributor exports.",
- ),
- )?;
-
- let mut overview = Doc::new("overview", "Fixture contract")?;
- overview.require("fixture-was-parsed", !ports.is_empty())?;
- overview.paragraph("This project reads its own fixture before composing the documents.")?;
- Ok(vec![doc.finish()?, overview.finish()?])
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use executable_docs::{lifecycle, runner::RunMode, runner::run};
- use std::sync::atomic::{AtomicUsize, Ordering};
-
- #[test]
- fn project_layout_and_audience_destinations_are_host_owned() {
- let temporary = tempfile::tempdir().unwrap();
- let root = temporary.path().canonicalize().unwrap();
- let project = root.join("source-only-project");
- let baseline = root.join("external-publisher/retained");
- std::fs::create_dir_all(project.join("fixtures")).unwrap();
- std::fs::create_dir_all(&baseline).unwrap();
- std::fs::write(project.join("fixtures/ports.txt"), "8080\n9090\n").unwrap();
- std::fs::write(baseline.join("publisher-owned.txt"), "Keep this file.\n").unwrap();
- let config = configuration(&project, &baseline).unwrap();
- let audiences = [Audience::Reader, Audience::Contributor];
- let executions = AtomicUsize::new(0);
- let generate = || {
- executions.fetch_add(1, Ordering::SeqCst);
- guide(&project)
- };
-
- run(&config, RunMode::Update, &audiences, generate).unwrap();
- assert_eq!(executions.load(Ordering::SeqCst), 1);
- run(&config, RunMode::Check, &audiences, generate).unwrap();
- let candidate = root.join("separate-review-output");
- run(
- &config,
- RunMode::Build(candidate.clone()),
- &audiences,
- generate,
- )
- .unwrap();
- assert_eq!(executions.load(Ordering::SeqCst), 3);
-
- let reader = lifecycle::read_tree(&candidate.join("website")).unwrap();
- let contributor = lifecycle::read_tree(&candidate.join("maintainers")).unwrap();
- assert!(reader.contains_key("manual/network/index.md"));
- assert!(reader.contains_key("handbook/overview.md"));
- assert!(reader.contains_key("downloads/evidence/ports/input.txt"));
- assert!(
- !reader
- .keys()
- .any(|path| path.contains("private-observation"))
- );
- assert!(
- contributor
- .keys()
- .any(|path| path.contains("private-observation"))
- );
- for (name, files) in [("published", reader), ("review-notes", contributor)] {
- assert_eq!(files, lifecycle::read_tree(&baseline.join(name)).unwrap());
- for bytes in files.values() {
- let text = String::from_utf8_lossy(bytes);
- assert!(!text.contains(root.to_str().unwrap()));
- }
- }
- assert_eq!(
- std::fs::read_to_string(baseline.join("publisher-owned.txt")).unwrap(),
- "Keep this file.\n"
- );
- assert!(!project.join("Cargo.toml").exists());
- assert!(!project.join(".git").exists());
- assert!(!candidate.join("reader").exists());
- assert!(!candidate.join("contributor").exists());
- }
-}
diff --git a/crates/doc-harness/examples/preview.rs b/crates/doc-harness/examples/preview.rs
deleted file mode 100644
index 992550c..0000000
--- a/crates/doc-harness/examples/preview.rs
+++ /dev/null
@@ -1,488 +0,0 @@
-//! Disposable static rendering for human review, separate from guide generation.
-use executable_docs::{
- Result,
- lifecycle::{self, Files, Ownership},
-};
-use pulldown_cmark::{Event, Options, Parser, Tag, html};
-use std::{
- collections::{BTreeMap, BTreeSet},
- env,
- path::Path,
-};
-
-fn main() -> std::result::Result<(), Box> {
- let args: Vec<_> = env::args().skip(1).collect();
- if args.len() != 2 {
- return Err("Usage: preview EXPORT_DIRECTORY NEW_PREVIEW_DIRECTORY".into());
- }
- let destination = Path::new(&args[1]);
- preview(Path::new(&args[0]), destination)?;
- println!("Preview: {}", destination.display());
- Ok(())
-}
-
-fn preview(source: &Path, destination: &Path) -> Result<()> {
- let source = source.canonicalize().map_err(|error| error.to_string())?;
- let destination = lifecycle::absolute(destination)?;
- lifecycle::ensure_separate_paths(&source, &destination)?;
- if !source.is_dir() {
- return Err("Preview source must be an exported directory".into());
- }
- if destination.exists() {
- return Err("Preview destination must be new; preserve earlier review evidence".into());
- }
- let mut files = lifecycle::read_tree(&source)?;
- files.remove(".ownership.json");
- // Validate the complete tree before rendering. In particular, a private or
- // unrecorded file added to a Reader export must never become a preview asset.
- executable_docs::store::check(&source, &files)?;
- let output = preview_files(&files)?;
- lifecycle::build(&destination, &output, Ownership::Dedicated)
-}
-
-fn preview_link(destination: &str, page: &str, pages: &BTreeMap) -> Result {
- if destination.contains(':') || destination.starts_with("//") {
- return Ok(destination.to_owned());
- }
- let (path, fragment) = destination
- .split_once('#')
- .map_or((destination, None), |(path, fragment)| {
- (path, Some(fragment))
- });
- if path.is_empty() {
- return Ok(destination.to_owned());
- }
- let decoded = decode_path(path)?;
- let mut target: Vec<_> = page.split('/').collect();
- target.pop();
- for part in decoded.split('/') {
- match part {
- "." => {}
- ".." => {
- target
- .pop()
- .ok_or_else(|| format!("Preview link escapes export: {destination}"))?;
- }
- _ => target.push(part),
- }
- }
- let Some(output) = pages.get(&target.join("/")) else {
- return Ok(destination.to_owned());
- };
- let mut directory: Vec<_> = pages[page].split('/').collect();
- directory.pop();
- let output: Vec<_> = output.split('/').collect();
- let common = directory
- .iter()
- .zip(&output)
- .take_while(|(left, right)| left == right)
- .count();
- let relative = std::iter::repeat_n("..", directory.len() - common)
- .chain(output[common..].iter().copied())
- .collect::>()
- .join("/");
- let mut link: String = relative
- .bytes()
- .map(|byte| {
- if byte.is_ascii_alphanumeric() || b"/-._~".contains(&byte) {
- (byte as char).to_string()
- } else {
- format!("%{byte:02X}")
- }
- })
- .collect();
- if let Some(fragment) = fragment {
- link.push('#');
- link.push_str(fragment);
- }
- Ok(link)
-}
-
-fn decode_path(path: &str) -> Result {
- let mut bytes = Vec::new();
- let mut position = 0;
- while position < path.len() {
- if path.as_bytes()[position] == b'%' {
- let escape = path
- .get(position + 1..position + 3)
- .ok_or_else(|| format!("Malformed preview link escape: {path}"))?;
- bytes.push(
- u8::from_str_radix(escape, 16)
- .map_err(|_| format!("Malformed preview link escape: {path}"))?,
- );
- position += 3;
- } else {
- bytes.push(path.as_bytes()[position]);
- position += 1;
- }
- }
- String::from_utf8(bytes).map_err(|_| format!("Preview link is not UTF-8: {path}"))
-}
-
-fn preview_files(files: &Files) -> Result {
- let manifest: serde_json::Value =
- serde_json::from_slice(&files["manifest.json"]).map_err(|error| error.to_string())?;
- let artifacts = manifest["artifacts"]
- .as_array()
- .ok_or("Manifest artifacts must be an array")?;
- // The store already owns schema validation. This review tool only needs the
- // inventory's page identities; extensions say nothing about artifact kind.
- let pages: BTreeMap<_, _> = artifacts
- .iter()
- .filter(|artifact| artifact["kind"] == "page")
- .map(|artifact| {
- let path = artifact["path"]
- .as_str()
- .ok_or("Manifest artifact requires a path")?;
- let output = Path::new(path).with_extension("html");
- Ok((
- path.to_owned(),
- output.to_str().ok_or("Non UTF-8 preview path")?.to_owned(),
- ))
- })
- .collect::>()?;
- let mut paths = BTreeSet::new();
- for path in files.keys().filter(|path| path.as_str() != "manifest.json") {
- let output = pages.get(path).unwrap_or(path);
- if paths.iter().any(|existing: &String| {
- existing == output
- || output.starts_with(&format!("{existing}/"))
- || existing.starts_with(&format!("{output}/"))
- }) {
- return Err(format!("Preview output collision: {output}"));
- }
- paths.insert(output.to_owned());
- }
- let mut output = Files::new();
- for (path, bytes) in files
- .iter()
- .filter(|(path, _)| path.as_str() != "manifest.json")
- {
- let Some(output_path) = pages.get(path) else {
- output.insert(path.clone(), bytes.clone());
- continue;
- };
- let source =
- std::str::from_utf8(bytes).map_err(|error| format!("Preview page {path}: {error}"))?;
- let parser = Parser::new_ext(
- source,
- Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH,
- )
- .map(|event| match event {
- Event::Start(Tag::Link {
- link_type,
- dest_url,
- title,
- id,
- }) => Ok(Event::Start(Tag::Link {
- link_type,
- dest_url: preview_link(&dest_url, path, &pages)?.into(),
- title,
- id,
- })),
- other => Ok(other),
- })
- .collect::>>()?;
- let mut body = String::new();
- html::push_html(&mut body, parser.into_iter());
- output.insert(output_path.clone(), format!(
- "Executable documentation review{body}"
- ).into_bytes());
- }
- Ok(output)
-}
-
-const STYLE: &str = r#"
-html { color-scheme: light; background: #f5f6f8; color: #17212b; }
-body { margin: 0; font: 17px/1.65 -apple-system, BlinkMacSystemFont, sans-serif; }
-main { max-width: 800px; margin: 42px auto; padding: 36px 48px; background: white; border: 1px solid #dde2e7; border-radius: 12px; }
-h1, h2, h3 { line-height: 1.2; color: #0b243c; } h1 { font-size: 34px; } h2 { margin-top: 38px; }
-a { color: #005d9f; } code { font: 0.88em/1.5 ui-monospace, monospace; background: #f1f3f6; padding: 2px 5px; border-radius: 3px; }
-pre { overflow: auto; padding: 18px; border: 1px solid #dce3e9; border-radius: 6px; background: #f7f9fb; } pre code { padding: 0; background: transparent; }
-blockquote { margin: 24px 0; border-left: 4px solid #ae7c19; background: #fff8e6; padding: 8px 20px; }
-img { max-width: 100%; } table { border-collapse: collapse; width: 100%; } th, td { text-align: left; border: 1px solid #dce3e9; padding: 8px; }
-@media(max-width: 700px) { main { margin: 0; padding: 22px; border: 0; } }
-"#;
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use executable_docs::{Artifact, Audience, Doc, Document, ExportLayout};
- use std::fs;
-
- fn doc(id: &str) -> Doc {
- let mut doc = Doc::new(id, id).unwrap();
- doc.require("executed", true).unwrap();
- doc
- }
-
- fn text(bytes: &[u8], extension: &str) -> Artifact {
- Artifact::new(bytes, "text/plain", extension, "preview-test", "v1").unwrap()
- }
-
- fn export(source: &Path, document: Document) {
- let files = document
- .render_with(Audience::Reader, &ExportLayout::default())
- .unwrap();
- executable_docs::store::build(source, &files).unwrap();
- }
-
- #[test]
- fn preview_rejects_overlapping_trees_before_creating_output() {
- let temporary = tempfile::tempdir().unwrap();
- let root = temporary.path().canonicalize().unwrap();
- let source = root.join("export");
- fs::create_dir(&source).unwrap();
- fs::write(source.join("guide.md"), "Retain the source.").unwrap();
- for destination in [&source, &source.join("preview"), &root] {
- assert!(preview(&source, destination).is_err());
- }
- assert_eq!(fs::read_dir(&source).unwrap().count(), 1);
- assert_eq!(
- fs::read_to_string(source.join("guide.md")).unwrap(),
- "Retain the source."
- );
- }
-
- #[test]
- fn preview_uses_inventory_for_pages_downloads_and_both_audiences() {
- let temporary = tempfile::tempdir().unwrap();
- let root = temporary.path().canonicalize().unwrap();
- let mut first = doc("start");
- first.heading_with_id("intro", 2, "Start here").unwrap();
- first
- .markdown(
- "~~Old wording~~\n\n| Option | Value |\n| --- | --- |\n| Retries | 3 |\n\n\
- [Other](nested/other.source#target)\n[Self](#intro)\n\
- [Remote](https://example.com/other.md#section)\n\
- [Email](mailto:editor@example.md)",
- )
- .unwrap();
- let markdown_download =
- b"# Download remains Markdown\n[Other](../manual/nested/other.source#target)\n";
- let download = first
- .resource_at(
- "download",
- text(markdown_download, "md"),
- "downloads/raw.md",
- )
- .unwrap();
- first.paragraph(download.link("Download source")).unwrap();
- let hidden = first
- .resource_at(
- "hidden",
- text(b"A documented hidden file", "txt"),
- ".fixture.txt",
- )
- .unwrap();
- first.paragraph(hidden.link("Hidden filename")).unwrap();
- let private = first
- .resource_at(
- "private",
- text(b"Private context", "txt"),
- "internal/session.txt",
- )
- .unwrap();
- first
- .note(("Private rationale: ", private.link("Session")))
- .unwrap();
- let mut other = doc("other");
- other.heading_with_id("target", 2, "Other").unwrap();
- other
- .markdown("[Back](../start.guide#intro)\n[Download](../../downloads/raw.md)")
- .unwrap();
- let documents = [first.finish().unwrap(), other.finish().unwrap()];
- let layout = ExportLayout::default()
- .page("start", "manual/start.guide")
- .unwrap()
- .page("other", "manual/nested/other.source")
- .unwrap();
- for (audience, name) in [
- (Audience::Reader, "reader"),
- (Audience::Contributor, "contributor"),
- ] {
- let source = root.join(format!("{name}-export"));
- let destination = root.join(format!("{name}-preview"));
- let files = executable_docs::render_with(&documents, audience, &layout).unwrap();
- executable_docs::store::build(&source, &files).unwrap();
- let retained = lifecycle::read_tree(&source).unwrap();
- preview(&source, &destination).unwrap();
- let html = fs::read_to_string(destination.join("manual/start.html")).unwrap();
- for target in [
- "nested/other.html#target",
- "#intro",
- "../downloads/raw.md",
- "https://example.com/other.md#section",
- "mailto:editor@example.md",
- ] {
- assert!(html.contains(&format!("href=\"{target}\"")), "{html}");
- }
- assert!(html.contains("Old wording"), "{html}");
- assert!(html.contains(""), "{html}");
- assert!(html.contains(""), "{html}");
- let nested = fs::read_to_string(destination.join("manual/nested/other.html")).unwrap();
- assert!(nested.contains("href=\"../start.html#intro\""), "{nested}");
- assert!(
- nested.contains("href=\"../../downloads/raw.md\""),
- "{nested}"
- );
- assert_eq!(
- fs::read(destination.join("downloads/raw.md")).unwrap(),
- markdown_download
- );
- assert!(!destination.join("downloads/raw.html").exists());
- assert_eq!(
- fs::read(destination.join(".fixture.txt")).unwrap(),
- b"A documented hidden file"
- );
- assert_eq!(
- html.contains("Private rationale"),
- audience == Audience::Contributor
- );
- assert_eq!(
- destination.join("internal/session.txt").exists(),
- audience == Audience::Contributor
- );
- assert!(!destination.join(".ownership.json").exists());
- assert!(!destination.join("manifest.json").exists());
- assert!(!destination.join("manual/start.guide").exists());
- assert_eq!(lifecycle::read_tree(&source).unwrap(), retained);
- }
- }
-
- #[test]
- fn preview_preserves_external_urls_and_resolves_encoded_page_names() {
- let pages = BTreeMap::from([
- ("manual/start.guide".into(), "manual/start.html".into()),
- (
- "manual/入口/other.source".into(),
- "manual/入口/other.html".into(),
- ),
- ]);
- let page = "manual/start.guide";
- assert_eq!(
- preview_link("%E5%85%A5%E5%8F%A3/other.source#target", page, &pages).unwrap(),
- "%E5%85%A5%E5%8F%A3/other.html#target"
- );
- for link in [
- "https://example.com/other.md#section",
- "mailto:editor@example.md",
- "//example.com/other.md",
- ] {
- assert_eq!(preview_link(link, page, &pages).unwrap(), link);
- }
- }
-
- #[test]
- fn preview_rejects_page_resource_and_directory_collisions_before_publication() {
- let temporary = tempfile::tempdir().unwrap();
- let root = temporary.path().canonicalize().unwrap();
- for (index, path) in ["guide.html", "guide.html/download.txt"]
- .into_iter()
- .enumerate()
- {
- let source = root.join(format!("export-{index}"));
- let destination = root.join(format!("preview-{index}"));
- let mut guide = doc("guide");
- let resource = guide
- .resource_at("collision", text(b"Retained bytes", "txt"), path)
- .unwrap();
- guide.paragraph(resource.link("Download")).unwrap();
- export(&source, guide.finish().unwrap());
- let error = preview(&source, &destination).unwrap_err();
- assert!(error.contains("Preview output collision"), "{error}");
- assert!(!destination.exists());
- assert_eq!(fs::read(source.join(path)).unwrap(), b"Retained bytes");
- }
- }
-
- #[test]
- fn preview_rejects_pages_that_map_to_the_same_html_path() {
- let temporary = tempfile::tempdir().unwrap();
- let root = temporary.path().canonicalize().unwrap();
- let source = root.join("export");
- let destination = root.join("preview");
- let documents = [
- doc("first").finish().unwrap(),
- doc("second").finish().unwrap(),
- ];
- let layout = ExportLayout::default()
- .page("first", "guide.md")
- .unwrap()
- .page("second", "guide.source")
- .unwrap();
- let files = executable_docs::render_with(&documents, Audience::Reader, &layout).unwrap();
- executable_docs::store::build(&source, &files).unwrap();
- assert!(
- preview(&source, &destination)
- .unwrap_err()
- .contains("Preview output collision")
- );
- assert!(!destination.exists());
- }
-
- #[test]
- fn preview_rejects_changed_or_unrecorded_source_files_and_preserves_existing_review() {
- let temporary = tempfile::tempdir().unwrap();
- let root = temporary.path().canonicalize().unwrap();
- let source = root.join("export");
- let destination = root.join("preview");
- let mut guide = doc("guide");
- guide.paragraph("Retain the source.").unwrap();
- export(&source, guide.finish().unwrap());
- fs::write(source.join("private.txt"), "Must never leak").unwrap();
- assert!(
- preview(&source, &destination)
- .unwrap_err()
- .contains("unrecorded")
- );
- assert!(!destination.exists());
- fs::remove_file(source.join("private.txt")).unwrap();
- fs::write(source.join("guide.md"), "Changed after export").unwrap();
- assert!(
- preview(&source, &destination)
- .unwrap_err()
- .contains("size/hash mismatch")
- );
- assert!(!destination.exists());
- fs::create_dir(&destination).unwrap();
- fs::write(destination.join("review.txt"), "Earlier evidence").unwrap();
- assert!(
- preview(&source, &destination)
- .unwrap_err()
- .contains("must be new")
- );
- assert_eq!(
- fs::read(destination.join("review.txt")).unwrap(),
- b"Earlier evidence"
- );
- }
-
- #[cfg(unix)]
- #[test]
- fn preview_rejects_symlinks_in_export_and_destination_components() {
- use std::os::unix::fs::symlink;
- let temporary = tempfile::tempdir().unwrap();
- let root = temporary.path().canonicalize().unwrap();
- let source = root.join("export");
- export(&source, doc("guide").finish().unwrap());
- symlink(source.join("guide.md"), source.join("linked.md")).unwrap();
- let destination = root.join("preview");
- assert!(
- preview(&source, &destination)
- .unwrap_err()
- .contains("symlink")
- );
- assert!(!destination.exists());
- fs::remove_file(source.join("linked.md")).unwrap();
- let alias = root.join("alias");
- symlink(&root, &alias).unwrap();
- assert!(
- preview(&source, &alias.join("preview"))
- .unwrap_err()
- .to_lowercase()
- .contains("symlink")
- );
- assert!(!destination.exists());
- }
-}
diff --git a/crates/doc-harness/src/artifact.rs b/crates/doc-harness/src/artifact.rs
deleted file mode 100644
index f5bbb16..0000000
--- a/crates/doc-harness/src/artifact.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-//! Owned artifact payloads and the explicit file-reading boundary.
-
-use crate::Result;
-use std::path::Path;
-
-/// Owned bytes and producer metadata. Files are read immediately, never at render time.
-#[derive(Clone, Debug)]
-pub struct Artifact {
- pub(crate) bytes: Vec,
- pub(crate) mime: String,
- pub(crate) extension: String,
- pub(crate) producer: String,
- pub(crate) profile: String,
-}
-
-impl Artifact {
- pub fn new(
- bytes: impl Into>,
- mime: &str,
- extension: &str,
- producer: &str,
- profile: &str,
- ) -> Result {
- let (major, minor) = mime
- .split_once('/')
- .ok_or("MIME type requires type/subtype")?;
- let token = |s: &str| {
- !s.is_empty()
- && s.bytes()
- .all(|b| b.is_ascii_alphanumeric() || b"!#$&^_.+-".contains(&b))
- };
- if !token(major) || !token(minor) {
- return Err("MIME type must be a concrete type/subtype without parameters".into());
- }
- if extension.is_empty()
- || !extension
- .bytes()
- .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
- {
- return Err(
- "Artifact extension must contain lowercase letters/digits without a dot".into(),
- );
- }
- for (label, value) in [("producer", producer), ("profile", profile)] {
- if value.is_empty() || value.chars().any(char::is_control) {
- return Err(format!(
- "Artifact {label} must be nonempty and contain no control characters"
- ));
- }
- }
- Ok(Self {
- bytes: bytes.into(),
- mime: mime.into(),
- extension: extension.into(),
- producer: producer.into(),
- profile: profile.into(),
- })
- }
-
- pub fn from_file(
- path: impl AsRef,
- mime: &str,
- extension: &str,
- producer: &str,
- profile: &str,
- ) -> Result {
- let bytes = std::fs::read(path.as_ref())
- .map_err(|e| format!("Cannot freeze artifact {}: {e}", path.as_ref().display()))?;
- Self::new(bytes, mime, extension, producer, profile)
- }
-}
diff --git a/crates/doc-harness/src/authoring.rs b/crates/doc-harness/src/authoring.rs
deleted file mode 100644
index 657a27f..0000000
--- a/crates/doc-harness/src/authoring.rs
+++ /dev/null
@@ -1,420 +0,0 @@
-//! Rust authoring operations and evidence acquisition before document freezing.
-
-use crate::{
- Artifact, Result,
- model::{
- Binding, BindingValue, Block, Check, Checked, Claim, Content, Document, Inline, IntoProse,
- Kind, Node, Reference, Resource, Target, valid_id,
- },
-};
-use serde::Serialize;
-use serde_json::Value;
-use std::{
- collections::BTreeMap,
- marker::PhantomData,
- sync::atomic::{AtomicU64, Ordering},
-};
-
-static NEXT_OWNER: AtomicU64 = AtomicU64::new(1);
-
-pub struct Doc {
- owner: u64,
- ids: BTreeMap,
- document: Document,
- errors: Vec,
-}
-
-impl Doc {
- pub fn new(id: &str, title: &str) -> Result {
- if !valid_id(id) {
- return Err(format!("Invalid document ID: {id:?}"));
- }
- if title.trim().is_empty() || title.chars().any(char::is_control) {
- return Err("Document title must be nonempty single-line text".into());
- }
- Ok(Self {
- owner: NEXT_OWNER.fetch_add(1, Ordering::Relaxed),
- ids: BTreeMap::new(),
- document: Document {
- id: id.into(),
- title: title.into(),
- nodes: Vec::new(),
- checks: BTreeMap::new(),
- artifacts: BTreeMap::new(),
- resource_paths: BTreeMap::new(),
- bindings: BTreeMap::new(),
- },
- errors: Vec::new(),
- })
- }
-
- fn guarded(&mut self, result: Result) -> Result {
- result.map_err(|error| {
- let contextual = format!("Document {}: {error}", self.document.id);
- self.errors.push(contextual.clone());
- contextual
- })
- }
-
- fn reserve(&mut self, id: &str, kind: Kind) -> Result {
- if !valid_id(id) {
- return Err(format!("Invalid evidence ID: {id:?}"));
- }
- if self.ids.contains_key(id) {
- return Err(format!("Duplicate evidence ID: {id}"));
- }
- self.ids.insert(id.into(), kind.clone());
- Ok(Reference {
- owner: self.owner,
- id: id.into(),
- kind,
- })
- }
-
- fn validate_reference(&self, reference: &Reference) -> Result<()> {
- if reference.owner != self.owner {
- return Err(format!("Foreign document/run handle: {}", reference.id));
- }
- if self.ids.get(&reference.id) != Some(&reference.kind) {
- return Err(format!("Missing or wrong-kind reference: {}", reference.id));
- }
- Ok(())
- }
-
- fn append(&mut self, content: Content) -> Result {
- self.append_with_id(None, content)
- }
-
- fn append_with_id(&mut self, id: Option<&str>, content: Content) -> Result {
- let result = (|| {
- let note = matches!(content, Content::Note { .. });
- let count = self
- .document
- .nodes
- .iter()
- .filter(|node| node.is_note() == note)
- .count();
- let prefix = if note { "note" } else { "block" };
- let node = Node {
- id: id
- .map(str::to_owned)
- .unwrap_or_else(|| format!("{prefix}-{:04}", count + 1)),
- content,
- };
- for reference in node.references() {
- self.validate_reference(reference)?;
- }
- if let Content::Paragraph(prose)
- | Content::MarkdownParts(prose)
- | Content::Note { prose, .. } = &node.content
- {
- for part in &prose.0 {
- if let Inline::Image(reference, _) = part
- && !self.document.artifacts[&reference.id]
- .mime
- .starts_with("image/")
- {
- return Err(format!(
- "Resource {} is not declared as an image",
- reference.id
- ));
- }
- }
- }
- let reference = self.reserve(&node.id, Kind::Block)?;
- self.document.nodes.push(node);
- Ok(Block(reference))
- })();
- self.guarded(result)
- }
-
- pub fn heading(&mut self, level: u8, text: &str) -> Result {
- self.append_heading(None, level, text)
- }
-
- /// Give a section an explicit anchor that survives inserting other blocks.
- /// IDs share the document's evidence namespace; duplicates fail. Other
- /// headings retain their positional IDs unless explicitly named here.
- pub fn heading_with_id(&mut self, id: &str, level: u8, text: &str) -> Result {
- self.append_heading(Some(id), level, text)
- }
-
- fn append_heading(&mut self, id: Option<&str>, level: u8, text: &str) -> Result {
- if !(1..=6).contains(&level) || text.trim().is_empty() || text.chars().any(char::is_control)
- {
- return self.guarded(Err(
- "Heading requires level 1..6 and nonempty single-line text".into(),
- ));
- }
- self.append_with_id(id, Content::Heading(level, text.into()))
- }
-
- pub fn paragraph(&mut self, prose: impl IntoProse) -> Result {
- self.append(Content::Paragraph(prose.into_prose()))
- }
-
- /// CommonMark plus tables and strikethrough. Reference links and code are parsed
- /// by pulldown-cmark; unresolved references and raw HTML fail explicitly.
- /// Local links target emitted files or explicit block anchors. External
- /// http(s)/mailto links are not network-verified.
- pub fn markdown(&mut self, text: &str) -> Result {
- let parsed = crate::markdown::markdown_links(text);
- if let Err(error) = parsed {
- return self.guarded(Err(error));
- }
- self.append(Content::Markdown(text.into()))
- }
-
- /// Preserve literal Markdown bytes while inserting typed values, bindings and
- /// resource views. No separator is appended. Literal HTML is still rejected;
- /// only typed Code/Keys bindings can generate the corresponding safe HTML.
- pub fn markdown_parts(&mut self, parts: impl IntoProse) -> Result {
- let parts = parts.into_prose();
- let validation = crate::markdown::validate_markdown_parts(&parts);
- self.guarded(validation)?;
- self.append(Content::MarkdownParts(parts))
- }
-
- pub fn binding(&mut self, id: &str, value: BindingValue) -> Result {
- let result = (|| {
- match &value {
- BindingValue::Text(text) | BindingValue::Code(text) => {
- if text.is_empty() || text.chars().any(char::is_control) {
- return Err("Binding labels must be nonempty single-line text".into());
- }
- }
- BindingValue::Keys(keys) => {
- if keys.is_empty()
- || keys
- .iter()
- .any(|text| text.is_empty() || text.chars().any(char::is_control))
- {
- return Err("Key bindings require nonempty single-line key labels".into());
- }
- }
- }
- let reference = self.reserve(id, Kind::Binding)?;
- self.document.bindings.insert(id.into(), value);
- Ok(Binding(reference))
- })();
- self.guarded(result)
- }
-
- pub fn note(&mut self, prose: impl IntoProse) -> Result {
- self.append(Content::Note {
- target: None,
- prose: prose.into_prose(),
- })
- }
-
- pub fn note_on(&mut self, target: &impl Target, prose: impl IntoProse) -> Result {
- self.append(Content::Note {
- target: Some(target.reference()),
- prose: prose.into_prose(),
- })
- }
-
- pub fn expect_eq(
- &mut self,
- id: &str,
- actual: T,
- expected: T,
- ) -> Result> {
- let result = (|| {
- let reference = self.reserve(id, Kind::Value)?;
- let mut actual_json = serde_json::to_value(&actual)
- .map_err(|e| format!("Cannot freeze observed value {id}: {e}"))?;
- let mut expected_json = serde_json::to_value(&expected)
- .map_err(|e| format!("Cannot freeze expected value {id}: {e}"))?;
- // Cargo can unify serde_json's preserve_order feature through the
- // host. Freeze all object keys in canonical order in either mode.
- actual_json.sort_all_objects();
- expected_json.sort_all_objects();
- if actual != expected {
- return Err(format!(
- "Check {id} failed: observed {actual_json}, expected {expected_json}"
- ));
- }
- self.document.checks.insert(
- id.into(),
- Check {
- id: id.into(),
- kind: "equality".into(),
- actual: actual_json.clone(),
- expected: expected_json,
- },
- );
- Ok(Checked {
- reference,
- observed: actual_json,
- marker: PhantomData,
- })
- })();
- self.guarded(result)
- }
-
- pub fn require(&mut self, id: &str, condition: bool) -> Result {
- let result = (|| {
- let reference = self.reserve(id, Kind::Claim)?;
- if !condition {
- return Err(format!("Claim {id} failed"));
- }
- self.document.checks.insert(
- id.into(),
- Check {
- id: id.into(),
- kind: "predicate".into(),
- actual: Value::Bool(true),
- expected: Value::Bool(true),
- },
- );
- Ok(Claim(reference))
- })();
- self.guarded(result)
- }
-
- pub fn resource(&mut self, id: &str, artifact: Artifact) -> Result {
- let result = (|| {
- let reference = self.reserve(id, Kind::Resource)?;
- self.document.artifacts.insert(id.into(), artifact);
- Ok(Resource(reference))
- })();
- self.guarded(result)
- }
-
- /// Register a resource at a safe host-owned relative path. The complete
- /// renderer also rejects collisions across documents and generated pages.
- pub fn resource_at(&mut self, id: &str, artifact: Artifact, path: &str) -> Result {
- let result = (|| {
- crate::paths::validate_resource_path(path)?;
- if self
- .document
- .resource_paths
- .values()
- .any(|existing| existing == path)
- {
- return Err(format!("Duplicate artifact path: {path}"));
- }
- let reference = self.reserve(id, Kind::Resource)?;
- self.document.resource_paths.insert(id.into(), path.into());
- self.document.artifacts.insert(id.into(), artifact);
- Ok(Resource(reference))
- })();
- self.guarded(result)
- }
-
- /// Register and place a text resource without replacing its producer metadata.
- /// The returned handle can be referenced or annotated again without executing
- /// the producer or copying the artifact into the document a second time.
- pub fn resource_code(
- &mut self,
- id: &str,
- artifact: Artifact,
- language: &str,
- ) -> Result {
- let resource = self.resource(id, artifact)?;
- self.code(&resource, language)?;
- Ok(resource)
- }
-
- /// Freeze UTF-8 text with the built-in text producer.
- pub fn text(&mut self, id: &str, text: impl Into) -> Result {
- let artifact = Artifact::new(
- text.into().into_bytes(),
- "text/plain",
- "txt",
- "executable-docs/text",
- "utf8-v1",
- );
- let artifact = self.guarded(artifact)?;
- self.resource(id, artifact)
- }
-
- /// Freeze a JSON value with the built-in deterministic JSON producer.
- pub fn json(&mut self, id: &str, value: &impl Serialize) -> Result {
- let result = (|| {
- // Value alone is not sorted when a host enables preserve_order.
- // Sort recursively, including objects inside arrays, before freezing.
- let mut value = serde_json::to_value(value).map_err(|e| e.to_string())?;
- value.sort_all_objects();
- let mut bytes = serde_json::to_vec_pretty(&value).map_err(|e| e.to_string())?;
- bytes.push(b'\n');
- Artifact::new(
- bytes,
- "application/json",
- "json",
- "executable-docs/json",
- "json-pretty-v1",
- )
- })();
- let artifact = self.guarded(result)?;
- self.resource(id, artifact)
- }
-
- pub fn text_code(
- &mut self,
- id: &str,
- text: impl Into,
- language: &str,
- ) -> Result {
- let resource = self.text(id, text)?;
- self.code(&resource, language)?;
- Ok(resource)
- }
-
- pub fn json_code(&mut self, id: &str, value: &impl Serialize) -> Result {
- let resource = self.json(id, value)?;
- self.code(&resource, "json")?;
- Ok(resource)
- }
-
- pub fn embed(&mut self, resource: &Resource, caption: &str) -> Result {
- self.append(Content::Embed(resource.reference(), caption.into()))
- }
-
- pub fn code(&mut self, resource: &Resource, language: &str) -> Result {
- let result = (|| {
- self.validate_reference(&resource.0)?;
- if !language
- .bytes()
- .all(|b| b.is_ascii_alphanumeric() || b"_+-".contains(&b))
- {
- return Err("Code language must be an ASCII identifier".into());
- }
- let artifact = &self.document.artifacts[resource.id()];
- if !(artifact.mime.starts_with("text/") || artifact.mime == "application/json") {
- return Err(format!(
- "Resource {} is not declared as text/JSON",
- resource.id()
- ));
- }
- std::str::from_utf8(&artifact.bytes)
- .map_err(|_| format!("Resource {} is not UTF-8", resource.id()))?;
- Ok(())
- })();
- self.guarded(result)?;
- self.append(Content::Code(resource.reference(), language.into()))
- }
-
- /// Freeze successful execution and intrinsic references. Export validates
- /// layout-dependent resource ownership and local links against its complete
- /// bundle; finishing alone does not mean the document is publishable.
- pub fn finish(self) -> Result {
- if !self.errors.is_empty() {
- return Err(format!(
- "Document {} is tainted by failed operations: {}",
- self.document.id,
- self.errors.join("; ")
- ));
- }
- if self.document.checks.is_empty() {
- return Err(format!(
- "Document {} has no executed checks",
- self.document.id
- ));
- }
- // Publication paths and cross-document links depend on the selected export layout.
- crate::validation::validate_document(&self.document)?;
- Ok(self.document)
- }
-}
diff --git a/crates/doc-harness/src/layout.rs b/crates/doc-harness/src/layout.rs
deleted file mode 100644
index 2907c27..0000000
--- a/crates/doc-harness/src/layout.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-use crate::model::{Document, valid_id};
-use crate::{Result, paths::validate_resource_path};
-use std::collections::BTreeMap;
-
-/// Bundle-relative Markdown paths, independent of a repository or build system.
-///
-/// Defaults preserve the ordinary `.md` and
-/// `assets//.` layout. Explicit
-/// [`crate::Doc::resource_at`] paths remain relative to the bundle root and are
-/// never prefixed or rewritten. `manifest.json` and `.ownership.json` (including
-/// their subtrees) are reserved at the bundle root by the export protocol.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct ExportLayout {
- pages: String,
- resources: String,
- page_overrides: BTreeMap,
-}
-
-impl Default for ExportLayout {
- fn default() -> Self {
- Self {
- pages: String::new(),
- resources: "assets".into(),
- page_overrides: BTreeMap::new(),
- }
- }
-}
-
-impl ExportLayout {
- /// Place default document pages under this bundle-relative directory.
- /// An empty string selects the bundle root. Overrides set by [`Self::page`]
- /// remain exact paths, independent of this prefix.
- pub fn pages_under(mut self, path: &str) -> Result {
- validate_directory(path)?;
- self.pages = path.into();
- Ok(self)
- }
-
- /// Place automatically named resources under this bundle-relative directory.
- /// An empty string selects the bundle root. Explicit resource paths are unchanged.
- pub fn resources_under(mut self, path: &str) -> Result {
- validate_directory(path)?;
- self.resources = path.into();
- Ok(self)
- }
-
- /// Set one document's exact bundle-relative page path. Duplicate assignments
- /// are errors, including identical assignments. Rendering rejects overrides
- /// for document IDs absent from the supplied bundle.
- pub fn page(mut self, document_id: &str, path: &str) -> Result {
- if !valid_id(document_id) {
- return Err(format!(
- "Invalid document ID in export layout: {document_id:?}"
- ));
- }
- validate_resource_path(path)?;
- if self.page_overrides.contains_key(document_id) {
- return Err(format!(
- "Duplicate page override for document: {document_id}"
- ));
- }
- self.page_overrides.insert(document_id.into(), path.into());
- Ok(self)
- }
-
- /// Resolve a document's page path without accessing a filesystem.
- pub fn page_path(&self, document_id: &str) -> String {
- self.page_overrides
- .get(document_id)
- .cloned()
- .unwrap_or_else(|| under(&self.pages, &format!("{document_id}.md")))
- }
-
- pub(crate) fn resource_path(&self, document: &Document, id: &str) -> String {
- document.resource_paths.get(id).cloned().unwrap_or_else(|| {
- under(
- &self.resources,
- &format!(
- "{}/{}.{}",
- document.id, id, document.artifacts[id].extension
- ),
- )
- })
- }
-
- pub(crate) fn validate_documents(&self, documents: &[Document]) -> Result<()> {
- for id in self.page_overrides.keys() {
- if !documents.iter().any(|document| &document.id == id) {
- return Err(format!("Page override names an unknown document: {id}"));
- }
- }
- Ok(())
- }
-}
-
-fn validate_directory(path: &str) -> Result<()> {
- if path.is_empty() {
- Ok(())
- } else {
- validate_resource_path(path)
- }
-}
-
-fn under(directory: &str, path: &str) -> String {
- if directory.is_empty() {
- path.into()
- } else {
- format!("{directory}/{path}")
- }
-}
diff --git a/crates/doc-harness/src/lib.rs b/crates/doc-harness/src/lib.rs
deleted file mode 100644
index 915532b..0000000
--- a/crates/doc-harness/src/lib.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-#![doc = include_str!("../README.md")]
-
-mod artifact;
-mod authoring;
-mod layout;
-pub mod lifecycle;
-mod manifest;
-mod markdown;
-mod model;
-mod paths;
-mod prose;
-mod render;
-pub mod runner;
-pub mod store;
-mod validation;
-
-pub use artifact::Artifact;
-pub use authoring::Doc;
-pub use layout::ExportLayout;
-pub use model::{
- Audience, Binding, BindingValue, Block, Checked, Claim, Document, IntoProse, Prose, Resource,
- Target,
-};
-pub use render::{render, render_with};
-
-pub type Result = std::result::Result;
-
-/// Combine text and typed handles without losing evidence references.
-/// The expression-list form returns a sequence; named interpolation returns
-/// `Result` and rejects missing, duplicate, or unused bindings.
-/// Each supplied expression is evaluated once, in order, through [`IntoProse`].
-#[macro_export]
-macro_rules! prose {
- ($text:literal, $($name:ident = $value:expr),+ $(,)?) => {
- $crate::Prose::interpolate(
- $text,
- [$( (::std::stringify!($name), $crate::IntoProse::into_prose($value)) ),+],
- )
- };
- ($($part:expr),* $(,)?) => {{
- let parts: ::std::vec::Vec<$crate::Prose> =
- ::std::vec![$($crate::IntoProse::into_prose($part)),*];
- parts
- }};
-}
diff --git a/crates/doc-harness/src/lifecycle.rs b/crates/doc-harness/src/lifecycle.rs
deleted file mode 100644
index e43ce3d..0000000
--- a/crates/doc-harness/src/lifecycle.rs
+++ /dev/null
@@ -1,686 +0,0 @@
-//! Shared exact artifact collection and filesystem publication.
-//!
-//! Dedicated trees retain their existing file format. Recorded trees add an
-//! ownership inventory; neither policy changes application-specific manifests.
-//!
-//! Each destination publishes independently. Renames and rollback protect against
-//! ordinary errors, but this is not a crash-atomic filesystem transaction. An
-//! interrupted transaction leaves named recovery material and fails closed.
-use crate::Result;
-use serde::{Deserialize, Serialize};
-use std::{
- collections::{BTreeMap, BTreeSet},
- fs::{self, OpenOptions},
- io::Write,
- path::{Component, Path, PathBuf},
-};
-
-pub type Files = BTreeMap>;
-const OWNERSHIP: &str = ".ownership.json";
-
-/// The adopter explicitly chooses whether its generated tree records ownership.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Ownership {
- /// Every existing file belongs to the dedicated generated tree. Updates
- /// reject files absent from the candidate and write no additional metadata.
- Dedicated,
- /// A `.ownership.json` inventory distinguishes generated and unowned files.
- Recorded,
-}
-
-/// Finish all generation and validation before calling a publication operation.
-/// This catches Rust panics, not process termination or side effects in user code.
-pub fn prepare(generate: impl FnOnce() -> Result) -> Result {
- std::panic::catch_unwind(std::panic::AssertUnwindSafe(generate)).map_err(|payload| {
- let detail = payload
- .downcast_ref::()
- .map(String::as_str)
- .or_else(|| payload.downcast_ref::<&str>().copied())
- .unwrap_or("unknown panic payload");
- format!("Documentation generation panicked; no output was published: {detail}")
- })?
-}
-
-/// Insert once, validating the application-owned relative output path.
-pub fn insert(files: &mut Files, path: String, bytes: Vec) -> Result<()> {
- validate_path(&path)?;
- match files.entry(path) {
- std::collections::btree_map::Entry::Vacant(entry) => {
- entry.insert(bytes);
- Ok(())
- }
- std::collections::btree_map::Entry::Occupied(entry) => {
- Err(format!("Duplicate artifact owner: {}", entry.key()))
- }
- }
-}
-
-/// Check a complete registration inventory independently of output rendering.
-pub fn check_inventory(expected: &BTreeSet, current: &BTreeSet) -> Result<()> {
- if expected == current {
- return Ok(());
- }
- Err(format!(
- "Artifact inventory differs. Missing: {:?}; unregistered: {:?}",
- expected.difference(current).collect::>(),
- current.difference(expected).collect::>()
- ))
-}
-
-/// Read a dedicated tree verbatim. Absent roots produce an empty inventory.
-/// Ownership metadata, when present, is an ordinary file in this raw view.
-/// A sibling writer lock or interrupted transaction prevents a partial read.
-pub fn read_tree(path: &Path) -> Result {
- let transaction = Transaction::paths(path)?;
- transaction.readable()?;
- read_all(&transaction.target)
-}
-
-/// Compare the complete expected inventory and every byte with a retained tree.
-/// Application-specific profile and format checks remain the caller's policy.
-pub fn compare(expected: &Files, current: &Files) -> Result<()> {
- let keys: BTreeSet<_> = current.keys().chain(expected.keys()).collect();
- let changed: Vec<_> = keys
- .into_iter()
- .filter_map(|name| match (current.get(name), expected.get(name)) {
- (None, Some(_)) => Some(format!("missing {name}")),
- (Some(_), None) => Some(format!("extra {name}")),
- (Some(a), Some(b)) if a != b => Some(format!("changed {name}")),
- _ => None,
- })
- .collect();
- if changed.is_empty() {
- Ok(())
- } else {
- Err(format!("Documentation drift: {}", changed.join(", ")))
- }
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct OwnershipRecord {
- schema_version: u32,
- files: Vec,
-}
-
-fn io_error(action: &str, path: &Path, error: std::io::Error) -> String {
- format!("{action} {}: {error}", path.display())
-}
-
-/// Normalize an output path for adapter preflight without creating it.
-/// Rejects parent traversal, filesystem roots, and existing symlink components.
-/// Publication repeats these checks; this does not reserve the destination.
-pub fn absolute(path: &Path) -> Result {
- let clean = absolute_base(path)?;
- if clean.file_name().is_none() {
- return Err("An output directory must have a name; filesystem roots are forbidden".into());
- }
- Ok(clean)
-}
-
-// A path-resolution base may be the filesystem root; an output tree may not.
-pub(crate) fn absolute_base(path: &Path) -> Result {
- let full = if path.is_absolute() {
- path.to_owned()
- } else {
- std::env::current_dir()
- .map_err(|e| e.to_string())?
- .join(path)
- };
- if full.components().any(|p| matches!(p, Component::ParentDir)) {
- return Err(format!(
- "Parent traversal is not allowed: {}",
- path.display()
- ));
- }
- let clean: PathBuf = full
- .components()
- .filter(|p| !matches!(p, Component::CurDir))
- .collect();
- no_symlinks(&clean)?;
- Ok(clean)
-}
-
-/// Reject overlapping output trees before generation, without creating paths.
-/// Existing targets are compared with the other path's existing ancestors by
-/// filesystem identity, so case aliases cannot hide a retained directory.
-/// Missing targets have no identity yet and receive lexical checks only. This
-/// preflight assumes a cooperative filesystem; it neither reserves paths nor
-/// protects against another process changing their identity after the check.
-pub fn ensure_separate_paths(left: &Path, right: &Path) -> Result<()> {
- let left = absolute(left)?;
- let right = absolute(right)?;
- let overlap = || {
- format!(
- "Output paths must not overlap: {} and {}",
- left.display(),
- right.display()
- )
- };
- if left.starts_with(&right) || right.starts_with(&left) {
- return Err(overlap());
- }
- for (target, other) in [(&left, &right), (&right, &left)] {
- if !exists(target)? {
- continue;
- }
- for ancestor in other.ancestors() {
- if exists(ancestor)?
- && same_file::is_same_file(target, ancestor)
- .map_err(|error| io_error("Compare path identity", ancestor, error))?
- {
- return Err(overlap());
- }
- }
- }
- Ok(())
-}
-
-fn no_symlinks(path: &Path) -> Result<()> {
- let mut part = PathBuf::new();
- for component in path.components() {
- part.push(component.as_os_str());
- match fs::symlink_metadata(&part) {
- Ok(meta) if meta.file_type().is_symlink() => {
- return Err(format!(
- "Symlinks are forbidden in output paths: {}",
- part.display()
- ));
- }
- Ok(_) => {}
- Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
- Err(error) => return Err(io_error("Inspect", &part, error)),
- }
- }
- Ok(())
-}
-
-fn exists(path: &Path) -> Result {
- match fs::symlink_metadata(path) {
- Ok(_) => Ok(true),
- Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
- Err(error) => Err(io_error("Inspect", path, error)),
- }
-}
-
-pub fn validate_path(key: &str) -> Result<()> {
- if key.is_empty()
- || key.contains('\\')
- || key
- .split('/')
- .any(|s| s.is_empty() || s == "." || s == "..")
- || Path::new(key)
- .components()
- .any(|p| !matches!(p, Component::Normal(_)))
- {
- return Err(format!("Unsafe or reserved output path: {key:?}"));
- }
- Ok(())
-}
-
-fn validate(files: &Files, ownership: Ownership) -> Result<()> {
- for key in files.keys() {
- validate_path(key)?;
- if ownership == Ownership::Recorded && key.split('/').next() == Some(OWNERSHIP) {
- return Err(format!("Reserved ownership path: {key}"));
- }
- let mut parent = Path::new(key).parent();
- while let Some(path) = parent.filter(|p| !p.as_os_str().is_empty()) {
- if files.contains_key(path.to_str().ok_or("Non UTF-8 output path")?) {
- return Err(format!(
- "Output file conflicts with a parent directory: {key}"
- ));
- }
- parent = path.parent();
- }
- }
- Ok(())
-}
-
-fn owned_tree(path: &Path, ownership: Ownership) -> Result {
- let (files, missing) = owned_tree_inventory(path, ownership)?;
- require_complete_ownership(&missing)?;
- Ok(files)
-}
-
-fn require_complete_ownership(missing: &BTreeSet) -> Result<()> {
- if let Some(name) = missing.first() {
- return Err(format!("Ownership inventory: missing {name}"));
- }
- Ok(())
-}
-
-fn owned_tree_inventory(path: &Path, ownership: Ownership) -> Result<(Files, BTreeSet)> {
- if ownership == Ownership::Dedicated {
- return Ok((read_all(path)?, BTreeSet::new()));
- }
- no_symlinks(path)?;
- let marker = path.join(OWNERSHIP);
- no_symlinks(&marker)?;
- let bytes = fs::read(&marker).map_err(|e| io_error("Read ownership marker", &marker, e))?;
- let record: OwnershipRecord = serde_json::from_slice(&bytes)
- .map_err(|e| format!("Invalid ownership marker {}: {e}", marker.display()))?;
- if record.schema_version != 1 {
- return Err("Unsupported ownership schema".into());
- }
- let mut expected = BTreeSet::new();
- for name in record.files {
- validate_path(&name)?;
- if name.split('/').next() == Some(OWNERSHIP) {
- return Err(format!("Reserved ownership path: {name}"));
- }
- if !expected.insert(name.clone()) {
- return Err(format!("Duplicate owned path: {name}"));
- }
- }
- let mut files = Files::new();
- visit(path, path, Some(&expected), &mut files)?;
- let missing = expected
- .into_iter()
- .filter(|name| !files.contains_key(name))
- .collect();
- Ok((files, missing))
-}
-
-fn visit(
- base: &Path,
- dir: &Path,
- expected: Option<&BTreeSet>,
- files: &mut Files,
-) -> Result<()> {
- no_symlinks(dir)?;
- for entry in fs::read_dir(dir).map_err(|e| io_error("Read directory", dir, e))? {
- let entry = entry.map_err(|e| e.to_string())?;
- let full = entry.path();
- let name = inventory_path(full.strip_prefix(base).map_err(|e| e.to_string())?)?;
- let kind = entry
- .file_type()
- .map_err(|e| io_error("Inspect", &full, e))?;
- if kind.is_symlink() {
- return Err(format!("Output symlink is forbidden: {}", full.display()));
- }
- if kind.is_dir() {
- if let Some(expected) = expected
- && !expected.iter().any(|p| p.starts_with(&format!("{name}/")))
- {
- return Err(format!("Unowned output directory: {name}"));
- }
- visit(base, &full, expected, files)?;
- } else if kind.is_file() {
- if let Some(expected) = expected {
- if name == OWNERSHIP {
- continue;
- }
- if !expected.contains(&name) {
- return Err(format!("Unowned output file: {name}"));
- }
- }
- insert(
- files,
- name,
- fs::read(&full).map_err(|e| io_error("Read", &full, e))?,
- )?;
- } else {
- return Err(format!("Unsupported output file type: {}", full.display()));
- }
- }
- Ok(())
-}
-
-// Inventories use `/` on every host, while filesystem paths use the native
-// separator. Convert components rather than replacing characters: a literal
-// backslash in a Unix filename must still fail the output-path policy.
-fn inventory_path(path: &Path) -> Result {
- let name = path
- .components()
- .map(|component| match component {
- Component::Normal(name) => name.to_str().ok_or("Non UTF-8 output path"),
- _ => Err("Unexpected non-relative output path component"),
- })
- .collect::, _>>()?
- .join("/");
- validate_path(&name)?;
- Ok(name)
-}
-
-fn read_all(path: &Path) -> Result {
- no_symlinks(path)?;
- let mut files = Files::new();
- if exists(path)? {
- visit(path, path, None, &mut files)?;
- }
- Ok(files)
-}
-
-struct Transaction {
- target: PathBuf,
- lock: PathBuf,
- stage: PathBuf,
- backup: PathBuf,
- locked: bool,
-}
-
-// Each published tree reserves these sibling paths for its transaction. Runner
-// preflight must keep other outputs out of all four locations, not just target.
-pub(crate) fn transaction_paths(path: &Path) -> Result<[PathBuf; 4]> {
- let transaction = Transaction::paths(path)?;
- Ok([
- transaction.target.clone(),
- transaction.lock.clone(),
- transaction.stage.clone(),
- transaction.backup.clone(),
- ])
-}
-
-impl Transaction {
- fn paths(path: &Path) -> Result {
- let target = absolute(path)?;
- let name = target
- .file_name()
- .and_then(|s| s.to_str())
- .ok_or("Non UTF-8 destination")?;
- let parent = target.parent().ok_or("Destination has no parent")?;
- Ok(Self {
- lock: parent.join(format!(".{name}.lock")),
- stage: parent.join(format!(".{name}.staging")),
- backup: parent.join(format!(".{name}.backup")),
- target,
- locked: false,
- })
- }
-
- fn acquire(path: &Path) -> Result {
- let mut transaction = Self::paths(path)?;
- let parent = transaction.target.parent().unwrap();
- fs::create_dir_all(parent).map_err(|e| io_error("Create output parent", parent, e))?;
- no_symlinks(parent)?;
- for path in [&transaction.lock, &transaction.stage, &transaction.backup] {
- no_symlinks(path)?;
- }
- let mut lock = OpenOptions::new().write(true).create_new(true).open(&transaction.lock)
- .map_err(|e| format!("Output lock unavailable at {}: {e}; inspect an interrupted run before removing a stale lock", transaction.lock.display()))?;
- if let Err(error) = writeln!(lock, "pid={}", std::process::id()) {
- let _ = fs::remove_file(&transaction.lock);
- return Err(io_error("Write output lock", &transaction.lock, error));
- }
- transaction.locked = true;
- Ok(transaction)
- }
-
- fn clean_state(&self) -> Result<()> {
- for path in [&self.stage, &self.backup] {
- no_symlinks(path)?;
- if exists(path)? {
- return Err(format!(
- "Interrupted transaction material at {}; preserve and inspect it before retrying (recover_backup restores a valid backup only when the destination is absent)",
- path.display()
- ));
- }
- }
- Ok(())
- }
-
- fn readable(&self) -> Result<()> {
- no_symlinks(&self.lock)?;
- if exists(&self.lock)? {
- return Err(format!(
- "Cannot check during an update or stale lock: {}",
- self.lock.display()
- ));
- }
- self.clean_state()
- }
-}
-
-impl Drop for Transaction {
- fn drop(&mut self) {
- if self.locked {
- let _ = fs::remove_file(&self.lock);
- }
- }
-}
-
-fn write_file(path: &Path, bytes: &[u8]) -> Result<()> {
- let mut file = OpenOptions::new()
- .write(true)
- .create_new(true)
- .open(path)
- .map_err(|e| io_error("Create staged file", path, e))?;
- file.write_all(bytes)
- .map_err(|e| io_error("Write staged file", path, e))?;
- file.sync_all()
- .map_err(|e| io_error("Sync staged file", path, e))
-}
-
-fn stage(transaction: &Transaction, files: &Files, ownership: Ownership) -> Result<()> {
- fs::create_dir(&transaction.stage)
- .map_err(|e| io_error("Create staging directory", &transaction.stage, e))?;
- let result = (|| {
- for (name, bytes) in files {
- let file = transaction.stage.join(name);
- fs::create_dir_all(file.parent().unwrap())
- .map_err(|e| io_error("Create staged directory", &file, e))?;
- write_file(&file, bytes)?;
- }
- if ownership == Ownership::Recorded {
- let marker = serde_json::to_vec_pretty(&OwnershipRecord {
- schema_version: 1,
- files: files.keys().cloned().collect(),
- })
- .map_err(|e| e.to_string())?;
- write_file(&transaction.stage.join(OWNERSHIP), &marker)?;
- }
- if owned_tree(&transaction.stage, ownership)? != *files {
- return Err("Staged output did not match the validated candidate".into());
- }
- Ok(())
- })();
- if result.is_err() {
- let _ = fs::remove_dir_all(&transaction.stage);
- }
- result
-}
-
-/// Compare a complete retained output without modifying it or creating a lock.
-/// The callback receives (retained, candidate) before exact byte comparison.
-pub fn check(
- path: &Path,
- files: &Files,
- ownership: Ownership,
- validate_current: impl FnOnce(&Files, &Files) -> Result<()>,
-) -> Result<()> {
- validate(files, ownership)?;
- let transaction = Transaction::paths(path)?;
- transaction.readable()?;
- let current = owned_tree(&transaction.target, ownership)?;
- validate_current(¤t, files)?;
- compare(files, ¤t)
-}
-
-/// Write a complete candidate into a new directory; never overwrite an existing one.
-pub fn build(path: &Path, files: &Files, ownership: Ownership) -> Result<()> {
- validate(files, ownership)?;
- let transaction = Transaction::acquire(path)?;
- transaction.clean_state()?;
- if exists(&transaction.target)? {
- return Err(format!(
- "Build destination already exists: {}",
- transaction.target.display()
- ));
- }
- stage(&transaction, files, ownership)?;
- if let Err(error) = fs::rename(&transaction.stage, &transaction.target) {
- let _ = fs::remove_dir_all(&transaction.stage);
- return Err(io_error("Publish candidate", &transaction.target, error));
- }
- Ok(())
-}
-
-/// Validate and stage all output before replacing a compatible owned baseline.
-pub fn update(
- path: &Path,
- files: &Files,
- ownership: Ownership,
- validate_current: impl FnOnce(&Files, &Files) -> Result<()>,
-) -> Result<()> {
- update_inner(
- path,
- files,
- ownership,
- |current, candidate, missing| {
- require_complete_ownership(missing)?;
- validate_current(current, candidate)
- },
- || Ok(()),
- )
-}
-
-// The structured store can validate explicit retirement against its retained
-// manifest. Generic lifecycle callers have no such evidence and remain strict.
-pub(crate) fn update_with_retirement(
- path: &Path,
- files: &Files,
- validate_current: impl FnOnce(&Files, &Files, &BTreeSet) -> Result<()>,
-) -> Result<()> {
- update_inner(
- path,
- files,
- Ownership::Recorded,
- validate_current,
- || Ok(()),
- )
-}
-
-fn update_inner(
- path: &Path,
- files: &Files,
- ownership: Ownership,
- validate_current: impl FnOnce(&Files, &Files, &BTreeSet) -> Result<()>,
- before_publish: impl FnOnce() -> Result<()>,
-) -> Result<()> {
- validate(files, ownership)?;
- let transaction = Transaction::acquire(path)?;
- transaction.clean_state()?;
- let had_baseline = exists(&transaction.target)?;
- if had_baseline {
- let (current, missing) = owned_tree_inventory(&transaction.target, ownership)?;
- validate_current(¤t, files, &missing)?;
- let obsolete: Vec<_> = current
- .keys()
- .filter(|name| !files.contains_key(*name))
- .collect();
- if !obsolete.is_empty() {
- return Err(format!(
- "Obsolete owned files require explicit removal before update: {obsolete:?}"
- ));
- }
- }
- stage(&transaction, files, ownership)?;
- if had_baseline && let Err(error) = fs::rename(&transaction.target, &transaction.backup) {
- let _ = fs::remove_dir_all(&transaction.stage);
- return Err(io_error(
- "Preserve baseline backup",
- &transaction.backup,
- error,
- ));
- }
- let publication = before_publish().and_then(|()| {
- fs::rename(&transaction.stage, &transaction.target)
- .map_err(|e| io_error("Publish baseline", &transaction.target, e))
- });
- if let Err(error) = publication {
- if had_baseline && let Err(rollback) = fs::rename(&transaction.backup, &transaction.target)
- {
- return Err(format!(
- "{error}; rollback failed: {rollback}; preserved backup: {}; staged candidate: {}",
- transaction.backup.display(),
- transaction.stage.display()
- ));
- }
- let _ = fs::remove_dir_all(&transaction.stage);
- return Err(error);
- }
- if had_baseline {
- fs::remove_dir_all(&transaction.backup).map_err(|e| {
- io_error(
- "Baseline published but backup cleanup failed; inspect preserved backup",
- &transaction.backup,
- e,
- )
- })?;
- }
- Ok(())
-}
-
-/// Explicit recovery for the unambiguous interrupted state with no destination.
-/// An existing destination is never replaced, and staged candidates are retained.
-pub fn recover_backup(
- path: &Path,
- ownership: Ownership,
- validate_backup: impl FnOnce(&Files) -> Result<()>,
-) -> Result<()> {
- let transaction = Transaction::acquire(path)?;
- if exists(&transaction.target)? {
- return Err(
- "Recovery refused: destination exists; inspect it and the backup explicitly".into(),
- );
- }
- if !exists(&transaction.backup)? {
- return Err("Recovery refused: no preserved backup exists".into());
- }
- let backup = owned_tree(&transaction.backup, ownership)?;
- validate(&backup, ownership)?;
- validate_backup(&backup)?;
- fs::rename(&transaction.backup, &transaction.target)
- .map_err(|e| io_error("Restore preserved backup", &transaction.target, e))
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn inventory_paths_join_native_components_with_portable_separators() {
- let native = Path::new("assets").join("guide").join("evidence.txt");
- assert_eq!(
- inventory_path(&native).unwrap(),
- "assets/guide/evidence.txt"
- );
- assert!(inventory_path(Path::new("../outside")).is_err());
- }
-
- #[cfg(unix)]
- #[test]
- fn inventory_paths_do_not_reinterpret_unix_backslash_filenames() {
- assert!(inventory_path(Path::new(r"assets\evidence.txt")).is_err());
- }
-
- #[test]
- fn failed_publication_restores_previous_baseline() {
- let temp = tempfile::tempdir().unwrap();
- let root = temp.path().canonicalize().unwrap().join("baseline");
- let mut before = Files::from([
- (
- "manifest.json".into(),
- br#"{"schema_version":1,"audience":"reader","profiles":[],"artifacts":[]}"#
- .to_vec(),
- ),
- ("page.md".into(), b"before".to_vec()),
- ]);
- update(&root, &before, Ownership::Dedicated, |_, _| Ok(())).unwrap();
- let old = before.clone();
- before.insert("page.md".into(), b"after".to_vec());
- let error = update_inner(
- &root,
- &before,
- Ownership::Dedicated,
- |_, _, _| Ok(()),
- || Err("injected publish I/O failure".into()),
- )
- .unwrap_err();
- assert!(error.contains("injected"));
- check(&root, &old, Ownership::Dedicated, |_, _| Ok(())).unwrap();
- assert!(!root.parent().unwrap().join(".baseline.backup").exists());
- assert!(!root.parent().unwrap().join(".baseline.staging").exists());
- }
-}
diff --git a/crates/doc-harness/src/manifest.rs b/crates/doc-harness/src/manifest.rs
deleted file mode 100644
index 1ccf7ea..0000000
--- a/crates/doc-harness/src/manifest.rs
+++ /dev/null
@@ -1,514 +0,0 @@
-//! The existing export receipt: typed emission and tolerant, strict validation.
-//! Unknown JSON fields remain accepted; validation retains the original protocol
-//! rules before typed metadata is used by publication compatibility checks.
-use crate::{
- Artifact, Audience, Result,
- lifecycle::{self, Files},
- model::{Content, Document, Node},
- validation::visible_nodes,
-};
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-use sha2::{Digest, Sha256};
-use std::{
- collections::{BTreeMap, BTreeSet},
- path::Path,
-};
-
-#[derive(Serialize, Deserialize)]
-pub(crate) struct Manifest {
- schema_version: u64,
- pub(crate) audience: String,
- profiles: Vec,
- documents: Vec,
- pub(crate) artifacts: Vec,
-}
-#[derive(Serialize, Deserialize)]
-struct Profile {
- producer: String,
- profile: String,
-}
-#[derive(Serialize, Deserialize)]
-struct DocumentRecord {
- id: String,
- title: String,
- checks: Vec,
- blocks: Vec,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- bindings: Option>,
-}
-#[derive(Serialize, Deserialize)]
-struct CheckRecord {
- id: String,
- kind: String,
- passed: bool,
- #[serde(
- default,
- skip_serializing_if = "Option::is_none",
- deserialize_with = "present_value"
- )]
- actual: Option,
- #[serde(
- default,
- skip_serializing_if = "Option::is_none",
- deserialize_with = "present_value"
- )]
- expected: Option,
-}
-// JSON null is a present observation, distinct from an omitted private payload.
-fn present_value<'de, D: serde::Deserializer<'de>>(
- deserializer: D,
-) -> std::result::Result