From 0d542cef7486e0c283f3f7375486df9000fdd815 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Sun, 9 Aug 2026 00:29:56 +0530 Subject: [PATCH 1/9] chore: close local verification and preference gaps --- .github/workflows/ci.yml | 1 + docs/provenance.md | 10 ++ package.json | 3 +- scripts/check-dependency-inventory.mjs | 4 - scripts/check-fixture-byte-integrity.mjs | 28 +++++ scripts/check-fixture-byte-integrity.test.mjs | 36 +++++++ src-tauri/src/client_groups.rs | 100 +++++++++++++++--- src-tauri/src/commands.rs | 23 ++++ src-tauri/src/lib.rs | 16 +-- src/AllClientsScreen.tsx | 41 ++++++- 10 files changed, 229 insertions(+), 33 deletions(-) create mode 100644 scripts/check-fixture-byte-integrity.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42223e1..af9852b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,7 @@ jobs: cache: pnpm - run: corepack pnpm install --frozen-lockfile - run: corepack pnpm run license:check + - run: corepack pnpm test - run: corepack pnpm run build rust-format: diff --git a/docs/provenance.md b/docs/provenance.md index c29dd35..8fe1a58 100644 --- a/docs/provenance.md +++ b/docs/provenance.md @@ -37,6 +37,16 @@ Do not replace or add an icon, font, screenshot, fixture, or other asset unless its source, contributor authority, license, and required attribution are added to this record or `NOTICE`. +## Fixture byte-integrity boundary + +`scripts/check-fixture-byte-integrity.mjs` compares local worktree bytes with +the committed blob and verifies that every registered fixture location opts out +of Git text normalization. In CI, a normal checkout materialises the worktree +from that same blob, so the byte comparison cannot independently prove capture +fidelity there; the Git attribute check is the meaningful CI protection. This +does not establish SHA-256 or byte-length assertions for provenance-listed +fixtures. + ## Third-party material - JavaScript production dependencies are locked by `pnpm-lock.yaml` and listed diff --git a/package.json b/package.json index b5066cd..533cb63 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ }, "scripts": { "dev": "vite --host 127.0.0.1", - "build": "node --experimental-strip-types --test scripts/*.test.mjs && tsc && vite build", + "test": "node --experimental-strip-types --test scripts/*.test.mjs", + "build": "tsc && vite build", "check": "pnpm run build && pnpm run cargo:check", "tauri": "tauri", "tauri:dev": "tauri dev", diff --git a/scripts/check-dependency-inventory.mjs b/scripts/check-dependency-inventory.mjs index 554b84d..d2ce5ca 100644 --- a/scripts/check-dependency-inventory.mjs +++ b/scripts/check-dependency-inventory.mjs @@ -10,13 +10,9 @@ const checkFrontend = modes.size === 0 || modes.has("--frontend"); const checkRust = modes.size === 0 || modes.has("--rust"); const firstPartyRustPackages = new Set([ "bridge", - "bridge-tally-canonical", "bridge-tally-core", - "bridge-tally-incremental", - "bridge-tally-observability", "bridge-tally-primitives", "bridge-tally-protocol", - "bridge-tally-runtime", "bridge-tally-transport", "tally-protocol-simulator", ]); diff --git a/scripts/check-fixture-byte-integrity.mjs b/scripts/check-fixture-byte-integrity.mjs index 8ec68b4..fb5aeb7 100644 --- a/scripts/check-fixture-byte-integrity.mjs +++ b/scripts/check-fixture-byte-integrity.mjs @@ -11,6 +11,20 @@ const fixtureDirectories = [ "src-tauri/crates/tally-protocol-simulator/fixtures", "docs/tally/compatibility/fixtures", ]; +const discoveredFixtureDirectories = walkDirectories(repositoryRoot) + .filter((directory) => directory.endsWith("/fixture") || directory.endsWith("/fixtures")) + .map((directory) => relative(repositoryRoot, directory).replaceAll("\\", "/")) + .sort(); +const unexpectedFixtureDirectories = discoveredFixtureDirectories.filter( + (directory) => !fixtureDirectories.includes(directory), +); +if (unexpectedFixtureDirectories.length) { + throw new Error( + "unexpected fixture directories are not covered by byte-integrity policy:\n" + + unexpectedFixtureDirectories.map((directory) => `- ${directory}`).join("\n") + + "\nRegister each directory in fixtureDirectories and .gitattributes before adding fixtures.", + ); +} const fixtures = fixtureDirectories .flatMap((directory) => { const paths = walkFiles(join(repositoryRoot, directory)); @@ -40,6 +54,10 @@ if (attributeFailures.length) { const byteFailures = []; for (const fixture of fixtures) { + // In CI the checkout materialises this file from the same HEAD blob, so this + // comparison cannot independently establish captured-byte provenance there. + // The checked .gitattributes rule is the meaningful CI protection; this + // comparison still catches local worktree conversion or mutation. const committed = runGit(["show", "--no-textconv", `HEAD:${fixture}`], { allowFailure: true }); if (committed.status !== 0) { // A new fixture has no blob until its first commit. Attribute coverage still @@ -72,6 +90,16 @@ function walkFiles(directory) { return paths; } +function walkDirectories(directory) { + const directories = []; + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (!entry.isDirectory() || [".git", "node_modules", "target"].includes(entry.name)) continue; + const path = join(directory, entry.name); + directories.push(path, ...walkDirectories(path)); + } + return directories; +} + function runGit(args, { allowFailure = false } = {}) { const result = spawnSync("git", args, { cwd: repositoryRoot, diff --git a/scripts/check-fixture-byte-integrity.test.mjs b/scripts/check-fixture-byte-integrity.test.mjs new file mode 100644 index 0000000..74bd999 --- /dev/null +++ b/scripts/check-fixture-byte-integrity.test.mjs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../", import.meta.url)); + +test("an unregistered fixture directory fails the byte-integrity gate", async () => { + const directory = await mkdtemp(join(root, ".fixture-integrity-")); + const fixtureDirectory = join(directory, "fixtures"); + + try { + await mkdir(fixtureDirectory, { recursive: true }); + await writeFile(join(fixtureDirectory, "synthetic.xml"), "\n"); + + let failure = null; + try { + execFileSync(process.execPath, ["scripts/check-fixture-byte-integrity.mjs"], { + cwd: root, + encoding: "utf8", + stdio: "pipe", + }); + } catch (cause) { + failure = cause; + } + + assert.ok(failure, "an unregistered fixture directory must fail the gate"); + assert.match(`${failure.stderr}${failure.stdout}`, /unexpected fixture directories/); + } finally { + await rm(directory, { force: true, recursive: true }); + } +}); diff --git a/src-tauri/src/client_groups.rs b/src-tauri/src/client_groups.rs index f84588c..3b60cff 100644 --- a/src-tauri/src/client_groups.rs +++ b/src-tauri/src/client_groups.rs @@ -1,11 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -//! Operator-owned company filing labels. +//! Operator-owned company filing labels and all-client sort preference. //! //! These labels deliberately live in the ordinary application configuration //! directory, not in the encrypted Tally mirror. They are an operator's filing -//! choice rather than accounting data, and reading them must never resolve the -//! mirror key or prompt the operating-system keychain. +//! choice rather than accounting data. They contain neither accounting figures +//! nor mirror state, and reading them must never resolve the mirror key or +//! prompt the operating-system keychain. use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -17,11 +18,30 @@ const SCHEMA_VERSION: u8 = 1; pub type ClientGroupLabels = BTreeMap; +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ClientSortKey { + Client, + Receivable, + Overdue, + Unallocated, + Oldest, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct ClientSortPreference { + pub key: ClientSortKey, + pub desc: bool, +} + #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct ClientGroupLabelsFile { version: u8, labels: ClientGroupLabels, + #[serde(default, skip_serializing_if = "Option::is_none")] + sort: Option, } #[derive(Debug, thiserror::Error)] @@ -39,15 +59,19 @@ pub enum ClientGroupLabelsError { } pub fn load(directory: &Path) -> ClientGroupLabels { - try_load(directory).unwrap_or_default() + try_load(directory) + .map(|file| file.labels) + .unwrap_or_default() } -fn try_load(directory: &Path) -> Result { +pub fn load_sort_preference(directory: &Path) -> Option { + try_load(directory).ok().and_then(|file| file.sort) +} + +fn try_load(directory: &Path) -> Result { let contents = match std::fs::read_to_string(directory.join(FILE_NAME)) { Ok(contents) => contents, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - return Ok(ClientGroupLabels::new()) - } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(empty_file()), Err(error) => return Err(ClientGroupLabelsError::Read(error)), }; if contents.trim().is_empty() { @@ -63,7 +87,19 @@ fn try_load(directory: &Path) -> Result ClientGroupLabelsFile { + ClientGroupLabelsFile { + version: SCHEMA_VERSION, + labels: ClientGroupLabels::new(), + sort: None, + } } pub fn save_label( @@ -71,22 +107,33 @@ pub fn save_label( company_guid: &str, label: &str, ) -> Result<(), ClientGroupLabelsError> { - let mut labels = try_load(directory)?; + let mut file = try_load(directory)?; let company_guid = company_guid.trim(); let label = label.trim(); if label.is_empty() { - labels.remove(company_guid); + file.labels.remove(company_guid); } else { - labels.insert(company_guid.to_string(), label.to_string()); + file.labels + .insert(company_guid.to_string(), label.to_string()); } + save_file(directory, file) +} + +pub fn save_sort_preference( + directory: &Path, + sort: ClientSortPreference, +) -> Result<(), ClientGroupLabelsError> { + let mut file = try_load(directory)?; + file.sort = Some(sort); + save_file(directory, file) +} + +fn save_file(directory: &Path, file: ClientGroupLabelsFile) -> Result<(), ClientGroupLabelsError> { std::fs::create_dir_all(directory).map_err(ClientGroupLabelsError::Write)?; let path = directory.join(FILE_NAME); - let contents = serde_json::to_vec_pretty(&ClientGroupLabelsFile { - version: SCHEMA_VERSION, - labels, - }) - .expect("client group label schema always serializes"); + let contents = + serde_json::to_vec_pretty(&file).expect("client preference schema always serializes"); write_file_atomically(&path, &contents).map_err(ClientGroupLabelsError::Write)?; #[cfg(unix)] { @@ -281,4 +328,23 @@ mod tests { )); assert!(unreadable_path.is_dir()); } + + #[test] + fn sort_preference_survives_a_reload_without_changing_group_labels() { + let directory = tempfile::tempdir().expect("temporary config directory"); + save_label(directory.path(), "synthetic-company-guid", "North practice") + .expect("save label"); + let sort = ClientSortPreference { + key: ClientSortKey::Unallocated, + desc: false, + }; + + save_sort_preference(directory.path(), sort.clone()).expect("save sort preference"); + + assert_eq!(load_sort_preference(directory.path()), Some(sort)); + assert_eq!( + load(directory.path()).get("synthetic-company-guid"), + Some(&"North practice".to_string()) + ); + } } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7f98840..33e13e7 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2121,6 +2121,16 @@ pub fn load_client_group_labels(app: AppHandle) -> client_groups::ClientGroupLab client_groups::load(&directory) } +/// Reads the optional all-client sort preference from ordinary application +/// configuration. Like group labels, it never initialises the Tally mirror. +#[tauri::command] +pub fn load_client_sort_preference(app: AppHandle) -> Option { + let Ok(directory) = app.path().app_config_dir() else { + return None; + }; + client_groups::load_sort_preference(&directory) +} + #[derive(Debug, Deserialize)] pub struct SaveClientGroupLabelRequest { pub company_guid: String, @@ -2144,6 +2154,19 @@ pub fn save_client_group_label( .map_err(|_| "Bridge could not save this group label.".to_string()) } +/// Saves the optional all-client sort preference without accessing the Tally mirror. +#[tauri::command] +pub fn save_client_sort_preference( + app: AppHandle, + preference: client_groups::ClientSortPreference, +) -> Result<(), String> { + let directory = app.path().app_config_dir().map_err(|_| { + "Bridge could not locate its local client-preference configuration.".to_string() + })?; + client_groups::save_sort_preference(&directory, preference) + .map_err(|_| "Bridge could not save the all-client sort preference.".to_string()) +} + #[derive(Debug, Deserialize)] pub struct AllCompaniesOutstandingsRequest { pub config: TallyConfig, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1a4282f..1e52fc8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -97,6 +97,8 @@ pub fn run() { commands::fetch_tally_outstandings_all_companies, commands::load_client_group_labels, commands::save_client_group_label, + commands::load_client_sort_preference, + commands::save_client_sort_preference, commands::detect_tally_base_currency, commands::tally_persisted_company_profiles, commands::tally_mirror_explorer_page, @@ -235,9 +237,9 @@ mod security_config_tests { } #[cfg(test)] -mod client_group_label_mount_tests { +mod client_preference_mount_tests { #[test] - fn group_label_load_path_is_mirror_and_keychain_free() { + fn client_preference_commands_are_mirror_and_keychain_free() { let commands = include_str!("commands.rs"); let start = commands .find("pub fn load_client_group_labels") @@ -246,10 +248,12 @@ mod client_group_label_mount_tests { .find("pub struct AllCompaniesOutstandingsRequest") .map(|offset| start + offset) .expect("end of group-label commands"); - let group_label_commands = &commands[start..end]; + let client_preference_commands = &commands[start..end]; - assert!(group_label_commands.contains("app_config_dir")); - assert!(!group_label_commands.contains("LazyTallyMirror")); - assert!(!group_label_commands.contains("keyring")); + assert!(client_preference_commands.contains("load_client_sort_preference")); + assert!(client_preference_commands.contains("save_client_sort_preference")); + assert!(client_preference_commands.contains("app_config_dir")); + assert!(!client_preference_commands.contains("LazyTallyMirror")); + assert!(!client_preference_commands.contains("keyring")); } } diff --git a/src/AllClientsScreen.tsx b/src/AllClientsScreen.tsx index 638790b..490c04e 100644 --- a/src/AllClientsScreen.tsx +++ b/src/AllClientsScreen.tsx @@ -61,6 +61,20 @@ function formatCompact(value: string | undefined) { } type SortKey = "client" | "receivable" | "overdue" | "unallocated" | "oldest"; +type SortPreference = { key: SortKey; desc: boolean }; + +const defaultSort: SortPreference = { key: "overdue", desc: true }; + +function isSortPreference(value: unknown): value is SortPreference { + return Boolean( + value + && typeof value === "object" + && "key" in value + && "desc" in value + && ["client", "receivable", "overdue", "unallocated", "oldest"].includes(String(value.key)) + && typeof value.desc === "boolean", + ); +} /// Severity tiers reuse the ageing ramp used on the single-client screen, so a /// chip means the same thing in both places. @@ -73,7 +87,7 @@ function ageTier(days: number | null) { } export function AllClientsScreen({ config, companies, onOpenCompany, onBack }: Props) { - const [sort, setSort] = React.useState<{ key: SortKey; desc: boolean }>({ key: "overdue", desc: true }); + const [sort, setSort] = React.useState(defaultSort); const [entries, setEntries] = React.useState(null); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); @@ -104,6 +118,20 @@ export function AllClientsScreen({ config, companies, onOpenCompany, onBack }: P }; }, []); + React.useEffect(() => { + let active = true; + void invoke("load_client_sort_preference") + .then((preference) => { + if (active && isSortPreference(preference)) setSort(preference); + }) + // Sorting is an optional local display preference. Keep the safe default + // if its config file cannot be read. + .catch(() => {}); + return () => { + active = false; + }; + }, []); + const load = React.useCallback(async () => { if (companies.length === 0) return; const version = requestVersion.current + 1; @@ -236,6 +264,12 @@ export function AllClientsScreen({ config, companies, onOpenCompany, onBack }: P }); }, []); + const changeSort = React.useCallback((key: SortKey) => { + const next = sort.key === key ? { key, desc: !sort.desc } : { key, desc: key !== "client" }; + setSort(next); + void invoke("save_client_sort_preference", { preference: next }).catch(() => {}); + }, [sort]); + const renderRow = (row: (typeof rows)[number]) => { const partial = row.reasonCode ? outstandingsPartialState(row.reasonCode) : null; return ( @@ -358,10 +392,7 @@ export function AllClientsScreen({ config, companies, onOpenCompany, onBack }: P type="button" className={sort.key === key ? "is-sorted" : undefined} aria-sort={sort.key === key ? (sort.desc ? "descending" : "ascending") : "none"} - onClick={() => setSort((current) => - current.key === key - ? { key, desc: !current.desc } - : { key, desc: key !== "client" })} + onClick={() => changeSort(key)} > {label} From 0954603e8111f705ff80df10143189ec2b0c2da7 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Mon, 17 Aug 2026 22:20:58 +0530 Subject: [PATCH 2/9] fix(clients): isolate sort preference storage Keep the deny-unknown-fields v1 label file byte-compatible with the prior reader by persisting sort preference in its own versioned file. Red proof: the old v1 reader rejected the shared-file writer with unknown field 'sort' (exit 101). Mutation proof: forcing both stores back to client-group-labels-v1.json reproduced the same rollback failure (exit 101). Green proof: all 8 client_groups tests pass, including the explicit old-contract rollback check (exit 0); cargo fmt --check exits 0. --- src-tauri/src/client_groups.rs | 112 ++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 38 deletions(-) diff --git a/src-tauri/src/client_groups.rs b/src-tauri/src/client_groups.rs index 3b60cff..f20b017 100644 --- a/src-tauri/src/client_groups.rs +++ b/src-tauri/src/client_groups.rs @@ -15,6 +15,8 @@ use std::path::{Path, PathBuf}; const FILE_NAME: &str = "client-group-labels-v1.json"; const SCHEMA_VERSION: u8 = 1; +const SORT_FILE_NAME: &str = "client-sort-preference-v1.json"; +const SORT_SCHEMA_VERSION: u8 = 1; pub type ClientGroupLabels = BTreeMap; @@ -40,8 +42,13 @@ pub struct ClientSortPreference { struct ClientGroupLabelsFile { version: u8, labels: ClientGroupLabels, - #[serde(default, skip_serializing_if = "Option::is_none")] - sort: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ClientSortPreferenceFile { + version: u8, + sort: ClientSortPreference, } #[derive(Debug, thiserror::Error)] @@ -59,19 +66,24 @@ pub enum ClientGroupLabelsError { } pub fn load(directory: &Path) -> ClientGroupLabels { - try_load(directory) - .map(|file| file.labels) - .unwrap_or_default() + try_load(directory).unwrap_or_default() } pub fn load_sort_preference(directory: &Path) -> Option { - try_load(directory).ok().and_then(|file| file.sort) + let contents = std::fs::read_to_string(directory.join(SORT_FILE_NAME)).ok()?; + if contents.trim().is_empty() { + return None; + } + let file = serde_json::from_str::(&contents).ok()?; + (file.version == SORT_SCHEMA_VERSION).then_some(file.sort) } -fn try_load(directory: &Path) -> Result { +fn try_load(directory: &Path) -> Result { let contents = match std::fs::read_to_string(directory.join(FILE_NAME)) { Ok(contents) => contents, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(empty_file()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(ClientGroupLabels::new()) + } Err(error) => return Err(ClientGroupLabelsError::Read(error)), }; if contents.trim().is_empty() { @@ -87,19 +99,7 @@ fn try_load(directory: &Path) -> Result ClientGroupLabelsFile { - ClientGroupLabelsFile { - version: SCHEMA_VERSION, - labels: ClientGroupLabels::new(), - sort: None, - } + Ok(normalize(file.labels)) } pub fn save_label( @@ -107,39 +107,43 @@ pub fn save_label( company_guid: &str, label: &str, ) -> Result<(), ClientGroupLabelsError> { - let mut file = try_load(directory)?; + let mut labels = try_load(directory)?; let company_guid = company_guid.trim(); let label = label.trim(); if label.is_empty() { - file.labels.remove(company_guid); + labels.remove(company_guid); } else { - file.labels - .insert(company_guid.to_string(), label.to_string()); + labels.insert(company_guid.to_string(), label.to_string()); } - save_file(directory, file) + let contents = serde_json::to_vec_pretty(&ClientGroupLabelsFile { + version: SCHEMA_VERSION, + labels, + }) + .expect("client group label schema always serializes"); + save_bytes(directory, FILE_NAME, &contents).map_err(ClientGroupLabelsError::Write) } pub fn save_sort_preference( directory: &Path, sort: ClientSortPreference, -) -> Result<(), ClientGroupLabelsError> { - let mut file = try_load(directory)?; - file.sort = Some(sort); - save_file(directory, file) +) -> Result<(), std::io::Error> { + let contents = serde_json::to_vec_pretty(&ClientSortPreferenceFile { + version: SORT_SCHEMA_VERSION, + sort, + }) + .expect("client sort preference schema always serializes"); + save_bytes(directory, SORT_FILE_NAME, &contents) } -fn save_file(directory: &Path, file: ClientGroupLabelsFile) -> Result<(), ClientGroupLabelsError> { - std::fs::create_dir_all(directory).map_err(ClientGroupLabelsError::Write)?; - let path = directory.join(FILE_NAME); - let contents = - serde_json::to_vec_pretty(&file).expect("client preference schema always serializes"); - write_file_atomically(&path, &contents).map_err(ClientGroupLabelsError::Write)?; +fn save_bytes(directory: &Path, file_name: &str, contents: &[u8]) -> Result<(), std::io::Error> { + std::fs::create_dir_all(directory)?; + let path = directory.join(file_name); + write_file_atomically(&path, contents)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) - .map_err(ClientGroupLabelsError::Write)?; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; } Ok(()) } @@ -347,4 +351,36 @@ mod tests { Some(&"North practice".to_string()) ); } + + #[test] + fn sort_storage_remains_readable_by_the_old_v1_label_contract() { + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct OldClientGroupLabelsFile { + version: u8, + labels: ClientGroupLabels, + } + + let directory = tempfile::tempdir().expect("temporary config directory"); + save_label(directory.path(), "existing-guid", "Existing practice") + .expect("save existing label"); + save_sort_preference( + directory.path(), + ClientSortPreference { + key: ClientSortKey::Oldest, + desc: true, + }, + ) + .expect("save new sort preference"); + + let raw_labels = std::fs::read(directory.path().join(FILE_NAME)) + .expect("read label bytes after new writer"); + let old_reader: OldClientGroupLabelsFile = serde_json::from_slice(&raw_labels) + .expect("the previous release must still parse the label file after rollback"); + assert_eq!(old_reader.version, SCHEMA_VERSION); + assert_eq!( + old_reader.labels.get("existing-guid"), + Some(&"Existing practice".to_string()) + ); + } } From 94df397bd8d83564c25b5890e72785f4a549290d Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Mon, 17 Aug 2026 18:43:23 +0530 Subject: [PATCH 3/9] chore(tally): reseal F2X N1.3 compatibility surface Carry forward the N1.3 workflow and command surface while retaining the F2X runtime digest. Claims, evidence, and trusted keys remain byte-identical to the preserved N1.3 head. --- .../compatibility/compatibility-surface.json | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 7cbd46f..49dcae6 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -3,7 +3,7 @@ "files": [ { "path": ".github/workflows/ci.yml", - "sha256": "309fe37d7038a4a1eb6197edf32f652af50ded2017a55d55466e577b878b05ed" + "sha256": "04c2796dc7b382a0582c82d0bc86f3b9b819b7bf46df6cf23a36b385f0f2e832" }, { "path": ".github/workflows/dependency-security.yml", @@ -63,7 +63,7 @@ }, { "path": "docs/tally/compatibility/synthetic-write-canary-fixture.md", - "sha256": "9ed85e60adb7306f11496b47f5a86d49327abfdd8e2735678c521187b9ce76e4" + "sha256": "dd2f1c68c0925523af1468dd9c61330433130c0e72b7c713dfc1ef9205b4756f" }, { "path": "docs/tally/support-matrix.md", @@ -71,7 +71,7 @@ }, { "path": "package.json", - "sha256": "bc58b4532fc7279bdbaef868ff4ecda87fcb353647a2cbac7dd7cd06ee423cf3" + "sha256": "d6a5107c4c556fbf7e494f98b743d737cc47fa8bd01cecdc942066c53b4d5e86" }, { "path": "scripts/check-tally-live-read-boundary.mjs", @@ -79,7 +79,7 @@ }, { "path": "scripts/outstandings-copy.test.mjs", - "sha256": "8c9adb100a45704ca8ba72203a397defca3e8b01a297e96043c543cc052b980a" + "sha256": "58ca3cb255a8e54dc3a4590146b908b800fd4186d474eb05c3e1a46ae6c005a0" }, { "path": "scripts/tally-company-selection.test.mjs", @@ -95,7 +95,7 @@ }, { "path": "src-tauri/Cargo.lock", - "sha256": "7aac9a6af2fdab6e117af7d3aa26ba04434701fe02d3010010fa3f3793236212" + "sha256": "59012d808bcc29d0abe31d74c6f97d7afe2aa6acd33ec188fabf61d8b92f10cc" }, { "path": "src-tauri/Cargo.toml", @@ -143,7 +143,7 @@ }, { "path": "src-tauri/crates/bridge-tally-protocol/src/lib.rs", - "sha256": "e1c9082a214a125454c2bbddef8494283d41efa025e4acfc1525f0a22aa2bd1e" + "sha256": "c5c61049fdbedf31cfafb43d349961e736eb7c6a361ec08e410f2f94099b1200" }, { "path": "src-tauri/crates/bridge-tally-protocol/src/outstandings/completeness.rs", @@ -175,7 +175,7 @@ }, { "path": "src-tauri/crates/bridge-tally-protocol/tests/simulator_corpus.rs", - "sha256": "616eb8fa5e387bff74f62d8e775b7eb118860763b18ff63ad88ad5015f7f752f" + "sha256": "65da6e2a0cf543c46e1834d82f836b2966f9f574952b991259179cf743f907d6" }, { "path": "src-tauri/crates/bridge-tally-protocol/tests/stream_text_decoder.rs", @@ -219,7 +219,7 @@ }, { "path": "src-tauri/src/commands.rs", - "sha256": "8d13e54b487bf27984ed988abee126d90e63a21a72d12aa21a819796bff4c569" + "sha256": "a1956c6cb5039559b5dbd06b68f7041399a68901495c5c4d900adc6ff766a268" }, { "path": "src-tauri/src/db/encrypted.rs", @@ -303,7 +303,7 @@ }, { "path": "src-tauri/src/lib.rs", - "sha256": "5582994d94163235a885bce0d44eadc11b6d8eb9a36898610e94c2b1141efbae" + "sha256": "5a2e2c35da52a7c3b01c5810882a1eb3a9ea45fac82556b1eee1a715989585b9" }, { "path": "src-tauri/src/sync/coordinator.rs", @@ -339,7 +339,7 @@ }, { "path": "src-tauri/src/tally/runtime.rs", - "sha256": "1d2ae890a240344ad9a2a59b4624be2726d13c11e5e529a0b084e355f865deec" + "sha256": "4a4cb2bc46cfcadbb082550c8f24e735d8bfb9206f524d6bee4db8bbf69ef95d" }, { "path": "src-tauri/src/tally/serial_queue.rs", @@ -359,7 +359,7 @@ }, { "path": "src/OutstandingsScreen.tsx", - "sha256": "6835d23a31eb92e1037e8f1e9c268ea86c59554a6b7060009e4dfa086bfa03d6" + "sha256": "479aff297f63050a4903154e565062f774057bec2b9c452b81c5934782f9ea5e" }, { "path": "src/TallyReadinessFlow.tsx", @@ -371,7 +371,7 @@ }, { "path": "src/outstandings-copy.ts", - "sha256": "a2bcf820f694443ac122e91aa98fb354b6e2d5c6be3e1e9503b368a3a0a4f696" + "sha256": "0e78dd97d79c0166fe17a4e933733c369a7e4be3046ddbe3e38122ad5e234216" }, { "path": "src/outstandings-csv.ts", @@ -434,5 +434,5 @@ "sha256": "a27f294ee15e407b69fdfc73609e8708ac0509b6e6a8872daef5451fde61a8db" } ], - "manifest_sha256": "ded2577b0f66b63d6ea65fe3d315d69dce68b0fa930f2b6d9106b7e0b52b526d" + "manifest_sha256": "e705c7dfdc7b5e303dcc2aaf2d5a54a3697486a9c3d16c1088f58836b6d33dd5" } From 28e1bd8912cd7c7052f2dc1239d7dd0ed5402f16 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Tue, 18 Aug 2026 05:51:39 +0530 Subject: [PATCH 4/9] chore(tally): reseal F4X N1.3 compatibility surface Carry the N1.3 workflow and command surface over the F4X BILLREF presentation disclosure. Claims, evidence, and trusted-evidence keys remain byte-identical to the preserved N1.3 head. Compatibility gate: exit 0, unknown_claims=11, evidenced_claims=0. Claims/evidence/trusted identity checks: exit 0. --- docs/tally/compatibility/compatibility-matrix.json | 2 +- docs/tally/compatibility/compatibility-surface.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index dc6b52e..98cb9ed 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "ded2577b0f66b63d6ea65fe3d315d69dce68b0fa930f2b6d9106b7e0b52b526d", + "compatibility_surface_sha256": "80d542fa2c69def9d27ed48cf349863ead7b34c72f5e496d379276e8a2dab47e", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 49dcae6..6851d06 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -339,7 +339,7 @@ }, { "path": "src-tauri/src/tally/runtime.rs", - "sha256": "4a4cb2bc46cfcadbb082550c8f24e735d8bfb9206f524d6bee4db8bbf69ef95d" + "sha256": "f6c4a1507b00c2689c128be4991af14af7cbd3ad2cec684673bebff9bb24a3c9" }, { "path": "src-tauri/src/tally/serial_queue.rs", @@ -434,5 +434,5 @@ "sha256": "a27f294ee15e407b69fdfc73609e8708ac0509b6e6a8872daef5451fde61a8db" } ], - "manifest_sha256": "e705c7dfdc7b5e303dcc2aaf2d5a54a3697486a9c3d16c1088f58836b6d33dd5" + "manifest_sha256": "80d542fa2c69def9d27ed48cf349863ead7b34c72f5e496d379276e8a2dab47e" } From e6557fd4f5e4f8ea3bb52ac567c70053bd181dda Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Tue, 18 Aug 2026 11:42:06 +0530 Subject: [PATCH 5/9] fix(local): harden preference and fixture boundaries Use Windows replace-existing semantics for atomic local preference updates, preserve a sort chosen before the persisted preference returns, and exclude git-ignored fixture trees through one NUL-delimited check-ignore batch with normalized paths.Red before fix: the late-sort contract was missing (exit 1), and the ignored fixture test was rejected as an unexpected directory (1/2 failed; exit 1). Mutation proof: disabling replacement caused both repeated label and sort writes to fail (exit 101); always applying the late load failed the chosen-sort assertion (5/6 passed; exit 1).Verified: fixture tests 2/2, client grouping tests 6/6, fixture gate sealed 46 files, Impeccable detector clean, pnpm TypeScript/Vite build, client_groups tests 9/9, and cargo check --locked --workspace, exit 0. Windows host execution was not available locally; the new cfg(windows) API path remains for hosted Windows verification. --- scripts/check-fixture-byte-integrity.mjs | 28 +++++++- scripts/check-fixture-byte-integrity.test.mjs | 19 +++++ scripts/client-grouping.test.mjs | 9 ++- src-tauri/Cargo.toml | 2 +- src-tauri/src/client_groups.rs | 70 ++++++++++++++++++- src/AllClientsScreen.tsx | 12 +++- src/client-grouping.ts | 8 +++ 7 files changed, 140 insertions(+), 8 deletions(-) diff --git a/scripts/check-fixture-byte-integrity.mjs b/scripts/check-fixture-byte-integrity.mjs index fb5aeb7..d3e7a4b 100644 --- a/scripts/check-fixture-byte-integrity.mjs +++ b/scripts/check-fixture-byte-integrity.mjs @@ -11,9 +11,12 @@ const fixtureDirectories = [ "src-tauri/crates/tally-protocol-simulator/fixtures", "docs/tally/compatibility/fixtures", ]; -const discoveredFixtureDirectories = walkDirectories(repositoryRoot) +const repositoryDirectories = walkDirectories(repositoryRoot) + .map((directory) => relative(repositoryRoot, directory).replaceAll("\\", "/")); +const ignoredDirectories = gitIgnoredPaths(repositoryDirectories); +const discoveredFixtureDirectories = repositoryDirectories + .filter((directory) => !ignoredDirectories.has(directory)) .filter((directory) => directory.endsWith("/fixture") || directory.endsWith("/fixtures")) - .map((directory) => relative(repositoryRoot, directory).replaceAll("\\", "/")) .sort(); const unexpectedFixtureDirectories = discoveredFixtureDirectories.filter( (directory) => !fixtureDirectories.includes(directory), @@ -100,9 +103,28 @@ function walkDirectories(directory) { return directories; } -function runGit(args, { allowFailure = false } = {}) { +function gitIgnoredPaths(paths) { + if (!paths.length) return new Set(); + const result = runGit(["check-ignore", "-z", "--stdin"], { + allowFailure: true, + input: `${paths.join("\0")}\0`, + }); + if (result.status !== 0 && result.status !== 1) { + throw new Error(`git check-ignore failed with status ${result.status}`); + } + return new Set( + result.stdout + .toString("utf8") + .split("\0") + .filter(Boolean) + .map((path) => path.replaceAll("\\", "/")), + ); +} + +function runGit(args, { allowFailure = false, input } = {}) { const result = spawnSync("git", args, { cwd: repositoryRoot, + input, maxBuffer: 64 * 1024 * 1024, windowsHide: true, }); diff --git a/scripts/check-fixture-byte-integrity.test.mjs b/scripts/check-fixture-byte-integrity.test.mjs index 74bd999..ba8023e 100644 --- a/scripts/check-fixture-byte-integrity.test.mjs +++ b/scripts/check-fixture-byte-integrity.test.mjs @@ -34,3 +34,22 @@ test("an unregistered fixture directory fails the byte-integrity gate", async () await rm(directory, { force: true, recursive: true }); } }); + +test("a git-ignored fixture directory is outside the byte-integrity inventory", async () => { + const ignoredRoot = join(root, "dist"); + await mkdir(ignoredRoot, { recursive: true }); + const directory = await mkdtemp(join(ignoredRoot, ".fixture-integrity-")); + const fixtureDirectory = join(directory, "fixtures"); + + try { + await mkdir(fixtureDirectory, { recursive: true }); + await writeFile(join(fixtureDirectory, "synthetic.xml"), "\n"); + assert.doesNotThrow(() => execFileSync( + process.execPath, + ["scripts/check-fixture-byte-integrity.mjs"], + { cwd: root, encoding: "utf8", stdio: "pipe" }, + )); + } finally { + await rm(directory, { force: true, recursive: true }); + } +}); diff --git a/scripts/client-grouping.test.mjs b/scripts/client-grouping.test.mjs index 6805f40..88b8784 100644 --- a/scripts/client-grouping.test.mjs +++ b/scripts/client-grouping.test.mjs @@ -4,7 +4,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; -import { applyClientGroupLabel, groupClientRows, rollbackFailedClientGroupLabel, sumExactDecimals } from "../src/client-grouping.ts"; +import { applyClientGroupLabel, groupClientRows, reconcileLoadedSortPreference, rollbackFailedClientGroupLabel, sumExactDecimals } from "../src/client-grouping.ts"; test("a failed optimistic label save restores only the value that actually failed", () => { const persisted = { "synthetic-company-guid": "Original" }; @@ -27,6 +27,13 @@ test("a failed optimistic label save restores only the value that actually faile ); }); +test("a late preference load cannot overwrite a sort chosen during startup", () => { + const current = { key: "client", desc: false }; + const persisted = { key: "overdue", desc: true }; + assert.deepEqual(reconcileLoadedSortPreference(current, persisted, true), current); + assert.deepEqual(reconcileLoadedSortPreference(current, persisted, false), persisted); +}); + test("applying a group label preserves every company figure byte-for-byte", () => { const row = { companyGuid: "synthetic-company-guid", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d5f044e..1259dd9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -94,4 +94,4 @@ zip = { version = "8.6.0", default-features = false, features = ["deflate"] } libc = "0.2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_JobObjects", "Win32_System_Threading"] } +windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_JobObjects", "Win32_System_Threading"] } diff --git a/src-tauri/src/client_groups.rs b/src-tauri/src/client_groups.rs index f20b017..2f37c35 100644 --- a/src-tauri/src/client_groups.rs +++ b/src-tauri/src/client_groups.rs @@ -176,7 +176,7 @@ fn write_file_atomically(path: &Path, contents: &[u8]) -> Result<(), std::io::Er return Err(error); } drop(file); - if let Err(error) = std::fs::rename(&temporary, path) { + if let Err(error) = replace_file(&temporary, path) { let _ = std::fs::remove_file(&temporary); return Err(error); } @@ -188,6 +188,45 @@ fn write_file_atomically(path: &Path, contents: &[u8]) -> Result<(), std::io::Er )) } +#[cfg(not(windows))] +fn replace_file(temporary: &Path, destination: &Path) -> Result<(), std::io::Error> { + std::fs::rename(temporary, destination) +} + +#[cfg(windows)] +fn replace_file(temporary: &Path, destination: &Path) -> Result<(), std::io::Error> { + use std::os::windows::ffi::OsStrExt as _; + use windows_sys::Win32::Storage::FileSystem::{ + MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, + }; + + let temporary = temporary + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + let destination = destination + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + // SAFETY: both buffers are NUL-terminated and remain alive for the call. + // The source is a newly created sibling owned by this process. The flags + // provide Windows' replace-existing behavior and request durable metadata. + let replaced = unsafe { + MoveFileExW( + temporary.as_ptr(), + destination.as_ptr(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH, + ) + }; + if replaced == 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } +} + fn temporary_label_path(parent: &Path, file_name: &str, sequence: u32) -> PathBuf { parent.join(format!( ".{file_name}.{}.{}.tmp", @@ -352,6 +391,35 @@ mod tests { ); } + #[test] + fn replacement_sort_write_keeps_only_the_latest_complete_preference() { + let directory = tempfile::tempdir().expect("temporary config directory"); + save_sort_preference( + directory.path(), + ClientSortPreference { + key: ClientSortKey::Overdue, + desc: true, + }, + ) + .expect("first sort preference"); + let latest = ClientSortPreference { + key: ClientSortKey::Client, + desc: false, + }; + save_sort_preference(directory.path(), latest.clone()).expect("replacement preference"); + + assert_eq!(load_sort_preference(directory.path()), Some(latest)); + assert!(directory + .path() + .read_dir() + .expect("directory entries") + .all(|entry| !entry + .expect("directory entry") + .file_name() + .to_string_lossy() + .ends_with(".tmp"))); + } + #[test] fn sort_storage_remains_readable_by_the_old_v1_label_contract() { #[derive(Deserialize)] diff --git a/src/AllClientsScreen.tsx b/src/AllClientsScreen.tsx index 490c04e..4a7ec8d 100644 --- a/src/AllClientsScreen.tsx +++ b/src/AllClientsScreen.tsx @@ -3,7 +3,7 @@ import React from "react"; import { ChevronRight, RefreshCw } from "lucide-react"; import { invoke } from "@tauri-apps/api/core"; -import { applyClientGroupLabel, ClientGroupLabels, groupClientRows, rollbackFailedClientGroupLabel } from "./client-grouping"; +import { applyClientGroupLabel, ClientGroupLabels, groupClientRows, reconcileLoadedSortPreference, rollbackFailedClientGroupLabel } from "./client-grouping"; import { outstandingsPartialState } from "./outstandings-copy"; type CompanyRef = { name: string; guid: string }; @@ -95,6 +95,7 @@ export function AllClientsScreen({ config, companies, onOpenCompany, onBack }: P const [groupLabelError, setGroupLabelError] = React.useState(null); const persistedGroupLabels = React.useRef({}); const requestVersion = React.useRef(0); + const sortChangedDuringLoad = React.useRef(false); React.useEffect(() => { let active = true; @@ -122,7 +123,13 @@ export function AllClientsScreen({ config, companies, onOpenCompany, onBack }: P let active = true; void invoke("load_client_sort_preference") .then((preference) => { - if (active && isSortPreference(preference)) setSort(preference); + if (active && isSortPreference(preference)) { + setSort((current) => reconcileLoadedSortPreference( + current, + preference, + sortChangedDuringLoad.current, + )); + } }) // Sorting is an optional local display preference. Keep the safe default // if its config file cannot be read. @@ -266,6 +273,7 @@ export function AllClientsScreen({ config, companies, onOpenCompany, onBack }: P const changeSort = React.useCallback((key: SortKey) => { const next = sort.key === key ? { key, desc: !sort.desc } : { key, desc: key !== "client" }; + sortChangedDuringLoad.current = true; setSort(next); void invoke("save_client_sort_preference", { preference: next }).catch(() => {}); }, [sort]); diff --git a/src/client-grouping.ts b/src/client-grouping.ts index 110e4e9..294004b 100644 --- a/src/client-grouping.ts +++ b/src/client-grouping.ts @@ -26,6 +26,14 @@ export function rollbackFailedClientGroupLabel( return applyClientGroupLabel(current, companyGuid, persisted[companyGuid] ?? ""); } +export function reconcileLoadedSortPreference( + current: Sort, + persisted: Sort, + userChangedSort: boolean, +): Sort { + return userChangedSort ? current : persisted; +} + export type GroupableClientRow = { companyGuid: string; exactAmounts: { From 2f3207e60795387b4581f4ba170fa6913b2394de Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Tue, 18 Aug 2026 11:57:47 +0530 Subject: [PATCH 6/9] chore(tally): reseal F5X N1.3 compatibility surface --- .../compatibility/compatibility-matrix.json | 2 +- .../compatibility/compatibility-surface.json | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 98cb9ed..e42b72b 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "80d542fa2c69def9d27ed48cf349863ead7b34c72f5e496d379276e8a2dab47e", + "compatibility_surface_sha256": "b13527e85f20384f0e743f079a22855fe628faa4f7e6c1532555402c9d09f2f8", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 6851d06..534c993 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -63,7 +63,7 @@ }, { "path": "docs/tally/compatibility/synthetic-write-canary-fixture.md", - "sha256": "dd2f1c68c0925523af1468dd9c61330433130c0e72b7c713dfc1ef9205b4756f" + "sha256": "9ed85e60adb7306f11496b47f5a86d49327abfdd8e2735678c521187b9ce76e4" }, { "path": "docs/tally/support-matrix.md", @@ -79,7 +79,7 @@ }, { "path": "scripts/outstandings-copy.test.mjs", - "sha256": "58ca3cb255a8e54dc3a4590146b908b800fd4186d474eb05c3e1a46ae6c005a0" + "sha256": "8c9adb100a45704ca8ba72203a397defca3e8b01a297e96043c543cc052b980a" }, { "path": "scripts/tally-company-selection.test.mjs", @@ -99,7 +99,7 @@ }, { "path": "src-tauri/Cargo.toml", - "sha256": "3bb11f1d204fa55443e80686a1def19210b8756ace49585cb308b27e649b52a1" + "sha256": "c9bd8222ce43a2c6eb09efd4c61c2ac8093c581806a8b8776f3f5c42e5130ea3" }, { "path": "src-tauri/crates/bridge-tally-core/Cargo.toml", @@ -143,7 +143,7 @@ }, { "path": "src-tauri/crates/bridge-tally-protocol/src/lib.rs", - "sha256": "c5c61049fdbedf31cfafb43d349961e736eb7c6a361ec08e410f2f94099b1200" + "sha256": "e1c9082a214a125454c2bbddef8494283d41efa025e4acfc1525f0a22aa2bd1e" }, { "path": "src-tauri/crates/bridge-tally-protocol/src/outstandings/completeness.rs", @@ -175,7 +175,7 @@ }, { "path": "src-tauri/crates/bridge-tally-protocol/tests/simulator_corpus.rs", - "sha256": "65da6e2a0cf543c46e1834d82f836b2966f9f574952b991259179cf743f907d6" + "sha256": "616eb8fa5e387bff74f62d8e775b7eb118860763b18ff63ad88ad5015f7f752f" }, { "path": "src-tauri/crates/bridge-tally-protocol/tests/stream_text_decoder.rs", @@ -219,7 +219,7 @@ }, { "path": "src-tauri/src/commands.rs", - "sha256": "a1956c6cb5039559b5dbd06b68f7041399a68901495c5c4d900adc6ff766a268" + "sha256": "1788dbf6f84c47399ce645989468e93b5b9d41e59cfac11639d3caa96bf3adb7" }, { "path": "src-tauri/src/db/encrypted.rs", @@ -339,7 +339,7 @@ }, { "path": "src-tauri/src/tally/runtime.rs", - "sha256": "f6c4a1507b00c2689c128be4991af14af7cbd3ad2cec684673bebff9bb24a3c9" + "sha256": "ac652a70cd3b0b7184733ea08f6d8967f5b25f357d171d96a5c6d96f818d0563" }, { "path": "src-tauri/src/tally/serial_queue.rs", @@ -359,7 +359,7 @@ }, { "path": "src/OutstandingsScreen.tsx", - "sha256": "479aff297f63050a4903154e565062f774057bec2b9c452b81c5934782f9ea5e" + "sha256": "6835d23a31eb92e1037e8f1e9c268ea86c59554a6b7060009e4dfa086bfa03d6" }, { "path": "src/TallyReadinessFlow.tsx", @@ -371,7 +371,7 @@ }, { "path": "src/outstandings-copy.ts", - "sha256": "0e78dd97d79c0166fe17a4e933733c369a7e4be3046ddbe3e38122ad5e234216" + "sha256": "a2bcf820f694443ac122e91aa98fb354b6e2d5c6be3e1e9503b368a3a0a4f696" }, { "path": "src/outstandings-csv.ts", @@ -434,5 +434,5 @@ "sha256": "a27f294ee15e407b69fdfc73609e8708ac0509b6e6a8872daef5451fde61a8db" } ], - "manifest_sha256": "80d542fa2c69def9d27ed48cf349863ead7b34c72f5e496d379276e8a2dab47e" + "manifest_sha256": "b13527e85f20384f0e743f079a22855fe628faa4f7e6c1532555402c9d09f2f8" } From 3bee12ca72b1152df6b41435ea0cb025b62aed86 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Tue, 18 Aug 2026 12:10:59 +0530 Subject: [PATCH 7/9] chore(tally): reseal F5X N13 after clippy repair --- docs/tally/compatibility/compatibility-matrix.json | 2 +- docs/tally/compatibility/compatibility-surface.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index e42b72b..22c09f6 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "b13527e85f20384f0e743f079a22855fe628faa4f7e6c1532555402c9d09f2f8", + "compatibility_surface_sha256": "8ac77a13332a76dca4faffa98735ae7b5dc3cb4db4d39a8264d8e6bd37fb0fc6", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 534c993..5e8bad2 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -339,7 +339,7 @@ }, { "path": "src-tauri/src/tally/runtime.rs", - "sha256": "ac652a70cd3b0b7184733ea08f6d8967f5b25f357d171d96a5c6d96f818d0563" + "sha256": "893bf325dcd614d49ac661127333c0cf714a937776c609a442fbccc240d06da1" }, { "path": "src-tauri/src/tally/serial_queue.rs", @@ -434,5 +434,5 @@ "sha256": "a27f294ee15e407b69fdfc73609e8708ac0509b6e6a8872daef5451fde61a8db" } ], - "manifest_sha256": "b13527e85f20384f0e743f079a22855fe628faa4f7e6c1532555402c9d09f2f8" + "manifest_sha256": "8ac77a13332a76dca4faffa98735ae7b5dc3cb4db4d39a8264d8e6bd37fb0fc6" } From 8a5617edb9c7cbb415bce3bc112e3944c67b030d Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Tue, 18 Aug 2026 12:41:04 +0530 Subject: [PATCH 8/9] chore(tally): reseal F5X N13 after PR7 compile repair --- docs/tally/compatibility/compatibility-matrix.json | 2 +- docs/tally/compatibility/compatibility-surface.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 22c09f6..5cf7bb0 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "8ac77a13332a76dca4faffa98735ae7b5dc3cb4db4d39a8264d8e6bd37fb0fc6", + "compatibility_surface_sha256": "ac1117781b854ad6f82849b977f7a5954ffec5d4669c8c4327399d535d719c34", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 5e8bad2..8d9eb5c 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -339,7 +339,7 @@ }, { "path": "src-tauri/src/tally/runtime.rs", - "sha256": "893bf325dcd614d49ac661127333c0cf714a937776c609a442fbccc240d06da1" + "sha256": "1d2ae890a240344ad9a2a59b4624be2726d13c11e5e529a0b084e355f865deec" }, { "path": "src-tauri/src/tally/serial_queue.rs", @@ -434,5 +434,5 @@ "sha256": "a27f294ee15e407b69fdfc73609e8708ac0509b6e6a8872daef5451fde61a8db" } ], - "manifest_sha256": "8ac77a13332a76dca4faffa98735ae7b5dc3cb4db4d39a8264d8e6bd37fb0fc6" + "manifest_sha256": "ac1117781b854ad6f82849b977f7a5954ffec5d4669c8c4327399d535d719c34" } From 6112b18c76a3189533d560f29dc116321678ce8a Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Wed, 19 Aug 2026 00:35:19 +0530 Subject: [PATCH 9/9] chore(tally): reseal RB1 N13 compatibility surface Carry the h2 0.4.16 master dependency surface through the N13 stack level. Claims, evidence, and trusted keys are unchanged. Compatibility gate: exit 0, unknown_claims=11, evidenced_claims=0. --- docs/tally/compatibility/compatibility-matrix.json | 2 +- docs/tally/compatibility/compatibility-surface.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 5cf7bb0..09099f4 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "ac1117781b854ad6f82849b977f7a5954ffec5d4669c8c4327399d535d719c34", + "compatibility_surface_sha256": "225d2dfef642da0ee454a240247d616eba2ed60e19fa139ebbc5d9cff06fe0c1", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index 8d9eb5c..0bd5f7b 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -95,7 +95,7 @@ }, { "path": "src-tauri/Cargo.lock", - "sha256": "59012d808bcc29d0abe31d74c6f97d7afe2aa6acd33ec188fabf61d8b92f10cc" + "sha256": "7aac9a6af2fdab6e117af7d3aa26ba04434701fe02d3010010fa3f3793236212" }, { "path": "src-tauri/Cargo.toml", @@ -434,5 +434,5 @@ "sha256": "a27f294ee15e407b69fdfc73609e8708ac0509b6e6a8872daef5451fde61a8db" } ], - "manifest_sha256": "ac1117781b854ad6f82849b977f7a5954ffec5d4669c8c4327399d535d719c34" + "manifest_sha256": "225d2dfef642da0ee454a240247d616eba2ed60e19fa139ebbc5d9cff06fe0c1" }