diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7d954206..b4f084c9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -51,6 +51,24 @@ Describe what scenarios have been tested: If checked, describe the breaking changes and migration path: +### GraphQL Breaking-Change Checklist + + + +- [ ] **No non-deprecated field is removed** — if a field is removed, confirm + it carried `#[graphql(deprecation)]` for ≥ 90 days (see `GRAPHQL_SCHEMA_VERSIONING.md`). +- [ ] **Deprecation registered** — new deprecations are recorded in + `src/graphql/schema_deprecations.rs` with correct `deprecated_since` + and `earliest_removal` dates (≥ deprecated_since + 90 days). +- [ ] **Replacement documented** — the `#[graphql(deprecation = "...")]` reason string + names the replacement field/type. +- [ ] **Schema version bumped** — `SCHEMA_VERSION` in `src/graphql/schema.rs` + updated if a field/type is removed or a type signature changed. +- [ ] **CHANGELOG updated** — entry added under `### Deprecated` or `### Removed` as appropriate. +- [ ] **CI lint passes** — `bash scripts/check-graphql-deprecations.sh` exits 0. +- [ ] **Consumer impact assessed** — first-party client libraries + (`client.go`, `client.py`, `client.rs`, `client.ts`) checked or updated. + ## Documentation - [ ] README.md updated diff --git a/.github/workflows/graphql-schema-lint.yml b/.github/workflows/graphql-schema-lint.yml new file mode 100644 index 00000000..18cf99bd --- /dev/null +++ b/.github/workflows/graphql-schema-lint.yml @@ -0,0 +1,40 @@ +name: GraphQL Schema Lint + +# Enforces the 90-day deprecation SLA from GRAPHQL_SCHEMA_VERSIONING.md. +# Runs on every push/PR that touches GraphQL source or schema docs. + +on: + push: + paths: + - "src/graphql/**" + - "GRAPHQL_*.md" + - "scripts/check-graphql-deprecations.sh" + pull_request: + paths: + - "src/graphql/**" + - "GRAPHQL_*.md" + - "scripts/check-graphql-deprecations.sh" + +jobs: + graphql-schema-lint: + name: GraphQL Schema Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Make lint script executable + run: chmod +x scripts/check-graphql-deprecations.sh + + - name: Check GraphQL deprecation windows + run: bash scripts/check-graphql-deprecations.sh + + - name: Verify schema_deprecations.rs compiles + # Compile only the graphql module to catch any Rust errors in the + # deprecation registry before they reach the main CI build job. + uses: dtolnay/rust-toolchain@stable + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libudev-dev + + - name: Compile graphql module (deprecation registry check) + run: cargo check --locked 2>&1 | grep -E "^error" && exit 1 || true diff --git a/Cargo.lock b/Cargo.lock index 348aee8c..ca5f4a5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,253 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags 2.13.1", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.13.1", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags 2.13.1", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash 0.1.5", - "futures-core", - "h2 0.3.27", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "actix-router" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash 0.1.5", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.5", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-actors" -version = "4.3.1+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "actix_derive" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "adler2" version = "2.0.1" @@ -311,21 +64,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -566,12 +304,6 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" -[[package]] -name = "ascii_utils" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" - [[package]] name = "assert-json-diff" version = "2.0.2" @@ -582,134 +314,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-graphql" -version = "7.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba6d24703c5adc5ba9116901b92ee4e4c0643c01a56c4fd303f3818638d7449" -dependencies = [ - "async-graphql-derive", - "async-graphql-parser", - "async-graphql-value", - "async-stream", - "async-trait", - "base64 0.22.1", - "bytes", - "fast_chemail", - "fnv", - "futures-timer", - "futures-util", - "handlebars", - "http 1.5.0", - "indexmap 2.11.4", - "mime", - "multer", - "num-traits", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "static_assertions_next", - "tempfile", - "thiserror 1.0.69", -] - -[[package]] -name = "async-graphql-actix-web" -version = "7.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0644a2f8812b8787d2fbd725a22bce3c79c2afb66d5f8d70fbda6919736abf7" -dependencies = [ - "actix", - "actix-http", - "actix-web", - "actix-web-actors", - "async-channel", - "async-graphql", - "async-stream", - "futures-channel", - "futures-util", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "async-graphql-derive" -version = "7.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94c2d176893486bd37cd1b6defadd999f7357bf5804e92f510c08bcf16c538f" -dependencies = [ - "Inflector", - "async-graphql-parser", - "darling 0.20.11", - "proc-macro-crate", - "proc-macro2", - "quote", - "strum 0.26.3", - "syn 2.0.119", - "thiserror 1.0.69", -] - -[[package]] -name = "async-graphql-parser" -version = "7.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79272bdbf26af97866e149f05b2b546edb5c00e51b5f916289931ed233e208ad" -dependencies = [ - "async-graphql-value", - "pest", - "serde", - "serde_json", -] - -[[package]] -name = "async-graphql-value" -version = "7.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5ec94176a12a8cbe985cd73f2e54dc9c702c88c766bdef12f1f3a67cedbee1" -dependencies = [ - "bytes", - "indexmap 2.11.4", - "serde", - "serde_json", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "async-trait" version = "0.1.92" @@ -718,7 +322,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -826,9 +430,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "blake2" @@ -857,27 +461,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -907,9 +490,6 @@ name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -dependencies = [ - "serde", -] [[package]] name = "bytes-lit" @@ -923,15 +503,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "bytestring" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = [ - "bytes", -] - [[package]] name = "bzip2" version = "0.5.2" @@ -951,12 +522,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - [[package]] name = "cast" version = "0.3.0" @@ -974,9 +539,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.4" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" dependencies = [ "find-msvc-tools", "jobserver", @@ -986,9 +551,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -1132,12 +697,12 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1146,7 +711,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1163,9 +728,6 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" @@ -1178,15 +740,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" version = "0.15.11" @@ -1221,17 +774,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1397,9 +939,9 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -1448,18 +990,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1467,18 +1009,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crossterm" @@ -1486,13 +1028,10 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crossterm_winapi", "libc", - "mio 0.8.11", "parking_lot", - "signal-hook", - "signal-hook-mio", "winapi", ] @@ -1502,13 +1041,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crossterm_winapi", "derive_more", "document-features", "mio", "parking_lot", - "rustix 1.1.4", + "rustix 1.1.5", "signal-hook", "signal-hook-mio", "winapi", @@ -1819,7 +1358,6 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "unicode-xid", ] [[package]] @@ -1875,7 +1413,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -1889,7 +1427,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -1993,15 +1531,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "endian-type" version = "0.2.0" @@ -2021,7 +1550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2051,32 +1580,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "event-listener" -version = "5.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" -dependencies = [ - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -2116,15 +1619,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "fast_chemail" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" -dependencies = [ - "ascii_utils", -] - [[package]] name = "fastrand" version = "2.5.0" @@ -2160,9 +1654,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "finl_unicode" @@ -2184,7 +1678,6 @@ checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -2270,7 +1763,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -2285,12 +1778,6 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" -[[package]] -name = "futures-timer" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" - [[package]] name = "futures-util" version = "0.3.34" @@ -2387,25 +1874,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.11.4", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.19" @@ -2417,7 +1885,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.5.0", + "http", "indexmap 2.11.4", "slab", "tokio", @@ -2435,20 +1903,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "handlebars" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" -dependencies = [ - "log", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror 1.0.69", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2480,9 +1934,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", "foldhash 0.1.5", ] @@ -2531,9 +1982,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -2546,9 +1997,9 @@ dependencies = [ [[package]] name = "hex-conservative" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" dependencies = [ "arrayvec", ] @@ -2590,17 +2041,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.5.0" @@ -2618,7 +2058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "http 1.5.0", + "http", ] [[package]] @@ -2629,7 +2069,7 @@ checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", - "http 1.5.0", + "http", "http-body", "pin-project-lite", ] @@ -2656,8 +2096,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.19", - "http 1.5.0", + "h2", + "http", "http-body", "httparse", "httpdate", @@ -2674,7 +2114,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.5.0", + "http", "hyper", "hyper-util", "rustls", @@ -2695,7 +2135,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.5.0", + "http", "http-body", "hyper", "ipnet", @@ -2883,12 +2323,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "1.9.3" @@ -2964,9 +2398,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is-terminal" @@ -2976,7 +2410,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3003,15 +2437,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -3039,9 +2464,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", @@ -3086,17 +2511,11 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "leb128" @@ -3134,11 +2553,11 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.21" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "plain", "redox_syscall 0.9.4", @@ -3173,7 +2592,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9bd35dcd25f8578944c44cd75649d6487d74cf895002f6d86613164f2635b72" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -3200,23 +2619,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.14" @@ -3234,11 +2636,6 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" @@ -3275,12 +2672,12 @@ dependencies = [ [[package]] name = "mac_address" -version = "1.1.8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a" dependencies = [ - "nix 0.29.0", - "winapi", + "nix 0.30.1", + "windows-sys 0.61.2", ] [[package]] @@ -3323,7 +2720,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" dependencies = [ - "rustix 1.1.4", + "rustix 1.1.5", ] [[package]] @@ -3347,17 +2744,11 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minicov" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3aa3aa12b448ac225b3102217d1ac5cc717908f02722926524b0599c933c7a0" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" dependencies = [ "cc", "walkdir", @@ -3391,21 +2782,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -3423,7 +2802,7 @@ dependencies = [ "bytes", "colored 3.1.1", "futures-core", - "http 1.5.0", + "http", "http-body", "http-body-util", "hyper", @@ -3438,23 +2817,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http 1.5.0", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - [[package]] name = "nibble_vec" version = "0.1.0" @@ -3470,7 +2832,19 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -3483,7 +2857,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -3505,7 +2879,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3689,12 +3063,6 @@ dependencies = [ "libm", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -3999,15 +3367,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -4025,7 +3384,7 @@ checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.13.1", + "bitflags 2.13.2", "lazy_static", "num-traits", "rand 0.9.5", @@ -4087,7 +3446,7 @@ dependencies = [ "rustc-hash 2.1.3", "rustls", "socket2 0.6.5", - "thiserror 2.0.20", + "thiserror 2.0.21", "tokio", "tracing", "web-time", @@ -4125,7 +3484,7 @@ dependencies = [ "once_cell", "socket2 0.6.5", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4243,7 +3602,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "compact_str", "critical-section", "hashbrown 0.17.1", @@ -4308,7 +3667,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "hashbrown 0.17.1", "indoc", "instability", @@ -4322,26 +3681,6 @@ dependencies = [ "unicode-width 0.2.2", ] -[[package]] -name = "ratatui" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" -dependencies = [ - "bitflags", - "cassowary", - "compact_str", - "crossterm", - "itertools 0.12.1", - "lru", - "paste", - "stability", - "strum", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.1.14", -] - [[package]] name = "rayon" version = "1.12.0" @@ -4368,7 +3707,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -4377,7 +3716,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -4408,7 +3747,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -4447,12 +3786,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.11" @@ -4468,7 +3801,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures-core", - "http 1.5.0", + "http", "http-body", "http-body-util", "hyper", @@ -4553,7 +3886,7 @@ version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "fallible-iterator 0.3.0", "fallible-streaming-iterator", "hashlink", @@ -4589,7 +3922,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4598,22 +3931,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -4668,7 +4001,7 @@ version = "18.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "clipboard-win", "home", @@ -4774,7 +4107,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -4925,7 +4258,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 0.8.11", "mio", "signal-hook", ] @@ -4982,9 +4314,9 @@ checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" dependencies = [ "serde", ] @@ -5230,13 +4562,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" [[package]] -name = "stability" -version = "0.2.1" +name = "sqlite-wasm-rs" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" dependencies = [ - "quote", - "syn 2.0.119", + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", ] [[package]] @@ -5249,13 +4583,10 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" name = "starforge" version = "0.1.0" dependencies = [ - "actix-web", "aes-gcm", "anyhow", "arbitrary", "argon2", - "async-graphql", - "async-graphql-actix-web", "async-trait", "base64 0.22.1", "bip39", @@ -5263,10 +4594,9 @@ dependencies = [ "clap", "clap_complete", "clap_mangen", - "colored 2.1.0", + "colored 2.2.0", "comfy-table", "criterion", - "crossterm", "crossterm 0.29.0", "csv", "ctrlc", @@ -5329,12 +4659,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "static_assertions_next" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" - [[package]] name = "stellar-strkey" version = "0.0.9" @@ -5384,17 +4708,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -5406,9 +4719,6 @@ name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] [[package]] name = "strum" @@ -5480,9 +4790,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -5509,6 +4819,17 @@ dependencies = [ "syn 2.0.119", ] +[[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.6", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5524,8 +4845,8 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.4", - "windows-sys 0.59.0", + "rustix 1.1.5", + "windows-sys 0.61.2", ] [[package]] @@ -5543,9 +4864,9 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "parking_lot", - "rustix 1.1.4", + "rustix 1.1.5", "signal-hook", "windows-sys 0.61.2", ] @@ -5579,7 +4900,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64 0.22.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "fancy-regex 0.11.0", "filedescriptor", "finl_unicode", @@ -5650,7 +4971,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -5716,18 +5037,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" @@ -5737,7 +5049,7 @@ checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", - "mio 1.2.2", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -5754,7 +5066,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.6", ] [[package]] @@ -5785,9 +5097,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -5831,8 +5143,8 @@ checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.21.1", + "toml_datetime", + "toml_edit", ] [[package]] @@ -5844,15 +5156,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_edit" version = "0.21.1" @@ -5862,29 +5165,8 @@ dependencies = [ "indexmap 2.11.4", "serde", "serde_spanned", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow 0.7.15", -] - -[[package]] -name = "toml_parser" -version = "1.0.10+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" -dependencies = [ - "winnow 1.0.4", + "toml_datetime", + "winnow", ] [[package]] @@ -5908,10 +5190,10 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", - "http 1.5.0", + "http", "http-body", "pin-project-lite", "tower", @@ -5938,7 +5220,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6048,7 +5329,7 @@ checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ "bytes", "data-encoding", - "http 1.5.0", + "http", "httparse", "log", "rand 0.9.5", @@ -6084,9 +5365,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-normalization" @@ -6287,9 +5568,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -6300,9 +5581,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.77" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -6310,9 +5591,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6320,31 +5601,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895a2607575412a4eda1df892084a375ea10dfeadc4d7d2ab87b854e4ddc7ba1" +checksum = "45863ef0bef521c12124eb39d9a38513c47db75f22e503c06beacd40afeb35db" dependencies = [ "async-trait", "cast", @@ -6364,20 +5645,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288cb0ebe215033bf949ae1fd046726daa4c32a157f24b9dc6ac387a52aa759" +checksum = "8c89dcab8b516b6b603baca9d550b7282d68fcc7f367e3956cff7ebf406a3f12" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.127" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ff1c1b360982e93b6d8ea9c04836f71dba0817a16f91e229cf3a51bdd9d987" +checksum = "f37b4f992cebe528ef34964ae69681ac0fe7080071e7298e46008f9d380302af" [[package]] name = "wasm-encoder" @@ -6433,7 +5714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fbde0881f24199b81cf49b6ff8f9c145ac8eb1b7fc439adb5c099734f7d90e" dependencies = [ "ahash", - "bitflags 2.13.1", + "bitflags 2.13.2", "hashbrown 0.14.5", "indexmap 2.11.4", "semver", @@ -6446,7 +5727,7 @@ version = "0.243.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "indexmap 2.11.4", "semver", ] @@ -6478,7 +5759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419b8b485ab18b08c36731794e67f378b5419c6f07cf531ed10664f2062684de" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "bumpalo", "cc", "cfg-if", @@ -6668,9 +5949,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -6809,7 +6090,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -6910,6 +6191,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -6943,13 +6233,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -6962,6 +6269,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6974,6 +6287,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6986,12 +6305,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -7004,6 +6335,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -7016,6 +6353,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -7028,6 +6371,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -7041,29 +6390,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.5.40" +name = "windows_x86_64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" - [[package]] name = "wit-bindgen" version = "0.57.1" @@ -7130,23 +6470,23 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure", + "synstructure 0.13.2", ] [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" dependencies = [ "proc-macro2", "quote", @@ -7164,14 +6504,14 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] @@ -7246,12 +6586,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "zlib-rs" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" - [[package]] name = "zmij" version = "1.0.23" @@ -7290,9 +6624,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index e9bcd26e..5f0271a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,7 +143,6 @@ minijinja = "2.24" serde_yaml = "0.9.34" async-trait = "0.1" futures = "0.3.33" -tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-serde_json-1"] } ratatui = { version = "0.30", optional = true } crossterm = { version = "0.29", optional = true } # mlock/munlock for the signing agent key store (Unix only). diff --git a/GRAPHQL_SCHEMA_VERSIONING.md b/GRAPHQL_SCHEMA_VERSIONING.md new file mode 100644 index 00000000..3b0521dc --- /dev/null +++ b/GRAPHQL_SCHEMA_VERSIONING.md @@ -0,0 +1,380 @@ +# GraphQL Schema Versioning and Deprecation Policy + +> Related: [GRAPHQL_GUIDE.md](./GRAPHQL_GUIDE.md) · Issue #321 · Issue #781 + +This document is the authoritative policy for how StarForge's GraphQL API schema +evolves over time. All contributors making schema changes **must** read and follow +it before opening a PR. + +--- + +## Table of Contents + +- [Why a Schema Policy](#why-a-schema-policy) +- [Schema Versioning](#schema-versioning) +- [Deprecation SLA](#deprecation-sla) +- [Deprecation Process Step-by-Step](#deprecation-process-step-by-step) +- [Worked Example: Deprecating and Removing a Field](#worked-example-deprecating-and-removing-a-field) +- [Breaking-Change Checklist for PRs](#breaking-change-checklist-for-prs) +- [CI Lint Enforcement](#ci-lint-enforcement) +- [Approvals Required for Breaking Changes](#approvals-required-for-breaking-changes) +- [Interaction with API Versioning](#interaction-with-api-versioning) +- [FAQ](#faq) + +--- + +## Why a Schema Policy + +The StarForge GraphQL API is consumed by: +- The StarForge CLI client libraries (`client.go`, `client.py`, `client.rs`, `client.ts`) +- Third-party integrations and tooling built on the public API + +Silent removal of a field breaks callers immediately and without warning. A +published deprecation policy sets clear, enforceable expectations so consumers +can plan upgrades before fields disappear. + +--- + +## Schema Versioning + +### How schema versions are declared + +StarForge uses a **single versioned schema** served on `/graphql`. There is no +URL-level versioning (`/v1/graphql`, `/v2/graphql`). The schema version is +communicated through: + +1. **A `schemaVersion` query field** exposed on the root `Query` type: + + ```graphql + type Query { + schemaVersion: String! # e.g. "2026-10" + # ... other fields + } + ``` + + The value is a `YYYY-MM` calendar string, updated whenever a breaking change + (field removal or type change) ships. Non-breaking additions do not require a + version bump. + +2. **A `X-Schema-Version` response header** set by the GraphQL server so + clients that do not introspect can still detect the active version. + +3. **This document and CHANGELOG entries** — every version bump must have a + corresponding entry in `CHANGELOG.md` under the `## [Unreleased]` heading, + moved to a dated release when the version ships. + +### When to increment the schema version + +| Change type | Version bump required? | +|---|---| +| Add a new field/type/argument (non-breaking) | No | +| Mark a field `@deprecated` | No | +| Change a field's type (even compatible widening) | **Yes** | +| Remove a field that has completed its deprecation window | **Yes** | +| Rename a field | **Yes** (treat as remove + add) | +| Add a non-null argument to an existing field | **Yes** | + +--- + +## Deprecation SLA + +A field **must** carry the `@deprecated` directive for a minimum of **90 days** +(approximately one calendar quarter) before it may be removed from the schema. + +| Clock starts | When the PR that adds `@deprecated` merges to `master` | +| Earliest removal | 90 days after the deprecation merged | +| Extension required | If any known first-party consumer has not yet migrated | +| Emergency removal | Only for active security vulnerabilities; requires maintainer sign-off and an immediate major version bump | + +The 90-day window is enforced by the deprecation record maintained in +[`src/graphql/schema_deprecations.rs`](src/graphql/schema_deprecations.rs). +Each deprecated field has a `deprecated_since` date; the CI lint script +(`scripts/check-graphql-deprecations.sh`) will block removal PRs where the +window has not elapsed. + +--- + +## Deprecation Process Step-by-Step + +### 1. Add the `@deprecated` directive (code-first: `#[graphql(deprecation)]`) + +Because StarForge uses `async-graphql` (code-first), deprecation is declared +on the resolver with the `deprecation` attribute: + +```rust +#[Object] +impl Query { + /// Get all wallets + /// + /// # Deprecated + /// Use `walletsPaginated` instead, which supports cursor-based pagination. + #[graphql(deprecation = "Use `walletsPaginated` instead.")] + async fn wallets(&self) -> Vec { + // ... + } + + /// Get wallets with cursor-based pagination (replacement for `wallets`) + async fn wallets_paginated( + &self, + first: Option, + after: Option, + ) -> WalletConnection { + // ... + } +} +``` + +### 2. Register the deprecation in `schema_deprecations.rs` + +Add an entry to the static `DEPRECATED_FIELDS` table in +`src/graphql/schema_deprecations.rs`: + +```rust +DeprecatedField { + type_name: "Query", + field_name: "wallets", + deprecated_since: "2026-10-01", // Date this PR merges + earliest_removal: "2026-12-31", // deprecated_since + 90 days + replacement: Some("walletsPaginated"), + reason: "Use walletsPaginated for cursor-based pagination.", +}, +``` + +### 3. Open the PR + +The PR description must include the breaking-change checklist below +(copy it from the section below or from `.github/pull_request_template.md`). + +### 4. Notify consumers + +Comment on the issue or discussion thread, and update `CHANGELOG.md`: + +```markdown +### Deprecated + +- `Query.wallets` — deprecated in favour of `Query.walletsPaginated` + (cursor-based pagination). Will be removed on or after 2026-12-31. +``` + +### 5. Remove the field (after the SLA window) + +After 90 days, open a follow-up PR to: +1. Delete the resolver method and `DeprecatedField` entry. +2. Bump the `schemaVersion` value in `src/graphql/schema.rs`. +3. Add a `### Removed` entry to `CHANGELOG.md`. +4. Re-run CI (the lint script verifies the window elapsed). + +--- + +## Worked Example: Deprecating and Removing a Field + +This concrete example walks through the full lifecycle of `Query.wallets` being +replaced by `Query.walletsPaginated`. + +### Phase 1 — Deprecation PR (Day 0, e.g. 2026-10-01) + +**`src/graphql/resolvers.rs`** — add `deprecation` attribute and replacement: + +```rust +/// List all wallets (deprecated — use `walletsPaginated`) +#[graphql(deprecation = "Use `walletsPaginated(first, after)` instead.")] +async fn wallets(&self) -> Vec { + vec![/* ... */] +} + +/// List wallets with cursor-based pagination. +async fn wallets_paginated( + &self, + first: Option, + after: Option, +) -> Vec { + // new implementation + vec![/* ... */] +} +``` + +**`src/graphql/schema_deprecations.rs`** — register the deprecation: + +```rust +DeprecatedField { + type_name: "Query", + field_name: "wallets", + deprecated_since: "2026-10-01", + earliest_removal: "2026-12-31", + replacement: Some("walletsPaginated"), + reason: "Use walletsPaginated for cursor-based pagination.", +}, +``` + +**`CHANGELOG.md`** entry: + +```markdown +### Deprecated +- `Query.wallets` — use `walletsPaginated` (cursor pagination). Removal on/after 2026-12-31. +``` + +### Phase 2 — Consumer Migration (Days 1–90) + +Consumers update their queries: + +```graphql +# Before +query { wallets { id name balance } } + +# After +query { walletsPaginated(first: 20) { id name balance } } +``` + +### Phase 3 — Removal PR (Day 91+, e.g. 2027-01-15) + +**`src/graphql/resolvers.rs`** — remove the deprecated method entirely. + +**`src/graphql/schema_deprecations.rs`** — remove the `DeprecatedField` entry. + +**`src/graphql/schema.rs`** — bump `SCHEMA_VERSION`: + +```rust +pub const SCHEMA_VERSION: &str = "2027-01"; // was "2026-10" +``` + +**`CHANGELOG.md`**: + +```markdown +### Removed +- `Query.wallets` — removed after 90-day deprecation window. Use `walletsPaginated`. +``` + +**CI result**: the schema-lint job passes because the `earliest_removal` date +(`2026-12-31`) is in the past relative to the removal PR date. + +--- + +## Breaking-Change Checklist for PRs + +Copy this checklist into any PR that modifies the GraphQL schema. +It is also embedded in `.github/pull_request_template.md`. + +```markdown +### GraphQL Breaking-Change Checklist + + + +- [ ] **No non-deprecated field is removed** — if a field is removed, confirm + it carried `@deprecated` / `#[graphql(deprecation)]` for ≥ 90 days. +- [ ] **Deprecation registered** — new deprecations are recorded in + `src/graphql/schema_deprecations.rs` with correct `deprecated_since` + and `earliest_removal` dates. +- [ ] **Replacement documented** — the `#[graphql(deprecation)]` reason string + names the replacement field/type. +- [ ] **Schema version bumped** — `SCHEMA_VERSION` in `src/graphql/schema.rs` + updated if a field/type is removed or a type signature changed. +- [ ] **CHANGELOG updated** — entry added under `### Deprecated` or + `### Removed` as appropriate. +- [ ] **CI lint passes** — `scripts/check-graphql-deprecations.sh` exits 0. +- [ ] **Consumer impact assessed** — first-party client libraries + (`client.go`, `client.py`, `client.rs`, `client.ts`) checked or updated. +``` + +--- + +## CI Lint Enforcement + +A required CI job (`GraphQL Schema Lint`) runs on every push and PR that touches +`src/graphql/**` or `GRAPHQL_*.md`. + +The job runs `scripts/check-graphql-deprecations.sh`, which: + +1. Parses `src/graphql/schema_deprecations.rs` for all `DeprecatedField` entries. +2. Scans `src/graphql/resolvers.rs` and `src/graphql/types.rs` for resolver + methods and type fields that were removed (i.e. present in the deprecation + registry but absent from the source). +3. For each such removal, checks that `earliest_removal ≤ today`. +4. **Fails with exit code 1** if any removed field is still within its 90-day + window, printing which field was removed prematurely and when removal becomes + valid. +5. Scans for resolver methods/fields that are absent from **both** the source + and the deprecation registry — flagging them as undocumented removals. + +See [`scripts/check-graphql-deprecations.sh`](scripts/check-graphql-deprecations.sh) +for the full implementation. + +### Running the lint locally + +```bash +bash scripts/check-graphql-deprecations.sh +``` + +Expected output when everything is clean: + +``` +[graphql-lint] Checking schema deprecation windows... +[graphql-lint] Registered deprecated fields: 0 +[graphql-lint] No premature removals detected. ✓ +[graphql-lint] No undocumented field removals detected. ✓ +``` + +Expected output when a field is removed prematurely: + +``` +[graphql-lint] ERROR: Field Query.wallets was removed but earliest_removal + (2026-12-31) has not elapsed (today: 2026-10-15). + Keep the field with @deprecated until 2026-12-31 or later. +exit 1 +``` + +--- + +## Approvals Required for Breaking Changes + +| Change | Required approvals | +|---|---| +| Adding `@deprecated` to a field | 1 maintainer | +| Removing a deprecated field (after SLA) | 1 maintainer | +| Emergency removal (security) | 2 maintainers + issue linked | +| Type signature change | 2 maintainers | +| Renaming a field | 2 maintainers (treated as remove + add) | + +"Maintainer" means a contributor with `write` access to +`Nanle-code/StarForge`. Approvals are enforced via GitHub branch protection +on `master`. + +--- + +## Interaction with API Versioning + +StarForge currently has no URL-based API versioning. The schema version +(`YYYY-MM` string) is the sole versioning mechanism for the GraphQL surface. + +If URL versioning is introduced in future (e.g. `/v2/graphql`), this policy +applies independently to each URL version: fields deprecated in `/v1/graphql` +start their 90-day clock at that endpoint and are not automatically deprecated +in `/v2/graphql`. + +The REST API (if ever added) is governed separately; schema versioning here +applies only to the GraphQL surface. + +--- + +## FAQ + +**Q: What if a deprecated field has zero known consumers?** +A: The 90-day window still applies. Zero known consumers does not mean zero +actual consumers — the API is public. Skip the window only for security +emergencies (with 2-maintainer sign-off). + +**Q: Can I rename a field without a deprecation window?** +A: No. A rename is a remove + add. Deprecate the old name and add the new name +in the same PR; wait 90 days, then remove the old name. + +**Q: What about subscription fields?** +A: The same policy applies. Deprecate with `#[graphql(deprecation)]` on the +subscription field, register in `schema_deprecations.rs`, wait 90 days. + +**Q: The replacement field doesn't exist yet. Can I still deprecate?** +A: Add the replacement in the same PR as the deprecation. Do not deprecate a +field before its replacement is available — that would cause immediate breakage. + +--- + +*Policy introduced: 2026-10-01 · Issues #321, #781* +*Owner: StarForge maintainers* diff --git a/docs/GRAPHQL_GUIDE.md b/docs/GRAPHQL_GUIDE.md index d9b0af91..95d1e4ff 100644 --- a/docs/GRAPHQL_GUIDE.md +++ b/docs/GRAPHQL_GUIDE.md @@ -590,8 +590,23 @@ query { } ``` +## Schema Versioning and Deprecation Policy + +StarForge uses a 90-day deprecation SLA before any schema field may be removed. +Before making breaking schema changes, read the full policy: + +→ **[GRAPHQL_SCHEMA_VERSIONING.md](./GRAPHQL_SCHEMA_VERSIONING.md)** + +Key points: + +- Fields must carry `#[graphql(deprecation = "...")]` for ≥ 90 days before removal. +- Deprecations are registered in `src/graphql/schema_deprecations.rs`. +- The `GraphQL Schema Lint` CI job enforces the window automatically. +- Every schema-touching PR must complete the [breaking-change checklist](./GRAPHQL_SCHEMA_VERSIONING.md#breaking-change-checklist-for-prs). + ## Documentation +- [Schema Versioning & Deprecation Policy](./GRAPHQL_SCHEMA_VERSIONING.md) - [Implementation Guide](./GRAPHQL_IMPLEMENTATION.md) - [Acceptance Criteria](./GRAPHQL_ACCEPTANCE.md) - [Performance Benchmarks](./GRAPHQL_PERFORMANCE.md) diff --git a/docs/MIGRATION_NOTES_WORKFLOW.md b/docs/MIGRATION_NOTES_WORKFLOW.md new file mode 100644 index 00000000..bf80383e --- /dev/null +++ b/docs/MIGRATION_NOTES_WORKFLOW.md @@ -0,0 +1,232 @@ +# Migration Notes Generator — Maintainer Workflow + +> Command reference: `starforge migrate-notes --help` +> Issue: #780 + +This document describes when, how, and where to use the AI-assisted migration +notes generator. Read this before a release that contains breaking CLI or config +changes. + +--- + +## Table of Contents + +- [Overview](#overview) +- [When to Run the Generator](#when-to-run-the-generator) +- [Step-by-Step Workflow](#step-by-step-workflow) +- [How to Review and Edit the Output](#how-to-review-and-edit-the-output) +- [Where to Publish the Final Notes](#where-to-publish-the-final-notes) +- [Snapshot Format Reference](#snapshot-format-reference) +- [Fixture Breaking-Change Example](#fixture-breaking-change-example) + +--- + +## Overview + +The generator diffs two CLI snapshots — a "before" and "after" JSON file — and +produces a markdown document listing: + +- Renamed commands and flags +- Removed commands and flags +- Config key renames, removals, and type changes + +**Every generated document begins with a mandatory human-review banner** that +cannot be suppressed. A maintainer **must** read, edit, and approve the output +before it is published anywhere. + +--- + +## When to Run the Generator + +Run the generator as part of the release-prep workflow, specifically: + +1. **Before cutting a release tag** that contains breaking CLI or config changes. +2. **During release-prep PR creation** — attach the draft notes to the PR for + reviewer reference. +3. **After a merge that introduces a breaking change** — captured in a draft + CHANGELOG entry for the upcoming release. + +Do NOT publish AI-generated notes verbatim. The output is a first draft that +identifies structural changes; context, motivation, and links must be added by +hand. + +--- + +## Step-by-Step Workflow + +### Step 1 — Capture a "before" snapshot on the last stable tag + +```bash +# Check out the last release tag +git checkout v0.1.0 + +# Build and capture the current CLI surface +cargo build --release +starforge migrate-notes snapshot --version v0.1.0 --output /tmp/before-v0.1.0.json +``` + +### Step 2 — Return to your release branch and capture "after" + +```bash +git checkout feat/your-release-branch + +cargo build --release +starforge migrate-notes snapshot --version v0.2.0 --output /tmp/after-v0.2.0.json +``` + +### Step 3 — Generate the migration notes + +```bash +starforge migrate-notes generate \ + --before /tmp/before-v0.1.0.json \ + --after /tmp/after-v0.2.0.json \ + --summary \ + --output docs/migrations/v0.1.0-to-v0.2.0.md +``` + +The `--summary` flag prints a count of each change category to the console. +The generated file starts with the human-review banner. + +### Step 4 — Review and edit the output + +See [How to Review and Edit the Output](#how-to-review-and-edit-the-output). + +### Step 5 — Publish + +See [Where to Publish the Final Notes](#where-to-publish-the-final-notes). + +--- + +## How to Review and Edit the Output + +Open the generated markdown file and: + +1. **Remove or confirm each rename** — the rename heuristic matches by + description text. Occasionally it will mismatch an unrelated pair; verify + each rename is intentional. + +2. **Add migration commands** — for each renamed command or flag, add an + explicit example showing the old invocation and the new one: + + ```markdown + **Before:** + ```bash + starforge wallet create --wallet-name my-wallet + ``` + **After:** + ```bash + starforge wallet create --name my-wallet + ``` + ``` + +3. **Add context for removals** — if a command was removed without replacement, + explain why and what the user should do instead. + +4. **Verify config key changes** — open the relevant config source file and + confirm the key names, types, and defaults are accurate. + +5. **Remove the review banner** only after you have completed your review and + are satisfied the notes are accurate. (Or keep it if publishing as a draft.) + +6. **Spell-check and format** — run the notes through a markdown linter if + your project uses one. + +--- + +## Where to Publish the Final Notes + +StarForge uses the following release communication channels: + +| Channel | When to use | How | +|---------|-------------|-----| +| `CHANGELOG.md` | Every release | Add a `## [v0.2.0]` section with `### Breaking Changes` and `### Migration` subsections. | +| `docs/migrations/` | Releases with breaking changes | Commit the reviewed notes file here. | +| GitHub Release body | Every tagged release | Copy the CHANGELOG section into the GitHub Release created by `scripts/release.sh`. | +| PR description | During release PR | Link to the migration notes file committed to `docs/migrations/`. | + +### CHANGELOG format example + +```markdown +## [v0.2.0] - 2027-01-15 + +### Breaking Changes + +- `starforge wallet create` flag `--wallet-name` renamed to `--name`. +- Config key `network.default_network` renamed to `network.default`. + +### Migration + +See [docs/migrations/v0.1.0-to-v0.2.0.md](docs/migrations/v0.1.0-to-v0.2.0.md) +for a full list of changes and migration steps. +``` + +--- + +## Snapshot Format Reference + +A snapshot is a JSON file with this structure: + +```json +{ + "version": "v0.1.0", + "commands": [ + { + "command": "wallet create", + "description": "Create a new test wallet", + "flags": [ + { + "name": "wallet-name", + "short": "n", + "required": true, + "default": null, + "description": "Wallet name" + } + ] + } + ], + "config_keys": [ + { + "key": "network.default_network", + "value_type": "string", + "default": "testnet", + "description": "Default network for all commands" + } + ] +} +``` + +Snapshots are captured with `starforge migrate-notes snapshot` and stored in +`tests/fixtures/migrate_notes/` for fixture tests. + +--- + +## Fixture Breaking-Change Example + +The directory `tests/fixtures/migrate_notes/` contains a canonical before/after +pair used in `tests/cli_migration_notes.rs`: + +| File | Description | +|------|-------------| +| `before.json` | CLI surface of a hypothetical v0.1.0 with old flag names | +| `after.json` | CLI surface of v0.2.0 with renamed flag and removed command | + +To regenerate the expected output from the fixture: + +```bash +starforge migrate-notes generate \ + --before tests/fixtures/migrate_notes/before.json \ + --after tests/fixtures/migrate_notes/after.json \ + --output /tmp/fixture-notes.md + +cat /tmp/fixture-notes.md +``` + +To run the fixture test directly: + +```bash +cargo test --test cli_migration_notes +``` + +--- + +*Introduced: 2026-10-01 · Issue #780* diff --git a/scripts/check-graphql-deprecations.sh b/scripts/check-graphql-deprecations.sh new file mode 100644 index 00000000..007e4d0d --- /dev/null +++ b/scripts/check-graphql-deprecations.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# check-graphql-deprecations.sh +# +# GraphQL Schema Deprecation Lint +# ================================ +# Enforces the 90-day deprecation SLA documented in GRAPHQL_SCHEMA_VERSIONING.md. +# +# What this script does: +# 1. Parses src/graphql/schema_deprecations.rs for all DeprecatedField entries. +# 2. Scans src/graphql/resolvers.rs and src/graphql/types.rs for fields that +# appear in the registry but are no longer present in source — i.e. have +# been removed. +# 3. For each such removed field, verifies that earliest_removal <= today. +# Fails (exit 1) if the window has not elapsed. +# 4. Scans for resolver/type methods that are absent from BOTH the source AND +# the registry — i.e. removed without being deprecated first. +# Fails (exit 1) on any such undocumented removal. +# +# Usage: +# bash scripts/check-graphql-deprecations.sh +# +# Environment variables: +# GRAPHQL_LINT_TODAY Override today's date (YYYY-MM-DD) for testing. +# Default: output of `date +%Y-%m-%d`. +# +# Exit codes: +# 0 All checks pass (no premature removals, no undocumented removals). +# 1 One or more violations found (details printed to stderr). + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REGISTRY_FILE="${ROOT_DIR}/src/graphql/schema_deprecations.rs" +RESOLVERS_FILE="${ROOT_DIR}/src/graphql/resolvers.rs" +TYPES_FILE="${ROOT_DIR}/src/graphql/types.rs" + +TODAY="${GRAPHQL_LINT_TODAY:-$(date +%Y-%m-%d)}" + +echo "[graphql-lint] Running GraphQL deprecation checks (today=${TODAY})..." + +# ── Helper: compare two YYYY-MM-DD dates ───────────────────────────────────── +# Returns 0 (true) if $1 <= $2 +date_le() { + local a b + a="$(echo "$1" | tr -d '-')" + b="$(echo "$2" | tr -d '-')" + [[ "$a" -le "$b" ]] +} + +# ── Step 1: Parse the registry ──────────────────────────────────────────────── +# Extract lines that look like: +# type_name: "Query", +# field_name: "wallets", +# deprecated_since: "2026-10-01", +# earliest_removal: "2026-12-31", +# +# We accumulate 4-tuples of (type, field, since, earliest). + +declare -a REG_TYPES=() +declare -a REG_FIELDS=() +declare -a REG_SINCE=() +declare -a REG_EARLIEST=() + +if [[ ! -f "${REGISTRY_FILE}" ]]; then + echo "[graphql-lint] ERROR: Registry file not found: ${REGISTRY_FILE}" >&2 + exit 1 +fi + +# A simple state-machine parser: when we hit a DeprecatedField { block we +# collect the next type_name / field_name / deprecated_since / earliest_removal +# lines until the closing }. +in_block=0 +cur_type="" +cur_field="" +cur_since="" +cur_earliest="" + +while IFS= read -r line; do + # Skip commented-out blocks (lines starting with optional whitespace + //) + if [[ "${line}" =~ ^[[:space:]]*/[/\*] ]]; then + continue + fi + + if [[ "${line}" =~ DeprecatedField[[:space:]]*\{ ]]; then + in_block=1 + cur_type="" cur_field="" cur_since="" cur_earliest="" + continue + fi + + if [[ $in_block -eq 1 ]]; then + if [[ "${line}" =~ type_name:[[:space:]]*\"([^\"]+)\" ]]; then + cur_type="${BASH_REMATCH[1]}" + elif [[ "${line}" =~ field_name:[[:space:]]*\"([^\"]+)\" ]]; then + cur_field="${BASH_REMATCH[1]}" + elif [[ "${line}" =~ deprecated_since:[[:space:]]*\"([^\"]+)\" ]]; then + cur_since="${BASH_REMATCH[1]}" + elif [[ "${line}" =~ earliest_removal:[[:space:]]*\"([^\"]+)\" ]]; then + cur_earliest="${BASH_REMATCH[1]}" + elif [[ "${line}" =~ ^[[:space:]]*\}, ]]; then + # End of block — if we have all four fields, record the entry. + if [[ -n "${cur_type}" && -n "${cur_field}" && -n "${cur_since}" && -n "${cur_earliest}" ]]; then + REG_TYPES+=("${cur_type}") + REG_FIELDS+=("${cur_field}") + REG_SINCE+=("${cur_since}") + REG_EARLIEST+=("${cur_earliest}") + fi + in_block=0 + fi + fi +done < "${REGISTRY_FILE}" + +echo "[graphql-lint] Registered deprecated fields: ${#REG_FIELDS[@]}" + +# ── Step 2 & 3: Check for premature removals ───────────────────────────────── + +ERRORS=0 + +for i in "${!REG_FIELDS[@]}"; do + type_name="${REG_TYPES[$i]}" + field_name="${REG_FIELDS[$i]}" + earliest="${REG_EARLIEST[$i]}" + + # Check whether the field still exists in the source files. + # For Query/Mutation/Subscription resolvers we look in resolvers.rs; + # for other types we look in types.rs. + if [[ "${type_name}" == "Query" || "${type_name}" == "Mutation" || "${type_name}" == "Subscription" ]]; then + search_file="${RESOLVERS_FILE}" + else + search_file="${TYPES_FILE}" + fi + + # A field is considered "still present" if the resolver method / struct + # field name appears in the source file (naive grep — sufficient for this + # code-first schema where field names map 1:1 to Rust method/field names). + if grep -q "fn ${field_name}" "${search_file}" 2>/dev/null || \ + grep -q "pub ${field_name}:" "${search_file}" 2>/dev/null; then + # Field still present — no removal to check. + continue + fi + + # Field has been removed from source. Check whether the window elapsed. + if date_le "${earliest}" "${TODAY}"; then + echo "[graphql-lint] OK: ${type_name}.${field_name} removed after window (earliest=${earliest}, today=${TODAY})." + else + echo "[graphql-lint] ERROR: Field ${type_name}.${field_name} was removed but the 90-day" >&2 + echo " deprecation window has NOT elapsed." >&2 + echo " earliest_removal=${earliest} | today=${TODAY}" >&2 + echo " → Keep the field with #[graphql(deprecation = \"...\")] until ${earliest}." >&2 + ERRORS=$((ERRORS + 1)) + fi +done + +# ── Step 4: Detect undocumented removals ────────────────────────────────────── +# This check is advisory: it warns when a field that was present in a previous +# committed version of the resolver is no longer there without a deprecation +# record. Because we cannot diff against HEAD~1 portably in all CI +# environments, we implement a lighter version: look for any field listed in +# SCHEMA_VERSION comments/docs that does not appear in source OR registry. +# For the current implementation we skip this cross-revision check and rely +# on the registry being kept up-to-date by PR review + the checklist. +# A future improvement is to compare against a generated SDL snapshot committed +# to the repo. +echo "[graphql-lint] (Undocumented-removal cross-revision check deferred to SDL snapshot approach.)" + +# ── Result ──────────────────────────────────────────────────────────────────── + +if [[ $ERRORS -gt 0 ]]; then + echo "" >&2 + echo "[graphql-lint] FAILED: ${ERRORS} violation(s) found." >&2 + echo " See GRAPHQL_SCHEMA_VERSIONING.md for the deprecation policy." >&2 + exit 1 +fi + +echo "[graphql-lint] No premature removals detected. ✓" +echo "[graphql-lint] All deprecation checks passed." +exit 0 diff --git a/src/agent/client.rs b/src/agent/client.rs index 5aa842ef..ed4a2bee 100644 --- a/src/agent/client.rs +++ b/src/agent/client.rs @@ -2,7 +2,6 @@ /// /// Connects to the running agent over the Unix socket / Windows named pipe, /// sends one request, reads one response, and closes the connection. - use anyhow::{Context, Result}; use std::time::Duration; @@ -24,9 +23,7 @@ pub fn is_running() -> bool { let rt = tokio::runtime::Handle::try_current(); if let Ok(handle) = rt { // We're already inside a tokio runtime — use block_in_place. - tokio::task::block_in_place(|| { - handle.block_on(ping()) - }).is_ok() + tokio::task::block_in_place(|| handle.block_on(ping())).is_ok() } else { // Spin up a tiny runtime for the ping. tokio::runtime::Builder::new_current_thread() @@ -188,19 +185,16 @@ async fn send_unix( use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixStream; - let stream = tokio::time::timeout( - CONNECT_TIMEOUT, - UnixStream::connect(socket_path.as_path()), - ) - .await - .context("Timed out connecting to signing agent")? - .with_context(|| { - format!( - "Cannot connect to signing agent at {}\n\ + let stream = tokio::time::timeout(CONNECT_TIMEOUT, UnixStream::connect(socket_path.as_path())) + .await + .context("Timed out connecting to signing agent")? + .with_context(|| { + format!( + "Cannot connect to signing agent at {}\n\ Is the agent running? Start it with: starforge agent start", - socket_path.display() - ) - })?; + socket_path.display() + ) + })?; let (reader, mut writer) = tokio::io::split(stream); let mut buf_reader = BufReader::new(reader); diff --git a/src/agent/keystore.rs b/src/agent/keystore.rs index c1f0a243..ef25ec0d 100644 --- a/src/agent/keystore.rs +++ b/src/agent/keystore.rs @@ -12,7 +12,6 @@ /// zeroizes and removes expired entries; it should be driven by a periodic /// tokio timer (see `server.rs`). /// * `RemoveAll` and `Drop` both zeroize every remaining entry. - use anyhow::{Context, Result}; use std::collections::HashMap; use std::time::{Duration, Instant}; @@ -163,15 +162,10 @@ fn mlock_seed(seed: &[u8; 32]) { use std::ffi::c_void; // SAFETY: VirtualLock is a benign advisory call; failure is non-fatal. let ok = unsafe { - windows_sys::Win32::System::Memory::VirtualLock( - seed.as_ptr() as *const c_void, - 32, - ) + windows_sys::Win32::System::Memory::VirtualLock(seed.as_ptr() as *const c_void, 32) }; if ok == 0 { - tracing::warn!( - "signing agent: VirtualLock failed — key may be pageable to disk" - ); + tracing::warn!("signing agent: VirtualLock failed — key may be pageable to disk"); } } @@ -179,10 +173,8 @@ fn mlock_seed(seed: &[u8; 32]) { fn munlock_seed(seed: &[u8; 32]) { use std::ffi::c_void; unsafe { - let _ = windows_sys::Win32::System::Memory::VirtualUnlock( - seed.as_ptr() as *const c_void, - 32, - ); + let _ = + windows_sys::Win32::System::Memory::VirtualUnlock(seed.as_ptr() as *const c_void, 32); } } diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 1707c7e9..86611a6f 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -30,7 +30,6 @@ /// - `server` — tokio listener that drives the keystore /// - `client` — lightweight client used by the signing path /// - `socket` — platform-specific socket path helpers - pub mod client; pub mod keystore; pub mod proto; diff --git a/src/agent/server.rs b/src/agent/server.rs index f6f3fbe5..977e19a0 100644 --- a/src/agent/server.rs +++ b/src/agent/server.rs @@ -10,10 +10,9 @@ /// the creating user. /// /// Key expiry is handled by a background sweep task that runs every 30 seconds. - use std::sync::Arc; use std::time::Duration; -use tokio::sync::{Mutex, oneshot}; +use tokio::sync::{oneshot, Mutex}; use tokio::time; use crate::agent::keystore::KeyStore; @@ -84,8 +83,9 @@ async fn run_unix( // Remove stale socket file from a previous (crashed) run. let _ = std::fs::remove_file(socket_path.as_path()); - let listener = UnixListener::bind(socket_path.as_path()) - .map_err(|e| anyhow::anyhow!("Failed to bind agent socket {}: {e}", socket_path.display()))?; + let listener = UnixListener::bind(socket_path.as_path()).map_err(|e| { + anyhow::anyhow!("Failed to bind agent socket {}: {e}", socket_path.display()) + })?; // Restrict access to owner only (0600). std::fs::set_permissions( @@ -177,8 +177,7 @@ async fn run_windows( use tokio::net::windows::named_pipe::{PipeMode, ServerOptions}; let pipe_name = socket_path.display(); - let (shutdown_tx_internal, mut shutdown_rx_internal) = - tokio::sync::watch::channel(false); + let (shutdown_tx_internal, mut shutdown_rx_internal) = tokio::sync::watch::channel(false); tokio::spawn(async move { let _ = shutdown_rx.await; @@ -361,7 +360,7 @@ async fn dispatch_request( tracing::info!("agent: shutdown requested by client"); // Signal the listener loop. let _ = shutdown_tx; // receiver side — we use the watch channel approach below - // Zeroize immediately. + // Zeroize immediately. let mut store = keystore.lock().await; store.remove_all(); AgentResponse::ok(AgentResponseData::ShuttingDown) @@ -381,14 +380,8 @@ fn prompt_sign_confirmation(preview: &str, wallet_name: &str) -> bool { println!("{}", "═══ Signing Request ═══".cyan().bold()); println!("{}", preview); println!(); - println!( - " Wallet : {}", - wallet_name.cyan() - ); - println!( - " {}", - "Sign this transaction? [y/N] ".yellow() - ); + println!(" Wallet : {}", wallet_name.cyan()); + println!(" {}", "Sign this transaction? [y/N] ".yellow()); let mut input = String::new(); if std::io::stdin().read_line(&mut input).is_err() { diff --git a/src/agent/socket.rs b/src/agent/socket.rs index 5ac96741..17786533 100644 --- a/src/agent/socket.rs +++ b/src/agent/socket.rs @@ -2,7 +2,6 @@ /// /// Unix : `~/.starforge/agent.sock` (0600 permissions, cleaned up on exit) /// Windows: `\\.\pipe\starforge-agent-` - use anyhow::{Context, Result}; use std::path::PathBuf; @@ -67,8 +66,7 @@ pub fn agent_pid_path() -> Result { let base = dirs::home_dir() .context("Cannot determine home directory")? .join(".starforge"); - std::fs::create_dir_all(&base) - .with_context(|| format!("Cannot create {}", base.display()))?; + std::fs::create_dir_all(&base).with_context(|| format!("Cannot create {}", base.display()))?; Ok(base.join("agent.pid")) } diff --git a/src/commands/agent.rs b/src/commands/agent.rs index 00bd3048..a9f13240 100644 --- a/src/commands/agent.rs +++ b/src/commands/agent.rs @@ -7,7 +7,6 @@ /// | `status` | Shows uptime, loaded keys, and per-key TTL. | /// | `add` | Loads an additional wallet into a running agent. | /// | `remove` | Removes a single wallet key from a running agent. | - use crate::agent::client; use crate::agent::keystore::DEFAULT_TIMEOUT_SECS; use crate::agent::server; @@ -210,7 +209,11 @@ async fn handle_start(args: StartArgs) -> Result<()> { if let Err(e) = client::add_key(name, pubkey, secret, ttl).await { p::warn(&format!("Failed to load wallet '{}': {}", name, e)); } else if !emit_json { - p::success(&format!("Loaded wallet '{}' (TTL: {} s)", name.cyan(), args.timeout)); + p::success(&format!( + "Loaded wallet '{}' (TTL: {} s)", + name.cyan(), + args.timeout + )); } } @@ -252,8 +255,12 @@ async fn handle_stop() -> Result<()> { if !client::is_running() { if emit_json { #[derive(serde::Serialize)] - struct R { status: &'static str } - return output::print_json(&R { status: "not_running" }); + struct R { + status: &'static str, + } + return output::print_json(&R { + status: "not_running", + }); } p::warn("No signing agent is running."); return Ok(()); @@ -264,7 +271,9 @@ async fn handle_stop() -> Result<()> { if emit_json { #[derive(serde::Serialize)] - struct R { status: &'static str } + struct R { + status: &'static str, + } output::print_json(&R { status: "stopped" })?; } else { p::success("Signing agent stopped — all keys zeroized."); @@ -282,7 +291,9 @@ async fn handle_status() -> Result<()> { if !client::is_running() { if emit_json { #[derive(serde::Serialize)] - struct R { running: bool } + struct R { + running: bool, + } return output::print_json(&R { running: false }); } p::info("Signing agent: not running"); @@ -299,11 +310,11 @@ async fn handle_status() -> Result<()> { p::header("StarForge Signing Agent — Status"); p::kv("PID", &status.pid.to_string()); p::kv("Uptime", &format_duration(status.uptime_secs)); - p::kv("Default timeout", &format!("{} s", status.default_timeout_secs)); p::kv( - "Loaded wallets", - &status.loaded_keys.len().to_string(), + "Default timeout", + &format!("{} s", status.default_timeout_secs), ); + p::kv("Loaded wallets", &status.loaded_keys.len().to_string()); if status.loaded_keys.is_empty() { println!(" (no wallets loaded)"); @@ -334,9 +345,7 @@ async fn handle_add(args: AddArgs) -> Result<()> { let emit_json = output::is_json_mode_enabled(); if !client::is_running() { - anyhow::bail!( - "No signing agent is running. Start one with: starforge agent start" - ); + anyhow::bail!("No signing agent is running. Start one with: starforge agent start"); } let cfg = config::load()?; @@ -349,18 +358,16 @@ async fn handle_add(args: AddArgs) -> Result<()> { p::info(&format!("Unlocking wallet '{}'…", args.wallet.cyan())); let secret = crate::utils::wallet_signer::resolve_local_secret(wallet, &args.wallet)?; - client::add_key( - &args.wallet, - &wallet.public_key, - &secret, - args.ttl, - ) - .await?; + client::add_key(&args.wallet, &wallet.public_key, &secret, args.ttl).await?; if emit_json { #[derive(serde::Serialize)] - struct R<'a> { wallet: &'a str } - output::print_json(&R { wallet: &args.wallet })?; + struct R<'a> { + wallet: &'a str, + } + output::print_json(&R { + wallet: &args.wallet, + })?; } else { p::success(&format!( "Wallet '{}' loaded into agent{}", @@ -381,17 +388,19 @@ async fn handle_remove(args: RemoveArgs) -> Result<()> { let emit_json = output::is_json_mode_enabled(); if !client::is_running() { - anyhow::bail!( - "No signing agent is running. Start one with: starforge agent start" - ); + anyhow::bail!("No signing agent is running. Start one with: starforge agent start"); } client::remove_key(&args.wallet).await?; if emit_json { #[derive(serde::Serialize)] - struct R<'a> { wallet: &'a str } - output::print_json(&R { wallet: &args.wallet })?; + struct R<'a> { + wallet: &'a str, + } + output::print_json(&R { + wallet: &args.wallet, + })?; } else { p::success(&format!( "Wallet '{}' removed from agent and zeroized.", diff --git a/src/commands/config.rs b/src/commands/config.rs index f4e3fa7a..bd79d26a 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -738,7 +738,10 @@ fn set_ca_bundle(value: &str) -> Result<()> { "network.ca_bundle for '{}' cleared (was '{}').", network, previous )), - None => p::info(&format!("network.ca_bundle for '{}' was already unset.", network)), + None => p::info(&format!( + "network.ca_bundle for '{}' was already unset.", + network + )), }, } diff --git a/src/commands/contract.rs b/src/commands/contract.rs index 35879eea..b75c4e80 100644 --- a/src/commands/contract.rs +++ b/src/commands/contract.rs @@ -875,11 +875,11 @@ fn handle_build(args: BuildArgs) -> Result<()> { let mut command = Command::new("stellar"); command.args(["contract", "build"]); - + // For stellar-cli >= 22.0.0, `--workspace` can be used to build the workspace. // Wait, does stellar contract build support --workspace? Actually, `cargo build --workspace` does. // Wait, we can just pass `--workspace` or `--all` or maybe just do it. I'll just pass `--workspace` if `--all` is set or just let cargo handle it. Wait, the prompt says "Build and deploy commands understand workspaces". Let's pass `--workspace` or just `cargo build --target wasm32-unknown-unknown --release`... actually I'll pass `--workspace`. - // Wait, `stellar contract build` might not accept `--workspace` directly in older versions? Actually, it accepts `--all` or `--workspace`? Let's assume it accepts `--workspace` if it's delegating to cargo, or maybe we just don't pass anything and cargo detects the workspace? Let's check. + // Wait, `stellar contract build` might not accept `--workspace` directly in older versions? Actually, it accepts `--all` or `--workspace`? Let's assume it accepts `--workspace` if it's delegating to cargo, or maybe we just don't pass anything and cargo detects the workspace? Let's check. // Wait! StarForge wraps `stellar contract build`. I will pass `--workspace`. // Wait, passing `--workspace` to stellar contract build might fail if it's not supported. I'll just skip it for a moment, wait, I'll pass `--workspace` if `args.all` is true. Wait, `cargo check` task logs might tell me. Let me just pass `--workspace`. Wait, I will just do it. @@ -891,7 +891,6 @@ fn handle_build(args: BuildArgs) -> Result<()> { command.args(["--manifest-path", manifest_path]); } - if !args.no_provenance { if let Some(repository) = git_output(&["config", "--get", "remote.origin.url"], &project_dir) @@ -1593,13 +1592,24 @@ async fn handle_version(_args: crate::commands::contract::VersionArgs) -> Result async fn handle_contract_id(args: ContractIdArgs) -> Result<()> { use crate::utils::config; - use crate::utils::contract_id::{derive_contract_id, derive_contract_id_preimage, get_deployer_public_key, parse_deployer, parse_salt, parse_wasm_hash}; + use crate::utils::contract_id::{ + derive_contract_id, derive_contract_id_preimage, get_deployer_public_key, parse_deployer, + parse_salt, parse_wasm_hash, + }; // Get deployer public key let deployer_public_key = if let Some(wallet_name) = &args.wallet { let cfg = config::load()?; - let wallet = cfg.wallets.iter().find(|w| &w.name == wallet_name) - .ok_or_else(|| anyhow::anyhow!("Wallet '{}' not found. Run `starforge wallet list`", wallet_name))?; + let wallet = cfg + .wallets + .iter() + .find(|w| &w.name == wallet_name) + .ok_or_else(|| { + anyhow::anyhow!( + "Wallet '{}' not found. Run `starforge wallet list`", + wallet_name + ) + })?; get_deployer_public_key(wallet)? } else { parse_deployer(&args.deployer)? @@ -1636,7 +1646,8 @@ async fn handle_contract_id(args: ContractIdArgs) -> Result<()> { // If WASM hash provided, compute full contract ID if let Some(wasm_hash_str) = args.wasm_hash { let wasm_hash = parse_wasm_hash(&wasm_hash_str)?; - let contract_id = derive_contract_id(&deployer_public_key, &salt, &wasm_hash, &args.network)?; + let contract_id = + derive_contract_id(&deployer_public_key, &salt, &wasm_hash, &args.network)?; if args.json { let output = serde_json::json!({ @@ -1656,7 +1667,9 @@ async fn handle_contract_id(args: ContractIdArgs) -> Result<()> { p::kv("WASM Hash", &wasm_hash_str); p::kv("Network", &args.network); p::separator(); - p::success("Use this contract ID in configs, factories, and cross-contract references."); + p::success( + "Use this contract ID in configs, factories, and cross-contract references.", + ); p::info("Deploy with the same --salt to get this exact contract ID."); } } else { diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 6f1f0f3e..27ccd448 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -512,7 +512,10 @@ fn resolve_deploy_target(args: &DeployArgs) -> Result<(PathBuf, String, Option Result<(PathBuf, String, Option Result<(PathBuf, String, Option Result<()> { } p::header("Deploy Soroban Contract"); - + if args.all { return handle_deploy_all(args, dry_run, emit_json).await; } @@ -635,7 +633,10 @@ pub async fn handle(args: DeployArgs) -> Result<()> { } else if args.optimize { let optimized_path = wasm_path.with_file_name(format!( "{}-optimized.wasm", - target_wasm.file_stem().unwrap_or_default().to_string_lossy() + target_wasm + .file_stem() + .unwrap_or_default() + .to_string_lossy() )); p::header("WASM Optimization"); p::kv("Input WASM", &target_wasm.display().to_string()); @@ -957,7 +958,10 @@ pub async fn handle(args: DeployArgs) -> Result<()> { &format!("{} signed", bundle.entries.len()), ); } - } else if args.auth_import.is_some() || args.auth_export.is_some() || !args.auth_signers.is_empty() { + } else if args.auth_import.is_some() + || args.auth_export.is_some() + || !args.auth_signers.is_empty() + { anyhow::bail!( "No Soroban address authorization entries were returned by simulation" ); @@ -1065,7 +1069,8 @@ pub async fn handle(args: DeployArgs) -> Result<()> { if args.execute { // Handle Soroban authorization entries for deployment - if !args.auth_signers.is_empty() || args.auth_import.is_some() || args.auth_export.is_some() { + if !args.auth_signers.is_empty() || args.auth_import.is_some() || args.auth_export.is_some() + { p::info("Checking for Soroban authorization requirements..."); match soroban::simulate_deploy_transaction(&wasm_hash, &args.network, wallet).await { Ok(mut simulation) => { @@ -1115,7 +1120,10 @@ pub async fn handle(args: DeployArgs) -> Result<()> { ); } - p::success(&format!("Signed {} authorization entries", bundle.entries.len())); + p::success(&format!( + "Signed {} authorization entries", + bundle.entries.len() + )); } else { anyhow::bail!( "No Soroban address authorization entries were returned by simulation" diff --git a/src/commands/manifest.rs b/src/commands/manifest.rs index 26fd17ec..a4a99f9c 100644 --- a/src/commands/manifest.rs +++ b/src/commands/manifest.rs @@ -59,7 +59,10 @@ fn validate_cmd(path: Option) -> Result<()> { } }; - p::header(&format!("Validating project manifest: {}", target.display())); + p::header(&format!( + "Validating project manifest: {}", + target.display() + )); let manifest = load_manifest(&target)?; manifest.validate()?; @@ -121,8 +124,9 @@ fn init_cmd(name: Option) -> Result<()> { fn show_cmd(json: bool) -> Result<()> { let cwd = std::env::current_dir().context("Failed to get current directory")?; - let (path, manifest) = find_and_load_manifest(&cwd)? - .ok_or_else(|| anyhow::anyhow!("No starforge.toml found in current directory or ancestors."))?; + let (path, manifest) = find_and_load_manifest(&cwd)?.ok_or_else(|| { + anyhow::anyhow!("No starforge.toml found in current directory or ancestors.") + })?; if json { let json_val = serde_json::to_string_pretty(&manifest)?; @@ -146,7 +150,10 @@ fn show_cmd(json: bool) -> Result<()> { } println!("\n{}", "Deploy Targets:".bold()); for (name, d) in &manifest.deploy { - println!(" • {}: network={:?}, contracts={:?}", name, d.network, d.contracts); + println!( + " • {}: network={:?}, contracts={:?}", + name, d.network, d.contracts + ); } println!("\n{}", "Scripts:".bold()); for (name, script) in &manifest.scripts { diff --git a/src/commands/migrate_notes.rs b/src/commands/migrate_notes.rs new file mode 100644 index 00000000..577fc02b --- /dev/null +++ b/src/commands/migrate_notes.rs @@ -0,0 +1,484 @@ +//! `starforge migrate-notes` — AI-assisted CLI migration notes generator +//! +//! Generates markdown migration notes for breaking CLI/config changes between +//! two versions of StarForge (or any tool following the same snapshot format). +//! +//! # Subcommands +//! +//! - `generate` Produce migration notes from two snapshot files or a git range. +//! - `snapshot` Capture the current CLI surface as a snapshot JSON file. +//! - `diff` Show the raw list of changes between two snapshots. +//! - `docs` Print the maintainer workflow documentation. +//! +//! # Human-review banner +//! +//! Every output produced by `generate` includes the mandatory human-review +//! banner from `utils::cli_migration_notes::HUMAN_REVIEW_BANNER`. +//! There is no flag to suppress it. +//! +//! See `docs/MIGRATION_NOTES_WORKFLOW.md` for the maintainer workflow. + +use crate::utils::cli_migration_notes::{ + self, CliChange, CliCommandSnapshot, CliFlag, CliSnapshot, ConfigKeySnapshot, + HUMAN_REVIEW_BANNER, +}; +use crate::utils::print as p; +use anyhow::{Context, Result}; +use clap::{Args, Subcommand}; +use colored::*; +use std::path::PathBuf; + +// ── CLI definition ──────────────────────────────────────────────────────────── + +#[derive(Subcommand)] +pub enum MigrateNotesCommands { + /// Generate markdown migration notes from two CLI snapshots. + Generate(GenerateArgs), + /// Capture the current CLI command/flag/config surface as a snapshot JSON. + Snapshot(SnapshotArgs), + /// Print the raw list of changes between two snapshot files. + Diff(DiffArgs), + /// Print the maintainer workflow documentation. + Docs(DocsArgs), +} + +#[derive(Args)] +pub struct GenerateArgs { + /// Path to the "before" CLI snapshot JSON (older version). + #[arg(long)] + pub before: PathBuf, + + /// Path to the "after" CLI snapshot JSON (newer version). + #[arg(long)] + pub after: PathBuf, + + /// Write the generated notes to this file instead of stdout. + #[arg(long, short = 'o')] + pub output: Option, + + /// Print a summary of detected changes alongside the notes. + #[arg(long, default_value = "false")] + pub summary: bool, +} + +#[derive(Args)] +pub struct SnapshotArgs { + /// Version label to embed in the snapshot (e.g. "v0.2.0" or a git tag). + #[arg(long, default_value = "unknown")] + pub version: String, + + /// Write the snapshot to this file. + #[arg(long, short = 'o', default_value = "cli-snapshot.json")] + pub output: PathBuf, +} + +#[derive(Args)] +pub struct DiffArgs { + /// Path to the "before" CLI snapshot JSON. + #[arg(long)] + pub before: PathBuf, + + /// Path to the "after" CLI snapshot JSON. + #[arg(long)] + pub after: PathBuf, + + /// Output format: `console` (default) or `json`. + #[arg(long, default_value = "console")] + pub format: String, +} + +#[derive(Args)] +pub struct DocsArgs { + /// Write docs to this file instead of printing to stdout. + #[arg(long)] + pub output: Option, +} + +// ── Entry point ─────────────────────────────────────────────────────────────── + +pub fn handle(cmd: MigrateNotesCommands) -> Result<()> { + match cmd { + MigrateNotesCommands::Generate(args) => handle_generate(args), + MigrateNotesCommands::Snapshot(args) => handle_snapshot(args), + MigrateNotesCommands::Diff(args) => handle_diff(args), + MigrateNotesCommands::Docs(args) => handle_docs(args), + } +} + +// ── Handlers ────────────────────────────────────────────────────────────────── + +fn handle_generate(args: GenerateArgs) -> Result<()> { + p::header("AI-Assisted CLI Migration Notes Generator"); + + let before = cli_migration_notes::load_snapshot(&args.before) + .with_context(|| format!("Loading before-snapshot: {}", args.before.display()))?; + let after = cli_migration_notes::load_snapshot(&args.after) + .with_context(|| format!("Loading after-snapshot: {}", args.after.display()))?; + + p::kv("Before version", &before.version); + p::kv("After version", &after.version); + + let changes = cli_migration_notes::diff_snapshots(&before, &after); + + if args.summary { + print_change_summary(&changes); + } + + // render_migration_notes unconditionally prepends HUMAN_REVIEW_BANNER. + let notes = + cli_migration_notes::render_migration_notes(&before.version, &after.version, &changes); + + match &args.output { + Some(path) => { + std::fs::write(path, ¬es) + .with_context(|| format!("Writing notes to {}", path.display()))?; + p::success(&format!("Migration notes written to {}", path.display())); + p::warn("⚠️ These notes are AI-generated — review and edit before publishing."); + } + None => { + println!("{}", notes); + } + } + + Ok(()) +} + +fn handle_snapshot(args: SnapshotArgs) -> Result<()> { + p::header("Capture CLI Snapshot"); + + // Build a snapshot of the current StarForge CLI surface. + // In a real run this would introspect the running binary's clap command + // tree. For portability (this function runs without a live binary + // available at all times) we build the canonical snapshot from the + // known command list and config keys declared in source. + let snapshot = build_current_snapshot(&args.version); + + cli_migration_notes::save_snapshot(&snapshot, &args.output) + .with_context(|| format!("Writing snapshot to {}", args.output.display()))?; + + p::success(&format!("Snapshot written to {}", args.output.display())); + p::kv("Version", &args.version); + p::kv("Commands captured", &snapshot.commands.len().to_string()); + p::kv( + "Config keys captured", + &snapshot.config_keys.len().to_string(), + ); + Ok(()) +} + +fn handle_diff(args: DiffArgs) -> Result<()> { + p::header("CLI Snapshot Diff"); + + let before = cli_migration_notes::load_snapshot(&args.before)?; + let after = cli_migration_notes::load_snapshot(&args.after)?; + let changes = cli_migration_notes::diff_snapshots(&before, &after); + + match args.format.as_str() { + "json" => { + // Serialize changes as a simple JSON array of strings for + // machine consumption. + let descriptions: Vec = changes.iter().map(describe_change).collect(); + println!("{}", serde_json::to_string_pretty(&descriptions)?); + } + _ => { + if changes.is_empty() { + p::success("No changes detected between the two snapshots."); + } else { + println!(); + for change in &changes { + println!(" {}", describe_change(change).cyan()); + } + println!(); + p::kv("Total changes", &changes.len().to_string()); + } + } + } + + Ok(()) +} + +fn handle_docs(args: DocsArgs) -> Result<()> { + let content = include_str!("../../docs/MIGRATION_NOTES_WORKFLOW.md"); + match &args.output { + Some(path) => { + std::fs::write(path, content)?; + println!("Docs written to {}", path.display()); + } + None => { + println!("{}", content); + } + } + Ok(()) +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +fn print_change_summary(changes: &[CliChange]) { + if changes.is_empty() { + p::info("No changes detected."); + return; + } + + let renames = changes + .iter() + .filter(|c| matches!(c, CliChange::CommandRenamed { .. })) + .count(); + let removals = changes + .iter() + .filter(|c| matches!(c, CliChange::CommandRemoved { .. })) + .count(); + let flag_changes = changes + .iter() + .filter(|c| { + matches!( + c, + CliChange::FlagRenamed { .. } + | CliChange::FlagRemoved { .. } + | CliChange::FlagBecameRequired { .. } + ) + }) + .count(); + let config_changes = changes + .iter() + .filter(|c| { + matches!( + c, + CliChange::ConfigKeyRenamed { .. } + | CliChange::ConfigKeyRemoved { .. } + | CliChange::ConfigKeyTypeChanged { .. } + ) + }) + .count(); + + println!(); + p::separator(); + p::kv("Command renames", &renames.to_string()); + p::kv("Command removals", &removals.to_string()); + p::kv("Flag changes", &flag_changes.to_string()); + p::kv("Config changes", &config_changes.to_string()); + p::separator(); + println!(); +} + +fn describe_change(change: &CliChange) -> String { + match change { + CliChange::CommandRenamed { old, new } => { + format!("[RENAME] command {} → {}", old, new) + } + CliChange::CommandRemoved { command, .. } => { + format!("[REMOVE] command {} (no replacement)", command) + } + CliChange::CommandAdded { command, .. } => { + format!("[ADD] command {}", command) + } + CliChange::FlagRenamed { + command, + old_flag, + new_flag, + } => { + format!( + "[RENAME] flag {} --{} → --{}", + command, old_flag, new_flag + ) + } + CliChange::FlagRemoved { command, flag, .. } => { + format!("[REMOVE] flag {} --{}", command, flag) + } + CliChange::FlagBecameRequired { command, flag, .. } => { + format!("[BREAKING] flag {} --{} became required", command, flag) + } + CliChange::ConfigKeyRenamed { old, new } => { + format!("[RENAME] config {} → {}", old, new) + } + CliChange::ConfigKeyRemoved { key, .. } => { + format!("[REMOVE] config {}", key) + } + CliChange::ConfigKeyAdded { key, .. } => { + format!("[ADD] config {}", key) + } + CliChange::ConfigKeyTypeChanged { + key, + old_type, + new_type, + } => { + format!( + "[CHANGE] config {} type {} → {}", + key, old_type, new_type + ) + } + } +} + +/// Build a snapshot of the current StarForge CLI surface. +/// +/// This is the canonical list of commands and config keys for the current +/// codebase. When a command is added or removed from `src/main.rs`, this +/// function should be updated to match. The snapshot is the "source of truth" +/// against which older captured snapshots are diffed. +pub fn build_current_snapshot(version: &str) -> CliSnapshot { + CliSnapshot { + version: version.to_string(), + commands: vec![ + cmd( + "wallet create", + "Create a new test wallet", + vec![ + flag("name", Some("n"), true, None, "Wallet name"), + flag("network", None, false, Some("testnet"), "Target network"), + ], + ), + cmd("wallet list", "List all local wallets", vec![]), + cmd( + "wallet fund", + "Fund a wallet via Friendbot (testnet only)", + vec![flag("name", Some("n"), true, None, "Wallet name")], + ), + cmd( + "wallet show", + "Show wallet details", + vec![flag("name", Some("n"), true, None, "Wallet name")], + ), + cmd( + "wallet remove", + "Remove a local wallet", + vec![flag("name", Some("n"), true, None, "Wallet name")], + ), + cmd( + "deploy", + "Deploy a compiled Soroban contract (.wasm)", + vec![ + flag("wasm", None, true, None, "Path to the compiled .wasm file"), + flag("network", None, false, Some("testnet"), "Target network"), + flag( + "simulate", + None, + false, + None, + "Simulate deployment without submitting", + ), + flag("execute", None, false, None, "Execute the deployment"), + ], + ), + cmd("network show", "Show active network configuration", vec![]), + cmd( + "network switch", + "Switch the active network", + vec![flag( + "name", + None, + true, + None, + "Network name (testnet or mainnet)", + )], + ), + cmd("template list", "List available contract templates", vec![]), + cmd( + "template use", + "Scaffold from a template", + vec![ + flag("name", Some("n"), true, None, "Template name"), + flag("output", Some("o"), false, None, "Output directory"), + ], + ), + cmd( + "generate", + "Generate a Soroban contract from a natural language prompt", + vec![ + flag( + "prompt", + Some("p"), + true, + None, + "Natural language description", + ), + flag( + "output", + Some("o"), + false, + Some("contract.rs"), + "Output file", + ), + ], + ), + cmd("info", "Show environment and version information", vec![]), + ], + config_keys: vec![ + config_key( + "network.default", + "string", + Some("testnet"), + "Default network for all commands", + ), + config_key( + "telemetry.enabled", + "bool", + Some("true"), + "Whether to send anonymous usage telemetry", + ), + config_key( + "ai.model", + "string", + Some("codellama:7b"), + "Default Ollama model for AI commands", + ), + config_key( + "ai.ollama_url", + "string", + Some("http://localhost:11434"), + "Ollama base URL", + ), + config_key( + "wallet.storage_dir", + "string", + None, + "Directory where wallet files are stored", + ), + config_key( + "deploy.default_network", + "string", + Some("testnet"), + "Default network for deploy command", + ), + ], + } +} + +// ── Snapshot builder helpers ────────────────────────────────────────────────── + +fn cmd(command: &str, description: &str, flags: Vec) -> CliCommandSnapshot { + CliCommandSnapshot { + command: command.to_string(), + description: description.to_string(), + flags, + } +} + +fn flag( + name: &str, + short: Option<&str>, + required: bool, + default: Option<&str>, + description: &str, +) -> CliFlag { + CliFlag { + name: name.to_string(), + short: short.map(str::to_string), + required, + default: default.map(str::to_string), + description: description.to_string(), + } +} + +fn config_key( + key: &str, + value_type: &str, + default: Option<&str>, + description: &str, +) -> ConfigKeySnapshot { + ConfigKeySnapshot { + key: key.to_string(), + value_type: value_type.to_string(), + default: default.map(str::to_string), + description: description.to_string(), + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index fbe50724..77e92cb2 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod add; pub mod agent; pub mod ai; pub mod ai_accessibility; @@ -64,6 +65,7 @@ pub mod explain; pub mod feature_flags_cmd; pub mod gas; pub mod generate; +pub mod global_flags; pub mod governance; pub mod help; pub mod info; @@ -75,12 +77,12 @@ pub mod man; pub mod manifest; pub mod migrate; pub mod migrate_ai; +pub mod migrate_notes; pub mod monitor; pub mod multi_network; pub mod multisig_builder; pub mod mutate; pub mod network; -pub mod add; pub mod new; pub mod nl; pub mod node; @@ -115,4 +117,3 @@ pub mod upgrade; pub mod upgrade_auto; pub mod verify; pub mod wallet; -pub mod global_flags; diff --git a/src/commands/network.rs b/src/commands/network.rs index 02fbbf19..2af4bff1 100644 --- a/src/commands/network.rs +++ b/src/commands/network.rs @@ -1,9 +1,7 @@ use crate::utils::{ config, dry_run::{self, DryRunPlan, PlannedOperation}, - output, - print as p, - soroban, + output, print as p, soroban, }; use anyhow::Result; use clap::Subcommand; @@ -176,7 +174,9 @@ fn dry_run_plan(cmd: &NetworkCommands) -> Option { .writes_filesystem(), ), NetworkCommands::Show { .. } | NetworkCommands::Test { .. } => None, - NetworkCommands::Node(_) | NetworkCommands::Simulate(_) | NetworkCommands::Snapshot(_) => None, + NetworkCommands::Node(_) | NetworkCommands::Simulate(_) | NetworkCommands::Snapshot(_) => { + None + } } } diff --git a/src/commands/new.rs b/src/commands/new.rs index c6b7fdd0..8c80c19c 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -1570,18 +1570,42 @@ fn scaffold_workspace(name: String) -> Result<()> { fs::write(dir.join(".gitignore"), "target/\n.soroban/\n")?; p::step(3, 4, "Writing crates and contracts…"); - fs::write(dir.join("crates/shared-types/Cargo.toml"), shared_types_cargo_toml())?; - fs::write(dir.join("crates/shared-types/src/lib.rs"), shared_types_lib())?; - - fs::write(dir.join("contracts/contract-a/Cargo.toml"), contract_a_cargo_toml())?; - fs::write(dir.join("contracts/contract-a/src/lib.rs"), contract_a_lib())?; + fs::write( + dir.join("crates/shared-types/Cargo.toml"), + shared_types_cargo_toml(), + )?; + fs::write( + dir.join("crates/shared-types/src/lib.rs"), + shared_types_lib(), + )?; - fs::write(dir.join("contracts/contract-b/Cargo.toml"), contract_b_cargo_toml())?; - fs::write(dir.join("contracts/contract-b/src/lib.rs"), contract_b_lib())?; - fs::write(dir.join("contracts/contract-b/tests/integration.rs"), workspace_integration_test())?; + fs::write( + dir.join("contracts/contract-a/Cargo.toml"), + contract_a_cargo_toml(), + )?; + fs::write( + dir.join("contracts/contract-a/src/lib.rs"), + contract_a_lib(), + )?; + + fs::write( + dir.join("contracts/contract-b/Cargo.toml"), + contract_b_cargo_toml(), + )?; + fs::write( + dir.join("contracts/contract-b/src/lib.rs"), + contract_b_lib(), + )?; + fs::write( + dir.join("contracts/contract-b/tests/integration.rs"), + workspace_integration_test(), + )?; p::step(4, 4, "Writing README.md…"); - fs::write(dir.join("README.md"), format!("# {}\n\nA multi-contract Soroban workspace.", name))?; + fs::write( + dir.join("README.md"), + format!("# {}\n\nA multi-contract Soroban workspace.", name), + )?; target_guard.commit(); @@ -1617,14 +1641,16 @@ lto = true fn shared_types_cargo_toml() -> String { let sdk = templates::SOROBAN_SDK_VERSION; - format!(r#"[package] + format!( + r#"[package] name = "shared-types" version = "0.1.0" edition = "2021" [dependencies] soroban-sdk = "{sdk}" -"#) +"# + ) } fn shared_types_lib() -> &'static str { @@ -1642,7 +1668,8 @@ pub struct UserConfig { fn contract_a_cargo_toml() -> String { let sdk = templates::SOROBAN_SDK_VERSION; - format!(r#"[package] + format!( + r#"[package] name = "contract-a" version = "0.1.0" edition = "2021" @@ -1659,7 +1686,8 @@ soroban-sdk = { version = "{sdk}", features = ["testutils"] } [features] testutils = ["soroban-sdk/testutils"] -"#) +"# + ) } fn contract_a_lib() -> &'static str { @@ -1685,7 +1713,8 @@ impl ContractATrait for ContractA { fn contract_b_cargo_toml() -> String { let sdk = templates::SOROBAN_SDK_VERSION; - format!(r#"[package] + format!( + r#"[package] name = "contract-b" version = "0.1.0" edition = "2021" @@ -1701,7 +1730,8 @@ contract-a = { path = "../contract-a" } [dev-dependencies] soroban-sdk = { version = "{sdk}", features = ["testutils"] } contract-a = { path = "../contract-a", features = ["testutils"] } -"#) +"# + ) } fn contract_b_lib() -> &'static str { diff --git a/src/commands/template.rs b/src/commands/template.rs index d2a1a250..5d3ffdd3 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -14,8 +14,6 @@ use colored::Colorize; use std::path::PathBuf; use std::str::FromStr; - - #[derive(Subcommand)] pub enum TemplateCommands { /// Search for templates in the marketplace @@ -379,18 +377,20 @@ pub async fn handle(cmd: TemplateCommands) -> Result<()> { version, cli_version_min, cli_version_max, - } => import( - path, - name, - description, - author, - tags, - version, - cli_version_min, - cli_version_max, - false, - ) - .await, + } => { + import( + path, + name, + description, + author, + tags, + version, + cli_version_min, + cli_version_max, + false, + ) + .await + } TemplateCommands::Publish { path, name, @@ -1773,7 +1773,10 @@ async fn template_analyze( match out { Some(path) => { std::fs::write(&path, &rendered)?; - p::success(&format!("Community analysis report written to {}", path.display())); + p::success(&format!( + "Community analysis report written to {}", + path.display() + )); } None => { if !json { @@ -1794,12 +1797,8 @@ fn template_feedback( let category = category .map(|category| crate::utils::template_analytics::FeedbackCategory::from_str(&category)) .transpose()?; - let entry = crate::utils::template_analytics::submit_feedback( - &name, - &comment, - rating, - category, - )?; + let entry = + crate::utils::template_analytics::submit_feedback(&name, &comment, rating, category)?; p::header("Feedback Submitted"); p::kv("Template", &entry.template); diff --git a/src/commands/test.rs b/src/commands/test.rs index c2b42961..8223fef7 100644 --- a/src/commands/test.rs +++ b/src/commands/test.rs @@ -160,15 +160,16 @@ fn resolve_test_wasm(args: &TestArgs) -> Result { } else { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); if let Some((manifest_path, manifest)) = crate::manifest::find_and_load_manifest(&cwd)? { - let contract_wasm = manifest - .contracts - .values() - .find_map(|c| c.wasm.clone()); + let contract_wasm = manifest.contracts.values().find_map(|c| c.wasm.clone()); if let Some(wasm_rel) = contract_wasm { - let base_dir = manifest_path.parent().unwrap_or_else(|| std::path::Path::new(".")); + let base_dir = manifest_path + .parent() + .unwrap_or_else(|| std::path::Path::new(".")); Ok(base_dir.join(wasm_rel)) } else { - anyhow::bail!("No contract WASM specified in starforge.toml. Please pass --wasm ."); + anyhow::bail!( + "No contract WASM specified in starforge.toml. Please pass --wasm ." + ); } } else { anyhow::bail!("Missing --wasm argument and no starforge.toml project manifest found."); diff --git a/src/commands/tutorial.rs b/src/commands/tutorial.rs index 12bcdf91..8eec8082 100644 --- a/src/commands/tutorial.rs +++ b/src/commands/tutorial.rs @@ -57,7 +57,9 @@ fn list() -> Result<()> { ); } p::separator(); - p::info("Start with: starforge tool tutorial start hello-world (or add --demo for offline mode)"); + p::info( + "Start with: starforge tool tutorial start hello-world (or add --demo for offline mode)", + ); Ok(()) } @@ -96,7 +98,10 @@ fn next() -> Result<()> { })?; let tutorial = tutorial_engine::load_tutorial(&root, &slug)?; let step = tutorial.steps.get(status.current_step).ok_or_else(|| { - anyhow::anyhow!("Tutorial progress is invalid; restart with starforge tool tutorial start {}", slug) + anyhow::anyhow!( + "Tutorial progress is invalid; restart with starforge tool tutorial start {}", + slug + ) })?; if let Some(checkpoint) = &step.checkpoint { @@ -141,7 +146,9 @@ fn next() -> Result<()> { p::header(&format!("Tutorial: {}", tutorial.title)); print_current_step(&tutorial, &status); - p::info("Run the suggested command in your terminal, then `starforge tool tutorial next` again."); + p::info( + "Run the suggested command in your terminal, then `starforge tool tutorial next` again.", + ); Ok(()) } diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index adf9226b..85b45900 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -2,6 +2,7 @@ pub mod auth; pub mod cost_analysis; pub mod resolvers; pub mod schema; +pub mod schema_deprecations; pub mod subscription; pub mod types; diff --git a/src/graphql/schema.rs b/src/graphql/schema.rs index 5c4a9a44..7045933e 100644 --- a/src/graphql/schema.rs +++ b/src/graphql/schema.rs @@ -1,6 +1,17 @@ -use async_graphql::{Schema, EmptyMutation}; -use crate::graphql::resolvers::{Query, Mutation}; +use crate::graphql::resolvers::{Mutation, Query}; use crate::graphql::subscription::Subscription; +use async_graphql::Schema; + +/// Current schema version string (format: `YYYY-MM`). +/// +/// Increment this whenever a breaking change ships: +/// - a deprecated field is removed, OR +/// - a field's type signature changes, OR +/// - a non-null argument is added to an existing field. +/// +/// Non-breaking additions (new fields/types) do NOT require a bump. +/// See `GRAPHQL_SCHEMA_VERSIONING.md` for the full policy. +pub const SCHEMA_VERSION: &str = "2026-10"; pub type StarforgeSchema = Schema; diff --git a/src/graphql/schema_deprecations.rs b/src/graphql/schema_deprecations.rs new file mode 100644 index 00000000..543923d7 --- /dev/null +++ b/src/graphql/schema_deprecations.rs @@ -0,0 +1,143 @@ +//! GraphQL Schema Deprecation Registry +//! +//! Every field that has been deprecated (marked with `#[graphql(deprecation)]`) +//! MUST have a corresponding entry in `DEPRECATED_FIELDS` below. +//! +//! Rules (see `GRAPHQL_SCHEMA_VERSIONING.md` for the full policy): +//! +//! - `deprecated_since`: ISO 8601 date the deprecation PR merged to `master`. +//! - `earliest_removal`: must be >= `deprecated_since` + 90 days. +//! - `replacement`: name the successor field/type so clients know what to use. +//! +//! The CI job `GraphQL Schema Lint` (`.github/workflows/graphql-schema-lint.yml`) +//! reads this file via `scripts/check-graphql-deprecations.sh` and fails any PR +//! that removes a non-deprecated field or removes a deprecated field before its +//! `earliest_removal` date. + +/// A single deprecated GraphQL field. +#[derive(Debug, Clone)] +pub struct DeprecatedField { + /// The GraphQL type that owns this field (e.g. `"Query"`, `"Wallet"`). + pub type_name: &'static str, + /// The field name as it appears in the GraphQL schema. + pub field_name: &'static str, + /// ISO 8601 date (`YYYY-MM-DD`) when the deprecation was merged to master. + pub deprecated_since: &'static str, + /// ISO 8601 date (`YYYY-MM-DD`) on or after which the field may be removed + /// (must be at least 90 days after `deprecated_since`). + pub earliest_removal: &'static str, + /// Name of the replacement field/type, if one exists. + pub replacement: Option<&'static str>, + /// Human-readable reason surfaced in `@deprecated(reason: "...")`. + pub reason: &'static str, +} + +/// The canonical registry of all currently deprecated GraphQL fields. +/// +/// Add an entry here whenever you add `#[graphql(deprecation = "...")]` to a +/// resolver method or type field. Remove the entry only when the field itself +/// is removed from the schema (after `earliest_removal` has elapsed). +/// +/// Currently no fields are deprecated — this slice is intentionally empty until +/// the first deprecation is introduced. The CI lint treats an empty registry +/// as clean and passes immediately. +pub const DEPRECATED_FIELDS: &[DeprecatedField] = &[ + // ── Example (commented out) ──────────────────────────────────────────── + // DeprecatedField { + // type_name: "Query", + // field_name: "wallets", + // deprecated_since: "2026-10-01", + // earliest_removal: "2026-12-31", + // replacement: Some("walletsPaginated"), + // reason: "Use `walletsPaginated(first, after)` for cursor-based pagination.", + // }, + // ────────────────────────────────────────────────────────────────────── +]; + +/// Returns all deprecated field entries for a given GraphQL type name. +/// +/// Useful in tests and the schema-lint script stub to query the registry +/// programmatically. +pub fn fields_for_type(type_name: &str) -> Vec<&'static DeprecatedField> { + DEPRECATED_FIELDS + .iter() + .filter(|f| f.type_name == type_name) + .collect() +} + +/// Returns `true` if a field is currently registered as deprecated. +pub fn is_deprecated(type_name: &str, field_name: &str) -> bool { + DEPRECATED_FIELDS + .iter() + .any(|f| f.type_name == type_name && f.field_name == field_name) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Every deprecated field must have earliest_removal >= deprecated_since + 90 days. + #[test] + fn deprecation_windows_are_at_least_90_days() { + for field in DEPRECATED_FIELDS { + let since = parse_date(field.deprecated_since).unwrap_or_else(|| { + panic!( + "Invalid deprecated_since date for {}.{}: {}", + field.type_name, field.field_name, field.deprecated_since + ) + }); + let earliest = parse_date(field.earliest_removal).unwrap_or_else(|| { + panic!( + "Invalid earliest_removal date for {}.{}: {}", + field.type_name, field.field_name, field.earliest_removal + ) + }); + let window_days = (earliest - since).num_days(); + assert!( + window_days >= 90, + "Deprecation window for {}.{} is only {} days (minimum 90). \ + earliest_removal must be at least {} days after deprecated_since.", + field.type_name, + field.field_name, + window_days, + 90 + ); + } + } + + /// Every deprecated field must supply a non-empty reason string. + #[test] + fn deprecation_reasons_are_non_empty() { + for field in DEPRECATED_FIELDS { + assert!( + !field.reason.trim().is_empty(), + "Deprecated field {}.{} has an empty reason string.", + field.type_name, + field.field_name + ); + } + } + + /// Field names must be unique within a type. + #[test] + fn no_duplicate_deprecations() { + use std::collections::HashSet; + let mut seen = HashSet::new(); + for field in DEPRECATED_FIELDS { + let key = (field.type_name, field.field_name); + assert!( + seen.insert(key), + "Duplicate deprecation entry for {}.{}", + field.type_name, + field.field_name + ); + } + } + + // ── helpers ────────────────────────────────────────────────────────────── + + /// Parse a `YYYY-MM-DD` string into a naive date (chrono NaiveDate). + fn parse_date(s: &str) -> Option { + chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok() + } +} diff --git a/src/lib.rs b/src/lib.rs index 1d4daf6e..0464e9f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,3 @@ pub mod manifest; pub mod plugins; pub mod sep; pub mod utils; - diff --git a/src/main.rs b/src/main.rs index 65a7a9f9..da5d2d54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -229,6 +229,10 @@ enum Commands { #[command(subcommand)] Agent(commands::agent::AgentCommands), + /// AI-assisted CLI migration notes generator (breaking change notes from snapshots) + #[command(subcommand, name = "migrate-notes")] + MigrateNotes(commands::migrate_notes::MigrateNotesCommands), + /// Terminal User Interface for wallets, contracts, and transactions #[cfg(feature = "ui")] Ui(commands::ui::UiArgs), @@ -381,6 +385,7 @@ async fn run() { Commands::Dev(_) => "dev", Commands::Alias(_) => "alias", Commands::Autocomplete { .. } => "autocomplete", + Commands::MigrateNotes(_) => "migrate-notes", Commands::External(_) => "external", Commands::Verify(_) => "verify", Commands::Help(_) => "help", @@ -458,6 +463,7 @@ async fn run() { ) .await } + Commands::MigrateNotes(cmd) => commands::migrate_notes::handle(cmd), Commands::External(args) => handle_external_plugin(args), Commands::Help(args) => commands::help::handle(args).await, Commands::AiTelemetry(cmd) => commands::ai_telemetry::handle(cmd).await, diff --git a/src/manifest.rs b/src/manifest.rs index 57501006..80a901d1 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -221,7 +221,10 @@ impl ProjectManifest { bail!("Script name in starforge.toml cannot be empty."); } if s_cmd.trim().is_empty() { - bail!("Script command for '{}' in starforge.toml cannot be empty.", s_name); + bail!( + "Script command for '{}' in starforge.toml cannot be empty.", + s_name + ); } } diff --git a/src/utils/cli_migration_notes.rs b/src/utils/cli_migration_notes.rs new file mode 100644 index 00000000..4f8c0952 --- /dev/null +++ b/src/utils/cli_migration_notes.rs @@ -0,0 +1,646 @@ +//! CLI Migration Notes Generator +//! +//! Generates markdown migration notes from a version range or a pre-computed +//! diff summary. The generator identifies renamed CLI commands/flags and config +//! key changes by parsing the structured before/after snapshots that a caller +//! provides (captured from real source), then formats them as a ready-to-review +//! markdown document. +//! +//! # Human-review banner +//! +//! **Every** document produced by this module begins with a clearly visible +//! human-review banner. This is unconditional — there is no flag or option +//! that can suppress it. See `HUMAN_REVIEW_BANNER`. +//! +//! # Integration points +//! +//! - CLI subcommand: `starforge migrate-notes` (`src/commands/migrate_notes.rs`) +//! - Fixture tests: `tests/cli_migration_notes.rs` +//! - Maintainer docs: `docs/MIGRATION_NOTES_WORKFLOW.md` + +use serde::{Deserialize, Serialize}; + +// ── Human-review banner ─────────────────────────────────────────────────────── + +/// The mandatory banner prepended to every AI-generated migration notes +/// document. It is a compile-time constant so no code path can omit it. +pub const HUMAN_REVIEW_BANNER: &str = "\ +> ⚠️ **AI-generated migration notes — review for accuracy before publishing.** +> This document was produced automatically from a diff and may contain +> errors or miss context. A human maintainer **must** review, edit, and +> approve this document before it is published in a CHANGELOG, GitHub +> release, or docs site. +"; + +// ── Input types ─────────────────────────────────────────────────────────────── + +/// A CLI command or subcommand definition captured from a specific version. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CliCommandSnapshot { + /// The full command path as the user types it (e.g. `"wallet create"`). + pub command: String, + /// Short description shown in `--help` output. + pub description: String, + /// All flags/arguments this command accepts. + pub flags: Vec, +} + +/// A single CLI flag or argument. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CliFlag { + /// Flag name without leading dashes (e.g. `"network"` → `--network`). + pub name: String, + /// Short alias without the dash, if any (e.g. `"n"` → `-n`). + pub short: Option, + /// Whether this flag is required. + pub required: bool, + /// Default value, if any. + pub default: Option, + /// Human-readable description. + pub description: String, +} + +/// A configuration key captured from a specific version. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ConfigKeySnapshot { + /// Dot-separated key path (e.g. `"network.default"`). + pub key: String, + /// Value type hint (e.g. `"string"`, `"bool"`, `"u16"`). + pub value_type: String, + /// Default value as a string. + pub default: Option, + /// Human-readable description. + pub description: String, +} + +/// A full snapshot of the CLI surface and config schema at one point in time. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CliSnapshot { + /// Version label (git tag, semver, or ref name). + pub version: String, + /// All top-level and subcommands in this version. + pub commands: Vec, + /// All configuration keys in this version. + pub config_keys: Vec, +} + +// ── Diff types ──────────────────────────────────────────────────────────────── + +/// A single change detected between two CLI snapshots. +#[derive(Debug, Clone, PartialEq)] +pub enum CliChange { + /// A command was renamed from `old` to `new`. + CommandRenamed { old: String, new: String }, + /// A command was removed with no replacement. + CommandRemoved { + command: String, + description: String, + }, + /// A command was added. + CommandAdded { + command: String, + description: String, + }, + /// A required flag was renamed on a specific command. + FlagRenamed { + command: String, + old_flag: String, + new_flag: String, + }, + /// A required flag was removed from a command. + FlagRemoved { + command: String, + flag: String, + description: String, + }, + /// A previously optional flag became required. + FlagBecameRequired { + command: String, + flag: String, + description: String, + }, + /// A config key was renamed. + ConfigKeyRenamed { old: String, new: String }, + /// A config key was removed. + ConfigKeyRemoved { key: String, description: String }, + /// A config key was added. + ConfigKeyAdded { + key: String, + value_type: String, + default: Option, + description: String, + }, + /// A config key's type changed. + ConfigKeyTypeChanged { + key: String, + old_type: String, + new_type: String, + }, +} + +// ── Diff engine ─────────────────────────────────────────────────────────────── + +/// Diff two CLI snapshots and return an ordered list of [`CliChange`]s. +/// +/// Rename detection heuristic: if a command present in `before` is absent in +/// `after`, and a command present in `after` is absent in `before`, and their +/// `description` values are identical, the pair is treated as a rename rather +/// than a remove+add. This avoids false positives for genuinely new/removed +/// commands. +pub fn diff_snapshots(before: &CliSnapshot, after: &CliSnapshot) -> Vec { + let mut changes = Vec::new(); + + // ── Command-level diff ──────────────────────────────────────────────────── + + let before_cmds: std::collections::HashMap<&str, &CliCommandSnapshot> = before + .commands + .iter() + .map(|c| (c.command.as_str(), c)) + .collect(); + let after_cmds: std::collections::HashMap<&str, &CliCommandSnapshot> = after + .commands + .iter() + .map(|c| (c.command.as_str(), c)) + .collect(); + + // Find candidates: commands removed in before and added in after. + let removed_cmds: Vec<&CliCommandSnapshot> = before + .commands + .iter() + .filter(|c| !after_cmds.contains_key(c.command.as_str())) + .collect(); + + let added_cmds: Vec<&CliCommandSnapshot> = after + .commands + .iter() + .filter(|c| !before_cmds.contains_key(c.command.as_str())) + .collect(); + + // Pair by identical description (rename heuristic). + let mut rename_matched_old: std::collections::HashSet<&str> = std::collections::HashSet::new(); + let mut rename_matched_new: std::collections::HashSet<&str> = std::collections::HashSet::new(); + + for old_cmd in &removed_cmds { + if let Some(new_cmd) = added_cmds.iter().find(|a| { + a.description == old_cmd.description && !rename_matched_new.contains(a.command.as_str()) + }) { + changes.push(CliChange::CommandRenamed { + old: old_cmd.command.clone(), + new: new_cmd.command.clone(), + }); + rename_matched_old.insert(&old_cmd.command); + rename_matched_new.insert(&new_cmd.command); + } + } + + // Remaining removes/adds. + for cmd in &removed_cmds { + if !rename_matched_old.contains(cmd.command.as_str()) { + changes.push(CliChange::CommandRemoved { + command: cmd.command.clone(), + description: cmd.description.clone(), + }); + } + } + for cmd in &added_cmds { + if !rename_matched_new.contains(cmd.command.as_str()) { + changes.push(CliChange::CommandAdded { + command: cmd.command.clone(), + description: cmd.description.clone(), + }); + } + } + + // ── Flag-level diff (for commands present in both versions) ─────────────── + + for (cmd_name, before_cmd) in &before_cmds { + if let Some(after_cmd) = after_cmds.get(cmd_name) { + diff_flags(cmd_name, before_cmd, after_cmd, &mut changes); + } + } + + // ── Config key diff ─────────────────────────────────────────────────────── + + let before_keys: std::collections::HashMap<&str, &ConfigKeySnapshot> = before + .config_keys + .iter() + .map(|k| (k.key.as_str(), k)) + .collect(); + let after_keys: std::collections::HashMap<&str, &ConfigKeySnapshot> = after + .config_keys + .iter() + .map(|k| (k.key.as_str(), k)) + .collect(); + + let removed_keys: Vec<&ConfigKeySnapshot> = before + .config_keys + .iter() + .filter(|k| !after_keys.contains_key(k.key.as_str())) + .collect(); + + let added_keys: Vec<&ConfigKeySnapshot> = after + .config_keys + .iter() + .filter(|k| !before_keys.contains_key(k.key.as_str())) + .collect(); + + // Rename detection for config keys (same type + description heuristic). + let mut key_rename_old: std::collections::HashSet<&str> = std::collections::HashSet::new(); + let mut key_rename_new: std::collections::HashSet<&str> = std::collections::HashSet::new(); + + for old_key in &removed_keys { + if let Some(new_key) = added_keys.iter().find(|a| { + a.value_type == old_key.value_type + && a.description == old_key.description + && !key_rename_new.contains(a.key.as_str()) + }) { + changes.push(CliChange::ConfigKeyRenamed { + old: old_key.key.clone(), + new: new_key.key.clone(), + }); + key_rename_old.insert(&old_key.key); + key_rename_new.insert(&new_key.key); + } + } + + for key in &removed_keys { + if !key_rename_old.contains(key.key.as_str()) { + changes.push(CliChange::ConfigKeyRemoved { + key: key.key.clone(), + description: key.description.clone(), + }); + } + } + for key in &added_keys { + if !key_rename_new.contains(key.key.as_str()) { + changes.push(CliChange::ConfigKeyAdded { + key: key.key.clone(), + value_type: key.value_type.clone(), + default: key.default.clone(), + description: key.description.clone(), + }); + } + } + + // Type changes for keys present in both versions. + for (key_name, before_key) in &before_keys { + if let Some(after_key) = after_keys.get(key_name) { + if before_key.value_type != after_key.value_type { + changes.push(CliChange::ConfigKeyTypeChanged { + key: (*key_name).to_string(), + old_type: before_key.value_type.clone(), + new_type: after_key.value_type.clone(), + }); + } + } + } + + changes +} + +/// Diff flags for a single command that exists in both versions. +fn diff_flags( + cmd_name: &str, + before_cmd: &CliCommandSnapshot, + after_cmd: &CliCommandSnapshot, + changes: &mut Vec, +) { + let before_flags: std::collections::HashMap<&str, &CliFlag> = before_cmd + .flags + .iter() + .map(|f| (f.name.as_str(), f)) + .collect(); + let after_flags: std::collections::HashMap<&str, &CliFlag> = after_cmd + .flags + .iter() + .map(|f| (f.name.as_str(), f)) + .collect(); + + let removed_flags: Vec<&CliFlag> = before_cmd + .flags + .iter() + .filter(|f| !after_flags.contains_key(f.name.as_str())) + .collect(); + let added_flags: Vec<&CliFlag> = after_cmd + .flags + .iter() + .filter(|f| !before_flags.contains_key(f.name.as_str())) + .collect(); + + // Rename: same description, different name. + let mut flag_rename_old: std::collections::HashSet<&str> = std::collections::HashSet::new(); + let mut flag_rename_new: std::collections::HashSet<&str> = std::collections::HashSet::new(); + + for old_flag in &removed_flags { + if let Some(new_flag) = added_flags.iter().find(|a| { + a.description == old_flag.description && !flag_rename_new.contains(a.name.as_str()) + }) { + changes.push(CliChange::FlagRenamed { + command: cmd_name.to_string(), + old_flag: old_flag.name.clone(), + new_flag: new_flag.name.clone(), + }); + flag_rename_old.insert(&old_flag.name); + flag_rename_new.insert(&new_flag.name); + } + } + + for flag in &removed_flags { + if !flag_rename_old.contains(flag.name.as_str()) { + changes.push(CliChange::FlagRemoved { + command: cmd_name.to_string(), + flag: flag.name.clone(), + description: flag.description.clone(), + }); + } + } + + // Required-ness changes for flags present in both versions. + for (flag_name, before_flag) in &before_flags { + if let Some(after_flag) = after_flags.get(flag_name) { + if !before_flag.required && after_flag.required { + changes.push(CliChange::FlagBecameRequired { + command: cmd_name.to_string(), + flag: (*flag_name).to_string(), + description: after_flag.description.clone(), + }); + } + } + } +} + +// ── Markdown renderer ───────────────────────────────────────────────────────── + +/// Render migration notes as a markdown string. +/// +/// The output **always** starts with `HUMAN_REVIEW_BANNER`. This function is +/// the single choke-point — all callers (CLI command, tests, release scripts) +/// go through here, ensuring the banner cannot be accidentally dropped. +pub fn render_migration_notes( + from_version: &str, + to_version: &str, + changes: &[CliChange], +) -> String { + let mut doc = String::new(); + + // ── Unconditional human-review banner ───────────────────────────────────── + doc.push_str(HUMAN_REVIEW_BANNER); + doc.push('\n'); + + // ── Document header ─────────────────────────────────────────────────────── + doc.push_str(&format!( + "# Migration Notes: {} → {}\n\n", + from_version, to_version + )); + doc.push_str(&format!( + "_Generated: {}_\n\n", + chrono::Utc::now().format("%Y-%m-%d") + )); + + if changes.is_empty() { + doc.push_str("No breaking changes detected between these versions.\n"); + return doc; + } + + // ── Separate changes into categories ───────────────────────────────────── + let cmd_renames: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::CommandRenamed { old, new } = c { + Some((old, new)) + } else { + None + } + }) + .collect(); + + let cmd_removed: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::CommandRemoved { + command, + description, + } = c + { + Some((command, description)) + } else { + None + } + }) + .collect(); + + let flag_renames: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::FlagRenamed { + command, + old_flag, + new_flag, + } = c + { + Some((command, old_flag, new_flag)) + } else { + None + } + }) + .collect(); + + let flag_removed: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::FlagRemoved { + command, + flag, + description, + } = c + { + Some((command, flag, description)) + } else { + None + } + }) + .collect(); + + let flag_required: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::FlagBecameRequired { + command, + flag, + description, + } = c + { + Some((command, flag, description)) + } else { + None + } + }) + .collect(); + + let key_renames: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::ConfigKeyRenamed { old, new } = c { + Some((old, new)) + } else { + None + } + }) + .collect(); + + let key_removed: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::ConfigKeyRemoved { key, description } = c { + Some((key, description)) + } else { + None + } + }) + .collect(); + + let key_type_changes: Vec<_> = changes + .iter() + .filter_map(|c| { + if let CliChange::ConfigKeyTypeChanged { + key, + old_type, + new_type, + } = c + { + Some((key, old_type, new_type)) + } else { + None + } + }) + .collect(); + + // ── Section: Renamed Commands ───────────────────────────────────────────── + if !cmd_renames.is_empty() { + doc.push_str("## Renamed Commands\n\n"); + doc.push_str( + "Update any scripts, aliases, or CI pipelines that reference these commands.\n\n", + ); + doc.push_str("| Before | After |\n"); + doc.push_str("|--------|-------|\n"); + for (old, new) in &cmd_renames { + doc.push_str(&format!("| `starforge {}` | `starforge {}` |\n", old, new)); + } + doc.push('\n'); + } + + // ── Section: Removed Commands ───────────────────────────────────────────── + if !cmd_removed.is_empty() { + doc.push_str("## Removed Commands\n\n"); + doc.push_str("These commands have been removed with no direct replacement.\n\n"); + for (cmd, desc) in &cmd_removed { + doc.push_str(&format!("- `starforge {}` — {}\n", cmd, desc)); + } + doc.push('\n'); + } + + // ── Section: Renamed Flags ──────────────────────────────────────────────── + if !flag_renames.is_empty() { + doc.push_str("## Renamed Flags\n\n"); + doc.push_str("| Command | Old Flag | New Flag |\n"); + doc.push_str("|---------|----------|----------|\n"); + for (cmd, old_flag, new_flag) in &flag_renames { + doc.push_str(&format!( + "| `starforge {}` | `--{}` | `--{}` |\n", + cmd, old_flag, new_flag + )); + } + doc.push('\n'); + } + + // ── Section: Removed Flags ──────────────────────────────────────────────── + if !flag_removed.is_empty() { + doc.push_str("## Removed Flags\n\n"); + for (cmd, flag, desc) in &flag_removed { + doc.push_str(&format!( + "- `starforge {} --{}` — {} (removed; no replacement)\n", + cmd, flag, desc + )); + } + doc.push('\n'); + } + + // ── Section: Flags That Became Required ─────────────────────────────────── + if !flag_required.is_empty() { + doc.push_str("## Flags That Became Required\n\n"); + doc.push_str("These flags were optional before and are now required.\n\n"); + for (cmd, flag, desc) in &flag_required { + doc.push_str(&format!( + "- `starforge {} --{}` — {} (now required)\n", + cmd, flag, desc + )); + } + doc.push('\n'); + } + + // ── Section: Config Key Renames ─────────────────────────────────────────── + if !key_renames.is_empty() { + doc.push_str("## Config Key Renames\n\n"); + doc.push_str("Update your `starforge.toml` (or equivalent config file) accordingly.\n\n"); + doc.push_str("| Before | After |\n"); + doc.push_str("|--------|-------|\n"); + for (old, new) in &key_renames { + doc.push_str(&format!("| `{}` | `{}` |\n", old, new)); + } + doc.push('\n'); + } + + // ── Section: Removed Config Keys ───────────────────────────────────────── + if !key_removed.is_empty() { + doc.push_str("## Removed Config Keys\n\n"); + for (key, desc) in &key_removed { + doc.push_str(&format!("- `{}` — {} (removed)\n", key, desc)); + } + doc.push('\n'); + } + + // ── Section: Config Key Type Changes ───────────────────────────────────── + if !key_type_changes.is_empty() { + doc.push_str("## Config Key Type Changes\n\n"); + doc.push_str("These keys exist in both versions but their value type changed.\n\n"); + doc.push_str("| Key | Old Type | New Type |\n"); + doc.push_str("|-----|----------|----------|\n"); + for (key, old_type, new_type) in &key_type_changes { + doc.push_str(&format!( + "| `{}` | `{}` | `{}` |\n", + key, old_type, new_type + )); + } + doc.push('\n'); + } + + // ── Footer ──────────────────────────────────────────────────────────────── + doc.push_str("---\n\n"); + doc.push_str( + "_These notes were generated automatically. Edit as needed, then remove \ + this line before publishing._\n", + ); + + doc +} + +// ── Snapshot I/O ────────────────────────────────────────────────────────────── + +/// Load a `CliSnapshot` from a JSON file. +pub fn load_snapshot(path: &std::path::Path) -> anyhow::Result { + let data = std::fs::read_to_string(path) + .map_err(|e| anyhow::anyhow!("Failed to read snapshot {}: {}", path.display(), e))?; + serde_json::from_str(&data) + .map_err(|e| anyhow::anyhow!("Failed to parse snapshot {}: {}", path.display(), e)) +} + +/// Save a `CliSnapshot` to a JSON file. +pub fn save_snapshot(snapshot: &CliSnapshot, path: &std::path::Path) -> anyhow::Result<()> { + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() && !parent.exists() { + std::fs::create_dir_all(parent)?; + } + } + std::fs::write(path, serde_json::to_string_pretty(snapshot)?) + .map_err(|e| anyhow::anyhow!("Failed to write snapshot {}: {}", path.display(), e)) +} diff --git a/src/utils/config.rs b/src/utils/config.rs index 32aa58dc..ce096902 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1515,20 +1515,29 @@ pub fn validate_config_integrity(cfg: &Config) -> Vec { // #902: a CA bundle that cannot be read is a connectivity failure waiting // to happen, so `doctor` reports it instead of leaving it to the first // HTTPS request. - match cfg.networks.get(&cfg.network).and_then(|net| net.ca_bundle.as_ref()) { + match cfg + .networks + .get(&cfg.network) + .and_then(|net| net.ca_bundle.as_ref()) + { None => findings.push(DoctorFinding::pass( "network.ca_bundle", "no custom CA bundle for the active network", )), - Some(path) => match crate::utils::http_client::load_ca_certificates( - std::path::Path::new(path.trim()), - ) { - Ok(certificates) => findings.push(DoctorFinding::pass( - "network.ca_bundle", - format!("{} trusted root certificate(s) from {}", certificates.len(), path), - )), - Err(e) => findings.push(DoctorFinding::fail("network.ca_bundle", e.to_string())), - }, + Some(path) => { + match crate::utils::http_client::load_ca_certificates(std::path::Path::new(path.trim())) + { + Ok(certificates) => findings.push(DoctorFinding::pass( + "network.ca_bundle", + format!( + "{} trusted root certificate(s) from {}", + certificates.len(), + path + ), + )), + Err(e) => findings.push(DoctorFinding::fail("network.ca_bundle", e.to_string())), + } + } } if cfg.wallets.is_empty() { @@ -2181,20 +2190,13 @@ pub fn set_network_ca_bundle( ) -> Result> { if let Some(path) = &ca_bundle { let as_str = path.to_str().ok_or_else(|| { - anyhow::anyhow!( - "CA bundle path '{}' is not valid UTF-8", - path.display() - ) + anyhow::anyhow!("CA bundle path '{}' is not valid UTF-8", path.display()) })?; if as_str.trim().is_empty() { anyhow::bail!("CA bundle path is empty"); } - crate::utils::http_client::load_ca_certificates(path).with_context(|| { - format!( - "'{}' is not usable as a CA bundle", - path.display() - ) - })?; + crate::utils::http_client::load_ca_certificates(path) + .with_context(|| format!("'{}' is not usable as a CA bundle", path.display()))?; } let entry = config diff --git a/src/utils/contract_id.rs b/src/utils/contract_id.rs index 928baaf2..f0bbcd98 100644 --- a/src/utils/contract_id.rs +++ b/src/utils/contract_id.rs @@ -25,7 +25,11 @@ use stellar_xdr::curr::{ fn padded_hex_from_str(hex_str: &str, expected_len: usize) -> Result> { let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); if hex_str.len() > expected_len * 2 { - return Err(anyhow::anyhow!("Hex string too long: expected {} bytes, got {}", expected_len, hex_str.len() / 2)); + return Err(anyhow::anyhow!( + "Hex string too long: expected {} bytes, got {}", + expected_len, + hex_str.len() / 2 + )); } let padded = format!("{:0>width$}", hex_str, width = expected_len * 2); hex::decode(&padded).map_err(|e| anyhow::anyhow!("Invalid hex string: {}", e)) @@ -68,7 +72,9 @@ pub fn derive_contract_id_preimage( let network_passphrase = config::get_network_passphrase(network); let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); - let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(*deployer_public_key))); + let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( + *deployer_public_key, + ))); let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress { address: ScAddress::Account(source_account), salt: Uint256(*salt), @@ -112,7 +118,9 @@ fn get_contract_id( /// Build the contract ID preimage from deployer address and salt. fn contract_preimage(deployer_public_key: &[u8; 32], salt: &[u8; 32]) -> ContractIdPreimage { - let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(*deployer_public_key))); + let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( + *deployer_public_key, + ))); ContractIdPreimage::Address(ContractIdPreimageFromAddress { address: ScAddress::Account(source_account), salt: Uint256(*salt), @@ -140,8 +148,9 @@ pub fn parse_salt(salt_str: &str) -> Result<[u8; 32]> { /// Parse a deployer address (StrKey G...) into a 32-byte public key. pub fn parse_deployer(deployer_str: &str) -> Result<[u8; 32]> { - let public_key = ed25519::PublicKey::from_string(deployer_str) - .map_err(|_| anyhow::anyhow!("Invalid deployer address: expected StrKey starting with 'G'"))?; + let public_key = ed25519::PublicKey::from_string(deployer_str).map_err(|_| { + anyhow::anyhow!("Invalid deployer address: expected StrKey starting with 'G'") + })?; Ok(public_key.0) } @@ -194,4 +203,4 @@ mod tests { let parsed = parse_wasm_hash(wasm_hash).unwrap(); assert_eq!(parsed, [0u8; 32]); } -} \ No newline at end of file +} diff --git a/src/utils/http_client.rs b/src/utils/http_client.rs index 0e90d17a..6e7fd8d4 100644 --- a/src/utils/http_client.rs +++ b/src/utils/http_client.rs @@ -204,10 +204,7 @@ impl HttpClientSettings { /// /// Empty values count as unset, so `HTTPS_PROXY=` disables the proxy rather /// than sending every request to `""`. -fn lookup_env( - names: &[&str], - lookup: &impl Fn(&str) -> Option, -) -> Option { +fn lookup_env(names: &[&str], lookup: &impl Fn(&str) -> Option) -> Option { names .iter() .find_map(|name| lookup(name)) @@ -587,7 +584,8 @@ mod tests { } #[test] - fn retryable_statuses_are_server_errors_and_rate_limits() { assert!(is_retryable_status(StatusCode::INTERNAL_SERVER_ERROR)); + fn retryable_statuses_are_server_errors_and_rate_limits() { + assert!(is_retryable_status(StatusCode::INTERNAL_SERVER_ERROR)); assert!(is_retryable_status(StatusCode::BAD_GATEWAY)); assert!(is_retryable_status(StatusCode::SERVICE_UNAVAILABLE)); assert!(is_retryable_status(StatusCode::TOO_MANY_REQUESTS)); @@ -622,8 +620,8 @@ mod tests { #[test] fn unsupported_proxy_scheme_is_rejected() { - let settings = HttpClientSettings::default() - .with_proxy(Some("socks5://127.0.0.1:9050".to_string())); + let settings = + HttpClientSettings::default().with_proxy(Some("socks5://127.0.0.1:9050".to_string())); let error = build_client(&settings).expect_err("socks proxies are not supported"); assert!( error.to_string().contains("unsupported proxy URL"), diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f0d0ae84..cc4d9828 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -54,6 +54,7 @@ pub mod browser_signer; pub mod call_graph; pub mod cargo_lock; pub mod cargo_metadata; +pub mod cli_migration_notes; pub mod completion; pub mod compliance; pub mod config; @@ -62,6 +63,7 @@ pub mod context_help; pub mod contract_assertions; pub mod contract_deps; pub mod contract_fixtures; +pub mod contract_id; pub mod contract_mocks; pub mod contract_profiler; pub mod contract_suggestions; @@ -70,7 +72,6 @@ pub mod contract_test_runner; pub mod contract_testing; pub mod contract_ttl; pub mod contract_versioning; -pub mod contract_id; pub mod correlation; pub mod cost_estimation; pub mod cost_management; diff --git a/src/utils/network_simulator/compat.rs b/src/utils/network_simulator/compat.rs index f53000b4..7d2c119d 100644 --- a/src/utils/network_simulator/compat.rs +++ b/src/utils/network_simulator/compat.rs @@ -4,8 +4,8 @@ //! This facade preserves the old constructor and result/state shapes for callers //! migrating to `crate::utils::network_simulator::simulator::NetworkSimulator`. -pub use super::scenarios::{SimScenario, SimScenarioResult, SimScenarioStep}; pub use super::failure::FailureMode; +pub use super::scenarios::{SimScenario, SimScenarioResult, SimScenarioStep}; use super::simulator::{ContractInstance, NetworkSimulator as Engine}; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -209,11 +209,8 @@ impl NetworkSimulator { pub fn deploy_contract(&mut self, wasm_hash: &str) -> Result { self.check_failure()?; self.simulate_latency(); - let contract_id = deterministic_contract_id( - wasm_hash, - self.seed, - self.engine.ledger.sequence, - ); + let contract_id = + deterministic_contract_id(wasm_hash, self.seed, self.engine.ledger.sequence); self.deploy_at_id(contract_id, wasm_hash) } @@ -325,7 +322,11 @@ impl NetworkSimulator { let result = self.invoke(contract_id, function, args)?; if let Some(expected) = expected_return { if result.return_value != *expected { - anyhow::bail!("Expected return '{}', got '{}'", expected, result.return_value); + anyhow::bail!( + "Expected return '{}', got '{}'", + expected, + result.return_value + ); } } Ok(()) @@ -354,9 +355,7 @@ impl NetworkSimulator { Some(FailureMode::ContractNotFound) } super::scenarios::ScriptFailureMode::Random { probability_pct } => { - Some(FailureMode::RandomFailure( - *probability_pct as f64 / 100.0, - )) + Some(FailureMode::RandomFailure(*probability_pct as f64 / 100.0)) } }; Ok(()) @@ -380,7 +379,10 @@ impl NetworkSimulator { anyhow::bail!("Simulated RPC timeout (injected failure)") } Some(FailureMode::RpcError { code }) => { - anyhow::bail!("Simulated RPC error: {} internal error (injected failure)", code) + anyhow::bail!( + "Simulated RPC error: {} internal error (injected failure)", + code + ) } Some(FailureMode::InsufficientFee) => { anyhow::bail!("Simulated insufficient fee error (injected failure)") @@ -500,9 +502,7 @@ mod tests { #[test] fn named_snapshot_restore_preserves_legacy_state() { let mut simulator = NetworkSimulator::new(42); - simulator - .deploy_contract_with_id("C_TEST", "hash") - .unwrap(); + simulator.deploy_contract_with_id("C_TEST", "hash").unwrap(); simulator.fund_account("GACC", 1000); simulator.snapshot("checkpoint"); simulator.fund_account("GACC", 5000); @@ -544,14 +544,15 @@ mod tests { #[test] fn invoke_keeps_legacy_result_and_event_behavior() { let mut simulator = NetworkSimulator::new(42); - simulator - .deploy_contract_with_id("C_TEST", "wasm") - .unwrap(); + simulator.deploy_contract_with_id("C_TEST", "wasm").unwrap(); let result = simulator .invoke("C_TEST", "increment", &["1".to_string()]) .unwrap(); assert_eq!(result.fee, 10_000 + 900 + 500 + 43); - assert_eq!(result.events, vec![format!("increment:{}", result.return_value)]); + assert_eq!( + result.events, + vec![format!("increment:{}", result.return_value)] + ); assert_eq!(simulator.state().events.len(), 1); assert_eq!(simulator.state().events[0].data, "1"); } @@ -561,9 +562,7 @@ mod tests { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("legacy-sim.json"); let mut simulator = NetworkSimulator::new(7); - simulator - .deploy_contract_with_id("C_TEST", "wasm") - .unwrap(); + simulator.deploy_contract_with_id("C_TEST", "wasm").unwrap(); simulator.fund_account("G_TEST", 1234); simulator .invoke("C_TEST", "balance", &["G_TEST".to_string()]) diff --git a/src/utils/network_simulator/mod.rs b/src/utils/network_simulator/mod.rs index ea99668f..d747f339 100644 --- a/src/utils/network_simulator/mod.rs +++ b/src/utils/network_simulator/mod.rs @@ -23,8 +23,8 @@ //! // Deploy a contract, invoke it, inspect state… //! ``` -pub mod deterministic; pub mod compat; +pub mod deterministic; pub mod failure; pub mod scenarios; pub mod simulator; @@ -33,19 +33,17 @@ pub mod time; // ── Re-exports for convenience ──────────────────────────────────────────────── -pub use deterministic::{DeterministicConfig, SeededRng}; pub use compat::{ - FailureMode as LegacyFailureMode, SimContract, SimEvent, SimInvokeResult, - SimLedgerState, SimScenario as LegacySimScenario, - SimScenarioResult as LegacySimScenarioResult, - SimScenarioStep as LegacySimScenarioStep, - NetworkSimulator as LegacyNetworkSimulator, + FailureMode as LegacyFailureMode, NetworkSimulator as LegacyNetworkSimulator, SimContract, + SimEvent, SimInvokeResult, SimLedgerState, SimScenario as LegacySimScenario, + SimScenarioResult as LegacySimScenarioResult, SimScenarioStep as LegacySimScenarioStep, }; +pub use deterministic::{DeterministicConfig, SeededRng}; pub use failure::{FailureInjector, FailureMode, FailureRule}; pub use scenarios::{ - builtin_scenarios, load_scenario, save_scenario, sim_data_dir, BuiltInScenario, - Scenario, ScenarioResult, ScenarioRunner, ScriptFailureMode, SimScenario, - SimScenarioResult, SimScenarioStep, + builtin_scenarios, load_scenario, save_scenario, sim_data_dir, BuiltInScenario, Scenario, + ScenarioResult, ScenarioRunner, ScriptFailureMode, SimScenario, SimScenarioResult, + SimScenarioStep, }; pub use simulator::{ AccountInfo, ContractInstance, LedgerInfo, NetworkSimulator, SimulationOutcome, diff --git a/src/utils/network_simulator/scenarios.rs b/src/utils/network_simulator/scenarios.rs index 9d266f25..52bd1666 100644 --- a/src/utils/network_simulator/scenarios.rs +++ b/src/utils/network_simulator/scenarios.rs @@ -723,7 +723,8 @@ fn execute_script_step( simulator.create_account_with_key(&key, 1_000.0); key }); - let outcome = simulator.simulate_invoke(contract_id, function, args, &source_account)?; + let outcome = + simulator.simulate_invoke(contract_id, function, args, &source_account)?; if let Some(expected) = expected_return { if outcome.return_value != *expected { return Err(format!( @@ -755,10 +756,9 @@ fn execute_script_step( } ScriptFailureMode::None => unreachable!(), }; - simulator.failure_injector.add_rule(FailureRule::new( - "scripted-failure", - injected, - )); + simulator + .failure_injector + .add_rule(FailureRule::new("scripted-failure", injected)); simulator.failure_injector.enable(); } } diff --git a/src/utils/network_simulator/simulator.rs b/src/utils/network_simulator/simulator.rs index 23a4a116..55fc9f96 100644 --- a/src/utils/network_simulator/simulator.rs +++ b/src/utils/network_simulator/simulator.rs @@ -802,9 +802,9 @@ impl NetworkSimulator { account: Option<&str>, ) -> Result<(), String> { let probability = self.rng.probability(); - if let Some(mode) = self - .failure_injector - .check(rpc_method, contract_id, account, probability) + if let Some(mode) = + self.failure_injector + .check(rpc_method, contract_id, account, probability) { let (code, message) = failure_to_rpc_error(&mode); return Err(format!("RPC error {}: {}", code, message)); @@ -1238,7 +1238,10 @@ mod tests { let restored = NetworkSimulator::load_from_file(&path, 17).unwrap(); assert_eq!(restored.current_ledger(), sim.current_ledger()); - assert_eq!(restored.get_account(&account.public_key).unwrap().balance, 500.0); + assert_eq!( + restored.get_account(&account.public_key).unwrap().balance, + 500.0 + ); assert_eq!( restored.read_contract_storage("C_FIXED", "key"), Some(&"value".to_string()) diff --git a/src/utils/soroban.rs b/src/utils/soroban.rs index a8426a4a..2c50e8d5 100644 --- a/src/utils/soroban.rs +++ b/src/utils/soroban.rs @@ -28,8 +28,7 @@ fn build_http_client(timeout: Duration) -> Client { crate::utils::http_client::client_with_timeout(timeout) } -static HTTP_CLIENT: Lazy = - Lazy::new(|| build_http_client(Duration::from_secs(30))); +static HTTP_CLIENT: Lazy = Lazy::new(|| build_http_client(Duration::from_secs(30))); /// Global RPC budget manager (thread-safe for concurrent access). static RPC_BUDGET_MANAGER: Lazy> = @@ -65,7 +64,9 @@ pub fn require_feature(feature: ProtocolFeature, protocol_version: u32) -> Resul .map(|(_, minimum)| *minimum) .expect("all protocol features have a minimum version"); let capability = match feature { - ProtocolFeature::ContractConstructor => "contract constructor (create_contract_with_constructor)", + ProtocolFeature::ContractConstructor => { + "contract constructor (create_contract_with_constructor)" + } }; anyhow::bail!( @@ -233,11 +234,11 @@ pub async fn invoke_contract( ) -> Result { let source_wallet = match wallet { Some(wallet) => wallet.clone(), - None => config::load()? - .wallets - .into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("Simulation needs a source wallet; add one with `starforge wallet create`"))?, + None => config::load()?.wallets.into_iter().next().ok_or_else(|| { + anyhow::anyhow!( + "Simulation needs a source wallet; add one with `starforge wallet create`" + ) + })?, }; let mut simulation = simulate_transaction_from( contract_id, @@ -261,9 +262,8 @@ pub async fn invoke_contract( simulation.fee, ) .await?, - ) - } else { - None + ), + None => None, }; Ok(InvokeOutcome { simulation, @@ -281,11 +281,9 @@ pub async fn simulate_transaction( arg_types: &[String], network: &str, ) -> Result { - let wallet = config::load()? - .wallets - .into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("Simulation needs a source wallet; add one with `starforge wallet create`"))?; + let wallet = config::load()?.wallets.into_iter().next().ok_or_else(|| { + anyhow::anyhow!("Simulation needs a source wallet; add one with `starforge wallet create`") + })?; simulate_transaction_from(contract_id, function, args, arg_types, network, &wallet).await } @@ -521,11 +519,8 @@ pub async fn get_protocol_version(network: &str) -> Result { } pub async fn get_protocol_version_for_url(rpc_url: &str) -> Result { - if let Some((cached_at, protocol_version)) = PROTOCOL_VERSION_CACHE - .lock() - .unwrap() - .get(rpc_url) - .copied() + if let Some((cached_at, protocol_version)) = + PROTOCOL_VERSION_CACHE.lock().unwrap().get(rpc_url).copied() { if cached_at.elapsed() < PROTOCOL_VERSION_CACHE_TTL { return Ok(protocol_version); @@ -1635,8 +1630,14 @@ mod tests { .create_async() .await; - assert_eq!(get_protocol_version_for_url(&server.url()).await.unwrap(), 22); - assert_eq!(get_protocol_version_for_url(&server.url()).await.unwrap(), 22); + assert_eq!( + get_protocol_version_for_url(&server.url()).await.unwrap(), + 22 + ); + assert_eq!( + get_protocol_version_for_url(&server.url()).await.unwrap(), + 22 + ); mock.assert_async().await; } diff --git a/src/utils/soroban_auth.rs b/src/utils/soroban_auth.rs index 96bf416b..da533a9d 100644 --- a/src/utils/soroban_auth.rs +++ b/src/utils/soroban_auth.rs @@ -355,10 +355,8 @@ mod tests { #[test] fn exported_bundle_signature_round_trips_and_verifies() { let signing_key = SigningKey::from_bytes(&[19; 32]); - let address = stellar_strkey::ed25519::PublicKey( - signing_key.verifying_key().to_bytes(), - ) - .to_string(); + let address = + stellar_strkey::ed25519::PublicKey(signing_key.verifying_key().to_bytes()).to_string(); let payload = [3; 32]; let signature = signing_key.sign(&payload); let bundle = AuthEntryBundle { @@ -372,9 +370,11 @@ mod tests { signature: Some(hex::encode(signature.to_bytes())), }], }; - let path = std::env::temp_dir().join(format!("starforge-auth-{}.json", uuid::Uuid::new_v4())); + let path = + std::env::temp_dir().join(format!("starforge-auth-{}.json", uuid::Uuid::new_v4())); export_bundle(&bundle, &path).unwrap(); - let imported: AuthEntryBundle = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); + let imported: AuthEntryBundle = + serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); std::fs::remove_file(path).unwrap(); assert_eq!(imported, bundle); } diff --git a/src/utils/tutorial_engine.rs b/src/utils/tutorial_engine.rs index 6c6478fe..afe5db3c 100644 --- a/src/utils/tutorial_engine.rs +++ b/src/utils/tutorial_engine.rs @@ -33,13 +33,19 @@ pub fn verify_checkpoint(checkpoint: &TutorialCheckpoint, working_dir: &Path) -> TutorialCheckpoint::CliInstalled => { let executable = std::env::current_exe()?; if !executable.is_file() { - anyhow::bail!("StarForge executable was not found at {}", executable.display()); + anyhow::bail!( + "StarForge executable was not found at {}", + executable.display() + ); } } TutorialCheckpoint::WalletExists { name } => { let config = crate::utils::config::load()?; if !config.wallets.iter().any(|wallet| wallet.name == *name) { - anyhow::bail!("Wallet '{}' is not saved in the local StarForge configuration", name); + anyhow::bail!( + "Wallet '{}' is not saved in the local StarForge configuration", + name + ); } } TutorialCheckpoint::ContractProjectExists { path } => { @@ -54,14 +60,14 @@ pub fn verify_checkpoint(checkpoint: &TutorialCheckpoint, working_dir: &Path) -> } } TutorialCheckpoint::SimulationRuns => { - use crate::utils::network_simulator::scenarios::{ - BuiltInScenario, ScenarioRunner, - }; + use crate::utils::network_simulator::scenarios::{BuiltInScenario, ScenarioRunner}; let scenario = ScenarioRunner::load_built_in(BuiltInScenario::SimpleCounter, 42); let (_, result) = ScenarioRunner::run(scenario); if result.accounts.is_empty() || result.contracts.is_empty() { - anyhow::bail!("The local simple-counter simulation returned no accounts or contracts"); + anyhow::bail!( + "The local simple-counter simulation returned no accounts or contracts" + ); } } } @@ -299,11 +305,7 @@ mod tests { #[test] fn local_simulation_checkpoint_runs_without_network() { - assert!(verify_checkpoint( - &TutorialCheckpoint::SimulationRuns, - Path::new(".") - ) - .is_ok()); + assert!(verify_checkpoint(&TutorialCheckpoint::SimulationRuns, Path::new(".")).is_ok()); } #[test] diff --git a/src/utils/wallet_signer.rs b/src/utils/wallet_signer.rs index 27856d63..8761a423 100644 --- a/src/utils/wallet_signer.rs +++ b/src/utils/wallet_signer.rs @@ -109,10 +109,7 @@ impl SigningRequest { /// /// Used by the signing agent's keystore to sign without going through the /// StrKey → seed → `SigningKey` round-trip a second time. - pub fn local_secret_raw( - signing_key: &ed25519_dalek::SigningKey, - network: &str, - ) -> Self { + pub fn local_secret_raw(signing_key: &ed25519_dalek::SigningKey, network: &str) -> Self { use stellar_strkey::ed25519::PrivateKey; let secret_str = PrivateKey::from_bytes(signing_key.as_bytes()).to_string(); Self::local_secret(Zeroizing::new(secret_str), network) @@ -980,10 +977,7 @@ pub async fn sign_with_agent_fallback( /// Build a human-readable one-line preview for a signing request. fn build_signing_preview(transaction_xdr: &str, request: &SigningRequest) -> String { - let wallet = request - .wallet_name - .as_deref() - .unwrap_or("(unknown)"); + let wallet = request.wallet_name.as_deref().unwrap_or("(unknown)"); let network = &request.network; match &request.target { SigningTarget::Contract(Some(id)) => { diff --git a/tests/agent_integration.rs b/tests/agent_integration.rs index a9e83133..a7bc7459 100644 --- a/tests/agent_integration.rs +++ b/tests/agent_integration.rs @@ -13,9 +13,9 @@ mod agent_tests { use starforge::agent::client; use starforge::agent::keystore::{seed_to_public_key, stellar_secret_to_seed, KeyStore}; + use starforge::agent::proto::PROTOCOL_VERSION; use starforge::agent::server; use starforge::agent::socket::AgentSocketPath; - use starforge::agent::proto::PROTOCOL_VERSION; // ── Helpers ────────────────────────────────────────────────────────────── @@ -150,7 +150,11 @@ mod agent_tests { // Key should be present immediately. let status = client::get_status().await.unwrap(); - assert_eq!(status.loaded_keys.len(), 1, "key should be present before expiry"); + assert_eq!( + status.loaded_keys.len(), + 1, + "key should be present before expiry" + ); // Wait for expiry + a sweep cycle margin (server sweeps every 30 s, // so we call sweep by restarting the key with TTL 0 which immediately @@ -159,7 +163,10 @@ mod agent_tests { // already covers the sweep path directly). Here we verify the TTL counter: let key_info = &status.loaded_keys[0]; let ttl = key_info.ttl_remaining_secs.unwrap_or(999); - assert!(ttl <= 1, "TTL should be ≤1 s immediately after add, got {ttl}"); + assert!( + ttl <= 1, + "TTL should be ≤1 s immediately after add, got {ttl}" + ); }) .await; } diff --git a/tests/cli_global_flags.rs b/tests/cli_global_flags.rs index 9a65a270..ff30c108 100644 --- a/tests/cli_global_flags.rs +++ b/tests/cli_global_flags.rs @@ -7,11 +7,16 @@ fn generate_inventory_and_check() { let cmd = Cli::command(); let mut report = String::new(); report.push_str("# Flag Inventory Report\n\n"); - + let mut flags_by_name: HashMap> = HashMap::new(); let mut errors = Vec::new(); - fn walk(cmd: &clap::Command, path: &str, flags_by_name: &mut HashMap>, errors: &mut Vec) { + fn walk( + cmd: &clap::Command, + path: &str, + flags_by_name: &mut HashMap>, + errors: &mut Vec, + ) { let current_path = if path.is_empty() { cmd.get_name().to_string() } else { @@ -24,10 +29,13 @@ fn generate_inventory_and_check() { .entry(long.to_string()) .or_default() .push(current_path.clone()); - + match long { "source" => { - errors.push(format!("Command '{}' uses '--source' instead of '--wallet'", current_path)); + errors.push(format!( + "Command '{}' uses '--source' instead of '--wallet'", + current_path + )); } "network" | "wallet" | "yes" | "verbose" | "json" | "dry-run" => { // verify the standard properties @@ -66,6 +74,9 @@ fn generate_inventory_and_check() { if !errors.is_empty() { // Commented out the panic so we can generate the inventory without test failing right now. // In a real PR this would be a panic. - println!("The following flag inconsistencies were found:\n{}", errors.join("\n")); + println!( + "The following flag inconsistencies were found:\n{}", + errors.join("\n") + ); } } diff --git a/tests/cli_migration_notes.rs b/tests/cli_migration_notes.rs new file mode 100644 index 00000000..00c13d91 --- /dev/null +++ b/tests/cli_migration_notes.rs @@ -0,0 +1,282 @@ +//! Integration tests for the CLI migration notes generator. +//! +//! Tests the diff engine and markdown renderer against the canonical fixture +//! breaking-change set stored in `tests/fixtures/migrate_notes/`. +//! +//! Acceptance criteria verified here: +//! - Generator produces correct migration notes for the fixture change set. +//! - Human-review banner is present in every output (unconditional). +//! - Specific changes are detected: command rename, flag rename, flag +//! becoming required, config key rename, config key type change. + +use starforge::utils::cli_migration_notes::{ + diff_snapshots, load_snapshot, render_migration_notes, HUMAN_REVIEW_BANNER, +}; +use std::path::PathBuf; + +fn fixture(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/migrate_notes") + .join(name) +} + +// ── Fixture loading ─────────────────────────────────────────────────────────── + +#[test] +fn fixture_snapshots_load_without_error() { + load_snapshot(&fixture("before.json")).expect("before.json should parse"); + load_snapshot(&fixture("after.json")).expect("after.json should parse"); +} + +// ── Diff engine ─────────────────────────────────────────────────────────────── + +#[test] +fn diff_detects_command_rename() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let found = changes.iter().any(|c| { + matches!(c, + starforge::utils::cli_migration_notes::CliChange::CommandRenamed { old, new } + if old == "network-info" && new == "network show" + ) + }); + assert!( + found, + "Expected CommandRenamed {{ old: 'network-info', new: 'network show' }} in changes.\nGot: {:?}", + changes + ); +} + +#[test] +fn diff_detects_flag_rename() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let found = changes.iter().any(|c| { + matches!(c, + starforge::utils::cli_migration_notes::CliChange::FlagRenamed { command, old_flag, new_flag } + if command == "wallet create" && old_flag == "wallet-name" && new_flag == "name" + ) + }); + assert!( + found, + "Expected FlagRenamed {{ command: 'wallet create', old_flag: 'wallet-name', new_flag: 'name' }}.\nGot: {:?}", + changes + ); +} + +#[test] +fn diff_detects_flag_became_required() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let found = changes.iter().any(|c| { + matches!(c, + starforge::utils::cli_migration_notes::CliChange::FlagBecameRequired { command, flag, .. } + if command == "deploy" && flag == "network" + ) + }); + assert!( + found, + "Expected FlagBecameRequired {{ command: 'deploy', flag: 'network' }}.\nGot: {:?}", + changes + ); +} + +#[test] +fn diff_detects_flag_rename_dry_run_to_simulate() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let found = changes.iter().any(|c| { + matches!(c, + starforge::utils::cli_migration_notes::CliChange::FlagRenamed { command, old_flag, new_flag } + if command == "deploy" && old_flag == "dry-run" && new_flag == "simulate" + ) + }); + assert!( + found, + "Expected FlagRenamed {{ command: 'deploy', old_flag: 'dry-run', new_flag: 'simulate' }}.\nGot: {:?}", + changes + ); +} + +#[test] +fn diff_detects_config_key_rename() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let found = changes.iter().any(|c| { + matches!(c, + starforge::utils::cli_migration_notes::CliChange::ConfigKeyRenamed { old, new } + if old == "network.default_network" && new == "network.default" + ) + }); + assert!( + found, + "Expected ConfigKeyRenamed {{ old: 'network.default_network', new: 'network.default' }}.\nGot: {:?}", + changes + ); +} + +#[test] +fn diff_detects_config_key_type_change() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let found = changes.iter().any(|c| { + matches!(c, + starforge::utils::cli_migration_notes::CliChange::ConfigKeyTypeChanged { key, old_type, new_type } + if key == "deploy.timeout" && old_type == "u32" && new_type == "u64" + ) + }); + assert!( + found, + "Expected ConfigKeyTypeChanged {{ key: 'deploy.timeout', old_type: 'u32', new_type: 'u64' }}.\nGot: {:?}", + changes + ); +} + +// ── Markdown renderer ───────────────────────────────────────────────────────── + +#[test] +fn rendered_notes_always_contain_human_review_banner() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let notes = render_migration_notes(&before.version, &after.version, &changes); + + assert!( + notes.contains(HUMAN_REVIEW_BANNER), + "Migration notes MUST contain the human-review banner but it was absent.\n\ + First 500 chars of notes:\n{}", + ¬es[..notes.len().min(500)] + ); +} + +#[test] +fn banner_present_even_when_no_changes() { + // Diff an identical snapshot against itself — no changes, but banner must still appear. + let before = load_snapshot(&fixture("before.json")).unwrap(); + let changes: Vec = vec![]; + let notes = render_migration_notes(&before.version, &before.version, &changes); + + assert!( + notes.contains(HUMAN_REVIEW_BANNER), + "Banner must be present even when there are zero changes." + ); +} + +#[test] +fn rendered_notes_contain_version_header() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let notes = render_migration_notes(&before.version, &after.version, &changes); + + assert!( + notes.contains("v0.1.0") && notes.contains("v0.2.0"), + "Notes should reference both version strings.\nNotes snippet:\n{}", + ¬es[..notes.len().min(400)] + ); +} + +#[test] +fn rendered_notes_contain_renamed_command() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let notes = render_migration_notes(&before.version, &after.version, &changes); + + assert!( + notes.contains("network-info") && notes.contains("network show"), + "Notes should mention both old and new names for the renamed command." + ); +} + +#[test] +fn rendered_notes_contain_renamed_flag() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let notes = render_migration_notes(&before.version, &after.version, &changes); + + assert!( + notes.contains("wallet-name") && notes.contains("--name"), + "Notes should mention both old and new flag names for wallet create." + ); +} + +#[test] +fn rendered_notes_contain_config_key_rename() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + let notes = render_migration_notes(&before.version, &after.version, &changes); + + assert!( + notes.contains("network.default_network") && notes.contains("network.default"), + "Notes should mention both old and new config key names." + ); +} + +// ── Full fixture test (comprehensive) ──────────────────────────────────────── + +/// This is the primary acceptance test for issue #780: +/// "Generator produces notes for a fixture breaking change set." +#[test] +fn full_fixture_produces_expected_note_sections() { + let before = load_snapshot(&fixture("before.json")).unwrap(); + let after = load_snapshot(&fixture("after.json")).unwrap(); + let changes = diff_snapshots(&before, &after); + + // Must have detected some changes. + assert!( + !changes.is_empty(), + "Expected changes from the fixture pair but found none." + ); + + let notes = render_migration_notes(&before.version, &after.version, &changes); + + // Banner is present (unconditional). + assert!(notes.contains(HUMAN_REVIEW_BANNER), "Banner missing."); + + // Version header present. + assert!(notes.contains("v0.1.0") && notes.contains("v0.2.0")); + + // Renamed commands section. + assert!( + notes.contains("Renamed Commands") || notes.contains("network-info"), + "Expected a Renamed Commands section or reference to 'network-info'." + ); + + // Renamed flags section. + assert!( + notes.contains("Renamed Flags") || notes.contains("wallet-name"), + "Expected a Renamed Flags section." + ); + + // Config key rename section. + assert!( + notes.contains("Config Key Renames") || notes.contains("network.default_network"), + "Expected a Config Key Renames section." + ); + + // Footer present. + assert!( + notes.contains("AI-generated migration notes") || notes.contains("review"), + "Expected footer or review reminder." + ); +} diff --git a/tests/cli_smoke.rs b/tests/cli_smoke.rs index eff6a5cb..1990a858 100644 --- a/tests/cli_smoke.rs +++ b/tests/cli_smoke.rs @@ -844,8 +844,7 @@ fn config_doctor_reports_protocol_failure_without_hiding_rpc_health() { "{stdout}" ); assert!( - stdout.contains("protocol version detection failed") - && stdout.contains("Method not found"), + stdout.contains("protocol version detection failed") && stdout.contains("Method not found"), "{stdout}" ); @@ -955,10 +954,13 @@ fn constructor_deploy_with_protocol_21_fails_before_stellar_cli() { .expect("read fake Stellar CLI metadata") .permissions(); permissions.set_mode(0o755); - std::fs::set_permissions(&stellar_path, permissions) - .expect("make fake Stellar CLI executable"); + std::fs::set_permissions(&stellar_path, permissions).expect("make fake Stellar CLI executable"); let called_path = home.path().join("stellar-called"); - let path = format!("{}:{}", bin_dir.display(), std::env::var("PATH").unwrap_or_default()); + let path = format!( + "{}:{}", + bin_dir.display(), + std::env::var("PATH").unwrap_or_default() + ); let output = starforge(home.path()) .args([ @@ -983,7 +985,10 @@ fn constructor_deploy_with_protocol_21_fails_before_stellar_cli() { assert!(combined.contains("detected protocol 21"), "{combined}"); assert!(combined.contains("protocol 22"), "{combined}"); assert!(combined.contains("constructor"), "{combined}"); - assert!(!called_path.exists(), "Stellar CLI ran despite the protocol gate"); + assert!( + !called_path.exists(), + "Stellar CLI ran despite the protocol gate" + ); version_mock.assert(); } @@ -1273,20 +1278,22 @@ fn onboarding_completes_offline_and_failed_checkpoints_do_not_advance() { "--demo", ], ); - assert_success(&start, "starforge tool tutorial start onboarding-15-minute --demo"); - - let installed = onboarding_command( - home.path(), - working_dir.path(), - &["--version"], + assert_success( + &start, + "starforge tool tutorial start onboarding-15-minute --demo", ); + + let installed = onboarding_command(home.path(), working_dir.path(), &["--version"]); assert_success(&installed, "starforge --version"); let first_next = onboarding_command( home.path(), working_dir.path(), &["tool", "tutorial", "next"], ); - assert_success(&first_next, "starforge tool tutorial next (install checkpoint)"); + assert_success( + &first_next, + "starforge tool tutorial next (install checkpoint)", + ); let missing_wallet = onboarding_command( home.path(), @@ -1308,9 +1315,7 @@ fn onboarding_completes_offline_and_failed_checkpoints_do_not_advance() { &["tool", "tutorial", "status"], ); assert_success(&status, "starforge tool tutorial status"); - assert!( - String::from_utf8_lossy(&status.stdout).contains("step 2 of 4 (1 completed)") - ); + assert!(String::from_utf8_lossy(&status.stdout).contains("step 2 of 4 (1 completed)")); let wallet = onboarding_command( home.path(), @@ -1323,7 +1328,10 @@ fn onboarding_completes_offline_and_failed_checkpoints_do_not_advance() { working_dir.path(), &["tool", "tutorial", "next"], ); - assert_success(&wallet_next, "starforge tool tutorial next (wallet checkpoint)"); + assert_success( + &wallet_next, + "starforge tool tutorial next (wallet checkpoint)", + ); let missing_project = onboarding_command( home.path(), @@ -1359,18 +1367,15 @@ fn onboarding_completes_offline_and_failed_checkpoints_do_not_advance() { working_dir.path(), &["tool", "tutorial", "next"], ); - assert_success(&scaffold_next, "starforge tool tutorial next (scaffold checkpoint)"); + assert_success( + &scaffold_next, + "starforge tool tutorial next (scaffold checkpoint)", + ); let simulation = onboarding_command( home.path(), working_dir.path(), - &[ - "network", - "simulate", - "run", - "--scenario", - "simple-counter", - ], + &["network", "simulate", "run", "--scenario", "simple-counter"], ); assert_success( &simulation, @@ -1381,7 +1386,10 @@ fn onboarding_completes_offline_and_failed_checkpoints_do_not_advance() { working_dir.path(), &["tool", "tutorial", "next"], ); - assert_success(&completed, "starforge tool tutorial next (simulation checkpoint)"); + assert_success( + &completed, + "starforge tool tutorial next (simulation checkpoint)", + ); let completed_output = String::from_utf8_lossy(&completed.stdout); assert!(completed_output.contains("Tutorial complete!")); assert!(completed_output.contains("Elapsed")); diff --git a/tests/contract_id_prediction_test.rs b/tests/contract_id_prediction_test.rs index c17451e7..e858d484 100644 --- a/tests/contract_id_prediction_test.rs +++ b/tests/contract_id_prediction_test.rs @@ -14,7 +14,11 @@ use stellar_xdr::curr::{ fn padded_hex_from_str(hex_str: &str, expected_len: usize) -> Result, String> { let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); if hex_str.len() > expected_len * 2 { - return Err(format!("Hex string too long: expected {} bytes, got {}", expected_len, hex_str.len() / 2)); + return Err(format!( + "Hex string too long: expected {} bytes, got {}", + expected_len, + hex_str.len() / 2 + )); } let padded = format!("{:0>width$}", hex_str, width = expected_len * 2); hex::decode(&padded).map_err(|e| format!("Invalid hex string: {}", e)) @@ -31,7 +35,9 @@ fn derive_contract_id( let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); // Contract ID preimage = Address(deployer_account, salt) - let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(*deployer_public_key))); + let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( + *deployer_public_key, + ))); let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress { address: ScAddress::Account(source_account), salt: Uint256(*salt), @@ -64,7 +70,9 @@ fn derive_contract_id_preimage( ) -> Result { let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); - let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(*deployer_public_key))); + let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( + *deployer_public_key, + ))); let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress { address: ScAddress::Account(source_account), salt: Uint256(*salt), @@ -107,32 +115,33 @@ fn test_contract_id_derivation_matches_stellar_cli_reference() { // Using a known test account: GCKFBEIYTK6W7D4L6K4Q7R7V7W7X7Y7Z7A7B7C7D7E7F7G7H7I7J7K7L7M7N7O // This is just a placeholder - actual test would use a valid StrKey let deployer_public_key: [u8; 32] = [ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, - 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, + 0x1f, 0x20, ]; // Example salt (32 bytes) let salt: [u8; 32] = [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, - 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, - 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, ]; // Test testnet - let contract_id_testnet = derive_contract_id(&deployer_public_key, &salt, TESTNET_PASSPHRASE).unwrap(); + let contract_id_testnet = + derive_contract_id(&deployer_public_key, &salt, TESTNET_PASSPHRASE).unwrap(); assert!(contract_id_testnet.starts_with('C')); assert_eq!(contract_id_testnet.len(), 56); // Test mainnet - let contract_id_mainnet = derive_contract_id(&deployer_public_key, &salt, MAINNET_PASSPHRASE).unwrap(); + let contract_id_mainnet = + derive_contract_id(&deployer_public_key, &salt, MAINNET_PASSPHRASE).unwrap(); assert!(contract_id_mainnet.starts_with('C')); assert_eq!(contract_id_mainnet.len(), 56); // Test futurenet - let contract_id_futurenet = derive_contract_id(&deployer_public_key, &salt, FUTURENET_PASSPHRASE).unwrap(); + let contract_id_futurenet = + derive_contract_id(&deployer_public_key, &salt, FUTURENET_PASSPHRASE).unwrap(); assert!(contract_id_futurenet.starts_with('C')); assert_eq!(contract_id_futurenet.len(), 56); @@ -145,7 +154,7 @@ fn test_contract_id_derivation_matches_stellar_cli_reference() { #[test] fn test_contract_id_is_deterministic() { const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015"; - + let deployer_public_key: [u8; 32] = [0x42; 32]; let salt: [u8; 32] = [0x24; 32]; @@ -158,7 +167,7 @@ fn test_contract_id_is_deterministic() { #[test] fn test_different_salt_produces_different_id() { const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015"; - + let deployer_public_key: [u8; 32] = [0x42; 32]; let salt1: [u8; 32] = [0x00; 32]; let salt2: [u8; 32] = [0x01; 32]; @@ -166,13 +175,16 @@ fn test_different_salt_produces_different_id() { let id1 = derive_contract_id(&deployer_public_key, &salt1, TESTNET_PASSPHRASE).unwrap(); let id2 = derive_contract_id(&deployer_public_key, &salt2, TESTNET_PASSPHRASE).unwrap(); - assert_ne!(id1, id2, "Different salts must produce different contract IDs"); + assert_ne!( + id1, id2, + "Different salts must produce different contract IDs" + ); } #[test] fn test_different_deployer_produces_different_id() { const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015"; - + let deployer1: [u8; 32] = [0x01; 32]; let deployer2: [u8; 32] = [0x02; 32]; let salt: [u8; 32] = [0x00; 32]; @@ -180,7 +192,10 @@ fn test_different_deployer_produces_different_id() { let id1 = derive_contract_id(&deployer1, &salt, TESTNET_PASSPHRASE).unwrap(); let id2 = derive_contract_id(&deployer2, &salt, TESTNET_PASSPHRASE).unwrap(); - assert_ne!(id1, id2, "Different deployers must produce different contract IDs"); + assert_ne!( + id1, id2, + "Different deployers must produce different contract IDs" + ); } #[test] @@ -202,12 +217,13 @@ fn test_salt_parsing() { #[test] fn test_preimage_components() { const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015"; - + let deployer_public_key: [u8; 32] = [0x42; 32]; let salt: [u8; 32] = [0x24; 32]; - let components = derive_contract_id_preimage(&deployer_public_key, &salt, TESTNET_PASSPHRASE).unwrap(); - + let components = + derive_contract_id_preimage(&deployer_public_key, &salt, TESTNET_PASSPHRASE).unwrap(); + assert_eq!(components.network_passphrase, TESTNET_PASSPHRASE); assert_eq!(components.salt_hex, "24".repeat(32)); // 32 bytes of 0x24 assert!(!components.deployer_address.is_empty()); @@ -219,12 +235,12 @@ fn test_preimage_components() { #[test] fn test_zero_salt_and_deployer() { const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015"; - + let deployer: [u8; 32] = [0x00; 32]; let salt: [u8; 32] = [0x00; 32]; let contract_id = derive_contract_id(&deployer, &salt, TESTNET_PASSPHRASE).unwrap(); - + assert!(contract_id.starts_with('C')); assert_eq!(contract_id.len(), 56); // The result should be deterministic @@ -240,16 +256,16 @@ fn test_matches_stellar_cli_known_vectors() { // TODO: Add known test vectors from stellar-cli // For now, verify the structure is correct const TESTNET_PASSPHRASE: &str = "Test SDF Network ; September 2015"; - + let deployer_public_key: [u8; 32] = [0x01; 32]; let salt: [u8; 32] = [0x00; 32]; let contract_id = derive_contract_id(&deployer_public_key, &salt, TESTNET_PASSPHRASE).unwrap(); - + // Verify it's a valid StrKey contract ID let parsed = Contract::from_string(&contract_id).expect("Should be a valid contract StrKey"); assert_eq!(parsed.0.len(), 32); - + // The first character should be 'C' assert_eq!(contract_id.chars().next(), Some('C')); } @@ -267,4 +283,4 @@ fn test_salt_padding() { assert_eq!(padded[31], 0xef); // First 28 bytes should be zeros assert!(padded[..28].iter().all(|&b| b == 0)); -} \ No newline at end of file +} diff --git a/tests/fixtures/migrate_notes/after.json b/tests/fixtures/migrate_notes/after.json new file mode 100644 index 00000000..45681c5b --- /dev/null +++ b/tests/fixtures/migrate_notes/after.json @@ -0,0 +1,88 @@ +{ + "version": "v0.2.0", + "commands": [ + { + "command": "wallet create", + "description": "Create a new test wallet", + "flags": [ + { + "name": "name", + "short": "n", + "required": true, + "default": null, + "description": "Wallet name" + }, + { + "name": "network", + "short": null, + "required": false, + "default": "testnet", + "description": "Target network" + } + ] + }, + { + "command": "wallet list", + "description": "List all local wallets", + "flags": [] + }, + { + "command": "deploy", + "description": "Deploy a compiled Soroban contract (.wasm)", + "flags": [ + { + "name": "wasm", + "short": null, + "required": true, + "default": null, + "description": "Path to the compiled .wasm file" + }, + { + "name": "network", + "short": null, + "required": true, + "default": null, + "description": "Target network" + }, + { + "name": "simulate", + "short": null, + "required": false, + "default": null, + "description": "Simulate deployment without submitting" + } + ] + }, + { + "command": "network show", + "description": "Show active network configuration", + "flags": [] + } + ], + "config_keys": [ + { + "key": "network.default", + "value_type": "string", + "default": "testnet", + "description": "Default network for all commands" + }, + { + "key": "telemetry.enabled", + "value_type": "bool", + "default": "true", + "description": "Whether to send anonymous usage telemetry" + }, + { + "key": "ai.model", + "value_type": "string", + "default": "codellama:7b", + "description": "Default Ollama model for AI commands" + }, + { + "key": "deploy.timeout", + "value_type": "u64", + "default": "30", + "description": "Deployment timeout in seconds" + } + ] +} diff --git a/tests/fixtures/migrate_notes/before.json b/tests/fixtures/migrate_notes/before.json new file mode 100644 index 00000000..d8114c34 --- /dev/null +++ b/tests/fixtures/migrate_notes/before.json @@ -0,0 +1,88 @@ +{ + "version": "v0.1.0", + "commands": [ + { + "command": "wallet create", + "description": "Create a new test wallet", + "flags": [ + { + "name": "wallet-name", + "short": "n", + "required": true, + "default": null, + "description": "Wallet name" + }, + { + "name": "network", + "short": null, + "required": false, + "default": "testnet", + "description": "Target network" + } + ] + }, + { + "command": "wallet list", + "description": "List all local wallets", + "flags": [] + }, + { + "command": "deploy", + "description": "Deploy a compiled Soroban contract (.wasm)", + "flags": [ + { + "name": "wasm", + "short": null, + "required": true, + "default": null, + "description": "Path to the compiled .wasm file" + }, + { + "name": "network", + "short": null, + "required": false, + "default": "testnet", + "description": "Target network" + }, + { + "name": "dry-run", + "short": null, + "required": false, + "default": null, + "description": "Simulate deployment without submitting" + } + ] + }, + { + "command": "network-info", + "description": "Show active network configuration", + "flags": [] + } + ], + "config_keys": [ + { + "key": "network.default_network", + "value_type": "string", + "default": "testnet", + "description": "Default network for all commands" + }, + { + "key": "telemetry.enabled", + "value_type": "bool", + "default": "true", + "description": "Whether to send anonymous usage telemetry" + }, + { + "key": "ai.model", + "value_type": "string", + "default": "codellama:7b", + "description": "Default Ollama model for AI commands" + }, + { + "key": "deploy.timeout", + "value_type": "u32", + "default": "30", + "description": "Deployment timeout in seconds" + } + ] +} diff --git a/tests/http_client.rs b/tests/http_client.rs index ce91a035..e4fae8f3 100644 --- a/tests/http_client.rs +++ b/tests/http_client.rs @@ -50,10 +50,7 @@ impl TestServer { let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) .await .expect("bind the test server"); - let port = listener - .local_addr() - .expect("test server address") - .port(); + let port = listener.local_addr().expect("test server address").port(); let requests = Arc::new(Mutex::new(Vec::new())); let recorded = Arc::clone(&requests); @@ -156,9 +153,10 @@ async fn the_shared_client_reaches_a_local_server_and_identifies_itself() { head[0].lines().next().unwrap_or_default() ); assert!( - head[0] - .to_ascii_lowercase() - .contains(&format!("user-agent: {}", user_agent().to_ascii_lowercase())), + head[0].to_ascii_lowercase().contains(&format!( + "user-agent: {}", + user_agent().to_ascii_lowercase() + )), "the factory must set the starforge user agent: {head:?}" ); } @@ -273,10 +271,7 @@ async fn a_short_timeout_trips_before_a_slow_server_answers() { "the timeout must be honoured, took {:?}", started.elapsed() ); - assert!( - error.is_timeout(), - "expected a timeout error, got: {error}" - ); + assert!(error.is_timeout(), "expected a timeout error, got: {error}"); } // ── Custom CA bundle ──────────────────────────────────────────────────────── @@ -531,7 +526,12 @@ fn collect_offenders(directory: &Path, factory: &Path, offenders: &mut Vec