From a4f84e5f737b14a1824370046b2c4c90e586945f Mon Sep 17 00:00:00 2001 From: Dr Alexander Mikhalev Date: Wed, 14 Oct 2026 13:31:07 +0100 Subject: [PATCH 1/3] feat: prepare public alternatives and lab crates with locked dependencies --- .cargo/config.toml | 8 +- .gitea/workflows/ci.yml | 6 +- .github/workflows/release.yml | 4 +- Cargo.toml | 21 +- README.md | 12 +- crates/terraphim_alternatives/Cargo.lock | 1311 ++++++++++++ crates/terraphim_alternatives/Cargo.toml | 15 +- crates/terraphim_alternatives/LICENSE | 21 + crates/terraphim_alternatives/README.md | 23 + .../tests/fixtures/README.md | 3 + .../tests/fixtures/zed-plugin-fit.md | 15 + .../tests/text_boundaries.rs | 2 +- crates/terraphim_lab/Cargo.lock | 1763 +++++++++++++++++ crates/terraphim_lab/Cargo.toml | 19 +- crates/terraphim_lab/LICENSE | 21 + crates/terraphim_lab/README.md | 20 + crates/terraphim_lab/tests/fixtures/README.md | 10 + crates/terraphim_rolegraph_browser/Cargo.toml | 6 +- scripts/check-public-dependencies.py | 63 + scripts/ci/env.sh | 14 +- scripts/ci/registry.sh | 74 +- scripts/ci/wasm-coverage.sh | 3 +- tests/test_public_dependencies.py | 58 + 23 files changed, 3354 insertions(+), 138 deletions(-) create mode 100644 crates/terraphim_alternatives/Cargo.lock create mode 100644 crates/terraphim_alternatives/LICENSE create mode 100644 crates/terraphim_alternatives/README.md create mode 100644 crates/terraphim_alternatives/tests/fixtures/README.md create mode 100644 crates/terraphim_alternatives/tests/fixtures/zed-plugin-fit.md create mode 100644 crates/terraphim_lab/Cargo.lock create mode 100644 crates/terraphim_lab/LICENSE create mode 100644 crates/terraphim_lab/README.md create mode 100644 crates/terraphim_lab/tests/fixtures/README.md create mode 100644 scripts/check-public-dependencies.py create mode 100644 tests/test_public_dependencies.py diff --git a/.cargo/config.toml b/.cargo/config.toml index a288922..082732e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1 @@ -# The private Terraphim Cargo registry (Gitea packages). terraphim_automata, -# terraphim_types, terraphim_lsp_core and the editor's own published crates -# resolve from here. Reading needs a Gitea token with package read access: -# export CARGO_REGISTRIES_TERRAPHIM_TOKEN="Bearer " -# See README.md, "Run it locally". -[registries.terraphim] -index = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" +# All dependencies resolve from crates.io; no private registry credentials are needed. diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 56a3ea0..07e2839 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,10 +17,8 @@ # passing them on (or a runner without them). What this workflow does is # need none of them, so a runner can serve it without credentials: # -# - The Terraphim Cargo registry asks for a token only for its -# config.json. scripts/ci/registry.sh fetches the locked Terraphim -# crates without one, checks them against Cargo.lock and serves them to -# Cargo as a local registry; Cargo never contacts that registry. +# - Cargo dependencies resolve from crates.io; scripts/ci/registry.sh +# admits the public lockfile without private credentials. # - scripts/ci/env.sh, sourced by every step, removes every credential # variable and runs Cargo offline except in `cargo fetch` (run.sh # --network), which reaches crates.io only. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99ee779..84e461c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,8 @@ jobs: run: | python3 -m unittest discover -s tests -p 'test_release_*.py' -v python3 scripts/check-release-source.py + python3 -m unittest discover -s tests -p 'test_public_dependencies.py' -v + python3 scripts/check-public-dependencies.py - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -37,8 +39,6 @@ jobs: uses: jetli/trunk-action@v0.4.0 with: version: v0.21.14 - - name: Prepare locked registry dependencies - run: bash scripts/ci/registry.sh - name: Build isolated candidate run: | SOURCE_HEAD="$(python3 scripts/check-release-source.py)" diff --git a/Cargo.toml b/Cargo.toml index 222724c..46debe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,10 @@ sha2 = "0.10.9" unicode-normalization = "0.1.25" unicode-segmentation = "1.12" thiserror = "2" -terraphim_alternatives = { path = "crates/terraphim_alternatives" } -terraphim_lab = { path = "crates/terraphim_lab" } -# KG alternatives (issue #13): the pure terraphim_lsp core, pinned to an exact -# released version from the private registry (no sibling path). -terraphim_lsp_core = { version = "=1.23.0", registry = "terraphim" } +terraphim_alternatives = { version = "0.1.1", path = "crates/terraphim_alternatives" } +terraphim_lab = { version = "0.1.1", path = "crates/terraphim_lab" } +# Pure KG analysis shared with the language server, from crates.io. +terraphim_lsp_core = "=1.23.1" [dev-dependencies] terraphim_rolegraph_browser = { path = "crates/terraphim_rolegraph_browser" } @@ -61,10 +60,8 @@ harness = false [workspace] members = [".", "crates/terraphim_alternatives", "crates/terraphim_lab", "crates/terraphim_rolegraph_browser"] resolver = "2" -# terraphim_lsp_core depends on terraphim_alternatives 0.1 and terraphim_lab -# 0.1 from the registry. Point those dependencies at the workspace members -# (the published 0.1.0 releases are built from them) so the build has -# exactly one copy of the span model, the a/an rules and the Lab engine. -[patch.terraphim] -terraphim_alternatives = { path = "crates/terraphim_alternatives" } -terraphim_lab = { path = "crates/terraphim_lab" } +# Keep the language server core and browser on the same workspace span +# model and Lab engine. Published dependants resolve these versions on crates.io. +[patch.crates-io] +terraphim_alternatives = { version = "0.1.1", path = "crates/terraphim_alternatives" } +terraphim_lab = { version = "0.1.1", path = "crates/terraphim_lab" } diff --git a/README.md b/README.md index a2f15e5..c5a7f6c 100644 --- a/README.md +++ b/README.md @@ -119,15 +119,9 @@ cargo install trunk wasm-pack npm install -g terser clean-css-cli # used by Trunk's build hook ``` -**2. Access to the Terraphim crate registry** +**2. Clone and serve** -Some dependencies (`terraphim_automata`, `terraphim_types`, `terraphim_lsp_core` and the editor's own crates) come from the private Terraphim registry on `git.terraphim.cloud`. The repository already declares it in `.cargo/config.toml`; you only need a Gitea token with package read access: - -```bash -export CARGO_REGISTRIES_TERRAPHIM_TOKEN="Bearer " -``` - -**3. Clone and serve** +The Rust dependencies are available on crates.io. No private-registry account or token is required. ```bash git clone https://github.com/terraphim/terraphim-editor.git @@ -137,7 +131,7 @@ trunk serve Open . Trunk compiles the Rust to WebAssembly, serves the page and rebuilds on every change. The first build takes a few minutes; later ones take seconds. -**4. Check everything works (optional)** +**3. Check everything works (optional)** ```bash cargo test --workspace # Rust tests diff --git a/crates/terraphim_alternatives/Cargo.lock b/crates/terraphim_alternatives/Cargo.lock new file mode 100644 index 0000000..e653af2 --- /dev/null +++ b/crates/terraphim_alternatives/Cargo.lock @@ -0,0 +1,1311 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fst" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e73b7be6eeec913659aed78e8c72208f5e9c6a5fb7e0831a491946fd234104" +dependencies = [ + "lazy_static", + "lru", + "memoize-inner", +] + +[[package]] +name = "memoize-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd8f89255d8ff313afabed9a3c83ef0993cc056679dfd001f5111a026f876f7" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terraphim-markdown-parser" +version = "1.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c110e28d4055002dd82b6fa904e301ab6b752038224ce1b760c2daaa885119" +dependencies = [ + "markdown", + "serde", + "terraphim_types", + "thiserror 1.0.69", + "ulid", +] + +[[package]] +name = "terraphim_alternatives" +version = "0.1.1" +dependencies = [ + "criterion", + "serde", + "serde_json", + "terraphim_automata", + "terraphim_types", + "thiserror 2.0.21", + "unicode-segmentation", +] + +[[package]] +name = "terraphim_automata" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafb5b705cd815e5a5d44b0dc29f54635f059e32a11f2ec65fc3edcd2ba03c62" +dependencies = [ + "ahash 0.8.12", + "aho-corasick", + "bincode", + "fst", + "getrandom 0.3.4", + "log", + "memoize", + "regex", + "serde", + "serde_json", + "sha2", + "strsim", + "terraphim-markdown-parser", + "terraphim_types", + "thiserror 1.0.69", +] + +[[package]] +name = "terraphim_types" +version = "1.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7129126b62a2250f76a4a135881beaf87390b652b28db6d4a6c01ecc5bc750c" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "chrono", + "getrandom 0.3.4", + "log", + "schemars", + "serde", + "serde_json", + "strsim", + "thiserror 1.0.69", + "toml", + "ulid", + "uuid", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand", + "serde", + "uuid", + "web-time", +] + +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/terraphim_alternatives/Cargo.toml b/crates/terraphim_alternatives/Cargo.toml index b0b7e6f..0fc2895 100644 --- a/crates/terraphim_alternatives/Cargo.toml +++ b/crates/terraphim_alternatives/Cargo.toml @@ -1,21 +1,20 @@ [package] name = "terraphim_alternatives" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Span model for non-KG alternative-control state: human alternatives, ghosting, overflow and the trailing annotation block." license = "MIT" -repository = "https://git.terraphim.cloud/terraphim/terraphim-editor" -readme = false +repository = "https://github.com/terraphim/terraphim-editor" +readme = "README.md" keywords = ["markdown", "alternatives", "editor"] -# Published to the private registry so terraphim_lsp_core (terraphim-ai) can -# share the annotation-block parser and a/an rules (issue #66). -publish = ["terraphim"] +# Shared by the browser editor and language server. +publish = ["crates-io"] [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -terraphim_automata = { version = "2.1", registry = "terraphim" } -terraphim_types = { version = "1.22", registry = "terraphim" } +terraphim_automata = { version = "2.1.0" } +terraphim_types = { version = "1.22.3" } thiserror = "2" unicode-segmentation = "=1.13.3" diff --git a/crates/terraphim_alternatives/LICENSE b/crates/terraphim_alternatives/LICENSE new file mode 100644 index 0000000..c7cc148 --- /dev/null +++ b/crates/terraphim_alternatives/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Terraphim Editor Contributors + +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/terraphim_alternatives/README.md b/crates/terraphim_alternatives/README.md new file mode 100644 index 0000000..8d6362b --- /dev/null +++ b/crates/terraphim_alternatives/README.md @@ -0,0 +1,23 @@ +# terraphim_alternatives + +The shared document model used by [Terraphim Editor](https://github.com/terraphim/terraphim-editor) and its language server. It stores authored alternatives, ghosted text and overflow in a trailing Markdown annotation block, with UTF-16 offsets and re-anchoring after edits. + +Alternatives use the existing Terraphim `synonyms::` parser and typed thesaurus entries. Export creates an explicit sharing snapshot; ambiguous terms in separate groups are rejected rather than silently combining concepts. + +```rust +use terraphim_alternatives::{Document, Source, SpanKind, parse, write}; +use terraphim_types::NormalizedTermValue; + +let mut document = Document::new("Risk matters."); +let span = document.add_span(SpanKind::Word, 0, 4).unwrap(); +document.add_alternative(&span, "Concern", Source::Human, None).unwrap(); +let reopened = parse(&write(&document)).unwrap(); +let graph = reopened.export_synonym_thesaurus().unwrap(); +let risk = graph.get(&NormalizedTermValue::from("risk")).unwrap(); +let concern = graph.get(&NormalizedTermValue::from("concern")).unwrap(); +assert_eq!(risk.id, concern.id); +``` + +The crate has no DOM or network runtime and builds for native Rust and WebAssembly. Hosts own their history and persistence; the model supplies serialized state and mutation operations. Run `cargo test -p terraphim_alternatives` for annotation round trips, synonym registration/export, text boundaries, moves and re-anchoring. + +Licensed under MIT; see [LICENSE](LICENSE). diff --git a/crates/terraphim_alternatives/tests/fixtures/README.md b/crates/terraphim_alternatives/tests/fixtures/README.md new file mode 100644 index 0000000..d2dca0b --- /dev/null +++ b/crates/terraphim_alternatives/tests/fixtures/README.md @@ -0,0 +1,3 @@ +# Fixture provenance + +The annotation-block fixtures are Terraphim test data. `zed-plugin-fit.md` is a byte-identical copy of the existing Lab fixture, itself an excerpt of Terraphim project requirements. It lives in this package so the published crate's tests do not depend on a sibling checkout. All fixture bytes are regression inputs, not current product claims. diff --git a/crates/terraphim_alternatives/tests/fixtures/zed-plugin-fit.md b/crates/terraphim_alternatives/tests/fixtures/zed-plugin-fit.md new file mode 100644 index 0000000..0d71be2 --- /dev/null +++ b/crates/terraphim_alternatives/tests/fixtures/zed-plugin-fit.md @@ -0,0 +1,15 @@ +**No.** The Zed plan (`terraphim/zed-terraphim#1`) is a thin `wasm32-wasip2` adapter over LSP, MCP and ACP. Zed's extension API has no way to draw span decorations, add panels, bind hover-plus-arrow keys or set per-span opacity, so most of the spec (§3–§7) cannot be built there. `terraphim-editor`, which owns its DOM, is the only existing target where the spec can be built as written. + +Zed can still get a reduced version through `terraphim_lsp`: AI alternatives as code actions, Lab "mark" results as diagnostics, and an approximate trim preview. That only works if the span model and annotation format are shared between both targets (see "Shared model" below). + +Because of the last non-goal, even the thesaurus-backed alternatives provider (R-8.7) cannot run inside the extension. It would have to live in `terraphim_lsp`, which the extension starts as a language server. + +There are no hooks for editor decorations, gutter rendering, panels or views, keymaps or input handling, context-menu items, or text opacity. Inside Zed, the extension can only add UI through an LSP server, using whatever LSP features Zed itself renders. + +The span model should **not** go into `EngineEvent`. That contract (`crates/terraphim_engine_events`) covers agent evolution and approvals (`EvolutionProposed/Approved/Rejected/Applied`, `AllowOnce/AllowAlways/Reject/RejectAlways`), not document state. + +**Sequencing:** today there is only one committed consumer. Zed's Z2 is blocked behind #3224, and terraphim-ai main CI is red (#3325). So the crate starts as a workspace crate inside this repo (`crates/terraphim_alternatives`, `terraphim/terraphim-editor#2`). It is extracted and published only when `terraphim_lsp` adopts it (`terraphim/terraphim-editor#16`, server work in `terraphim/terraphim-ai#3409`). + +TACP/`EngineEvent` only needs an event if alternatives can come from an *agent* rather than the editor (e.g. `AlternativesProposed { doc, span_id, alts, source: ai, model }`). That is optional and can wait until an agent actually produces them. + +**Embedded trailing block (not a sidecar).** The shared crate still owns the schema. It now also parses and writes the trailing fenced block and splits the body from the annotations. In Zed and other LSP clients the block will be visible as raw text, and edits to it are user edits. `terraphim_lsp` must therefore (a) exclude the block from diagnostics, marks and re-anchoring, and (b) treat a hand-edited or malformed block as recoverable: keep the body, report one diagnostic, and do not drop annotations silently. An LSP `foldingRange` for the block would hide it in clients that honour folding; whether Zed collapses LSP folding ranges by default needs checking in the Z0 spike. diff --git a/crates/terraphim_alternatives/tests/text_boundaries.rs b/crates/terraphim_alternatives/tests/text_boundaries.rs index 1904464..43de46d 100644 --- a/crates/terraphim_alternatives/tests/text_boundaries.rs +++ b/crates/terraphim_alternatives/tests/text_boundaries.rs @@ -146,7 +146,7 @@ fn markdown_openers_split_lowercase_code_emphasis_and_links() { #[test] fn shared_markdown_policy_preserves_exact_zed_fixture_positions() { - let text = include_str!("../../terraphim_lab/tests/fixtures/zed-plugin-fit.md"); + let text = include_str!("fixtures/zed-plugin-fit.md"); let ranges = sentence_ranges(text); for expected in [ ByteRange { diff --git a/crates/terraphim_lab/Cargo.lock b/crates/terraphim_lab/Cargo.lock new file mode 100644 index 0000000..4e13776 --- /dev/null +++ b/crates/terraphim_lab/Cargo.lock @@ -0,0 +1,1763 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cached" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" +dependencies = [ + "ahash 0.8.12", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.15.5", + "once_cell", + "thiserror 2.0.21", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fst" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e73b7be6eeec913659aed78e8c72208f5e9c6a5fb7e0831a491946fd234104" +dependencies = [ + "lazy_static", + "lru", + "memoize-inner", +] + +[[package]] +name = "memoize-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd8f89255d8ff313afabed9a3c83ef0993cc056679dfd001f5111a026f876f7" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "terraphim-markdown-parser" +version = "1.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c110e28d4055002dd82b6fa904e301ab6b752038224ce1b760c2daaa885119" +dependencies = [ + "markdown", + "serde", + "terraphim_types", + "thiserror 1.0.69", + "ulid", +] + +[[package]] +name = "terraphim_alternatives" +version = "0.1.1" +dependencies = [ + "serde", + "serde_json", + "terraphim_automata", + "terraphim_types", + "thiserror 2.0.21", + "unicode-segmentation", +] + +[[package]] +name = "terraphim_automata" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafb5b705cd815e5a5d44b0dc29f54635f059e32a11f2ec65fc3edcd2ba03c62" +dependencies = [ + "ahash 0.8.12", + "aho-corasick", + "bincode", + "fst", + "getrandom 0.3.4", + "log", + "memoize", + "regex", + "serde", + "serde_json", + "sha2", + "strsim", + "terraphim-markdown-parser", + "terraphim_types", + "thiserror 1.0.69", +] + +[[package]] +name = "terraphim_lab" +version = "0.1.1" +dependencies = [ + "ahash 0.8.12", + "criterion", + "markdown", + "serde", + "serde_json", + "terraphim_alternatives", + "terraphim_automata", + "terraphim_rolegraph", + "terraphim_types", + "thiserror 2.0.21", +] + +[[package]] +name = "terraphim_rolegraph" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5508b660432cb43e8c19ee6ebc2200f7127451ee49d3ad5c932015eb5bdfe746" +dependencies = [ + "ahash 0.8.12", + "aho-corasick", + "cached", + "itertools 0.14.0", + "log", + "regex", + "serde", + "serde_json", + "terraphim_automata", + "terraphim_rolegraph_core", + "terraphim_types", + "thiserror 1.0.69", + "tokio", + "ulid", + "unicode-segmentation", + "url", +] + +[[package]] +name = "terraphim_rolegraph_core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daeefa93feef1d3cfb5ccf7271d1be16e720cb59758a6a83a95c4481754b2d4" +dependencies = [ + "ahash 0.8.12", + "serde", + "serde_json", + "terraphim_types", + "thiserror 1.0.69", +] + +[[package]] +name = "terraphim_types" +version = "1.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7129126b62a2250f76a4a135881beaf87390b652b28db6d4a6c01ecc5bc750c" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "chrono", + "getrandom 0.3.4", + "log", + "schemars", + "serde", + "serde_json", + "strsim", + "thiserror 1.0.69", + "toml", + "ulid", + "uuid", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand", + "serde", + "uuid", + "web-time", +] + +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/terraphim_lab/Cargo.toml b/crates/terraphim_lab/Cargo.toml index ed54bdc..46a67aa 100644 --- a/crates/terraphim_lab/Cargo.toml +++ b/crates/terraphim_lab/Cargo.toml @@ -1,23 +1,22 @@ [package] name = "terraphim_lab" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Deterministic Lab mark engine: typos and punctuation, weakest, long and convoluted sentences, off-tone words, hedges and filler. No DOM; builds native and wasm32." license = "MIT" -repository = "https://git.terraphim.cloud/terraphim/terraphim-editor" -readme = false +repository = "https://github.com/terraphim/terraphim-editor" +readme = "README.md" keywords = ["markdown", "writing", "lab", "editor"] -# Published to the private registry so terraphim_lsp_core (terraphim-ai) can -# produce Lab-mark diagnostics with this engine (issue #68). -publish = ["terraphim"] +# Shared by the browser editor and language server. +publish = ["crates-io"] [dependencies] # KG matching (Aho-Corasick, LeftmostLongest, ASCII case-insensitive, # word-boundary filtered) and KG markdown parsing. Default features only: # no tokio, no reqwest, builds for wasm32-unknown-unknown. -terraphim_automata = { version = "2.1", registry = "terraphim" } +terraphim_automata = { version = "2.1.0" } # Thesaurus, NormalizedTerm and the rolegraph Node/Edge data structures. -terraphim_types = { version = "1.22.2", registry = "terraphim" } +terraphim_types = { version = "1.22.3" } ahash = { version = "0.8", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -27,7 +26,7 @@ thiserror = "2" markdown = "1.0.0-alpha.21" # The editor's one word definition (issue #59), shared with the Write_On # counter. Its dependencies are a subset of this crate's, so it adds nothing. -terraphim_alternatives = { version = "0.1", path = "../terraphim_alternatives", registry = "terraphim" } +terraphim_alternatives = { version = "0.1.1", path = "../terraphim_alternatives" } # criterion pulls in rayon, and terraphim_rolegraph pulls in tokio "full"; # neither compiles for wasm32, so both are native-only dev-dependencies. @@ -35,7 +34,7 @@ terraphim_alternatives = { version = "0.1", path = "../terraphim_alternatives", criterion = "0.5" # Conformance only: rebuilds the fixture rolegraph with the real RoleGraph and # checks the committed JSON the engine consumes is what upstream produces. -terraphim_rolegraph = { version = "2.0", registry = "terraphim" } +terraphim_rolegraph = { version = "2.0.1" } [[bench]] name = "lab_bench" diff --git a/crates/terraphim_lab/LICENSE b/crates/terraphim_lab/LICENSE new file mode 100644 index 0000000..c7cc148 --- /dev/null +++ b/crates/terraphim_lab/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Terraphim Editor Contributors + +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/terraphim_lab/README.md b/crates/terraphim_lab/README.md new file mode 100644 index 0000000..b7811e5 --- /dev/null +++ b/crates/terraphim_lab/README.md @@ -0,0 +1,20 @@ +# terraphim_lab + +The deterministic writing-analysis engine used by [Terraphim Editor](https://github.com/terraphim/terraphim-editor) and its language server. It proposes typo corrections, highlights long or convoluted sentences, applies role-specific tone/hedge lists, and plans trims using knowledge-graph concepts and rolegraph scores. + +Analysis borrows the original Markdown. It returns proposed spans and edits; the host decides which to apply. Offsets use UTF-16 code units so native and browser hosts can exchange the same ranges. Protected Markdown structures are excluded from prose edits. + +```rust +use terraphim_lab::{LabAction, LabConfig, MarkKind, mark}; + +let config = LabConfig::with_defaults().unwrap(); +let marks = mark("I recieve it.", &config, LabAction::TyposAndPunctuation); +assert_eq!(marks[0].kind, MarkKind::Typo); +assert_eq!(marks[0].proposal.as_deref(), Some("receive")); +``` + +Role lists support the Terraphim/Logseq-compatible `synonyms::` syntax. `LabConfig` accepts an existing role thesaurus and graph. The built-in lists and test fixtures are included in the package. The runtime has no DOM or network dependency and builds for native Rust and WebAssembly; native conformance tests use the real `terraphim_rolegraph` implementation. + +Run `cargo test -p terraphim_lab` to check actions, rolegraph conformance, trim targets, protected ranges and text invariants. + +Licensed under MIT; see [LICENSE](LICENSE). diff --git a/crates/terraphim_lab/tests/fixtures/README.md b/crates/terraphim_lab/tests/fixtures/README.md new file mode 100644 index 0000000..6ea619b --- /dev/null +++ b/crates/terraphim_lab/tests/fixtures/README.md @@ -0,0 +1,10 @@ +# Fixture provenance + +These fixed excerpts are regression inputs. Their original line wrapping is intentional and is tested; they are not product copy. + +- `three-men-ch1.md`: Jerome K. Jerome, *Three Men in a Boat (To Say Nothing of the Dog)* (1889), opening of chapter I. Source: [Project Gutenberg eBook 308](https://www.gutenberg.org/ebooks/308), credited to David Price and Margaret Price. The Gutenberg header/footer and picture markers were removed when the fixture was created. +- `walden-economy.md`: Henry David Thoreau, *Walden* (1854), opening three paragraphs of “Economy”. Source: [Project Gutenberg eBook 205](https://www.gutenberg.org/ebooks/205), credited to Judith Boss and David Widger. The Gutenberg header/footer were removed when the fixture was created. + +The linked catalogue entries identify both literary works as public domain in the USA. These excerpts are attributed to their original authors; the crate's MIT notice does not imply Terraphim authored them. + +`zed-plugin-fit.md` and `zed-plugin-fit-full.md` are existing Terraphim project requirements excerpts, retained unchanged as test inputs. The small domain/corpus notes and generated role/thesaurus JSON are Terraphim test fixtures. The literary fixture source and transformations were originally recorded in `docs/research/lab-heuristics.md` in this repository. diff --git a/crates/terraphim_rolegraph_browser/Cargo.toml b/crates/terraphim_rolegraph_browser/Cargo.toml index d70fd68..61bb5b9 100644 --- a/crates/terraphim_rolegraph_browser/Cargo.toml +++ b/crates/terraphim_rolegraph_browser/Cargo.toml @@ -9,9 +9,9 @@ crate-type = ["cdylib", "rlib"] [dependencies] ahash = "0.8" -terraphim_rolegraph = { version = "=2.0.1", registry = "terraphim", default-features = false, features = ["kg-integration"] } -terraphim_automata = { version = "=2.1.0", registry = "terraphim", default-features = false } -terraphim_types = { version = "=1.22.3", registry = "terraphim", default-features = false } +terraphim_rolegraph = { version = "=2.0.1", default-features = false, features = ["kg-integration"] } +terraphim_automata = { version = "=2.1.0", default-features = false } +terraphim_types = { version = "=1.22.3", default-features = false } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["raw_value"] } thiserror = "2" diff --git a/scripts/check-public-dependencies.py b/scripts/check-public-dependencies.py new file mode 100644 index 0000000..91f6880 --- /dev/null +++ b/scripts/check-public-dependencies.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Reject private dependencies and temporary path-patched release lockfiles.""" +import argparse +from pathlib import Path +import re +import sys +import tomllib + +CRATES_IO = "registry+https://github.com/rust-lang/crates.io-index" +WORKSPACE = {"terraphim-editor", "terraphim_alternatives", "terraphim_lab", "terraphim_rolegraph_browser"} +REQUIRED = { + "terraphim_types": "1.22.3", + "terraphim-markdown-parser": "1.20.5", + "terraphim_automata": "2.1.0", + "terraphim_rolegraph_core": "0.1.1", + "terraphim_rolegraph": "2.0.1", + "terraphim_lsp_core": "1.23.1", +} + + +def problems(lock): + """Return failures for package source, archive identity and required versions.""" + issues = [] + found = set() + for package in lock.get("package", []): + name = package["name"] + version = package["version"] + source = package.get("source") + if source is None: + if name not in WORKSPACE: + issues.append(f"{name}: unpublished local dependency (operator path patch)") + elif source != CRATES_IO: + issues.append(f"{name}: source is not the public crates.io registry") + elif re.fullmatch(r"[0-9a-f]{64}", package.get("checksum", "")) is None: + issues.append(f"{name}: missing or invalid archive checksum") + if name in REQUIRED: + if version != REQUIRED[name]: + issues.append(f"{name}: unexpected version {version}; expected {REQUIRED[name]}") + elif source == CRATES_IO: + found.add(name) + for name in sorted(REQUIRED.keys() - found): + issues.append(f"{name} {REQUIRED[name]}: missing published registry identity") + return issues + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--lock", type=Path, default=Path(__file__).resolve().parents[1] / "Cargo.lock") + args = parser.parse_args() + try: + issues = problems(tomllib.loads(args.lock.read_text())) + except (OSError, tomllib.TOMLDecodeError, KeyError) as error: + print(f"public dependencies: cannot validate lockfile: {error}", file=sys.stderr) + return 1 + if issues: + print("public dependencies: NOT QUALIFIED\n" + "\n".join(issues), file=sys.stderr) + return 1 + print("public dependency lockfile admitted; Cargo must fetch and verify archives") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/env.sh b/scripts/ci/env.sh index e8c566f..4b61862 100755 --- a/scripts/ci/env.sh +++ b/scripts/ci/env.sh @@ -18,19 +18,17 @@ # and CARGO_INCREMENTAL are inherited unchanged: only kache writes here. # # Secrets. The runner hands its own environment to every step, tokens -# included. No step needs any of them: the Terraphim registry crates come -# from a token-free local registry (scripts/ci/registry.sh), crates.io needs -# no credentials and the runner reports status itself. So every variable +# included. Public crate downloads need no credentials and the runner +# reports status itself. So every variable # whose name marks it as a credential is removed for every step, and Cargo # runs offline except in the fetch step. This keeps the credentials out of # what the build is given; it cannot keep them from a pull request that # edits this file or the workflow, because the runner starts every step # with them (see .gitea/workflows/ci.yml). # -# Cargo home. target/ci-cargo-home, so that its config.toml (written by -# scripts/ci/registry.sh) can replace the Terraphim registry without -# touching the runner's own ~/.cargo, and so that its registry cache, like -# target/, persists per runner. Rustup and its toolchains are unaffected. +# Cargo home. target/ci-cargo-home keeps the registry cache job-local, +# without inheriting source replacements from the runner's ~/.cargo. +# Rustup and its toolchains are unaffected. # # Tools. wasm-pack, wasm-bindgen, trunk, chromedriver, terser and # clean-css-cli are installed by scripts/ci/tools.sh under target/ci-tools, @@ -44,8 +42,6 @@ export CARGO_BUILD_BUILD_DIR="$CI_ROOT/target" export CARGO_NET_RETRY=5 export CARGO_HOME="$CI_ROOT/target/ci-cargo-home" -export CI_TERRAPHIM_INDEX="https://git.terraphim.cloud/api/packages/terraphim/cargo/" -export CI_REGISTRY_DIR="$CI_ROOT/target/ci-registry" # Names (upper-cased) that mark a credential: one of these words as a whole # `_`-separated part of the name. scripts/ci/secrets-audit.sh checks that diff --git a/scripts/ci/registry.sh b/scripts/ci/registry.sh index 146f5da..c803682 100755 --- a/scripts/ci/registry.sh +++ b/scripts/ci/registry.sh @@ -1,75 +1,7 @@ #!/bin/bash - -# Makes the Terraphim registry crates in Cargo.lock available to Cargo -# without a registry token. -# -# The Terraphim registry serves its index entries and crate downloads to -# anyone on the tailnet; only its config.json asks for a token, and Cargo -# reads config.json before anything else. So this step downloads, without -# credentials, the index entry and the .crate of every package Cargo.lock -# takes from that registry, checks each crate against Cargo.lock's SHA-256, -# and lays them out as a Cargo local registry in target/ci-registry. The -# Cargo home of scripts/ci/env.sh (target/ci-cargo-home) replaces the -# registry with that directory, so no Cargo command in CI talks to the -# registry or needs its token. The token is removed from every step -# (scripts/ci/env.sh). -# -# target/ persists between jobs on a runner, so the local registry is -# rebuilt from scratch on every run. A new or bumped Terraphim crate in -# Cargo.lock is picked up with no change here. +# Retained entry point for existing CI: admit only public crates.io lockfiles. +# This never downloads private packages or reads registry credentials. set -euo pipefail source "$(dirname "$0")/env.sh" cd "$CI_ROOT" - -die() { - echo "registry: $*" >&2 - exit 1 -} - -# One "name version checksum" line per Cargo.lock package from the registry. -locked="$(awk -v src="source = \"sparse+$CI_TERRAPHIM_INDEX\"" ' - /^\[\[package\]\]$/ { name = ""; version = ""; source = "" } - /^name = / { name = $3 } - /^version = / { version = $3 } - $0 == src { source = 1 } - /^checksum = / { if (source) print name, version, $3 } -' Cargo.lock | tr -d '"')" -[ -n "$locked" ] || die "Cargo.lock takes nothing from $CI_TERRAPHIM_INDEX" - -work="$CI_REGISTRY_DIR.new" -rm -rf "$work" -mkdir -p "$work/index" -while read -r name version checksum; do - lower="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')" - case "${#lower}" in - 1) prefix=1 ;; - 2) prefix=2 ;; - 3) prefix="3/${lower:0:1}" ;; - *) prefix="${lower:0:2}/${lower:2:2}" ;; - esac - mkdir -p "$work/index/$prefix" - # Only the locked version's index line, so Cargo can resolve nothing else. - curl -sSf --retry 3 "$CI_TERRAPHIM_INDEX$prefix/$lower" | - awk -v want="\"vers\":\"$version\"" 'index($0, want)' >"$work/index/$prefix/$lower" - [ -s "$work/index/$prefix/$lower" ] || die "$name $version is not in the registry index" - crate="$work/$name-$version.crate" - curl -sSf --retry 3 -o "$crate" "${CI_TERRAPHIM_INDEX}api/v1/crates/$name/$version/download" - actual="$(sha256sum "$crate" | cut -d' ' -f1)" - [ "$actual" = "$checksum" ] || die "$name $version: SHA-256 $actual, Cargo.lock has $checksum" - echo "registry: $name $version" -done <<<"$locked" - -rm -rf "$CI_REGISTRY_DIR" -mv "$work" "$CI_REGISTRY_DIR" - -mkdir -p "$CARGO_HOME" -cat >"$CARGO_HOME/config.toml" < Date: Wed, 14 Oct 2026 14:19:39 +0100 Subject: [PATCH 2/3] build: qualify the editor against public crate releases --- .github/workflows/public-root.yml | 51 ++++ Cargo.lock | 329 +++++++++------------- scripts/build-rolegraph.sh | 3 +- scripts/check-release-source.py | 44 ++- scripts/ci/env.sh | 5 +- scripts/ci/prepare-public-cargo-home.py | 27 ++ scripts/ci/registry.sh | 1 + scripts/ci/rolegraph-production-check.py | 7 +- scripts/ci/run.sh | 2 +- scripts/release-dist.sh | 5 +- src/document.rs | 2 +- src/host.rs | 2 +- tests/test_public_cargo_home.py | 75 +++++ tests/test_release_source.py | 52 +++- tests/test_rolegraph_public_provenance.py | 37 +++ 15 files changed, 429 insertions(+), 213 deletions(-) create mode 100644 .github/workflows/public-root.yml create mode 100644 scripts/ci/prepare-public-cargo-home.py create mode 100644 tests/test_public_cargo_home.py create mode 100644 tests/test_rolegraph_public_provenance.py diff --git a/.github/workflows/public-root.yml b/.github/workflows/public-root.yml new file mode 100644 index 0000000..241e68d --- /dev/null +++ b/.github/workflows/public-root.yml @@ -0,0 +1,51 @@ +name: Public editor workspace + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +env: + RUSTUP_TOOLCHAIN: '1.96' + PYTHONDONTWRITEBYTECODE: '1' + CARGO_TERM_COLOR: never + +jobs: + public-workspace: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.12' + - name: Exercise source and public dependency admission + run: | + python3 -m unittest discover -s tests -p 'test_release_*.py' -v + python3 -m unittest discover -s tests -p 'test_public_dependencies.py' -v + python3 -m unittest discover -s tests -p 'test_public_cargo_home.py' -v + python3 -m unittest discover -s tests -p 'test_rolegraph_public_provenance.py' -v + python3 scripts/check-release-source.py + bash scripts/ci/registry.sh + - name: Install qualified Rust toolchain + run: | + rustup toolchain install 1.96 --profile minimal --component rustfmt --component clippy + rustup target add wasm32-unknown-unknown --toolchain 1.96 + - name: Fetch locked public packages and verify graph provenance + run: | + set -euo pipefail + bash scripts/ci/run.sh --network cargo fetch --locked + bash scripts/ci/run.sh cargo metadata --locked --format-version 1 > "$RUNNER_TEMP/editor-metadata.json" + python3 scripts/ci/rolegraph-production-check.py metadata < "$RUNNER_TEMP/editor-metadata.json" + - name: Check formatting, lint, native behaviour and Wasm compilation + run: | + bash scripts/ci/run.sh cargo fmt --all --check + bash scripts/ci/run.sh cargo clippy --locked --workspace --all-targets -- -D warnings + bash scripts/ci/run.sh cargo test --locked --workspace --all-targets + bash scripts/ci/run.sh cargo test --locked --workspace --doc + bash scripts/ci/run.sh cargo check --locked --workspace --target wasm32-unknown-unknown --lib diff --git a/Cargo.lock b/Cargo.lock index 6a63e95..4916b27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -59,9 +59,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" @@ -77,20 +77,20 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "basic-toml" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" dependencies = [ "serde", ] @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cached" @@ -143,7 +143,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -160,9 +160,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "shlex", @@ -170,9 +170,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chrono" @@ -217,18 +217,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstyle", "clap_lex", @@ -236,9 +236,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "console_error_panic_hook" @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -313,24 +313,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -363,7 +363,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -374,7 +374,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -395,7 +395,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -406,15 +406,15 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.13.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "find-msvc-tools" @@ -521,12 +521,13 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -557,9 +558,9 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "humansize" @@ -716,13 +717,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -745,9 +746,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" @@ -774,9 +775,9 @@ checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "litemap" @@ -786,9 +787,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "log" -version = "0.4.22" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" @@ -801,18 +802,18 @@ dependencies = [ [[package]] name = "markdown" -version = "1.0.0-alpha.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6491e6c702bf7e3b24e769d800746d5f2c06a6c6a2db7992612e0f429029e81" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" dependencies = [ "unicode-id", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoize" @@ -885,7 +886,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -900,9 +901,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -970,9 +971,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1029,9 +1030,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1039,9 +1040,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1103,7 +1104,7 @@ dependencies = [ "rinja_parser", "rustc-hash", "serde", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -1119,9 +1120,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustversion" @@ -1129,12 +1130,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - [[package]] name = "same-file" version = "1.0.6" @@ -1165,7 +1160,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -1195,7 +1190,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1206,19 +1201,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1255,9 +1251,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "stable_deref_trait" @@ -1284,9 +1280,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -1295,9 +1291,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -1312,7 +1308,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1342,8 +1338,8 @@ dependencies = [ [[package]] name = "terraphim-markdown-parser" version = "1.20.5" -source = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" -checksum = "1c3e63a7253f8b4473696c2625193b3e92c609642f062acce7ac92e9b0ea1581" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c110e28d4055002dd82b6fa904e301ab6b752038224ce1b760c2daaa885119" dependencies = [ "markdown", "serde", @@ -1354,7 +1350,7 @@ dependencies = [ [[package]] name = "terraphim_alternatives" -version = "0.1.0" +version = "0.1.1" dependencies = [ "criterion", "serde", @@ -1368,8 +1364,8 @@ dependencies = [ [[package]] name = "terraphim_automata" version = "2.1.0" -source = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" -checksum = "ad5a407db023baabcaec8b33df8389e23c3ca7588c982ae0015085fbdfecd34f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafb5b705cd815e5a5d44b0dc29f54635f059e32a11f2ec65fc3edcd2ba03c62" dependencies = [ "ahash 0.8.12", "aho-corasick", @@ -1390,7 +1386,7 @@ dependencies = [ [[package]] name = "terraphim_lab" -version = "0.1.0" +version = "0.1.1" dependencies = [ "ahash 0.8.12", "criterion", @@ -1406,9 +1402,9 @@ dependencies = [ [[package]] name = "terraphim_lsp_core" -version = "1.23.0" -source = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" -checksum = "edec0a48905e898acae5bb143e90658cbb056e6e01e1befcaf55b24b26cda31c" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f3bb41e32eb2a7ef99e4bb41c9a0cb0026eccab3be3ec4c3f58cd0efe91073f" dependencies = [ "serde", "serde_json", @@ -1422,8 +1418,8 @@ dependencies = [ [[package]] name = "terraphim_rolegraph" version = "2.0.1" -source = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" -checksum = "bc5411d235a5072a5e65ef7ad2e1ea253f94e7728b1a91e84df06c9c89de66b5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5508b660432cb43e8c19ee6ebc2200f7127451ee49d3ad5c932015eb5bdfe746" dependencies = [ "ahash 0.8.12", "aho-corasick", @@ -1460,8 +1456,8 @@ dependencies = [ [[package]] name = "terraphim_rolegraph_core" version = "0.1.1" -source = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" -checksum = "ebb433f85eaa0fbd4a25b061a96377af3d63da2782078936ec8ae77e9ef649d1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daeefa93feef1d3cfb5ccf7271d1be16e720cb59758a6a83a95c4481754b2d4" dependencies = [ "ahash 0.8.12", "serde", @@ -1473,8 +1469,8 @@ dependencies = [ [[package]] name = "terraphim_types" version = "1.22.3" -source = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" -checksum = "e92580b4c80002d731975a9a9dc0f862e4a0d47394f1d09bd8eebdab17b6cb25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7129126b62a2250f76a4a135881beaf87390b652b28db6d4a6c01ecc5bc750c" dependencies = [ "ahash 0.8.12", "anyhow", @@ -1517,7 +1513,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -1528,7 +1524,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1559,9 +1555,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.2" +version = "1.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" dependencies = [ "pin-project-lite", ] @@ -1627,21 +1623,21 @@ dependencies = [ [[package]] name = "unicase" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-id" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" @@ -1678,9 +1674,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.27.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -1762,7 +1758,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -1805,7 +1801,7 @@ checksum = "3b84b5ac638bfb168196a1a461fcc8f46a294a18b1b6be52133b4e0db122cc9f" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1836,11 +1832,11 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -1864,7 +1860,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -1875,7 +1871,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -1902,15 +1898,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1920,70 +1907,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "winnow" version = "0.7.15" @@ -2024,28 +1947,28 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.119", ] [[package]] @@ -2065,7 +1988,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] @@ -2099,5 +2022,11 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/scripts/build-rolegraph.sh b/scripts/build-rolegraph.sh index 1fd2a4f..b89d38b 100644 --- a/scripts/build-rolegraph.sh +++ b/scripts/build-rolegraph.sh @@ -1,6 +1,6 @@ #!/bin/bash # Build the separate Worker WASM module; never writes committed dist/ by default. -# Uses the published custom-registry packages and the root workspace lock. +# Uses the published crates.io packages and the admitted public workspace lock. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" @@ -10,6 +10,7 @@ if [ -n "${TE_ROLEGRAPH_SOURCE:-}" ]; then exit 1 fi +python3 scripts/check-public-dependencies.py METADATA="$(cargo metadata --locked --format-version 1 --manifest-path "$ROOT/Cargo.toml")" python3 scripts/ci/rolegraph-production-check.py metadata <<< "$METADATA" VERSION="$(python3 -c 'import json,sys; m=json.load(sys.stdin); print(next(p["version"] for p in m["packages"] if p["name"]=="wasm-bindgen"))' <<< "$METADATA")" diff --git a/scripts/check-release-source.py b/scripts/check-release-source.py index af79bcc..6b176d0 100644 --- a/scripts/check-release-source.py +++ b/scripts/check-release-source.py @@ -1,13 +1,51 @@ #!/usr/bin/env python3 """Refuse release builds from source bytes that are not a clean Git commit.""" import argparse +import importlib.util +import tomllib +from urllib.parse import urlsplit from pathlib import Path import subprocess +PUBLIC_REPOSITORY = "https://github.com/terraphim/terraphim-editor" +SPEC = importlib.util.spec_from_file_location("public_dependencies", Path(__file__).with_name("check-public-dependencies.py")) +PUBLIC_DEPENDENCIES = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PUBLIC_DEPENDENCIES) + + +def check_public_origin(repo): + """Read the origin internally; never echo a credential-bearing remote URL.""" + raw = subprocess.check_output(["git", "remote", "get-url", "origin"], cwd=repo, text=True, stderr=subprocess.DEVNULL).strip() + if raw == "git@github.com:terraphim/terraphim-editor.git": + return PUBLIC_REPOSITORY + try: + parsed = urlsplit(raw) + port = parsed.port + except ValueError: + raise ValueError("release origin must identify the public Terraphim editor repository without embedded credentials") from None + allowed_user = parsed.username is None if parsed.scheme == "https" else parsed.username == "git" + if (parsed.scheme not in {"https", "ssh"} or parsed.hostname != "github.com" + or not allowed_user or parsed.password or port is not None + or parsed.path not in {"/terraphim/terraphim-editor", "/terraphim/terraphim-editor.git"} + or parsed.query or parsed.fragment): + raise ValueError("release origin must identify the public Terraphim editor repository without embedded credentials") + return PUBLIC_REPOSITORY + + +def check_public_lock(repo): + lock = tomllib.loads((repo / "Cargo.lock").read_text()) + if PUBLIC_DEPENDENCIES.problems(lock): + raise ValueError("release requires the qualified public crates.io dependency lock") + config = tomllib.loads((repo / ".cargo/config.toml").read_text()) + if config.get("source") or config.get("registries") or config.get("registry", {}).get("default", "crates-io") != "crates-io": + raise ValueError("release Cargo configuration must not replace public dependency sources") + + def check_source(repo, expected_head=None): """Return the clean source HEAD, rejecting changed or untracked release inputs.""" repo = Path(repo).resolve() + check_public_origin(repo) head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip() if expected_head is not None and head != expected_head: raise ValueError("source HEAD changed; rebuild from the admitted commit") @@ -16,12 +54,14 @@ def check_source(repo, expected_head=None): ) if status: raise ValueError("release source contains modified or untracked files; admit source first") - required = ["public/roles/default.json", "scripts/build-dist.sh", "scripts/publish-editor-dist.py"] + required = ["public/roles/default.json", "scripts/build-dist.sh", "scripts/publish-editor-dist.py", + "Cargo.toml", "Cargo.lock", ".cargo/config.toml", "scripts/check-public-dependencies.py"] for name in required: subprocess.run(["git", "ls-files", "--error-unmatch", "--", name], cwd=repo, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if (repo / name).is_symlink() or not (repo / name).is_file(): raise ValueError(f"release input must be a tracked regular file: {name}") + check_public_lock(repo) return head @@ -32,7 +72,7 @@ def main(): args = parser.parse_args() try: print(check_source(args.repo, args.expected_head)) - except (ValueError, subprocess.CalledProcessError) as error: + except (ValueError, OSError, subprocess.CalledProcessError) as error: parser.exit(1, f"release source refused: {error}\n") diff --git a/scripts/ci/env.sh b/scripts/ci/env.sh index 4b61862..61e780c 100755 --- a/scripts/ci/env.sh +++ b/scripts/ci/env.sh @@ -26,8 +26,9 @@ # edits this file or the workflow, because the runner starts every step # with them (see .gitea/workflows/ci.yml). # -# Cargo home. target/ci-cargo-home keeps the registry cache job-local, -# without inheriting source replacements from the runner's ~/.cargo. +# Cargo home. The admission step resets target/ci-cargo-home before each run. +# It is shared only across that run's steps and never inherits old source +# replacements or credentials from a prior run or the runner's ~/.cargo. # Rustup and its toolchains are unaffected. # # Tools. wasm-pack, wasm-bindgen, trunk, chromedriver, terser and diff --git a/scripts/ci/prepare-public-cargo-home.py b/scripts/ci/prepare-public-cargo-home.py new file mode 100644 index 0000000..85460da --- /dev/null +++ b/scripts/ci/prepare-public-cargo-home.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Reset only the checkout-owned CI cache before this run uses public Cargo.""" +from pathlib import Path +import shutil + + +def prepare(root): + """Create an empty CI Cargo home without following filesystem aliases.""" + root = Path(root).resolve() + target = root / "target" + home = target / "ci-cargo-home" + if target.is_symlink() or home.is_symlink(): + raise ValueError("CI Cargo storage must not be a symlink") + if target.exists() and not target.is_dir(): + raise ValueError("CI target must be a directory") + if home.exists() and not home.is_dir(): + raise ValueError("CI Cargo home must be a directory") + target.mkdir(exist_ok=True) + if home.exists(): + shutil.rmtree(home) + home.mkdir(mode=0o700) + return home + + +if __name__ == "__main__": + prepare(Path(__file__).resolve().parents[2]) + print("Fresh checkout-owned CI Cargo home prepared") diff --git a/scripts/ci/registry.sh b/scripts/ci/registry.sh index c803682..187a36e 100755 --- a/scripts/ci/registry.sh +++ b/scripts/ci/registry.sh @@ -5,3 +5,4 @@ set -euo pipefail source "$(dirname "$0")/env.sh" cd "$CI_ROOT" python3 scripts/check-public-dependencies.py +python3 scripts/ci/prepare-public-cargo-home.py diff --git a/scripts/ci/rolegraph-production-check.py b/scripts/ci/rolegraph-production-check.py index 1451f32..cef65a2 100644 --- a/scripts/ci/rolegraph-production-check.py +++ b/scripts/ci/rolegraph-production-check.py @@ -6,7 +6,8 @@ import subprocess import sys -REGISTRY = "sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/" +REGISTRY = "registry+https://github.com/rust-lang/crates.io-index" +REPOSITORY = "https://github.com/terraphim/terraphim-core" VERSIONS = { "terraphim_rolegraph": "2.0.1", "terraphim_types": "1.22.3", @@ -19,8 +20,8 @@ def metadata(data): for name, version in VERSIONS.items(): matches = [p for p in data["packages"] if p["name"] == name] - if len(matches) != 1 or matches[0]["version"] != version or matches[0]["source"] != REGISTRY: - raise ValueError(f"{name}: expected one published {version} from {REGISTRY}") + if len(matches) != 1 or matches[0]["version"] != version or matches[0]["source"] != REGISTRY or matches[0].get("repository") != REPOSITORY: + raise ValueError(f"{name}: expected one published {version} from {REGISTRY} with public GitHub source") bridge = next(p for p in data["packages"] if p["name"] == "terraphim_rolegraph_browser") if bridge["id"] not in data["workspace_members"]: raise ValueError("bridge is not a root workspace member") diff --git a/scripts/ci/run.sh b/scripts/ci/run.sh index ce6d977..963b855 100755 --- a/scripts/ci/run.sh +++ b/scripts/ci/run.sh @@ -21,5 +21,5 @@ while [ "$#" -gt 0 ]; do done source "$(dirname "$0")/env.sh" cd "$CI_ROOT" -echo "+ $*" +echo "+ $*" >&2 exec "$@" diff --git a/scripts/release-dist.sh b/scripts/release-dist.sh index 3b97f25..3eaa4ec 100755 --- a/scripts/release-dist.sh +++ b/scripts/release-dist.sh @@ -23,6 +23,9 @@ mkdir -p target STAGE="$(mktemp -d "$PWD/target/release-staging-XXXXXXXX")" MANIFEST="target/release-manifests/${STAGE##*/}.json" echo "release-dist: isolated stage $STAGE; metadata $MANIFEST" -trunk build --release --dist "$STAGE" +export RUSTUP_TOOLCHAIN=1.96 +bash scripts/ci/registry.sh +bash scripts/ci/run.sh --network cargo fetch --locked +bash scripts/ci/run.sh trunk build --release --locked --dist "$STAGE" python3 scripts/check-release-source.py --expected-head "$SOURCE_HEAD" python3 scripts/publish-editor-dist.py --stage "$STAGE" --manifest "$MANIFEST" --source-head "$SOURCE_HEAD" ${ARGS[@]+"${ARGS[@]}"} diff --git a/src/document.rs b/src/document.rs index 60e5355..dabbb25 100644 --- a/src/document.rs +++ b/src/document.rs @@ -3208,7 +3208,7 @@ pub fn recover_history_transaction_json( history_bytes(source)?; HISTORY_BRIDGE.with(|cell| { let mut bridge = cell.borrow_mut(); - if !bridge.pending.as_ref().is_some_and(|p| p.token == token) { return Err(history_error("unknown-token", "unknown recovery token")); } + if bridge.pending.as_ref().is_none_or(|p| p.token != token) { return Err(history_error("unknown-token", "unknown recovery token")); } let mut trial = DocumentSession::new(); let opened = trial.open(source); let response = json!({"ok":true,"recovered":true,"rolledBack":false,"token":token,"opened":opened.to_json(),"annotations":trial.annotations_json()}); diff --git a/src/host.rs b/src/host.rs index 159770b..ee538a1 100644 --- a/src/host.rs +++ b/src/host.rs @@ -131,7 +131,7 @@ impl HostSession { /// # Errors /// A missing or different pending operation is refused. pub fn reject(&mut self, id: u64) -> Result<(), String> { - if !self.pending.as_ref().is_some_and(|p| p.id == id) { + if self.pending.as_ref().is_none_or(|p| p.id != id) { return Err("unknown proposal".into()); } self.pending = None; diff --git a/tests/test_public_cargo_home.py b/tests/test_public_cargo_home.py new file mode 100644 index 0000000..44ba436 --- /dev/null +++ b/tests/test_public_cargo_home.py @@ -0,0 +1,75 @@ +"""Actual filesystem tests for fresh per-run CI Cargo storage.""" +import importlib.util +from pathlib import Path +import tempfile +import json +import subprocess +import sys +import unittest + +SPEC = importlib.util.spec_from_file_location("public_cargo_home", Path(__file__).resolve().parents[1] / "scripts/ci/prepare-public-cargo-home.py") +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class PublicCargoHomeTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) / "checkout" + self.root.mkdir() + + def test_old_source_replacement_credentials_and_cache_are_removed(self): + home = MODULE.prepare(self.root) + (home / "config.toml").write_text('[source.crates-io]\nreplace-with="old-private-source"\n') + (home / "credentials.toml").write_text("# non-secret test fixture\n") + (home / "registry").mkdir() + (home / "registry/old-cache").write_text("old cache") + self.assertEqual(MODULE.prepare(self.root), home) + self.assertEqual(list(home.iterdir()), []) + + def test_other_target_outputs_and_global_home_untouched(self): + sibling = self.root / "target/compiled-output" + sibling.parent.mkdir(); sibling.write_text("keep") + global_home = Path(self.temp.name) / "global-cargo" + global_home.mkdir(); (global_home / "config.toml").write_text("keep global") + MODULE.prepare(self.root) + self.assertEqual(sibling.read_text(), "keep") + self.assertEqual((global_home / "config.toml").read_text(), "keep global") + + def test_symlinked_target_or_home_is_refused(self): + outside = Path(self.temp.name) / "outside" + outside.mkdir(); sentinel = outside / "sentinel"; sentinel.write_text("keep") + target = self.root / "target"; target.symlink_to(outside, target_is_directory=True) + with self.assertRaises(ValueError): MODULE.prepare(self.root) + target.unlink(); target.mkdir() + (target / "ci-cargo-home").symlink_to(outside, target_is_directory=True) + with self.assertRaises(ValueError): MODULE.prepare(self.root) + self.assertEqual(sentinel.read_text(), "keep") + + def test_internal_symlink_is_removed_without_following_it(self): + home = MODULE.prepare(self.root) + outside = Path(self.temp.name) / "outside.txt"; outside.write_text("keep") + (home / "old-cache-link").symlink_to(outside) + MODULE.prepare(self.root) + self.assertEqual(outside.read_text(), "keep") + + def test_ci_wrapper_preserves_machine_readable_stdout(self): + root = Path(__file__).resolve().parents[1] + result = subprocess.run(['bash', str(root / 'scripts/ci/run.sh'), sys.executable, + '-c', 'import json; print(json.dumps({"actual": "metadata-output"}))'], + capture_output=True, text=True, check=True) + self.assertEqual(json.loads(result.stdout), {'actual': 'metadata-output'}) + self.assertTrue(result.stderr.startswith('+ ')) + + def test_workflows_prepare_before_cargo(self): + root = Path(__file__).resolve().parents[1] + entry = (root / "scripts/ci/registry.sh").read_text() + self.assertLess(entry.index("check-public-dependencies.py"), entry.index("prepare-public-cargo-home.py")) + for path in (".gitea/workflows/ci.yml", ".github/workflows/release.yml", ".github/workflows/public-root.yml"): + text = (root / path).read_text() + self.assertLess(text.index("scripts/ci/registry.sh"), text.index("cargo fetch" if path.startswith(".github") else "cargo fmt")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_release_source.py b/tests/test_release_source.py index 8611bcb..766102c 100644 --- a/tests/test_release_source.py +++ b/tests/test_release_source.py @@ -1,5 +1,6 @@ """Real Git/filesystem checks of the release source admission boundary.""" import importlib.util +import os from pathlib import Path import subprocess import tempfile @@ -16,10 +17,15 @@ def setUp(self): self.addCleanup(self.temp.cleanup) self.repo = Path(self.temp.name) self.git("init", "-q") - for name in ("public/roles/default.json", "scripts/build-dist.sh", "scripts/publish-editor-dist.py", ".gitignore"): + for name in ("public/roles/default.json", "scripts/build-dist.sh", "scripts/publish-editor-dist.py", "Cargo.toml", "scripts/check-public-dependencies.py", ".gitignore"): path = self.repo / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text("target/\n" if name == ".gitignore" else "admitted\n") + (self.repo / ".cargo").mkdir() + (self.repo / ".cargo/config.toml").write_text("# Public registry defaults\n") + lock = 'version = 4\n' + ''.join('[[package]]\nname="' + name + '"\nversion="' + version + '"\nsource="' + MODULE.PUBLIC_DEPENDENCIES.CRATES_IO + '"\nchecksum="' + "a" * 64 + '"\n' for name, version in MODULE.PUBLIC_DEPENDENCIES.REQUIRED.items()) + (self.repo / "Cargo.lock").write_text(lock) + self.git("remote", "add", "origin", MODULE.PUBLIC_REPOSITORY + ".git") self.git("add", ".") self.git("-c", "user.name=Release Test", "-c", "user.email=release-test@example.invalid", "-c", "commit.gpgsign=false", "commit", "-qm", "fixture") self.head = self.git("rev-parse", "HEAD").strip() @@ -27,6 +33,24 @@ def setUp(self): def git(self, *args): return subprocess.check_output(["git", *args], cwd=self.repo, text=True) + def test_local_release_rejects_override_before_build_and_uses_public_locked_order(self): + root = Path(__file__).resolve().parents[1] + script = root / 'scripts/release-dist.sh' + subprocess.run(['bash', '-n', str(script)], check=True) + result = subprocess.run(['bash', str(script), '--dry-run'], + env={**os.environ, 'TE_ROLEGRAPH_SOURCE': '/fixture/forbidden'}, + capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('TE_ROLEGRAPH_SOURCE is forbidden', result.stderr) + self.assertNotIn('isolated stage', result.stdout) + text = script.read_text() + commands = ['python3 scripts/check-release-source.py', 'export RUSTUP_TOOLCHAIN=1.96', + 'bash scripts/ci/registry.sh', 'bash scripts/ci/run.sh --network cargo fetch --locked', + 'bash scripts/ci/run.sh trunk build --release --locked', + 'python3 scripts/check-release-source.py --expected-head', 'python3 scripts/publish-editor-dist.py'] + positions = [text.index(command) for command in commands] + self.assertEqual(positions, sorted(positions)) + def test_clean_commit_and_ignored_build_outputs(self): (self.repo / "target").mkdir() (self.repo / "target/output").write_text("build bytes") @@ -50,6 +74,32 @@ def test_changed_head_and_missing_required_input_are_refused(self): with self.assertRaises(subprocess.CalledProcessError): MODULE.check_source(self.repo) + def test_private_unrelated_and_credential_bearing_origins_are_refused_without_echo(self): + for url in ("https://git.terraphim.cloud/terraphim/terraphim-editor", "https://github.com/other/editor", "https://fixture-secret@github.com/terraphim/terraphim-editor", "https://github.com/terraphim/terraphim-editor?token=fixture-secret", "https://github.com:fixture-secret/terraphim/terraphim-editor"): + self.git("remote", "set-url", "origin", url) + with self.subTest(url=url), self.assertRaises(ValueError) as caught: + MODULE.check_source(self.repo) + self.assertNotIn("fixture-secret", str(caught.exception)) + + def test_public_ssh_origin_is_admitted(self): + for url in ("git@github.com:terraphim/terraphim-editor.git", "ssh://git@github.com/terraphim/terraphim-editor.git"): + self.git("remote", "set-url", "origin", url) + self.assertEqual(MODULE.check_source(self.repo), self.head) + + def test_committed_private_lock_and_source_replacement_are_refused(self): + lock = self.repo / "Cargo.lock" + lock.write_text(lock.read_text().replace(MODULE.PUBLIC_DEPENDENCIES.CRATES_IO, "sparse+https://example.invalid/private/")) + self.git("add", ".") + self.git("-c", "user.name=Release Test", "-c", "user.email=release-test@example.invalid", "-c", "commit.gpgsign=false", "commit", "-qm", "private lock") + with self.assertRaisesRegex(ValueError, "public crates.io"): + MODULE.check_source(self.repo) + lock.write_text(lock.read_text().replace("sparse+https://example.invalid/private/", MODULE.PUBLIC_DEPENDENCIES.CRATES_IO)) + (self.repo / ".cargo/config.toml").write_text('[source.crates-io]\nreplace-with="private"\n') + self.git("add", ".") + self.git("-c", "user.name=Release Test", "-c", "user.email=release-test@example.invalid", "-c", "commit.gpgsign=false", "commit", "-qm", "source replacement") + with self.assertRaisesRegex(ValueError, "replace public"): + MODULE.check_source(self.repo) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_rolegraph_public_provenance.py b/tests/test_rolegraph_public_provenance.py new file mode 100644 index 0000000..eb8cbb9 --- /dev/null +++ b/tests/test_rolegraph_public_provenance.py @@ -0,0 +1,37 @@ +"""Public graph metadata admission and refusals; full Cargo output is checked in release CI.""" +import copy +import importlib.util +from pathlib import Path +import unittest + +SPEC = importlib.util.spec_from_file_location("rolegraph_provenance", Path(__file__).resolve().parents[1] / "scripts/ci/rolegraph-production-check.py") +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class RolegraphPublicProvenanceTests(unittest.TestCase): + def metadata(self): + return {"packages": [{"name": name, "version": version, "source": MODULE.REGISTRY, "repository": MODULE.REPOSITORY} for name, version in MODULE.VERSIONS.items()] + [{"name": "terraphim_rolegraph_browser", "id": "path+file:///fixture/browser#0.1.0"}], "workspace_members": ["path+file:///fixture/browser#0.1.0"]} + + def test_public_registry_and_source_shape_accepted(self): + MODULE.metadata(self.metadata()) + + def test_private_registry_path_patch_and_git_fork_refused(self): + for source in ("sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/", None, "git+https://github.com/other/fork#0123"): + data = self.metadata(); data["packages"][0]["source"] = source + with self.subTest(source=source), self.assertRaises(ValueError): MODULE.metadata(data) + + def test_wrong_repository_version_or_duplicate_refused(self): + for field, value in (("repository", "https://git.terraphim.cloud/terraphim/terraphim-core"), ("version", "0.0.0")): + data = self.metadata(); data["packages"][0][field] = value + with self.subTest(field=field), self.assertRaises(ValueError): MODULE.metadata(data) + data = self.metadata(); data["packages"].append(copy.deepcopy(data["packages"][0])) + with self.assertRaises(ValueError): MODULE.metadata(data) + + def test_browser_bridge_must_be_actual_workspace_member(self): + data = self.metadata(); data["workspace_members"] = [] + with self.assertRaises(ValueError): MODULE.metadata(data) + + +if __name__ == "__main__": + unittest.main() From c96e77a60f1d8237879959fa4fefaec5b0047e2d Mon Sep 17 00:00:00 2001 From: Dr Alexander Mikhalev Date: Wed, 14 Oct 2026 14:21:51 +0100 Subject: [PATCH 3/3] ci: publish verified editor prereleases from immutable artifacts --- .../workflows/editor-release-contracts.yml | 42 ++ .github/workflows/release.yml | 167 ++++--- release/editor.md | 13 + scripts/ci/release-node/package-lock.json | 59 +++ scripts/ci/release-node/package.json | 7 + scripts/publish-editor-release.py | 442 ++++++++++++++++++ tests/test_editor_release.py | 235 ++++++++++ 7 files changed, 897 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/editor-release-contracts.yml create mode 100644 release/editor.md create mode 100644 scripts/ci/release-node/package-lock.json create mode 100644 scripts/ci/release-node/package.json create mode 100644 scripts/publish-editor-release.py create mode 100644 tests/test_editor_release.py diff --git a/.github/workflows/editor-release-contracts.yml b/.github/workflows/editor-release-contracts.yml new file mode 100644 index 0000000..cddba2a --- /dev/null +++ b/.github/workflows/editor-release-contracts.yml @@ -0,0 +1,42 @@ +name: Editor release publication contract +on: + pull_request: + paths: + - '.github/workflows/release.yml' + - '.github/workflows/editor-release-contracts.yml' + - 'scripts/publish-editor-release.py' + - 'scripts/check-release-source.py' + - 'scripts/check-public-dependencies.py' + - 'scripts/check-release-readiness.py' + - 'scripts/ci/release-node/**' + - 'tests/test_editor_release.py' + - 'tests/browser/**' + push: + branches: [main] + paths: + - '.github/workflows/release.yml' + - '.github/workflows/editor-release-contracts.yml' + - 'scripts/publish-editor-release.py' + - 'scripts/check-release-source.py' + - 'scripts/check-public-dependencies.py' + - 'scripts/check-release-readiness.py' + - 'scripts/ci/release-node/**' + - 'tests/test_editor_release.py' + - 'tests/browser/**' +permissions: + contents: read +jobs: + contract: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + PYTHONDONTWRITEBYTECODE: '1' + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.12' + - name: Verify real Git, archive, saved-output and immutable release boundaries + run: python3 -m unittest discover -s tests -p 'test_editor_release.py' -v diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84e461c..460e3db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,96 +1,127 @@ -name: Release candidate +name: Release qualified editor prerelease on: push: - tags: - - 'v*' + tags: ['v*'] workflow_dispatch: permissions: contents: read +concurrency: + group: editor-release-${{ github.ref }} + cancel-in-progress: false + +env: + RUSTUP_TOOLCHAIN: '1.96' + PYTHONDONTWRITEBYTECODE: '1' + CARGO_TERM_COLOR: never + RELEASE_TAG: ${{ github.ref_name }} + jobs: candidate: + if: github.repository == 'terraphim/terraphim-editor' && github.ref_type == 'tag' runs-on: ubuntu-latest + timeout-minutes: 60 + outputs: + artifact-id: ${{ steps.qualified.outputs.artifact-id }} steps: - - uses: actions/checkout@v4 - - name: Verify release source admission - run: | - python3 -m unittest discover -s tests -p 'test_release_*.py' -v - python3 scripts/check-release-source.py - python3 -m unittest discover -s tests -p 'test_public_dependencies.py' -v - python3 scripts/check-public-dependencies.py - - name: Install Rust - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: - targets: wasm32-unknown-unknown - - name: Install pinned bundle tools - uses: actions/setup-node@v4 + python-version: '3.12' + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '22' - - name: Prepare locked minifiers and matching bindgen CLI + - name: Verify clean public source, immutable RC tag and admission policy + run: | + python3 -m unittest discover -s tests -p 'test_release_*.py' -v + python3 -m unittest discover -s tests -p 'test_editor_release.py' -v + python3 -m unittest discover -s tests -p 'test_public_*.py' -v + python3 -m unittest discover -s tests -p 'test_rolegraph_public_provenance.py' -v + python3 scripts/publish-editor-release.py admit --tag "$RELEASE_TAG" + bash scripts/ci/registry.sh + - name: Install qualified Rust toolchain + run: | + rustup toolchain install 1.96 --profile minimal --component rustfmt --component clippy + rustup target add wasm32-unknown-unknown --toolchain 1.96 + - name: Fetch public dependencies and qualify native Rust + run: | + bash scripts/ci/run.sh --network cargo fetch --locked + bash scripts/ci/run.sh cargo fmt --all --check + bash scripts/ci/run.sh cargo clippy --locked --workspace --all-targets -- -D warnings + bash scripts/ci/run.sh cargo test --locked --workspace --all-targets + bash scripts/ci/run.sh cargo test --locked --workspace --doc + - name: Install pinned bundlers and browser drivers run: | mkdir -p target/release-tools/node cp scripts/ci/node/package.json scripts/ci/node/package-lock.json target/release-tools/node/ npm ci --prefix target/release-tools/node --ignore-scripts --no-audit --no-fund + npm ci --prefix scripts/ci/release-node --ignore-scripts --no-audit --no-fund echo "$PWD/target/release-tools/node/node_modules/.bin" >> "$GITHUB_PATH" - cargo install wasm-bindgen-cli --version 0.2.129 --locked - - name: Install Trunk - uses: jetli/trunk-action@v0.4.0 - with: - version: v0.21.14 - - name: Build isolated candidate + echo "$PWD/target/release-tools/rust/bin" >> "$GITHUB_PATH" + bash scripts/ci/run.sh --network cargo install wasm-bindgen-cli --version 0.2.129 --locked --root "$PWD/target/release-tools/rust" + bash scripts/ci/run.sh --network cargo install trunk --version 0.21.14 --locked --root "$PWD/target/release-tools/rust" + python3 -m pip install 'playwright==1.57.0' + google-chrome --version + - name: Build one isolated browser candidate run: | - SOURCE_HEAD="$(python3 scripts/check-release-source.py)" - bash scripts/ci/run.sh --network cargo fetch --locked + SOURCE_HEAD="$(python3 scripts/publish-editor-release.py admit --tag "$RELEASE_TAG")" bash scripts/ci/run.sh trunk build --release --locked --dist "$PWD/target/release-staging-ci" python3 scripts/check-release-source.py --expected-head "$SOURCE_HEAD" python3 scripts/ci/rolegraph-production-check.py assets target/release-staging-ci - python3 scripts/ci/runtime-assets-check.py --check target/release-staging-ci/vendor - - name: Inventory and archive all admitted nested assets + python3 scripts/publish-editor-release.py package --stage target/release-staging-ci --output target/candidate --tag "$RELEASE_TAG" + - name: Verify actual WASM, Chrome synonym outputs and offline delivery run: | - python3 - <<'PY' - import hashlib, importlib.util, json, pathlib, subprocess, tarfile - spec = importlib.util.spec_from_file_location('publisher', 'scripts/publish-editor-dist.py') - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - stage = pathlib.Path('target/release-staging-ci').resolve() - assets = module.inventory(stage) - head = subprocess.check_output(['python3', 'scripts/check-release-source.py'], text=True).strip() - manifest = {'schema': 1, 'sourceHead': head, 'assets': assets} - output = pathlib.Path('target/candidate') - output.mkdir() - raw = (json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode() - (output / 'assets-manifest.json').write_bytes(raw) - with tarfile.open(output / 'editor-candidate.tar.gz', 'w:gz') as archive: - for name in sorted(assets): - archive.add(stage / name, arcname=name, recursive=False) - (output / 'qualification.json').write_text(json.dumps({ - 'status': 'candidate-only', 'fullParityAccepted': False, - 'manifestSha256': hashlib.sha256(raw).hexdigest(), - 'archiveSha256': hashlib.sha256((output / 'editor-candidate.tar.gz').read_bytes()).hexdigest(), - 'sourceHead': head, - }, indent=2) + '\n') - archive = output / 'editor-candidate.tar.gz' - qualification = output / 'qualification.json' - archive_sha = hashlib.sha256(archive.read_bytes()).hexdigest() - release = { - 'schema': 1, 'releaseId': 'editor-' + head, 'channel': 'candidate', - 'artifacts': [{'host': 'browser', 'version': 'candidate-' + head[:12], - 'sourceCommit': head, 'path': archive.name, 'bytes': archive.stat().st_size, - 'sha256': archive_sha}], - 'evidence': [{'host': 'browser', 'kind': 'receipt', 'artifactSha256': archive_sha, - 'path': qualification.name, 'bytes': qualification.stat().st_size, - 'sha256': hashlib.sha256(qualification.read_bytes()).hexdigest()}], - 'limitations': [{'host': 'browser', 'requirement': 'all41', 'status': 'OPEN', - 'issue': 'https://git.terraphim.cloud/terraphim/terraphim-editor/issues/122'}], - } - (output / 'release-manifest.json').write_text(json.dumps(release, indent=2) + '\n') - PY - python3 scripts/check-release-readiness.py target/candidate/release-manifest.json --root target/candidate --claim integrity - - name: Retain candidate for qualification - uses: actions/upload-artifact@v4 + python3 scripts/publish-editor-release.py qualify --folder target/candidate --evidence "$RUNNER_TEMP/editor-evidence" --tag "$RELEASE_TAG" + python3 scripts/publish-editor-release.py verify --folder target/candidate --tag "$RELEASE_TAG" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + id: qualified with: - name: editor-candidate-${{ github.sha }} + name: editor-qualified-${{ github.sha }}-${{ github.run_attempt }} path: target/candidate/ if-no-files-found: error + - name: Retain actual outputs even when qualification fails + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: editor-observed-${{ github.sha }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/editor-evidence/ + if-no-files-found: ignore + publish: + needs: candidate + if: github.repository == 'terraphim/terraphim-editor' && github.ref_type == 'tag' + environment: editor-release + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: write + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.12' + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + artifact-ids: ${{ needs.candidate.outputs.artifact-id }} + merge-multiple: true + path: ${{ runner.temp }}/editor-qualified + - name: Revalidate exact candidate before protected publication + run: python3 scripts/publish-editor-release.py verify --folder "$RUNNER_TEMP/editor-qualified" --tag "$RELEASE_TAG" + - name: Publish immutable prerelease and verify anonymous downloads + env: + GH_TOKEN: ${{ github.token }} + run: python3 scripts/publish-editor-release.py publish --folder "$RUNNER_TEMP/editor-qualified" --tag "$RELEASE_TAG" --receipt "$RUNNER_TEMP/editor-published.json" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: editor-published-${{ github.sha }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/editor-published.json + if-no-files-found: error diff --git a/release/editor.md b/release/editor.md new file mode 100644 index 0000000..d2550eb --- /dev/null +++ b/release/editor.md @@ -0,0 +1,13 @@ +# Publishing an editor prerelease + +The `Release qualified editor prerelease` workflow builds and publishes an immutable browser package. It runs on an editor tag such as `v0.1.0-rc.2`, or a manual run selecting that existing tag. The version must match the root package, the tag must resolve to the checked-out clean public source, and that commit must already belong to `origin/main`. + +Before creating a new tag, merge the reviewed source and verify the public-workspace CI. Check that the intended tag and release do not already exist. The repository's `editor-release` GitHub environment must restrict deployment to approved editor RC tags. The protected publish job needs only the repository-provided `GITHUB_TOKEN` with `contents: write`; pull requests and candidate qualification receive no publication credential. + +The workflow fetches only the admitted public Cargo lock in a fresh Cargo home. It builds one browser archive, then tests that exact archive with 15 actual WASM host tests, the Chrome synonym-sharing flow and the offline full-page/embed probe. The UI flow saves annotated Markdown, reopens it, checks native undo/redo and downloads the actual synonym graph and clean document. Logs, returned files and screenshots are retained with their checksums. These are targeted prerelease checks; they do not qualify all 41 requirements or native Safari, Firefox and Zed. + +Publication downloads the successful qualification job's immutable Actions artifact ID and revalidates every asset, receipt, source and lock identity. It creates or resumes a draft prerelease, uploads only absent files, downloads and compares them, then publishes. Every public asset is downloaded anonymously and compared again. Neither release assets nor tags are replaced. + +If publication fails after qualification, rerun **only failed jobs** in the same workflow run. The publisher consumes the original qualification artifact even when the run-attempt number changes. An identical existing draft can resume; a different asset or an incomplete already-public release is refused. Do not rebuild or overwrite a partially published immutable package to make a retry pass. + +The browser archive and assets manifest retain their existing consumer names and asset map. `sourceRepository` and `cargoLockSha256` provide additional public provenance. `host-session.js` stays outside the browser archive; VS Code pins it separately from the exact source commit. After publication, consumers must use the verified public archive/manifest hashes, then run their own installed-host checks before their next marketplace release. diff --git a/scripts/ci/release-node/package-lock.json b/scripts/ci/release-node/package-lock.json new file mode 100644 index 0000000..cb455ae --- /dev/null +++ b/scripts/ci/release-node/package-lock.json @@ -0,0 +1,59 @@ +{ + "name": "terraphim-editor-release-browser-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "terraphim-editor-release-browser-tests", + "version": "1.0.0", + "dependencies": { + "playwright": "1.57.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/scripts/ci/release-node/package.json b/scripts/ci/release-node/package.json new file mode 100644 index 0000000..36fe058 --- /dev/null +++ b/scripts/ci/release-node/package.json @@ -0,0 +1,7 @@ +{ + "name": "terraphim-editor-release-browser-tests", + "private": true, + "version": "1.0.0", + "description": "Pinned browser driver for qualification of actual prerelease outputs", + "dependencies": { "playwright": "1.57.0" } +} diff --git a/scripts/publish-editor-release.py b/scripts/publish-editor-release.py new file mode 100644 index 0000000..650e32e --- /dev/null +++ b/scripts/publish-editor-release.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +"""Qualify fixed editor artifacts and publish the same bytes as an immutable prerelease.""" +import argparse +import hashlib +import importlib.util +import json +from pathlib import Path, PurePosixPath +import re +import subprocess +import sys +import tarfile +import tempfile +import tomllib +from urllib.parse import urlparse + +ROOT = Path(__file__).resolve().parents[1] +REPOSITORY = 'terraphim/terraphim-editor' +SOURCE_URL = 'https://github.com/' + REPOSITORY +SHA_RE = re.compile(r'[0-9a-f]{40}') +SHA256_RE = re.compile(r'[0-9a-f]{64}') +FILES = ('editor-candidate.tar.gz', 'assets-manifest.json', 'qualification.json', + 'saved-outputs.tar.gz', 'release-manifest.json', 'SHA256SUMS') +GATES = ('wasm-host', 'chrome-synonyms', 'chrome-offline') + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load_module(name, relative): + spec = importlib.util.spec_from_file_location(name, ROOT / relative) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def git(root, *args): + return subprocess.check_output(['git', *args], cwd=root, text=True, timeout=30).strip() + + +def admit_source(tag, root=ROOT): + """Require an immutable RC of the root package and a commit reviewed on main.""" + version = tomllib.loads((root / 'Cargo.toml').read_text())['package']['version'] + if not re.fullmatch('v' + re.escape(version) + r'-rc\.[1-9][0-9]*', tag): + raise ValueError('Expected an editor prerelease tag matching the root package version') + source = load_module('release_source', 'scripts/check-release-source.py').check_source(root) + if git(root, 'rev-parse', f'refs/tags/{tag}^{{commit}}') != source: + raise ValueError('Tag does not identify this exact clean source') + if subprocess.run(['git', 'merge-base', '--is-ancestor', source, 'refs/remotes/origin/main'], + cwd=root, capture_output=True, timeout=30).returncode: + raise ValueError('Tagged source must already belong to public main') + return source + + +def safe_members(archive): + """Read bounded regular archive members, rejecting traversal, links and duplicates.""" + result = {} + total = 0 + with tarfile.open(archive, 'r:gz') as handle: + for member in handle: + path = PurePosixPath(member.name) + if (not member.isfile() or path.is_absolute() or str(path) != member.name + or any(p in ('', '.', '..') for p in member.name.split('/')) + or '\\' in member.name or member.name in result): + raise ValueError('Unsafe or duplicate archive member') + total += member.size + if total > 128 * 1024 * 1024 or len(result) >= 2048: + raise ValueError('Archive exceeds size or member bound') + result[member.name] = handle.extractfile(member).read() + if not result: + raise ValueError('Empty archive') + return result + + +def check_assets(folder, source, lock_sha): + manifest = json.loads((folder / 'assets-manifest.json').read_text()) + if (manifest.get('schema') != 1 or manifest.get('sourceHead') != source + or manifest.get('sourceRepository') != SOURCE_URL + or manifest.get('cargoLockSha256') != lock_sha): + raise ValueError('Asset provenance differs from exact public source and lock') + files = safe_members(folder / 'editor-candidate.tar.gz') + assets = manifest['assets'] + if not isinstance(assets, dict) or not assets or set(files) != set(assets): + raise ValueError('Archive and asset manifest members differ') + if 'host-session.js' in files: + raise ValueError('Host bridge remains a separately pinned source asset') + for name, raw in files.items(): + if hashlib.sha256(raw).hexdigest() != assets[name]: + raise ValueError('Archive member checksum differs') + for required in ('index.html', 'terraphim_editor.js', 'terraphim_editor_bg.wasm', 'roles/default.json'): + if required not in files: + raise ValueError('Missing editor delivery member') + return files + + +def checksums(text): + result = {} + for line in text.splitlines(): + match = re.fullmatch(r'([0-9a-f]{64}) ([A-Za-z0-9][A-Za-z0-9_.-]*)', line) + if not match or match[2] in result: + raise ValueError('Malformed or duplicate checksum entry') + result[match[2]] = match[1] + return result + + +def check_qualification(folder, source, lock_sha, tag): + report = json.loads((folder / 'qualification.json').read_text()) + if (report.get('status') != 'PASS' or report.get('tag') != tag + or report.get('sourceCommit') != source or report.get('sourceRepository') != SOURCE_URL + or report.get('cargoLockSha256') != lock_sha + or report.get('archiveSha256') != digest(folder / 'editor-candidate.tar.gz') + or report.get('manifestSha256') != digest(folder / 'assets-manifest.json') + or report.get('savedOutputsSha256') != digest(folder / 'saved-outputs.tar.gz') + or report.get('fullParityAccepted') is not False): + raise ValueError('Qualification differs from exact candidate/source or overstates parity') + gates = report.get('gates', []) + if (len(gates) != len(GATES) or {g.get('name') for g in gates} != set(GATES) + or any(g.get('status') != 'PASS' for g in gates)): + raise ValueError('All actual targeted qualification gates must pass') + outputs = safe_members(folder / 'saved-outputs.tar.gz') + expected = report.get('outputs') + if not expected or set(outputs) != set(expected): + raise ValueError('Saved output inventory differs') + for name, raw in outputs.items(): + if hashlib.sha256(raw).hexdigest() != expected[name]: + raise ValueError('Saved output checksum differs') + for gate in gates: + name = gate['name'] + '.log' + if name not in outputs or hashlib.sha256(outputs[name]).hexdigest() != gate.get('logSha256'): + raise ValueError('Actual gate log is missing or differs') + tap = outputs['wasm-host.log'].decode() + if not re.search(r'(?m)^# pass 15\s*$', tap) or not re.search(r'(?m)^# fail 0\s*$', tap): + raise ValueError('Actual WASM test output is incomplete') + if not outputs.get('story/annotated.md'): + raise ValueError('Saved annotated source is missing') + browser = json.loads(outputs['story/browser-receipt.json']) + graph = json.loads(outputs['story/knowledge-graph.json']) + clean = outputs['story/corpus/project-brief.md'].decode() + if (browser.get('status') != 'PASS' or not all(browser.get(k) is True for k in + ('nativeKeyboardUndoRedo', 'saveReopen', 'actualDownloads')) + or browser.get('graph') != graph or browser.get('clean') != clean + or 'concern' in clean.lower() or 'Risk:' not in clean or 'Decision:' not in clean + or graph['data']['risk']['id'] != graph['data']['concern']['id']): + raise ValueError('Actual saved synonym graph/document outputs failed') + offline = json.loads(outputs['offline.json']) + if (offline.get('status') != 'PASS' or offline.get('engine') != 'chrome' + or len(offline.get('scenarios', [])) != 2 + or any(s.get('result', {}).get('saveReopenExport') is not True or s.get('externalRequests') != [] + or s.get('freshContext') is not True for s in offline['scenarios'])): + raise ValueError('Actual Chrome full-page/embed delivery did not pass') + return report + + +def verify_package(folder, tag, root=ROOT): + folder = Path(folder) + files = [folder / name for name in FILES] + if set(p.name for p in folder.iterdir()) != set(FILES) or any(p.is_symlink() or not p.is_file() for p in files): + raise ValueError('Expected only the complete regular release file set') + source = admit_source(tag, root) + lock_sha = digest(root / 'Cargo.lock') + check_assets(folder, source, lock_sha) + check_qualification(folder, source, lock_sha, tag) + expected = {p.name: digest(p) for p in files if p.name != 'SHA256SUMS'} + if checksums((folder / 'SHA256SUMS').read_text()) != expected: + raise ValueError('Release checksums differ') + readiness = load_module('release_readiness', 'scripts/check-release-readiness.py') + report = readiness.validate_release(folder / 'release-manifest.json', folder) + if not readiness.evaluate_claim(report, 'browser', 'integrity')['allowed']: + raise ValueError('Release integrity/evidence binding failed') + artifact = report['manifest']['artifacts'] + if (len(artifact) != 1 or artifact[0]['sourceCommit'] != source or artifact[0]['version'] != tag + or report['manifest']['channel'] != 'candidate' + or not any(item['host'] == 'browser' and item['requirement'] == 'all41' and item['status'] == 'OPEN' + for item in report['manifest']['limitations'])): + raise ValueError('Prerelease identity or explicit limitations missing') + return source, files + + +def package(stage, output, tag): + source = admit_source(tag) + if output.exists(): + raise ValueError('Use a new candidate directory') + publisher = load_module('dist_inventory', 'scripts/publish-editor-dist.py') + assets = publisher.inventory(stage) + output.mkdir(parents=True) + with tarfile.open(output / 'editor-candidate.tar.gz', 'w:gz') as handle: + for name in sorted(assets): + handle.add(stage / name, arcname=name, recursive=False) + manifest = {'schema': 1, 'sourceHead': source, 'sourceRepository': SOURCE_URL, + 'cargoLockSha256': digest(ROOT / 'Cargo.lock'), 'assets': assets} + (output / 'assets-manifest.json').write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n') + check_assets(output, source, manifest['cargoLockSha256']) +def gh(*args): + return subprocess.check_output(["gh", *args], text=True, timeout=120) + + +def api(path): + return json.loads(gh("api", "--hostname", "github.com", path)) + + +def select_release(pages, tag): + found = [item for page in pages for item in page if item["tag_name"] == tag] + if len(found) > 1: + raise ValueError("Ambiguous release tag") + return found[0] if found else None + + +def release(repo, tag): + # Listing includes drafts; the public tag endpoint returns 404 for drafts. + pages = json.loads(gh("api", "--hostname", "github.com", "--paginate", "--slurp", + f"repos/{repo}/releases?per_page=100")) + return select_release(pages, tag) + + +def verify_remote_tag(repo, tag, source): + value = api(f"repos/{repo}/git/ref/tags/{tag}")["object"] + for _ in range(5): + if value["type"] == "commit" and value["sha"] == source: + return + if value["type"] != "tag" or not SHA_RE.fullmatch(value["sha"]): + break + value = api(f"repos/{repo}/git/tags/{value['sha']}")["object"] + raise ValueError("Remote tag does not resolve to the qualified source commit") + + +def resume_plan(current, tag, files): + if current.get("tag_name") != tag or current.get("prerelease") is not True: + raise ValueError("Release tag or preview channel differs") + if type(current.get("draft")) is not bool: + raise ValueError("Release draft state is missing") + assets = current["assets"] + names = [item["name"] for item in assets] + wanted = {p.name: p for p in files} + if len(names) != len(set(names)) or not set(names).issubset(wanted): + raise ValueError("Unexpected or duplicate remote release assets") + for item in assets: + if item.get("state") != "uploaded" or item.get("size") != wanted[item["name"]].stat().st_size: + raise ValueError("Existing asset is incomplete or differs in size; refusing replacement") + if item.get("digest") not in (None, "sha256:" + digest(wanted[item["name"]])): + raise ValueError("Existing remote asset digest differs; refusing replacement") + missing = [p for p in files if p.name not in names] + if missing and not current["draft"]: + raise ValueError("Published release has missing assets; refusing repair in place") + return missing + + +def anonymous_download(url, destination): + """Anonymous HTTPS GET, without curl config, auth headers, cookies or netrc.""" + parsed = urlparse(url) + if (parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password + or parsed.query or parsed.fragment): + raise ValueError("Anonymous download requires an ordinary HTTPS URL") + if destination.exists() or destination.is_symlink(): + raise ValueError("Refusing to overwrite a download") + result = subprocess.run([ + "curl", "--disable", "--fail", "--silent", "--show-error", "--location", + "--proto", "=https", "--proto-redir", "=https", "--max-redirs", "5", + "--connect-timeout", "15", "--max-time", "300", "--max-filesize", str(128 * 1024 * 1024), + "--retry", "2", "--output", str(destination), "--write-out", "%{http_code}", url, + ], text=True, capture_output=True) + if result.returncode or result.stdout != "200": + raise ValueError(f"Anonymous GET failed: HTTP {result.stdout}; curl {result.returncode}") + return destination.read_bytes() + + +def publish(repo, tag, files, source): + if repo != REPOSITORY: + raise ValueError("Invalid GitHub repository") + repository = api(f"repos/{repo}") + if repository.get("private") is not False or repository.get("full_name", "").lower() != repo.lower(): + raise ValueError("Publication requires the exact public GitHub repository") + verify_remote_tag(repo, tag, source) + current = release(repo, tag) + with tempfile.TemporaryDirectory(prefix="terraphim-publish-") as folder: + temp = Path(folder) + if current is None: + notes = temp / "notes.md" + notes.write_text(f"Terraphim editor prerelease {tag}; source `{source}`.\n\n" + "The attached browser archive passed real WASM host-session tests and " + "Chrome synonym creation, swap, undo/redo, save/reopen and graph/text downloads. " + "Offline full-page and embed delivery were checked. Saved outputs and receipts are attached. " + "This prerelease does not qualify all 41 requirements or native Safari, Firefox and Zed.\n") + gh("release", "create", tag, "--repo", f"github.com/{repo}", "--verify-tag", + "--draft", "--prerelease", "--latest=false", "--title", tag, "--notes-file", str(notes)) + current = release(repo, tag) + if current is None: + raise ValueError("Draft release could not be recovered") + missing = resume_plan(current, tag, files) + # Verify every existing asset before mutating an incomplete draft. + for path in files: + if path not in missing: + gh("release", "download", tag, "--repo", f"github.com/{repo}", + "--pattern", path.name, "--dir", str(temp)) + if (temp / path.name).read_bytes() != path.read_bytes(): + raise ValueError(f"Existing {path.name} differs; refusing replacement") + for path in missing: + gh("release", "upload", tag, str(path), "--repo", f"github.com/{repo}") + gh("release", "download", tag, "--repo", f"github.com/{repo}", + "--pattern", path.name, "--dir", str(temp)) + if (temp / path.name).read_bytes() != path.read_bytes(): + raise ValueError("Uploaded asset readback differs") + current = release(repo, tag) + if current is None or resume_plan(current, tag, files): + raise ValueError("Release asset set is incomplete") + verify_remote_tag(repo, tag, source) + if current["draft"]: + gh("release", "edit", tag, "--repo", f"github.com/{repo}", "--verify-tag", "--draft=false") + published = release(repo, tag) + if published is None or published["draft"] or resume_plan(published, tag, files): + raise ValueError("Release is not publicly published with the exact asset set") + verify_remote_tag(repo, tag, source) + for path in files: + url = f"https://github.com/{repo}/releases/download/{tag}/{path.name}" + if anonymous_download(url, temp / ("public-" + path.name)) != path.read_bytes(): + raise ValueError("Anonymous release readback differs") + return {"schema": 1, "status": "PUBLISHED_PUBLIC_VERIFIED", "repository": repo, + "tag": tag, "sourceCommit": source, "releaseId": published["id"], + "assets": [{"name": p.name, "sha256": digest(p), "bytes": p.stat().st_size} for p in files]} + + + +def qualify(folder, evidence, tag): + """Run actual built WASM and Chrome; archive returned files, never proposed inputs.""" + import functools + import http.server + import os + import threading + source = admit_source(tag) + lock_sha = digest(ROOT / 'Cargo.lock') + payload = check_assets(folder, source, lock_sha) + if evidence.exists() or (folder / 'qualification.json').exists(): + raise ValueError('Use new evidence paths; retain previous output') + evidence.mkdir(parents=True) + gates = [] + with tempfile.TemporaryDirectory(prefix='editor-release-qualify-') as temporary: + staged = Path(temporary) / 'dist' + staged.mkdir() + for name, raw in payload.items(): + target = staged / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(raw) + # Node needs an explicit ESM suffix; only a byte-identical test copy is renamed. + (staged / 'terraphim_editor.mjs').write_bytes(payload['terraphim_editor.js']) + (staged / 'package.json').write_text('{"type":"module"}\n') + env = dict(os.environ) + env['TERRAPHIM_HOST_WASM'] = str(staged) + env['TERRAPHIM_PLAYWRIGHT_MODULE'] = str(ROOT / 'scripts/ci/release-node/node_modules/playwright') + env['TERRAPHIM_STORY_OUT'] = str(evidence / 'story') + server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(staged))) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + env['TERRAPHIM_STORY_URL'] = f'http://127.0.0.1:{server.server_port}/' + commands = [ + ('wasm-host', ['node', '--test', '--test-reporter=tap', 'tests/browser/host-session-conformance.cjs', 'tests/browser/authored-synonyms.cjs']), + ('chrome-synonyms', ['node', 'tests/browser/synonym-sharing.cjs']), + ('chrome-offline', [sys.executable, 'scripts/ci/editor-offline-check.py', str(staged), '--engine', 'chrome', '--report', str(evidence / 'offline.json')]), + ] + try: + for name, command in commands: + result = subprocess.run(command, cwd=ROOT, env=env, capture_output=True, text=True, timeout=240) + log = evidence / (name + '.log') + log.write_text(result.stdout + result.stderr) + if result.returncode: + raise ValueError(f'{name} failed; retained {log.name}') + if name == 'wasm-host' and not re.search(r'(?m)^# pass 15\s*$', result.stdout): + raise ValueError('Expected all 15 actual WASM host tests') + gates.append({'name': name, 'status': 'PASS', 'command': command, 'logSha256': digest(log)}) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=10) + outputs = {} + with tarfile.open(folder / 'saved-outputs.tar.gz', 'w:gz') as handle: + for path in sorted(evidence.rglob('*')): + if path.is_symlink(): + raise ValueError('Symlink evidence forbidden') + if path.is_file(): + name = path.relative_to(evidence).as_posix() + outputs[name] = digest(path) + handle.add(path, arcname=name, recursive=False) + report = {'schema': 1, 'status': 'PASS', 'tag': tag, 'sourceRepository': SOURCE_URL, + 'sourceCommit': source, 'cargoLockSha256': lock_sha, + 'archiveSha256': digest(folder / 'editor-candidate.tar.gz'), + 'manifestSha256': digest(folder / 'assets-manifest.json'), + 'savedOutputsSha256': digest(folder / 'saved-outputs.tar.gz'), + 'gates': gates, 'outputs': outputs, 'fullParityAccepted': False, + 'limitations': 'Targeted Chrome and WASM checks only; all41 and native Safari/Firefox/Zed are not qualified by this run.'} + (folder / 'qualification.json').write_text(json.dumps(report, indent=2) + '\n') + check_qualification(folder, source, lock_sha, tag) + def file(name): + path = folder / name + return {'path': name, 'bytes': path.stat().st_size, 'sha256': digest(path)} + archive = file('editor-candidate.tar.gz') + manifest = {'schema': 1, 'releaseId': 'editor-' + source, 'channel': 'candidate', + 'artifacts': [{**archive, 'host': 'browser', 'version': tag, 'sourceCommit': source}], + 'evidence': [{**file(name), 'host': 'browser', 'kind': 'receipt', 'artifactSha256': archive['sha256']} + for name in ('qualification.json', 'saved-outputs.tar.gz')], + 'limitations': [{'host': 'browser', 'requirement': 'all41', 'status': 'OPEN', + 'issue': SOURCE_URL + '/blob/' + source + '/docs/requirements/traceability.md'}]} + (folder / 'release-manifest.json').write_text(json.dumps(manifest, indent=2) + '\n') + (folder / 'SHA256SUMS').write_text(''.join(digest(folder / name) + ' ' + name + '\n' for name in FILES if name != 'SHA256SUMS')) + verify_package(folder, tag) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='command', required=True) + admit = sub.add_parser('admit'); admit.add_argument('--tag', required=True) + build = sub.add_parser('package'); build.add_argument('--tag', required=True); build.add_argument('--stage', type=Path, required=True); build.add_argument('--output', type=Path, required=True) + test = sub.add_parser('qualify'); test.add_argument('--tag', required=True); test.add_argument('--folder', type=Path, required=True); test.add_argument('--evidence', type=Path, required=True) + verify = sub.add_parser('verify'); verify.add_argument('--tag', required=True); verify.add_argument('--folder', type=Path, required=True) + release = sub.add_parser('publish'); release.add_argument('--tag', required=True); release.add_argument('--folder', type=Path, required=True); release.add_argument('--receipt', type=Path, required=True) + args = parser.parse_args() + if args.command == 'admit': + print(admit_source(args.tag)) + elif args.command == 'package': + package(args.stage, args.output, args.tag) + elif args.command == 'qualify': + qualify(args.folder, args.evidence, args.tag) + elif args.command == 'verify': + source, files = verify_package(args.folder, args.tag) + print(json.dumps({'status': 'VERIFIED', 'source': source, 'files': [p.name for p in files]})) + else: + if args.receipt.exists() or args.receipt.is_symlink(): + raise ValueError('Use a new publication receipt path') + source, files = verify_package(args.folder, args.tag) + with tempfile.TemporaryDirectory(prefix='editor-publication-snapshot-') as temporary: + snapshot = Path(temporary) + for path in files: + (snapshot / path.name).write_bytes(path.read_bytes()) + source, snapshots = verify_package(snapshot, args.tag) + result = publish(REPOSITORY, args.tag, snapshots, source) + args.receipt.parent.mkdir(parents=True, exist_ok=True) + with args.receipt.open('x') as output: + output.write(json.dumps(result, indent=2) + '\n') + + +if __name__ == '__main__': + try: + main() + except (ValueError, KeyError, TypeError, OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired, tarfile.TarError) as error: + sys.exit(f'editor release refused: {error}') diff --git a/tests/test_editor_release.py b/tests/test_editor_release.py new file mode 100644 index 0000000..0a91bb7 --- /dev/null +++ b/tests/test_editor_release.py @@ -0,0 +1,235 @@ +"""Actual Git/archive/filesystem refusal tests; fixtures are not runtime qualification.""" +import copy +import hashlib +import importlib.util +import io +import json +from pathlib import Path +import subprocess +import tarfile +import tempfile +import unittest + +SPEC = importlib.util.spec_from_file_location('editor_release', Path(__file__).resolve().parents[1] / 'scripts/publish-editor-release.py') +M = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(M) + + +def sha(raw): + return hashlib.sha256(raw).hexdigest() + + +def archive(path, members): + with tarfile.open(path, 'w:gz') as output: + for name, raw in members: + entry = tarfile.TarInfo(name); entry.size = len(raw) + output.addfile(entry, io.BytesIO(raw)) + + +class EditorReleaseTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(); self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name); self.folder = self.root / 'candidate'; self.folder.mkdir() + self.source = 'a' * 40; self.lock = 'b' * 64; self.tag = 'v0.1.0-rc.2' + self.members = {name: ('fixture:' + name).encode() for name in ('index.html', 'terraphim_editor.js', 'terraphim_editor_bg.wasm', 'roles/default.json')} + archive(self.folder / 'editor-candidate.tar.gz', self.members.items()) + self.manifest = {'schema': 1, 'sourceHead': self.source, 'sourceRepository': M.SOURCE_URL, + 'cargoLockSha256': self.lock, 'assets': {name: sha(raw) for name, raw in self.members.items()}} + self.write_manifest() + self.graph = {'data': {'risk': {'id': 1, 'nterm': 'risk'}, 'concern': {'id': 1, 'nterm': 'risk'}}} + clean = 'Risk: Timber. Decision: Backup.' + browser = {'status': 'PASS', 'nativeKeyboardUndoRedo': True, 'saveReopen': True, + 'actualDownloads': True, 'graph': self.graph, 'clean': clean} + offline = {'status': 'PASS', 'engine': 'chrome', 'scenarios': [{'result': {'saveReopenExport': True}, 'externalRequests': [], 'freshContext': True}] * 2} + self.outputs = {'story/knowledge-graph.json': json.dumps(self.graph).encode(), + 'story/corpus/project-brief.md': clean.encode(), + 'story/browser-receipt.json': json.dumps(browser).encode(), + 'offline.json': json.dumps(offline).encode(), + 'story/annotated.md': b'Explicit protocol fixture, not runtime evidence', + 'wasm-host.log': b'# pass 15\n# fail 0\n', 'chrome-synonyms.log': b'fixture', 'chrome-offline.log': b'fixture'} + self.report = {'status': 'PASS', 'tag': self.tag, 'sourceCommit': self.source, 'sourceRepository': M.SOURCE_URL, + 'cargoLockSha256': self.lock, 'archiveSha256': M.digest(self.folder / 'editor-candidate.tar.gz'), + 'manifestSha256': M.digest(self.folder / 'assets-manifest.json'), 'fullParityAccepted': False, + 'gates': [{'name': name, 'status': 'PASS', 'logSha256': sha(self.outputs[name + '.log'])} for name in M.GATES]} + self.write_outputs() + + def write_manifest(self): + (self.folder / 'assets-manifest.json').write_text(json.dumps(self.manifest)) + + def write_outputs(self): + archive(self.folder / 'saved-outputs.tar.gz', self.outputs.items()) + self.report.update(savedOutputsSha256=M.digest(self.folder / 'saved-outputs.tar.gz'), outputs={n: sha(raw) for n, raw in self.outputs.items()}) + self.write_report() + + def write_report(self): + (self.folder / 'qualification.json').write_text(json.dumps(self.report)) + + def test_actual_archive_members_and_hashes_are_verified(self): + self.assertEqual(M.check_assets(self.folder, self.source, self.lock), self.members) + self.manifest['assets']['index.html'] = '0' * 64; self.write_manifest() + with self.assertRaises(ValueError): M.check_assets(self.folder, self.source, self.lock) + + def test_wrong_source_repository_or_lock_refused(self): + for key in ('sourceHead', 'sourceRepository', 'cargoLockSha256'): + original = self.manifest[key]; self.manifest[key] = 'wrong'; self.write_manifest() + with self.subTest(key=key), self.assertRaises(ValueError): M.check_assets(self.folder, self.source, self.lock) + self.manifest[key] = original + + def test_unknown_members_and_embedded_bridge_refused(self): + for name in ('extra.js', 'host-session.js'): + items = {**self.members, name: b'extra'} + archive(self.folder / 'editor-candidate.tar.gz', items.items()) + if name == 'host-session.js': + self.manifest['assets'][name] = sha(b'extra'); self.write_manifest() + with self.subTest(name=name), self.assertRaises(ValueError): M.check_assets(self.folder, self.source, self.lock) + + def test_archive_traversal_duplicates_links_and_empty_refused(self): + path = self.folder / 'bad.tar.gz' + for entries in ([('../escape', b'x')], [('/absolute', b'x')], [('a', b'x'), ('a', b'y')], [('a/./b', b'x')], []): + archive(path, entries) + with self.subTest(entries=entries), self.assertRaises(ValueError): M.safe_members(path) + with tarfile.open(path, 'w:gz') as output: + link = tarfile.TarInfo('link'); link.type = tarfile.SYMTYPE; link.linkname = 'elsewhere'; output.addfile(link) + with self.assertRaises(ValueError): M.safe_members(path) + + def test_targeted_qualification_protocol_accepts_bound_fixture(self): + self.assertEqual(M.check_qualification(self.folder, self.source, self.lock, self.tag), self.report) + + def test_fail_missing_duplicate_gates_and_full_parity_overclaim_refused(self): + original = copy.deepcopy(self.report) + changes = [{'fullParityAccepted': True}, {'status': 'INCOMPLETE'}, {'gates': self.report['gates'][:2]}, + {'gates': [self.report['gates'][0]] * 3}, {'gates': [{**g, 'status': 'FAIL'} for g in self.report['gates']]}, + {'archiveSha256': '0' * 64}, {'sourceCommit': 'c' * 40}] + for change in changes: + self.report = {**original, **change}; self.write_report() + with self.subTest(change=change), self.assertRaises(ValueError): M.check_qualification(self.folder, self.source, self.lock, self.tag) + + def test_changed_saved_graph_or_clean_text_refused_even_with_rehashed_archive(self): + original = dict(self.outputs) + graph = copy.deepcopy(self.graph); graph['data']['concern']['id'] = 2 + for name, raw in [('story/knowledge-graph.json', json.dumps(graph).encode()), ('story/corpus/project-brief.md', b'Concern: supplied input')]: + self.outputs = {**original, name: raw}; self.write_outputs() + with self.subTest(name=name), self.assertRaises(ValueError): M.check_qualification(self.folder, self.source, self.lock, self.tag) + + def test_changed_output_hash_refused(self): + self.report['outputs']['offline.json'] = '0' * 64; self.write_report() + with self.assertRaises(ValueError): M.check_qualification(self.folder, self.source, self.lock, self.tag) + + def test_resume_only_identical_draft_assets_and_never_repair_public_release(self): + files = [self.folder / 'editor-candidate.tar.gz', self.folder / 'assets-manifest.json'] + item = lambda p: {'name': p.name, 'state': 'uploaded', 'size': p.stat().st_size, 'digest': 'sha256:' + M.digest(p)} + current = {'tag_name': self.tag, 'prerelease': True, 'draft': True, 'assets': [item(files[0])]} + self.assertEqual(M.resume_plan(current, self.tag, files), [files[1]]) + current['draft'] = False + with self.assertRaises(ValueError): M.resume_plan(current, self.tag, files) + current['assets'].append(item(files[1])) + self.assertEqual(M.resume_plan(current, self.tag, files), []) + current['assets'][0]['digest'] = 'sha256:' + '0' * 64 + with self.assertRaises(ValueError): M.resume_plan(current, self.tag, files) + + def test_ambiguous_release_and_asset_metadata_refused(self): + current = {'tag_name': self.tag, 'prerelease': True, 'draft': True, 'assets': []} + with self.assertRaises(ValueError): M.select_release([[current], [current]], self.tag) + file = self.folder / 'assets-manifest.json' + for assets in ([{'name': 'surprise'}], [{'name': file.name}] * 2, [{'name': file.name, 'state': 'new', 'size': file.stat().st_size}]): + current['assets'] = assets + with self.assertRaises(ValueError): M.resume_plan(current, self.tag, [file]) + + def test_log_counts_or_missing_annotated_output_cannot_be_forged_by_rehashing(self): + original = dict(self.outputs) + for name, raw in [('wasm-host.log', b'# pass 14\n# fail 0\n'), ('story/annotated.md', b'')]: + self.outputs = {**original, name: raw} + for gate in self.report['gates']: + gate['logSha256'] = sha(self.outputs[gate['name'] + '.log']) + self.write_outputs() + with self.subTest(name=name), self.assertRaises(ValueError): M.check_qualification(self.folder, self.source, self.lock, self.tag) + + def test_wrong_publication_repository_is_refused_before_any_api_call(self): + with self.assertRaises(ValueError): M.publish('other/repository', self.tag, [], self.source) + + def test_checksum_manifest_rejects_malformed_duplicate_names(self): + valid = 'a' * 64 + ' editor-candidate.tar.gz\n' + self.assertEqual(M.checksums(valid), {'editor-candidate.tar.gz': 'a' * 64}) + for value in (valid * 2, 'bad file', 'a' * 64 + ' ../escape\n'): + with self.assertRaises(ValueError): M.checksums(value) + + +class SourceTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(); self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.git('init', '-q') + for name in ['public/roles/default.json', 'scripts/build-dist.sh', 'scripts/publish-editor-dist.py', 'scripts/check-public-dependencies.py', '.cargo/config.toml']: + path = self.root / name; path.parent.mkdir(parents=True, exist_ok=True); path.write_text('' if name.endswith('toml') else 'fixture') + (self.root / 'Cargo.toml').write_text('[package]\nname="terraphim-editor"\nversion="0.1.0"\n') + guard = M.load_module('public_dependencies', 'scripts/check-public-dependencies.py') + (self.root / 'Cargo.lock').write_text('version=4\n' + ''.join('[[package]]\nname="'+n+'"\nversion="'+v+'"\nsource="'+guard.CRATES_IO+'"\nchecksum="'+'a'*64+'"\n' for n,v in guard.REQUIRED.items())) + self.git('remote', 'add', 'origin', M.SOURCE_URL + '.git') + self.git('add', '.') + self.commit('fixture'); self.source = self.git('rev-parse', 'HEAD') + self.git('update-ref', 'refs/remotes/origin/main', self.source) + self.git('tag', 'v0.1.0-rc.2') + + def git(self, *args): + return subprocess.check_output(['git', *args], cwd=self.root, text=True, stderr=subprocess.DEVNULL).strip() + + def commit(self, message): + self.git('-c', 'user.name=Fixture', '-c', 'user.email=test@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-qm', message) + + def test_complete_file_package_checks_source_outputs_manifest_and_sums_together(self): + fixture = EditorReleaseTests(); fixture.setUp(); self.addCleanup(fixture.temp.cleanup) + fixture.source = self.source; fixture.lock = M.digest(self.root / 'Cargo.lock') + fixture.manifest.update(sourceHead=self.source, cargoLockSha256=fixture.lock); fixture.write_manifest() + fixture.report.update(sourceCommit=self.source, cargoLockSha256=fixture.lock, + manifestSha256=M.digest(fixture.folder / 'assets-manifest.json')); fixture.write_report() + def item(name): + p = fixture.folder / name + return {'path': name, 'bytes': p.stat().st_size, 'sha256': M.digest(p)} + archive_item = item('editor-candidate.tar.gz') + manifest = {'schema': 1, 'releaseId': 'fixture', 'channel': 'candidate', + 'artifacts': [{**archive_item, 'host': 'browser', 'version': fixture.tag, 'sourceCommit': self.source}], + 'evidence': [{**item('qualification.json'), 'host': 'browser', 'kind': 'receipt', 'artifactSha256': archive_item['sha256']}], + 'limitations': [{'host': 'browser', 'requirement': 'all41', 'status': 'OPEN', 'issue': 'fixture'}]} + def write(): + (fixture.folder / 'release-manifest.json').write_text(json.dumps(manifest)) + (fixture.folder / 'SHA256SUMS').write_text(''.join(M.digest(fixture.folder / n) + ' ' + n + '\n' for n in M.FILES if n != 'SHA256SUMS')) + write() + source, files = M.verify_package(fixture.folder, fixture.tag, self.root) + self.assertEqual(source, self.source); self.assertEqual({p.name for p in files}, set(M.FILES)) + manifest['limitations'][0]['status'] = 'RESOLVED'; write() + with self.assertRaises(ValueError): M.verify_package(fixture.folder, fixture.tag, self.root) + + def test_clean_reviewed_rc_accepted(self): + self.assertEqual(M.admit_source('v0.1.0-rc.2', self.root), self.source) + + def test_stable_wrong_component_and_version_tags_refused(self): + for tag in ('main', 'v0.1.0', 'v0.2.0-rc.2', 'terraphim_lab-v0.1.1', 'v0.1.0-rc.2;echo unsafe'): + with self.subTest(tag=tag), self.assertRaises(ValueError): M.admit_source(tag, self.root) + + def test_tag_mismatch_dirty_and_unreviewed_branch_refused(self): + (self.root / 'new-file').write_text('changed') + with self.assertRaises(ValueError): M.admit_source('v0.1.0-rc.2', self.root) + self.git('add', '.'); self.commit('new commit') + with self.assertRaises(ValueError): M.admit_source('v0.1.0-rc.2', self.root) + self.git('tag', 'v0.1.0-rc.3') + with self.assertRaisesRegex(ValueError, 'main'): M.admit_source('v0.1.0-rc.3', self.root) + + +class WorkflowTests(unittest.TestCase): + def test_only_protected_dependent_job_can_publish_exact_qualified_artifact(self): + workflow = (Path(__file__).resolve().parents[1] / '.github/workflows/release.yml').read_text() + build, publish = workflow.split(' publish:', 1) + self.assertNotIn('contents: write', build) + self.assertNotIn('GH_TOKEN:', build) + self.assertIn('needs: candidate', publish) + self.assertIn('environment: editor-release', publish) + self.assertIn('artifact-ids: ${{ needs.candidate.outputs.artifact-id }}', publish) + self.assertLess(publish.index('publish-editor-release.py verify'), publish.index('GH_TOKEN:')) + self.assertIn("github.ref_type == 'tag'", publish) + self.assertNotIn('--clobber', workflow) + self.assertNotIn('continue-on-error', workflow) + self.assertNotIn('pull_request_target', workflow) + + +if __name__ == '__main__': + unittest.main()