From f575ad372228029c2d24da3f126da29b3a272f0a Mon Sep 17 00:00:00 2001 From: Kayos Date: Wed, 13 May 2026 09:04:28 -0700 Subject: [PATCH] scaffold v0.1: postgres+pgvector inside-container, schema, markdown ingest, CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skald is a generic story-writer. The database is the product; the binary is the tooling. Everything story-specific lives in rows, not in code. cwho's monorepo + binary-per-role pattern transplanted to this domain. What this commit ships: - Cargo workspace (resolver=3, edition 2024): skald-core (lib) + skald (bin) - Migration 0001: stories, characters, canon_facts, chapters, chapter_summaries, passages (vector(1536)), generation_runs, audit_findings, tags. pgvector + pg_trgm extensions. ivfflat index deferred until we have data (post-import the first ~1k passages and add the index). - skald-core::ingest — markdown parser for the cwho/coast-down shape: '# Title' → '## Chapter N — date' headings → '# Continuity Bible' section with character roster (real + fictional sub-sections) + setting / mystery / historical / liberty / hook sub-sections. Decomposed into structured rows; original bullet body preserved in key_facts/body fields for fidelity. 6 unit tests cover the shape. - skald-core::db — Postgres connection pool + migration runner. - skald-core::models — row types via sqlx::FromRow. - skald binary — clap CLI: 'serve' (http + migrations) and 'import-markdown' (one-shot ingest). - Dockerfile — multi-stage: rust:1.95-bookworm builder, pgvector/ pgvector:pg17 runtime, tini under PID 1, custom entrypoint.sh that boots embedded postgres then execs skald serve. - compose.yml — singleton container, postgres data in volume, story corpus mounted read-only at /seed. Decisions locked 2026-05-13: 1. DB in same container 'till we have a real working tool' (cobb) 2. postgres+pgvector (NOT sqlite) — keeps semantic-search story 3. Network-not-socket connection (postgresql://localhost:5432) from day one so future split is config-only, not code-rewrite Not yet wired: - Web UI - clawdforge calls (gen → cleanup → canon-audit pipeline) - Embedding pass - TTS sidecar --- .gitignore | 8 + Cargo.lock | 2735 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 32 + Dockerfile | 59 + README.md | 84 ++ compose.yml | 37 + entrypoint.sh | 37 + migrations/0001_init.sql | 189 +++ skald-core/Cargo.toml | 23 + skald-core/src/db.rs | 22 + skald-core/src/ingest.rs | 510 +++++++ skald-core/src/lib.rs | 13 + skald-core/src/models.rs | 75 ++ skald/Cargo.toml | 28 + skald/src/import.rs | 38 + skald/src/main.rs | 85 ++ skald/src/serve.rs | 90 ++ 17 files changed, 4065 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 compose.yml create mode 100644 entrypoint.sh create mode 100644 migrations/0001_init.sql create mode 100644 skald-core/Cargo.toml create mode 100644 skald-core/src/db.rs create mode 100644 skald-core/src/ingest.rs create mode 100644 skald-core/src/lib.rs create mode 100644 skald-core/src/models.rs create mode 100644 skald/Cargo.toml create mode 100644 skald/src/import.rs create mode 100644 skald/src/main.rs create mode 100644 skald/src/serve.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fc9a35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target/ +*.swp +*.swo +.DS_Store +.env +*.env.local +.idea/ +.vscode/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a6a5981 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2735 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[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 = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.5", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "maud" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "skald" +version = "0.0.1" +dependencies = [ + "anyhow", + "axum", + "chrono", + "clap", + "maud", + "serde", + "serde_json", + "skald-core", + "sqlx", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "skald-core" +version = "0.0.1" +dependencies = [ + "anyhow", + "chrono", + "regex", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +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", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "chrono", + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "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_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9c1cd05 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] +resolver = "3" +members = ["skald-core", "skald"] + +[workspace.package] +version = "0.0.1" +edition = "2024" +license = "MIT" +authors = ["Cobb (Jacob Hayes)"] +repository = "http://192.168.0.5:3001/cobb/skald" + +[workspace.dependencies] +tokio = { version = "1", features = ["full"] } +axum = "0.8" +tower = "0.5" +tower-http = { version = "0.6", features = ["trace", "limit"] } +sqlx = { version = "0.8", default-features = false, features = [ + "postgres", "runtime-tokio", "tls-rustls", + "chrono", "uuid", "macros", "migrate", +] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +clap = { version = "4", features = ["derive", "env"] } +anyhow = "1" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "chrono"] } +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["v4", "serde"] } +regex = "1" +async-trait = "0.1" +maud = "0.27" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f34f0df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +# Multi-stage build for skald. +# +# Stage 1: compile the rust binary against rust:1-bookworm. +# Stage 2: pgvector/pgvector:pg17 (debian-bookworm postgres with +# pgvector preinstalled) + tini + the skald binary. +# +# v0.1 ships postgres inside the same container ("singleton till we +# have a real working tool"). When we extract the DB out, swap the +# runtime base to debian:bookworm-slim, drop entrypoint.sh, point +# DATABASE_URL at the external pg. +# +# Build context is the workspace root: +# docker build -t skald:latest . + +# ─── builder ────────────────────────────────────────────────────── +FROM rust:1.95-bookworm AS builder +WORKDIR /build + +# Cache the dependency graph: copy manifests first, fetch + build +# stubs, THEN drop in real sources. +COPY Cargo.toml Cargo.lock ./ +COPY skald-core/Cargo.toml skald-core/Cargo.toml +COPY skald/Cargo.toml skald/Cargo.toml +COPY migrations migrations + +RUN mkdir -p skald-core/src skald/src \ + && echo 'pub fn placeholder() {}' > skald-core/src/lib.rs \ + && echo 'fn main() {}' > skald/src/main.rs \ + && cargo build --release -p skald \ + && rm -rf skald-core/src skald/src + +COPY skald-core skald-core +COPY skald skald + +RUN touch skald-core/src/lib.rs skald/src/main.rs \ + && cargo build --release -p skald + +# ─── runtime ────────────────────────────────────────────────────── +FROM pgvector/pgvector:pg17 AS runtime + +# tini for sane signal handling / zombie reaping under PID 1. +RUN apt-get update \ + && apt-get install -y --no-install-recommends tini ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /build/target/release/skald /usr/local/bin/skald +COPY --from=builder /build/migrations /var/lib/skald/migrations +COPY entrypoint.sh /usr/local/bin/skald-entrypoint.sh +RUN chmod +x /usr/local/bin/skald-entrypoint.sh + +ENV RUST_LOG=info \ + SKALD_LISTEN=0.0.0.0:7780 \ + POSTGRES_USER=skald \ + POSTGRES_DB=skald + +EXPOSE 7780 + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/skald-entrypoint.sh"] +CMD ["serve"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b16cd5 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# skald + +Long-form story-writer with canon-keeping, sequel continuity, and +(future) self-hosted audiobook narration. Database is the source of +truth — the writer is the tooling. + +Named for the Old Norse poets who composed and memorized kings' +sagas across generations. + +## Status: v0.1 — scaffold + +What's wired: + +- Rust workspace (`skald-core` + `skald`) +- Postgres schema for stories, characters, canon facts, chapters, + passages, generation runs, audit findings, tags +- pgvector extension installed for future similarity search +- `skald import-markdown` ingests a story file (chapters + bible) + into the schema +- `skald serve` exposes `/health` and runs migrations on boot +- Single-container deploy: postgres + skald in one image + +Not yet wired: + +- Web UI (the inbox + browse + queue surface) +- clawdforge calls (the actual generate / cleanup / canon-audit + pipeline) +- Embeddings + similarity search +- TTS sidecar + +## v0.1 smoke + +```bash +docker compose -p skald up -d +docker exec skald skald import-markdown \ + --path /seed/coast-down.md \ + --title "The Coast-Down" + +curl http://lucy:7780/health +# → { ok: true, db_ok: true, story_count: 1, ... } +``` + +## Schema (cheat sheet) + +``` +stories → meta + status + parent/root for series +characters → real or fictional, story-scoped +canon_facts → setting, mystery, theme, rule, historical_anchor, hook +chapters → full prose body +chapter_summaries → short summaries for cheap context loading +passages → paragraph-level + embedding vector(1536) +generation_runs → every LLM call logged +audit_findings → canon audit output (severity + area) +tags → arbitrary labels +``` + +## Architecture (v0.1 + the plan) + +``` +┌─────────────────────────────────┐ +│ skald container │ +│ ┌───────────┐ ┌────────────┐ │ +│ │ postgres │ │ skald-rust │ │ +│ │ pgvector │←─│ axum + cli │ │ +│ │ localhost │ │ :7780 │ │ +│ └───────────┘ └─────┬──────┘ │ +└─────────────────────────┼────────┘ + │ HTTP (future) + ↓ + ┌──────────┐ + │clawdforge│ + └─────┬────┘ + ↓ + opus calls +``` + +v1.0+: extract postgres to its own container on db-net. skald +becomes pure stateless rust, connects via `DATABASE_URL`. Migration +is a connection-string change + a network move; the binary doesn't +care where the DB lives. + +## License + +MIT. diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..b976b50 --- /dev/null +++ b/compose.yml @@ -0,0 +1,37 @@ +# Standalone compose stack for skald v0.1. Postgres lives in the +# same container — single deployable unit "till we have a real +# working tool" (cobb's call, 2026-05-13). +# +# To deploy on Lucy: +# sudo mkdir -p /mnt/cache/appdata/skald/{pgdata,seed} +# sudo cp .md /mnt/cache/appdata/skald/seed/ +# sudo cp skald.env /mnt/cache/appdata/secrets/skald.env # POSTGRES_PASSWORD=... +# docker compose -p skald up -d +# +# To import the first story: +# docker exec skald skald import-markdown \ +# --path /seed/.md \ +# --title "" + +services: + skald: + image: lucy-registry:5000/skald:latest + container_name: skald + restart: unless-stopped + ports: + - "7780:7780" + env_file: + - /mnt/cache/appdata/secrets/skald.env + volumes: + # Postgres data — persist across container recreates. + - /mnt/cache/appdata/skald/pgdata:/var/lib/postgresql/data + # Markdown corpus to import via `docker exec skald skald import-markdown`. + - /mnt/cache/appdata/skald/seed:/seed:ro + environment: + RUST_LOG: ${RUST_LOG:-info} + SKALD_LOG_FORMAT: json + labels: + org.sulkta.domain: "sulkta" + org.sulkta.owner: "cobb" + org.sulkta.managed-by: "compose" + org.sulkta.role: "skald" diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..2c6ca85 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Skald container entrypoint. +# +# Boots the embedded postgres via the pgvector image's own +# docker-entrypoint, waits for it to accept connections, then execs +# `skald` in the foreground. Tini is PID 1 (so it can reap zombies + +# forward signals); we are PID 2; postgres becomes our child. +# +# This is explicitly "DB in the same container, for now" — when we +# split the DB out (see project notes), the entrypoint reduces to +# `exec /usr/local/bin/skald "$@"` and the pg startup goes away. + +set -eo pipefail + +# Hand off to the pgvector image's own initdb + start dance. +/usr/local/bin/docker-entrypoint.sh postgres & +PG_PID=$! + +# Wait for postgres to accept connections — initdb-on-first-run can +# take a few seconds. 60s cap so we don't hang forever. +for i in $(seq 1 120); do + if pg_isready -h localhost -p 5432 -U "${POSTGRES_USER:-skald}" -d "${POSTGRES_DB:-skald}" >/dev/null 2>&1; then + echo "skald-entrypoint: postgres ready after ${i} polls" + break + fi + if [ "$i" -eq 120 ]; then + echo "skald-entrypoint: postgres failed to become ready after 60s" >&2 + kill "$PG_PID" 2>/dev/null || true + exit 1 + fi + sleep 0.5 +done + +# Exec skald in the foreground. Container's lifecycle now tracks +# skald — if skald exits, the container exits, postgres comes down +# with it, restart policy decides whether to recycle. +exec /usr/local/bin/skald "$@" diff --git a/migrations/0001_init.sql b/migrations/0001_init.sql new file mode 100644 index 0000000..65264b6 --- /dev/null +++ b/migrations/0001_init.sql @@ -0,0 +1,189 @@ +-- Skald v0.1 schema. Database is the source of truth; the writer is +-- generic tooling that knows nothing hardcoded about any specific +-- story. Every story is rows. +-- +-- pgvector for embedding-based callback search across past prose; +-- pg_trgm for fuzzy character-name lookups. + +CREATE EXTENSION IF NOT EXISTS vector; +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- One row per story (or per sequel). parent_story_id chains a +-- series; root_story_id is the head of the chain (denormalized for +-- cheap series scans). +CREATE TABLE stories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'seed' + CHECK (status IN ( + 'seed', 'draft', 'generating', 'cleaning', + 'auditing', 'complete', 'failed' + )), + prompt TEXT, + model TEXT, + parent_story_id UUID REFERENCES stories(id) ON DELETE SET NULL, + root_story_id UUID REFERENCES stories(id) ON DELETE SET NULL, + series_name TEXT, + word_count_target INTEGER, + word_count_actual INTEGER NOT NULL DEFAULT 0, + summary TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_stories_parent ON stories(parent_story_id); +CREATE INDEX idx_stories_root ON stories(root_story_id); +CREATE INDEX idx_stories_status ON stories(status); +CREATE INDEX idx_stories_series ON stories(series_name) WHERE series_name IS NOT NULL; + +-- Characters: real (historical) or fictional. The bible blob is +-- decomposed enough to be searchable but the original prose blob +-- stays in key_facts for full fidelity. +CREATE TABLE characters ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + story_id UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE, + name TEXT NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('real', 'fictional')), + role TEXT, + voice_traits TEXT, + key_facts TEXT NOT NULL, + aliases TEXT[] NOT NULL DEFAULT '{}', + first_seen_chapter INTEGER, + state_at_latest TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_characters_story ON characters(story_id); +CREATE INDEX idx_characters_name_trgm ON characters USING gin (name gin_trgm_ops); +CREATE INDEX idx_characters_story_kind ON characters(story_id, kind); + +-- Canon facts: everything that's bible-shaped but not a character. +-- Setting details, mystery threads, themes, rules, historical +-- anchors, fictional liberties, suggested hooks for sequels. +CREATE TABLE canon_facts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + story_id UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE, + category TEXT NOT NULL CHECK (category IN ( + 'setting', 'event', 'rule', 'theme', + 'mystery', 'liberty', 'hook', 'historical_anchor' + )), + title TEXT NOT NULL, + body TEXT NOT NULL, + weight INTEGER NOT NULL DEFAULT 1, + source_chapter INTEGER, + resolved BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_canon_facts_story_category ON canon_facts(story_id, category); + +-- Chapters: full prose body, stored in DB (markdown). One row per +-- chapter; UNIQUE(story_id, n) prevents duplicate insertion. +CREATE TABLE chapters ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + story_id UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE, + n INTEGER NOT NULL, + title TEXT, + body_md TEXT NOT NULL, + word_count INTEGER NOT NULL DEFAULT 0, + generated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (story_id, n) +); + +CREATE INDEX idx_chapters_story ON chapters(story_id); + +-- Per-chapter short summary. The writer pulls these instead of full +-- chapter prose when assembling context for a sequel — much cheaper +-- on tokens. Generated by a separate LLM pass after the chapter is +-- finished. +CREATE TABLE chapter_summaries ( + chapter_id UUID PRIMARY KEY REFERENCES chapters(id) ON DELETE CASCADE, + body TEXT NOT NULL, + generated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Passages: paragraph-level prose with embedding vectors for +-- similarity search. Embeddings nullable so v0.1 import doesn't +-- require an embedding pass — we fill them in lazily when we +-- actually need semantic recall. +CREATE TABLE passages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chapter_id UUID NOT NULL REFERENCES chapters(id) ON DELETE CASCADE, + paragraph_n INTEGER NOT NULL, + body TEXT NOT NULL, + embedding vector(1536), + embedded_at TIMESTAMPTZ, + UNIQUE (chapter_id, paragraph_n) +); + +CREATE INDEX idx_passages_chapter ON passages(chapter_id); +-- ivfflat index on `embedding` is deferred until we have data — +-- ivfflat requires training rows to build, and an empty-table +-- index degrades query plans. Add after first ~1k passages. + +-- Every LLM call we make is logged. Useful for cost tracking, +-- forensics, "why is this chapter weird?" investigations. +CREATE TABLE generation_runs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + story_id UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE, + kind TEXT NOT NULL CHECK (kind IN ( + 'gen', 'cleanup', 'audit', + 'summary', 'embed' + )), + clawdforge_session_id TEXT, + tokens_in INTEGER, + tokens_out INTEGER, + cost_estimate_cents INTEGER, + started_at TIMESTAMPTZ NOT NULL DEFAULT now(), + ended_at TIMESTAMPTZ, + status TEXT NOT NULL DEFAULT 'running' + CHECK (status IN ('running', 'succeeded', 'failed')), + error TEXT +); + +CREATE INDEX idx_generation_runs_story ON generation_runs(story_id); +CREATE INDEX idx_generation_runs_kind ON generation_runs(kind); + +-- Canon audit findings. Third-Opus reads parent + sequel + bible +-- and flags any continuity drift, character voice shift, retconned +-- facts, timeline contradictions. +CREATE TABLE audit_findings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + story_id UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE, + run_id UUID REFERENCES generation_runs(id) ON DELETE SET NULL, + severity TEXT NOT NULL CHECK (severity IN ('info', 'warn', 'crit')), + area TEXT NOT NULL CHECK (area IN ( + 'character', 'continuity', 'tone', + 'fact', 'timeline', 'other' + )), + body TEXT NOT NULL, + resolved BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_audit_findings_story ON audit_findings(story_id); + +-- Arbitrary user-applied labels. Genre, mood, status filters, etc. +CREATE TABLE tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + story_id UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE, + name TEXT NOT NULL, + UNIQUE (story_id, name) +); + +CREATE INDEX idx_tags_story ON tags(story_id); + +-- Auto-touch stories.updated_at whenever anything changes on the +-- story row itself. Cascade-only — not triggered by child writes. +CREATE OR REPLACE FUNCTION touch_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER stories_updated_at + BEFORE UPDATE ON stories + FOR EACH ROW + EXECUTE FUNCTION touch_updated_at(); diff --git a/skald-core/Cargo.toml b/skald-core/Cargo.toml new file mode 100644 index 0000000..81695bb --- /dev/null +++ b/skald-core/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "skald-core" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +description = "Skald's shared lib: db models, schema migrations, markdown ingest, context assembly." + +[dependencies] +tokio = { workspace = true } +sqlx = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +regex = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/skald-core/src/db.rs b/skald-core/src/db.rs new file mode 100644 index 0000000..2193933 --- /dev/null +++ b/skald-core/src/db.rs @@ -0,0 +1,22 @@ +//! Postgres connection pool helper. + +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use sqlx::{ConnectOptions, PgPool}; +use std::str::FromStr; +use std::time::Duration; + +/// Connect to postgres, run pending migrations, return the pool. +pub async fn connect_and_migrate(url: &str) -> anyhow::Result<PgPool> { + let mut opts = PgConnectOptions::from_str(url)?; + // sqlx logs every query at INFO by default; that's hostile to + // production logs. Pull it down to debug. + opts = opts.log_statements(tracing::log::LevelFilter::Debug); + + let pool = PgPoolOptions::new() + .max_connections(10) + .acquire_timeout(Duration::from_secs(10)) + .connect_with(opts) + .await?; + crate::MIGRATOR.run(&pool).await?; + Ok(pool) +} diff --git a/skald-core/src/ingest.rs b/skald-core/src/ingest.rs new file mode 100644 index 0000000..e8d9c7d --- /dev/null +++ b/skald-core/src/ingest.rs @@ -0,0 +1,510 @@ +//! Parse a long-form story markdown file into the rows we'll store +//! in the database. The parser knows the shape we generated in the +//! 2026-05-13 Coast-Down side-quest (chapters as `## Chapter N — date`, +//! then a `# Continuity Bible` section with structured subsections), +//! but isn't story-specific — any markdown that follows that shape +//! parses cleanly. Other shapes go through `parse_story_file` and +//! fail loudly so the operator can adjust the doc, not the code. + +use anyhow::{Context, bail}; +use regex::Regex; +use sqlx::PgPool; +use std::path::Path; +use std::sync::OnceLock; +use uuid::Uuid; + +/// What we extract from a story markdown file before touching the +/// database. +#[derive(Debug, Clone)] +pub struct ParsedStory { + pub title: String, + pub chapters: Vec<ParsedChapter>, + pub characters: Vec<ParsedCharacter>, + pub canon_facts: Vec<ParsedFact>, +} + +#[derive(Debug, Clone)] +pub struct ParsedChapter { + pub n: i32, + pub title: Option<String>, + pub body: String, + pub paragraphs: Vec<String>, +} + +#[derive(Debug, Clone)] +pub struct ParsedCharacter { + pub name: String, + pub kind: CharacterKind, + pub key_facts: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CharacterKind { + Real, + Fictional, +} + +impl CharacterKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Real => "real", + Self::Fictional => "fictional", + } + } +} + +#[derive(Debug, Clone)] +pub struct ParsedFact { + pub category: FactCategory, + pub title: String, + pub body: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FactCategory { + Setting, + Mystery, + HistoricalAnchor, + Liberty, + Hook, + Event, + Rule, + Theme, +} + +impl FactCategory { + pub fn as_str(self) -> &'static str { + match self { + Self::Setting => "setting", + Self::Mystery => "mystery", + Self::HistoricalAnchor => "historical_anchor", + Self::Liberty => "liberty", + Self::Hook => "hook", + Self::Event => "event", + Self::Rule => "rule", + Self::Theme => "theme", + } + } +} + +/// Parse a story markdown file. +pub fn parse_story_file(path: &Path) -> anyhow::Result<ParsedStory> { + let raw = std::fs::read_to_string(path) + .with_context(|| format!("read {}", path.display()))?; + parse_story(&raw) +} + +/// Parse a story markdown string. See module-level docs for the +/// shape it expects. +pub fn parse_story(raw: &str) -> anyhow::Result<ParsedStory> { + let bible_split: Vec<&str> = raw.splitn(2, "\n# Continuity Bible").collect(); + let pre_bible = bible_split[0]; + let bible_body = bible_split.get(1).copied().unwrap_or(""); + + let title = extract_title(pre_bible).context("no title heading found")?; + let chapters = parse_chapters(pre_bible); + let (characters, canon_facts) = parse_bible(bible_body); + + if chapters.is_empty() { + bail!("no chapters parsed — expected `## Chapter N — …` headings"); + } + + Ok(ParsedStory { + title, + chapters, + characters, + canon_facts, + }) +} + +fn extract_title(pre_bible: &str) -> Option<String> { + for line in pre_bible.lines() { + let line = line.trim_end(); + if let Some(rest) = line.strip_prefix("# ") { + let t = rest.trim(); + if !t.is_empty() { + return Some(t.to_string()); + } + } + } + None +} + +fn parse_chapters(pre_bible: &str) -> Vec<ParsedChapter> { + let mut chapters: Vec<ParsedChapter> = Vec::new(); + let mut cur_title: Option<String> = None; + let mut cur_body: Vec<&str> = Vec::new(); + + for line in pre_bible.lines() { + if let Some(rest) = line.strip_prefix("## ") { + // Flush previous chapter. + if cur_title.is_some() { + push_chapter(&mut chapters, cur_title.take(), &cur_body); + cur_body.clear(); + } + cur_title = Some(rest.trim().to_string()); + } else if cur_title.is_some() { + cur_body.push(line); + } + } + if cur_title.is_some() { + push_chapter(&mut chapters, cur_title.take(), &cur_body); + } + chapters +} + +fn push_chapter(out: &mut Vec<ParsedChapter>, title: Option<String>, lines: &[&str]) { + let title = title.unwrap_or_default(); + let body = lines.join("\n").trim().to_string(); + if body.is_empty() { + return; + } + let n = (out.len() + 1) as i32; + let paragraphs = split_paragraphs(&body); + out.push(ParsedChapter { + n, + title: if title.is_empty() { None } else { Some(title) }, + body, + paragraphs, + }); +} + +/// Split a chapter body into paragraphs. Blank-line delimited; `---` +/// (markdown horizontal rule) is treated as a paragraph break and +/// dropped. +fn split_paragraphs(body: &str) -> Vec<String> { + let mut paragraphs: Vec<String> = Vec::new(); + let mut cur: Vec<&str> = Vec::new(); + for line in body.lines() { + let trimmed = line.trim(); + let is_break = trimmed.is_empty() || trimmed == "---" || trimmed == "***"; + if is_break { + if !cur.is_empty() { + paragraphs.push(cur.join("\n").trim().to_string()); + cur.clear(); + } + } else { + cur.push(line); + } + } + if !cur.is_empty() { + paragraphs.push(cur.join("\n").trim().to_string()); + } + paragraphs +} + +fn parse_bible(bible_body: &str) -> (Vec<ParsedCharacter>, Vec<ParsedFact>) { + let mut characters: Vec<ParsedCharacter> = Vec::new(); + let mut canon_facts: Vec<ParsedFact> = Vec::new(); + if bible_body.is_empty() { + return (characters, canon_facts); + } + + // Section boundaries: lines starting with `## ` partition the + // bible into named sections. + let mut cur_section: Option<String> = None; + let mut cur_body: Vec<&str> = Vec::new(); + + for line in bible_body.lines() { + if let Some(rest) = line.strip_prefix("## ") { + flush_bible_section(cur_section.take(), &cur_body, &mut characters, &mut canon_facts); + cur_body.clear(); + cur_section = Some(rest.trim().to_string()); + } else if cur_section.is_some() { + cur_body.push(line); + } + } + flush_bible_section(cur_section, &cur_body, &mut characters, &mut canon_facts); + + (characters, canon_facts) +} + +fn flush_bible_section( + section: Option<String>, + body_lines: &[&str], + characters: &mut Vec<ParsedCharacter>, + canon_facts: &mut Vec<ParsedFact>, +) { + let Some(section) = section else { return }; + let body = body_lines.join("\n").trim().to_string(); + if body.is_empty() { + return; + } + + let lower = section.to_lowercase(); + if lower.starts_with("character roster") { + characters.extend(parse_character_roster(&body)); + } else if let Some(category) = section_to_category(&lower) { + canon_facts.push(ParsedFact { + category, + title: section, + body, + }); + } + // Sections we don't recognize get silently dropped. That's fine + // for v0.1; the operator can re-import after adjusting the doc. +} + +fn section_to_category(lower_title: &str) -> Option<FactCategory> { + if lower_title.starts_with("setting") { + Some(FactCategory::Setting) + } else if lower_title.starts_with("open mystery") || lower_title.starts_with("mystery") { + Some(FactCategory::Mystery) + } else if lower_title.starts_with("verified historical") || lower_title.contains("historical events") { + Some(FactCategory::HistoricalAnchor) + } else if lower_title.starts_with("fictional liberties") || lower_title.starts_with("liberties") { + Some(FactCategory::Liberty) + } else if lower_title.contains("hook") || lower_title.contains("next-chapter") || lower_title.contains("suggested next") { + Some(FactCategory::Hook) + } else { + None + } +} + +fn parse_character_roster(body: &str) -> Vec<ParsedCharacter> { + let mut out: Vec<ParsedCharacter> = Vec::new(); + let mut kind: Option<CharacterKind> = None; + let mut cur_name: Option<String> = None; + let mut cur_body: Vec<String> = Vec::new(); + + fn flush( + cur_name: &mut Option<String>, + cur_body: &mut Vec<String>, + kind: Option<CharacterKind>, + out: &mut Vec<ParsedCharacter>, + ) { + if let (Some(name), Some(kind)) = (cur_name.take(), kind) + && !name.is_empty() + { + let body = cur_body.join(" ").trim().to_string(); + out.push(ParsedCharacter { + name, + kind, + key_facts: body, + }); + } + cur_body.clear(); + } + + for line in body.lines() { + let trimmed = line.trim(); + if let Some(rest) = line.strip_prefix("### ") { + // New sub-section → flush current entry first. + flush(&mut cur_name, &mut cur_body, kind, &mut out); + let s = rest.trim().to_lowercase(); + kind = if s.starts_with("real") { + Some(CharacterKind::Real) + } else if s.starts_with("fictional") { + Some(CharacterKind::Fictional) + } else { + None + }; + } else if let Some(stripped) = trimmed.strip_prefix("- ") { + // New character bullet → flush previous. + flush(&mut cur_name, &mut cur_body, kind, &mut out); + if let Some((name, rest)) = split_bold_name(stripped) { + cur_name = Some(name); + let rest = rest.trim_start_matches([':', '—', '-', ' ']).trim(); + if !rest.is_empty() { + cur_body.push(rest.to_string()); + } + } + } else if !trimmed.is_empty() && cur_name.is_some() { + // Continuation of the current bullet. + cur_body.push(line.trim_start().to_string()); + } + } + flush(&mut cur_name, &mut cur_body, kind, &mut out); + out +} + +/// Extract the **bold** name at the start of a bullet body. +/// Returns (name, rest-of-bullet). +fn split_bold_name(s: &str) -> Option<(String, &str)> { + static RE: OnceLock<Regex> = OnceLock::new(); + let re = RE.get_or_init(|| Regex::new(r"^\*\*(.+?)\*\*\s*(.*)$").unwrap()); + let caps = re.captures(s)?; + let name = caps.get(1)?.as_str().trim().to_string(); + let rest_match = caps.get(2)?; + Some((name, &s[rest_match.start()..rest_match.end()])) +} + +/// Insert a parsed story into the database. Returns the story's id. +pub async fn import_to_db(pool: &PgPool, parsed: ParsedStory) -> anyhow::Result<Uuid> { + let mut tx = pool.begin().await?; + + let total_words: i32 = parsed + .chapters + .iter() + .map(|c| word_count(&c.body)) + .sum(); + + let story_id: Uuid = sqlx::query_scalar( + "INSERT INTO stories (title, status, word_count_actual) + VALUES ($1, 'seed', $2) + RETURNING id", + ) + .bind(&parsed.title) + .bind(total_words) + .fetch_one(&mut *tx) + .await?; + + // root_story_id self-references on the seed row. + sqlx::query("UPDATE stories SET root_story_id = id WHERE id = $1") + .bind(story_id) + .execute(&mut *tx) + .await?; + + for chapter in &parsed.chapters { + let words = word_count(&chapter.body); + let chapter_id: Uuid = sqlx::query_scalar( + "INSERT INTO chapters (story_id, n, title, body_md, word_count) + VALUES ($1, $2, $3, $4, $5) + RETURNING id", + ) + .bind(story_id) + .bind(chapter.n) + .bind(chapter.title.as_deref()) + .bind(&chapter.body) + .bind(words) + .fetch_one(&mut *tx) + .await?; + + for (i, para) in chapter.paragraphs.iter().enumerate() { + sqlx::query( + "INSERT INTO passages (chapter_id, paragraph_n, body) + VALUES ($1, $2, $3)", + ) + .bind(chapter_id) + .bind(i as i32 + 1) + .bind(para) + .execute(&mut *tx) + .await?; + } + } + + for ch in &parsed.characters { + sqlx::query( + "INSERT INTO characters (story_id, name, kind, key_facts) + VALUES ($1, $2, $3, $4)", + ) + .bind(story_id) + .bind(&ch.name) + .bind(ch.kind.as_str()) + .bind(&ch.key_facts) + .execute(&mut *tx) + .await?; + } + + for fact in &parsed.canon_facts { + sqlx::query( + "INSERT INTO canon_facts (story_id, category, title, body) + VALUES ($1, $2, $3, $4)", + ) + .bind(story_id) + .bind(fact.category.as_str()) + .bind(&fact.title) + .bind(&fact.body) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(story_id) +} + +fn word_count(s: &str) -> i32 { + s.split_whitespace().count() as i32 +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &str = r#"# Sample Tale + +## Chapter One — Monday, May 1 + +The morning was bright. The bread was warm. The cat sat in the sun. + +She drank her coffee slowly. + +## Chapter Two — Tuesday, May 2 + +The cat moved to the windowsill. + +She watched the rain. + +# Continuity Bible + +## Character Roster + +### Real historical figures + +- **Anya Petrov** — 34, baker. Real. Husband died in 1985. + Two children. + +### Fictional characters + +- **Boris** — 50, the cat. Black with one white paw. + +## Setting Bible + +A small village in northern Ukraine in May 1985. + +## Open Mystery Threads + +1. Whose footprints in the flour bin? +2. Why does Boris meow at midnight? +"#; + + #[test] + fn parses_title() { + let p = parse_story(SAMPLE).unwrap(); + assert_eq!(p.title, "Sample Tale"); + } + + #[test] + fn parses_chapter_count_and_numbering() { + let p = parse_story(SAMPLE).unwrap(); + assert_eq!(p.chapters.len(), 2); + assert_eq!(p.chapters[0].n, 1); + assert_eq!(p.chapters[1].n, 2); + assert!(p.chapters[0].title.as_deref().unwrap().starts_with("Chapter One")); + } + + #[test] + fn paragraphs_split_on_blank_line_and_hr() { + let p = parse_story(SAMPLE).unwrap(); + // Chapter 1 has 2 paragraphs (the bright-morning one + the + // coffee-drinking one). + assert_eq!(p.chapters[0].paragraphs.len(), 2); + } + + #[test] + fn parses_real_and_fictional_characters() { + let p = parse_story(SAMPLE).unwrap(); + assert_eq!(p.characters.len(), 2); + let anya = p.characters.iter().find(|c| c.name == "Anya Petrov").unwrap(); + assert_eq!(anya.kind, CharacterKind::Real); + assert!(anya.key_facts.contains("baker")); + let boris = p.characters.iter().find(|c| c.name == "Boris").unwrap(); + assert_eq!(boris.kind, CharacterKind::Fictional); + } + + #[test] + fn parses_canon_fact_sections() { + let p = parse_story(SAMPLE).unwrap(); + let setting = p.canon_facts.iter().find(|f| f.category == FactCategory::Setting).unwrap(); + assert!(setting.body.contains("northern Ukraine")); + let mystery = p.canon_facts.iter().find(|f| f.category == FactCategory::Mystery).unwrap(); + assert!(mystery.body.contains("footprints")); + } + + #[test] + fn missing_chapters_errors() { + let bad = "# Title only\n\nSome body text but no chapters."; + let err = parse_story(bad).unwrap_err(); + assert!(err.to_string().contains("no chapters"), "{err}"); + } +} diff --git a/skald-core/src/lib.rs b/skald-core/src/lib.rs new file mode 100644 index 0000000..d0ff16e --- /dev/null +++ b/skald-core/src/lib.rs @@ -0,0 +1,13 @@ +//! Skald's shared kernel. +//! +//! Database schema, row types, markdown ingest, and (later) context +//! assembly for LLM calls. The story-independence rule: nothing in +//! this crate knows about any specific story. Every story is rows. + +pub mod db; +pub mod ingest; +pub mod models; + +/// Embeds the workspace `migrations/` directory at compile time. +/// Run via `MIGRATOR.run(&pool).await` at boot. +pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../migrations"); diff --git a/skald-core/src/models.rs b/skald-core/src/models.rs new file mode 100644 index 0000000..a35af38 --- /dev/null +++ b/skald-core/src/models.rs @@ -0,0 +1,75 @@ +//! Row types. Mirror the schema in `migrations/0001_init.sql`. +//! +//! These are deliberately thin — no business logic. Queries that need +//! to project subsets of fields can use `sqlx::query_as!` against +//! their own narrower types; these full structs are for the cases +//! where we want the whole row. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Story { + pub id: Uuid, + pub title: String, + pub status: String, + pub prompt: Option<String>, + pub model: Option<String>, + pub parent_story_id: Option<Uuid>, + pub root_story_id: Option<Uuid>, + pub series_name: Option<String>, + pub word_count_target: Option<i32>, + pub word_count_actual: i32, + pub summary: Option<String>, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Character { + pub id: Uuid, + pub story_id: Uuid, + pub name: String, + pub kind: String, + pub role: Option<String>, + pub voice_traits: Option<String>, + pub key_facts: String, + pub aliases: Vec<String>, + pub first_seen_chapter: Option<i32>, + pub state_at_latest: Option<String>, + pub created_at: DateTime<Utc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct CanonFact { + pub id: Uuid, + pub story_id: Uuid, + pub category: String, + pub title: String, + pub body: String, + pub weight: i32, + pub source_chapter: Option<i32>, + pub resolved: bool, + pub created_at: DateTime<Utc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Chapter { + pub id: Uuid, + pub story_id: Uuid, + pub n: i32, + pub title: Option<String>, + pub body_md: String, + pub word_count: i32, + pub generated_at: DateTime<Utc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Passage { + pub id: Uuid, + pub chapter_id: Uuid, + pub paragraph_n: i32, + pub body: String, +} diff --git a/skald/Cargo.toml b/skald/Cargo.toml new file mode 100644 index 0000000..ae9e280 --- /dev/null +++ b/skald/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "skald" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +description = "Skald: long-form story-writer with canon-keeping. DB-is-source-of-truth; writer is the tooling." + +[[bin]] +name = "skald" +path = "src/main.rs" + +[dependencies] +skald-core = { path = "../skald-core" } +tokio = { workspace = true } +axum = { workspace = true } +tower-http = { workspace = true } +sqlx = { workspace = true } +clap = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } +maud = { workspace = true } diff --git a/skald/src/import.rs b/skald/src/import.rs new file mode 100644 index 0000000..4b6822f --- /dev/null +++ b/skald/src/import.rs @@ -0,0 +1,38 @@ +//! `skald import-markdown` subcommand. Reads a story markdown file, +//! parses it, writes the result into postgres, prints a one-line +//! summary the operator can copy/paste. + +use std::path::Path; + +use skald_core::db; +use skald_core::ingest; + +pub async fn run(database_url: &str, path: &Path, title_override: Option<&str>) -> anyhow::Result<()> { + tracing::info!(path = %path.display(), "parsing markdown"); + let mut parsed = ingest::parse_story_file(path)?; + + if let Some(t) = title_override { + parsed.title = t.to_string(); + } + + let chapter_count = parsed.chapters.len(); + let paragraph_count: usize = parsed.chapters.iter().map(|c| c.paragraphs.len()).sum(); + let character_count = parsed.characters.len(); + let fact_count = parsed.canon_facts.len(); + + tracing::info!( + title = %parsed.title, + chapters = chapter_count, + paragraphs = paragraph_count, + characters = character_count, + canon_facts = fact_count, + "parsed; connecting to database", + ); + + let pool = db::connect_and_migrate(database_url).await?; + let story_id = ingest::import_to_db(&pool, parsed).await?; + println!( + "imported story {story_id}: {chapter_count} chapters / {paragraph_count} paragraphs / {character_count} characters / {fact_count} canon-facts" + ); + Ok(()) +} diff --git a/skald/src/main.rs b/skald/src/main.rs new file mode 100644 index 0000000..e64f675 --- /dev/null +++ b/skald/src/main.rs @@ -0,0 +1,85 @@ +//! skald — CLI entry point. +//! +//! Two subcommands today: +//! skald serve — boot the http server (v0.1 = /health + migrations) +//! skald import-markdown — ingest a story markdown file into the DB + +mod import; +mod serve; + +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::{Parser, Subcommand}; + +#[derive(Debug, Parser)] +#[command( + name = "skald", + version, + about = "Long-form story-writer. Database is the source of truth; the writer is the tooling." +)] +struct Cli { + /// Postgres connection URL. Defaults to `postgresql://skald:skald@localhost:5432/skald`. + #[arg(long, env = "DATABASE_URL", default_value = "postgresql://skald:skald@localhost:5432/skald")] + database_url: String, + + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Debug, Subcommand)] +enum Cmd { + /// Start the http server. v0.1 exposes /health and runs migrations on boot. + Serve { + #[arg(long, env = "SKALD_LISTEN", default_value = "0.0.0.0:7780")] + listen: String, + }, + /// Ingest a story markdown file into the database. Creates a new + /// `stories` row + chapters + characters + canon_facts. Idempotent + /// only at the title level: re-importing the same file makes a + /// second story row. + ImportMarkdown { + /// Path to the markdown file. + #[arg(long)] + path: PathBuf, + /// Override the title (defaults to the markdown's first `#` heading). + #[arg(long)] + title: Option<String>, + }, +} + +#[tokio::main] +async fn main() -> ExitCode { + init_logging(); + match run().await { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("skald fatal: {e:#}"); + ExitCode::FAILURE + } + } +} + +async fn run() -> anyhow::Result<()> { + let cli = Cli::parse(); + match cli.cmd { + Cmd::Serve { listen } => serve::run(&cli.database_url, &listen).await, + Cmd::ImportMarkdown { path, title } => { + import::run(&cli.database_url, &path, title.as_deref()).await + } + } +} + +fn init_logging() { + use tracing_subscriber::{EnvFilter, fmt, prelude::*, registry}; + + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + let format = std::env::var("SKALD_LOG_FORMAT").unwrap_or_else(|_| "pretty".into()); + + let registry = registry().with(filter); + if format == "json" { + registry.with(fmt::layer().json()).init(); + } else { + registry.with(fmt::layer()).init(); + } +} diff --git a/skald/src/serve.rs b/skald/src/serve.rs new file mode 100644 index 0000000..145accf --- /dev/null +++ b/skald/src/serve.rs @@ -0,0 +1,90 @@ +//! HTTP server (v0.1). +//! +//! Today this is intentionally tiny: connect to postgres, run any +//! pending migrations, expose `/health`, and stay alive. The web +//! GUI + clawdforge wiring lands in v0.2. + +use std::time::Duration; + +use axum::Router; +use axum::extract::State; +use axum::routing::get; +use axum::Json; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use skald_core::db; +use sqlx::PgPool; +use tokio::signal::unix::{SignalKind, signal}; + +#[derive(Clone)] +struct AppState { + pool: PgPool, + started_at: DateTime<Utc>, +} + +pub async fn run(database_url: &str, listen: &str) -> anyhow::Result<()> { + tracing::info!(listen, version = env!("CARGO_PKG_VERSION"), "skald serve starting"); + + let pool = db::connect_and_migrate(database_url).await?; + tracing::info!("database connected, migrations applied"); + + let state = AppState { + pool, + started_at: Utc::now(), + }; + let router = Router::new() + .route("/health", get(health)) + .with_state(state); + + let listener = tokio::net::TcpListener::bind(listen).await?; + tracing::info!(listen, "api listening"); + + let serve = axum::serve(listener, router).with_graceful_shutdown(shutdown()); + match tokio::time::timeout(Duration::from_secs(15), serve).await { + Ok(r) => r?, + Err(_) => tracing::warn!("graceful shutdown timed out after 15s — exiting anyway"), + } + Ok(()) +} + +async fn shutdown() { + let ctrl_c = async { + let _ = tokio::signal::ctrl_c().await; + }; + let term = async { + if let Ok(mut s) = signal(SignalKind::terminate()) { + s.recv().await; + } + }; + tokio::select! { _ = ctrl_c => {}, _ = term => {} } + tracing::info!("shutdown signal received"); +} + +#[derive(Serialize)] +struct Health { + ok: bool, + version: &'static str, + uptime_secs: i64, + db_ok: bool, + story_count: i64, +} + +async fn health(State(state): State<AppState>) -> Json<Health> { + let row: Result<(i64,), _> = sqlx::query_as("SELECT count(*) FROM stories") + .fetch_one(&state.pool) + .await; + let (db_ok, story_count) = match row { + Ok((n,)) => (true, n), + Err(e) => { + tracing::warn!(error = %e, "health: db query failed"); + (false, 0) + } + }; + Json(Health { + ok: db_ok, + version: env!("CARGO_PKG_VERSION"), + uptime_secs: (Utc::now() - state.started_at).num_seconds(), + db_ok, + story_count, + }) +}