commit f575ad372228029c2d24da3f126da29b3a272f0a Author: Kayos Date: Wed May 13 09:04:28 2026 -0700 scaffold v0.1: postgres+pgvector inside-container, schema, markdown ingest, CLI 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 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, + }) +}