diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 6201001..a914dbe 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -125,3 +125,4 @@ {"id":"runledger-y1o","title":"Harden lifecycle timeout recovery after 0.11 review","description":"Resolve comprehensive-review findings structurally: centralize queue timeout policy, bound prestart release recovery, type PostgreSQL lock contention for runtime policy, retry transient heartbeat lock waits within one fixed lease-maintenance deadline, and close PostgreSQL 18 regression gaps. Commit each logical slice separately and finish with the full repository suite.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-08-26T13:03:13.908525Z","created_by":"aa","updated_at":"2026-08-26T13:22:54.908241Z","closed_at":"2026-08-26T13:22:54.907662Z","close_reason":"Implemented and verified structural lifecycle timeout recovery across PostgreSQL and runtime; full workspace tests and Clippy pass on PostgreSQL 18.4.","source_repo":"runledger","source_repo_path":"/Users/aa/Documents/runledger","compaction_level":0,"original_size":0} {"id":"runledger-ze4","title":"Specify durable capacity policy APIs and rollout compatibility","description":"Use docs/research/distributed-capacity-controls.md to freeze the capacity policy, requirement, and enqueue-options contracts. Specify explicit opt-in bindings versus mandatory bindings, missing/paused policy behavior, unit costs, live limit changes, rolling-window admission-time semantics, no refunds, and immutable windows. Trace direct enqueue, intents and promotion, workflow enqueue/append/release/continuation/recovery, direct replay, and schedule firing. Define old snapshot decoding, canonical idempotency comparison, admission UUIDs despite attempt-number reuse, public struct literal compatibility, old-worker lease guards, schema migration copies, and staged activation/rollback. Deliver a contract with representative old/new fixtures; no production implementation. Independent of the protocol prototype; both block the execution plan.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-09-06T14:11:24.100982Z","created_by":"aa","updated_at":"2026-09-06T15:11:59.909674Z","closed_at":"2026-09-06T15:11:59.909542Z","close_reason":"Specified additive options/token contracts, globally scoped unit policies, durable owner membership, all producer/recovery routes, canonical old/new fixtures, and staged activation/rollback in docs/distributed-capacity-plan.md and capacity-contract-fixtures.json. Full-plan refinement continues under runledger-bva; no feature implementation is claimed.","source_repo":"runledger","source_repo_path":"/Users/aa/Documents/runledger","compaction_level":0,"original_size":0} {"id":"runledger-zr5","title":"Validate distributed capacity plan against local consumers","description":"Audit current Runledger consumers under ~/Documents, deduplicate checkouts, trace actual concurrency/rate controls and adoption paths, and update the capacity epic with source-backed consumer-fit findings. Source inspection only unless experiments are necessary; preserve consumer worktrees.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-09-06T18:23:32.746890Z","created_by":"aa","updated_at":"2026-09-06T18:41:00.615263Z","closed_at":"2026-09-06T18:41:00.615074Z","close_reason":"Completed source audit of 16 consumer checkouts deduplicated into six products. Recorded findings and exact local source references in docs/research/distributed-capacity-consumer-audit-2026-09-06.md; updated concurrency adoption criteria and ten epic task descriptions, deferred P17 pending a qualifying exact-admission consumer, and verified all dependency edges/readiness. Source inspection only; no downstream compilation, database run, deployment, or measured code deletion is claimed.","source_repo":"runledger","source_repo_path":"/Users/aa/Documents/runledger","compaction_level":0,"original_size":0} +{"id":"runledger-hcf","title":"Validate actual Cargo foundation without adapter-pin cycles","description":"Consume Cargo dependency source metadata; preserve ordinary-build source and migration checks, ignore unrelated adapter pins, test drift and clean Git consumers; retain session-profile release fixes from b83abe4.","status":"in_progress","priority":1,"issue_type":"task","created_at":"2026-09-21T14:37:20.493529Z","created_by":"aa","updated_at":"2026-09-21T14:42:23.522336Z","source_repo":"runledger","source_repo_path":"/Users/aa/Documents/runledger","compaction_level":0,"original_size":0} diff --git a/Cargo.lock b/Cargo.lock index 8a5342f..8a17970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -592,7 +592,7 @@ dependencies = [ "chrono", "once_cell", "phf", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -2355,7 +2355,9 @@ dependencies = [ "sha2 0.11.0", "sqlx", "static_assertions", + "tempfile", "tokio", + "toml", "tracing", ] @@ -2625,6 +2627,15 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3282,6 +3293,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tonic" version = "0.14.6" @@ -3806,6 +3856,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/README.md b/README.md index cef5745..179e8df 100644 --- a/README.md +++ b/README.md @@ -92,11 +92,25 @@ orchestration in `runtime`, and SQL/state-machine logic in `postgres`. ## Installation This branch is **unpublished coordinated development**. Its Batter dependency -is not a registry release. A sibling checkout is mandatory; from a fresh clone -run `bash scripts/bootstrap-batter.sh`. The pinned revision lives in -`runledger-postgres/batter-revision`; ordinary Cargo builds check the foundation -sources against that pin locally as well as in CI. Do not use the old registry -installation instructions for this branch. +is not a registry release. For workspace development, from a fresh clone run +`bash scripts/bootstrap-batter.sh` to create the paired sibling. The reviewed +revision lives in `runledger-postgres/batter-revision`. Ordinary Cargo builds +verify the actual core and SQLx sources reported by Cargo dependency metadata, +including their inherited workspace manifest settings, against that revision. +Unrelated adapter pins and manifest formatting can change independently. The +bootstrap's `RUNLEDGER_BATTER_SOURCE` selects a clone/check target only; it cannot +override Cargo dependency selection or make a different compiled source pass. +Do not use the old registry installation instructions for this branch. + +Git consumers need no sibling: select an immutable Runledger Git revision and +add the following **consumer workspace-root** patch. `BATTER_REV` must contain +the reviewed foundation (or a descendant with identical foundation inputs). +If the consumer also uses Batter directly, select that same Batter revision. +Cargo does not inherit patches from dependencies. + +The check requires the reviewed ancestor in the actual source checkout's Git +history; shallow development clones must fetch it. It is a source-consistency +guard, not protection against a deliberately modified Cargo build/configuration. For a service next to the paired `runledger` and `batter` checkouts: @@ -114,6 +128,13 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } runledger-test-support = { path = "../runledger/runledger-test-support" } ``` +For Git consumption instead of the paired paths above, add this root patch: + +```toml +[patch."https://github.com/bpcakes/runledger"] +batter-sqlx = { git = "https://github.com/bpcakes/batter", rev = "BATTER_REV" } +``` + These sources require **Rust 1.94+** and **PostgreSQL 18+**. Older PostgreSQL releases are not supported, even when an extension supplies an equivalent `uuidv7()` function. See [PostgreSQL requirements](#postgresql-requirements). diff --git a/docs/evidence/foundation-integration-2026-09-21.md b/docs/evidence/foundation-integration-2026-09-21.md new file mode 100644 index 0000000..65fc642 --- /dev/null +++ b/docs/evidence/foundation-integration-2026-09-21.md @@ -0,0 +1,41 @@ +# Foundation integration across parallel streams + +Owning Bead: `runledger-hcf`. Review base: `b83abe4`. + +Preserves the session-profile release restoration in `b83abe4`. The new paired +Batter foundation combines session profiles and operation-context atomic +completion. Native queue/profile behavior is unchanged by this integration. + +`build.rs` now consumes Cargo dependency metadata from the actual SQLx package, +which forwards its actual core package directory. Both must belong to the same +reviewed source tree. Tracked and untracked foundation input drift is rejected. +Inherited workspace dependency requirements, package settings, lints and resolver +remain checked semantically. Adapter dependencies and comments are not foundation +inputs. The immutable pin and migration-copy check remain enforced on ordinary +Cargo builds; a separately selected clean sibling cannot bless patched code. + +The check does not freeze the application's third-party lockfile, features or +Cargo configuration. As before, consumers own those inputs and must validate +their final graph. Cargo's foundation `links` identities reject duplicate core +or SQLx implementations. All packages remain unpublished. + +Regression coverage includes clean sources, adapter pin changes, inherited +settings drift, tracked and ignored-untracked source drift, split core/SQLx roots, +and independent migration drift. Final executed validation is recorded below. + +Executed locally on macOS, Rust 1.94.1, PostgreSQL 18.6 (`postgres:18`): + +- Five foundation-pin tests; six live database-profile tests. +- All 374 PostgreSQL package tests/doctests. +- Workspace/all-target compilation and PostgreSQL/all-target Clippy. +- README contract and formatting checks. +- A fresh Git-only consumer compiled the actual dependency graph without a + sibling, with `RUNLEDGER_BATTER_SOURCE` deliberately nonexistent. This used + local Git transport and is distinct from hosted GitHub consumer validation. +- Locked standalone consumer compilation and all nine packaged archive smoke + tests. Native review identified its stale independent lockfile; Cargo + regenerated that lockfile and both locked checks then passed. + +The linked-worktree tracker import reported a pre-existing normalization conflict +for `runledger-wtb`. This change preserves that record and includes only the new +owning task's tracker export; no unrelated tracker repair is claimed. diff --git a/runledger-postgres/Cargo.toml b/runledger-postgres/Cargo.toml index e3ca97e..5ceaddf 100644 --- a/runledger-postgres/Cargo.toml +++ b/runledger-postgres/Cargo.toml @@ -33,6 +33,11 @@ tokio = { workspace = true } tracing = { workspace = true } [dev-dependencies] +toml = { version = "1.1.6", default-features = false, features = ["std", "parse", "serde"] } +tempfile = "3" static_assertions = "1.1.0" runledger-test-support = { workspace = true } tokio = { workspace = true, features = ["io-util", "net"] } + +[build-dependencies] +toml = { version = "1.1.6", default-features = false, features = ["std", "parse", "serde"] } diff --git a/runledger-postgres/batter-revision b/runledger-postgres/batter-revision index 7faf230..60865ef 100644 --- a/runledger-postgres/batter-revision +++ b/runledger-postgres/batter-revision @@ -1 +1 @@ -d7728e633b3b767edde8aea40daf358ad4917ea4 +497e41e3241e7972d68a61dc2e19a5dd8694ea2e diff --git a/runledger-postgres/build.rs b/runledger-postgres/build.rs index e854ab7..042c337 100644 --- a/runledger-postgres/build.rs +++ b/runledger-postgres/build.rs @@ -5,15 +5,12 @@ use std::{ error::Error, fs, path::{Path, PathBuf}, - process::Command, }; -const PIN: &str = include_str!("batter-revision"); -const INPUTS: &[&str] = &["Cargo.toml", "crates/batter-core", "crates/batter-sqlx"]; +pub mod foundation_source; fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=batter-revision"); - println!("cargo:rerun-if-env-changed=RUNLEDGER_BATTER_SOURCE"); let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); let vendored_dir = manifest.join("migrations"); let canonical_dir = manifest.join("../migrations"); @@ -22,42 +19,15 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed={}", canonical_dir.display()); enforce_migration_sync(&canonical_dir, &vendored_dir); } - let source = env::var_os("RUNLEDGER_BATTER_SOURCE") - .map_or_else(|| manifest.join("../../batter"), PathBuf::from); - let revision = PIN.trim(); - if revision.len() != 40 || !revision.bytes().all(|c| c.is_ascii_hexdigit()) { - return Err("invalid immutable Batter source revision".into()); - } - for input in INPUTS { - println!("cargo:rerun-if-changed={}", source.join(input).display()); - } - // Accept companion-only descendants, but require identical foundation code, - // manifests and untracked-source state. This avoids circular whole-repo pins. - let mut args = vec!["diff", "--quiet", revision, "--"]; - args.extend_from_slice(INPUTS); - if !git(&source, &args)?.status.success() { - return Err("Batter foundation differs from the coordinated pin; run scripts/bootstrap-batter.sh or deliberately update the reviewed pin".into()); - } - let unknown = git( - &source, - &[ - "ls-files", - "--others", - "--exclude-standard", - "--", - "crates/batter-core", - "crates/batter-sqlx", - ], - )?; - if !unknown.status.success() || !unknown.stdout.is_empty() { - return Err( - "untracked Batter foundation inputs are not part of the pinned source graph".into(), - ); - } + // Cargo's direct dependency metadata describes the code actually compiled. + // An unrelated clean sibling or RUNLEDGER_BATTER_SOURCE cannot bless it. + let sqlx = PathBuf::from(env::var("DEP_BATTER_SQLX_FOUNDATION_SOURCE")?); + let core = PathBuf::from(env::var("DEP_BATTER_SQLX_FOUNDATION_CORE_SOURCE")?); + foundation_source::verify(&sqlx, &core, include_str!("batter-revision").trim())?; Ok(()) } -fn enforce_migration_sync(canonical_dir: &Path, vendored_dir: &Path) { +pub fn enforce_migration_sync(canonical_dir: &Path, vendored_dir: &Path) { let canonical = load_sql_files(canonical_dir); let vendored = load_sql_files(vendored_dir); @@ -93,11 +63,3 @@ fn load_sql_files(dir: &Path) -> BTreeMap { files } - -fn git(source: &Path, args: &[&str]) -> Result { - Command::new("git") - .arg("-C") - .arg(source) - .args(args) - .output() -} diff --git a/runledger-postgres/foundation_source.rs b/runledger-postgres/foundation_source.rs new file mode 100644 index 0000000..b8d9527 --- /dev/null +++ b/runledger-postgres/foundation_source.rs @@ -0,0 +1,104 @@ +//! Build-time checks for the actual unpublished Cargo foundation. +use std::{collections::BTreeSet, error::Error, fs, path::Path, process::Command}; + +const INPUTS: &[&str] = &["crates/batter-core", "crates/batter-sqlx"]; + +pub fn verify(sqlx: &Path, core: &Path, revision: &str) -> Result<(), Box> { + if revision.len() != 40 || !revision.bytes().all(|c| c.is_ascii_hexdigit()) { + return Err("invalid immutable Batter source revision".into()); + } + let sqlx = sqlx.canonicalize()?; + let core = core.canonicalize()?; + let root = sqlx + .parent() + .and_then(Path::parent) + .ok_or("missing Batter workspace")?; + if root.join(INPUTS[0]).canonicalize()? != core || root.join(INPUTS[1]).canonicalize()? != sqlx + { + return Err("Cargo selected different Batter foundation source roots".into()); + } + for input in INPUTS.iter().copied().chain(["Cargo.toml"]) { + println!("cargo:rerun-if-changed={}", root.join(input).display()); + } + let mut args = vec!["diff", "--quiet", revision, "--"]; + args.extend_from_slice(INPUTS); + if !git(root, &args)?.status.success() { + return Err("actual Batter foundation differs from the coordinated pin".into()); + } + let mut args = vec!["ls-files", "--others", "--"]; + args.extend_from_slice(INPUTS); + let unknown = git(root, &args)?; + if !unknown.status.success() || !unknown.stdout.is_empty() { + return Err("untracked actual Batter foundation inputs are not reviewed".into()); + } + let expected = git(root, &["show", &format!("{revision}:Cargo.toml")])?; + if !expected.status.success() { + return Err("reviewed Batter revision is unavailable in the actual source checkout".into()); + } + let expected: toml::Table = String::from_utf8(expected.stdout)?.parse()?; + let actual: toml::Table = fs::read_to_string(root.join("Cargo.toml"))?.parse()?; + let mut inherited = BTreeSet::new(); + for input in INPUTS { + let manifest: toml::Table = + fs::read_to_string(root.join(input).join("Cargo.toml"))?.parse()?; + inherited_dependencies(&manifest, &mut inherited); + } + if projection(&actual, &inherited) != projection(&expected, &inherited) { + return Err( + "inherited Batter foundation manifest inputs differ from the coordinated pin".into(), + ); + } + Ok(()) +} + +// Include dependencies under target-specific and build/dev tables as well. +fn inherited_dependencies(table: &toml::Table, names: &mut BTreeSet) { + for (key, value) in table { + let Some(table) = value.as_table() else { + continue; + }; + if matches!( + key.as_str(), + "dependencies" | "build-dependencies" | "dev-dependencies" + ) { + for (name, dependency) in table { + if dependency.get("workspace").and_then(toml::Value::as_bool) == Some(true) { + names.insert(name.clone()); + } + } + } else { + inherited_dependencies(table, names); + } + } +} + +fn projection(root: &toml::Table, inherited: &BTreeSet) -> toml::Table { + let mut result = toml::Table::new(); + if let Some(workspace) = root.get("workspace").and_then(toml::Value::as_table) { + for key in ["package", "lints", "resolver"] { + if let Some(value) = workspace.get(key) { + result.insert(key.into(), value.clone()); + } + } + if let Some(dependencies) = workspace + .get("dependencies") + .and_then(toml::Value::as_table) + { + let selected = dependencies + .iter() + .filter(|(name, _)| inherited.contains(*name)) + .map(|(name, value)| (name.clone(), value.clone())) + .collect(); + result.insert("dependencies".into(), toml::Value::Table(selected)); + } + } + result +} + +fn git(source: &Path, args: &[&str]) -> Result { + Command::new("git") + .arg("-C") + .arg(source) + .args(args) + .output() +} diff --git a/runledger-postgres/tests/foundation_pin.rs b/runledger-postgres/tests/foundation_pin.rs new file mode 100644 index 0000000..adebe96 --- /dev/null +++ b/runledger-postgres/tests/foundation_pin.rs @@ -0,0 +1,158 @@ +//! These fixtures mutate disposable clones, never either development stream. +#[path = "../build.rs"] +#[allow( + dead_code, + reason = "exercise build-time migration and source checks without running main" +)] +mod build_guard; +use build_guard::foundation_source; + +use std::{fs, path::PathBuf, process::Command}; + +const PIN: &str = include_str!("../batter-revision"); + +struct Fixture { + _temp: tempfile::TempDir, + root: PathBuf, +} + +impl Fixture { + fn new() -> Self { + let temp = tempfile::tempdir().expect("source-pin fixture operation"); + let root = temp.path().join("batter"); + let source = std::env::var_os("RUNLEDGER_BATTER_SOURCE") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../batter")); + assert!( + Command::new("git") + .args(["clone", "--quiet", "--shared"]) + .arg(source) + .arg(&root) + .status() + .expect("source-pin fixture operation") + .success() + ); + assert!( + Command::new("git") + .arg("-C") + .arg(&root) + .args(["checkout", "--quiet", "--detach", PIN.trim()]) + .status() + .expect("source-pin fixture operation") + .success() + ); + Self { _temp: temp, root } + } + + fn check(&self) -> Result<(), Box> { + foundation_source::verify( + &self.root.join("crates/batter-sqlx"), + &self.root.join("crates/batter-core"), + PIN.trim(), + ) + } + + fn change(&self, path: &str, from: &str, to: &str) { + let path = self.root.join(path); + let original = fs::read_to_string(&path).expect("source-pin fixture operation"); + assert!(original.contains(from)); + fs::write(path, original.replace(from, to)).expect("source-pin fixture operation"); + } +} + +#[test] +fn companion_pin_and_formatting_changes_do_not_change_foundation() { + let fixture = Fixture::new(); + fixture.check().expect("source-pin fixture operation"); + fixture.change( + "Cargo.toml", + "bfc949bbc32fb2cc5731fb743632b2e432d1f5ae", + &"f".repeat(40), + ); + fixture.change( + "Cargo.toml", + "[workspace]", + "# adapter-only coordination\n[workspace]", + ); + fixture.check().expect("source-pin fixture operation"); +} + +#[test] +fn inherited_dependency_and_package_settings_remain_guarded() { + for (from, to) in [ + ("1.53.1", "1.53.2"), + ("edition = \"2024\"", "edition = \"2021\""), + ] { + let fixture = Fixture::new(); + fixture.change("Cargo.toml", from, to); + assert!( + fixture + .check() + .expect_err("fixture drift must be rejected") + .to_string() + .contains("manifest inputs differ") + ); + } +} + +#[test] +fn tracked_and_ignored_untracked_foundation_drift_is_rejected() { + let fixture = Fixture::new(); + fixture.change("crates/batter-core/src/lib.rs", "//!", "//! changed "); + assert!( + fixture + .check() + .expect_err("fixture drift must be rejected") + .to_string() + .contains("foundation differs") + ); + let fixture = Fixture::new(); + fs::write( + fixture.root.join("crates/batter-sqlx/unreviewed.rs"), + "// drift", + ) + .expect("source-pin fixture operation"); + fs::write(fixture.root.join(".gitignore"), "unreviewed.rs\n") + .expect("source-pin fixture operation"); + assert!( + fixture + .check() + .expect_err("fixture drift must be rejected") + .to_string() + .contains("untracked actual") + ); +} + +#[test] +fn clean_sibling_cannot_bless_another_compiled_core() { + let fixture = Fixture::new(); + let other = Fixture::new(); + let error = foundation_source::verify( + &fixture.root.join("crates/batter-sqlx"), + &other.root.join("crates/batter-core"), + PIN.trim(), + ) + .expect_err("fixture drift must be rejected"); + assert!( + error + .to_string() + .contains("different Batter foundation source roots") + ); +} + +#[test] +fn migration_sync_remains_independent_of_source_selection() { + let temp = tempfile::tempdir().expect("source-pin fixture operation"); + let canonical = temp.path().join("canonical"); + let vendored = temp.path().join("vendored"); + fs::create_dir(&canonical).expect("source-pin fixture operation"); + fs::create_dir(&vendored).expect("source-pin fixture operation"); + fs::write(canonical.join("001.sql"), "SELECT 1;").expect("source-pin fixture operation"); + fs::write(vendored.join("001.sql"), "SELECT 1;").expect("source-pin fixture operation"); + build_guard::enforce_migration_sync(&canonical, &vendored); + fs::write(vendored.join("001.sql"), "SELECT 2;").expect("source-pin fixture operation"); + assert!( + std::panic::catch_unwind(|| build_guard::enforce_migration_sync(&canonical, &vendored)) + .is_err() + ); +} diff --git a/scripts/bootstrap-batter.sh b/scripts/bootstrap-batter.sh index 8037e37..8cc0097 100644 --- a/scripts/bootstrap-batter.sh +++ b/scripts/bootstrap-batter.sh @@ -9,12 +9,12 @@ if [[ ! -e "$foundation" ]]; then git -C "$foundation" checkout --detach "$revision" fi # Never switch, overwrite or clean an existing sibling worktree. -if ! git -C "$foundation" diff --quiet "$revision" -- Cargo.toml crates/batter-core crates/batter-sqlx; then +if ! git -C "$foundation" diff --quiet "$revision" -- crates/batter-core crates/batter-sqlx; then echo "Existing Batter foundation differs from the reviewed pin; no files were changed." >&2 exit 1 fi -if [[ -n "$(git -C "$foundation" ls-files --others --exclude-standard -- crates/batter-core crates/batter-sqlx)" ]]; then +if [[ -n "$(git -C "$foundation" ls-files --others -- crates/batter-core crates/batter-sqlx)" ]]; then echo "Untracked foundation sources: commit/update the coordinated pin before building." >&2 exit 1 fi -echo "Coordinated Batter foundation matches $revision" +echo "Foundation files match $revision; Cargo builds also verify actual sources and inherited manifest inputs." diff --git a/scripts/test-coordinated-pin.sh b/scripts/test-coordinated-pin.sh index f09ac74..b11a97c 100644 --- a/scripts/test-coordinated-pin.sh +++ b/scripts/test-coordinated-pin.sh @@ -1,28 +1,4 @@ #!/usr/bin/env bash set -euo pipefail -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -fixture="$(mktemp -d -t runledger-source-pin.XXXXXXXX)" -foundation="${RUNLEDGER_BATTER_SOURCE:-$repo_root/../batter}" -# Keep diagnostic fixture artifacts available on failure; never alter the real -# sibling. This tiny local shared clone needs no network access. -git clone --quiet --shared "$foundation" "$fixture/batter" -rustc --edition=2024 "$repo_root/runledger-postgres/build.rs" -o "$fixture/guard" -export CARGO_MANIFEST_DIR="$repo_root/runledger-postgres" -export RUNLEDGER_BATTER_SOURCE="$fixture/batter" -"$fixture/guard" -mkdir -p "$fixture/workspace/runledger-postgres" -cp -R "$repo_root/migrations" "$fixture/workspace/migrations" -cp -R "$repo_root/runledger-postgres/migrations" "$fixture/workspace/runledger-postgres/migrations" -printf '\n-- deliberate migration drift control\n' > "$fixture/workspace/runledger-postgres/migrations/000000000001_unreviewed.sql" -if CARGO_MANIFEST_DIR="$fixture/workspace/runledger-postgres" "$fixture/guard" >"$fixture/migration-rejection.log" 2>&1; then - echo "Build guard accepted an unsynchronized migration copy." >&2 - exit 1 -fi -grep -Fq 'out of sync with the canonical root migrations' "$fixture/migration-rejection.log" -printf '\n# unreviewed fixture mutation\n' >> "$fixture/batter/Cargo.toml" -if "$fixture/guard" >"$fixture/rejection.log" 2>&1; then - echo "Source pin accepted unreviewed foundation input." >&2 - exit 1 -fi -grep -Fq 'differs from the coordinated pin' "$fixture/rejection.log" -echo "Local Cargo guard accepts companion-only changes and rejects foundation and migration drift. Fixture: $fixture" +cd "$(dirname "${BASH_SOURCE[0]}")/.." +cargo test -p runledger-postgres --test foundation_pin --locked diff --git a/smoke/external-consumer/Cargo.lock b/smoke/external-consumer/Cargo.lock index a35749c..3881902 100644 --- a/smoke/external-consumer/Cargo.lock +++ b/smoke/external-consumer/Cargo.lock @@ -434,7 +434,7 @@ dependencies = [ "chrono", "once_cell", "phf 0.11.3", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -1948,6 +1948,7 @@ dependencies = [ "sha2 0.11.0", "sqlx", "tokio", + "toml", "tracing", ] @@ -2171,6 +2172,15 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2773,6 +2783,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tonic" version = "0.14.6" @@ -3256,6 +3305,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + [[package]] name = "wit-bindgen" version = "0.57.1"