Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions docs/provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/tally/compatibility/compatibility-matrix.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e",
"compatibility_surface_sha256": "ded2577b0f66b63d6ea65fe3d315d69dce68b0fa930f2b6d9106b7e0b52b526d",
"compatibility_surface_sha256": "225d2dfef642da0ee454a240247d616eba2ed60e19fa139ebbc5d9cff06fe0c1",
"claims": [
{
"claim_id": "erp9-6-6-3-windows-education-xml-one-company",
Expand Down
12 changes: 6 additions & 6 deletions docs/tally/compatibility/compatibility-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": [
{
"path": ".github/workflows/ci.yml",
"sha256": "309fe37d7038a4a1eb6197edf32f652af50ded2017a55d55466e577b878b05ed"
"sha256": "04c2796dc7b382a0582c82d0bc86f3b9b819b7bf46df6cf23a36b385f0f2e832"
},
{
"path": ".github/workflows/dependency-security.yml",
Expand Down Expand Up @@ -71,7 +71,7 @@
},
{
"path": "package.json",
"sha256": "bc58b4532fc7279bdbaef868ff4ecda87fcb353647a2cbac7dd7cd06ee423cf3"
"sha256": "d6a5107c4c556fbf7e494f98b743d737cc47fa8bd01cecdc942066c53b4d5e86"
},
{
"path": "scripts/check-tally-live-read-boundary.mjs",
Expand Down Expand Up @@ -99,7 +99,7 @@
},
{
"path": "src-tauri/Cargo.toml",
"sha256": "3bb11f1d204fa55443e80686a1def19210b8756ace49585cb308b27e649b52a1"
"sha256": "c9bd8222ce43a2c6eb09efd4c61c2ac8093c581806a8b8776f3f5c42e5130ea3"
},
{
"path": "src-tauri/crates/bridge-tally-core/Cargo.toml",
Expand Down Expand Up @@ -219,7 +219,7 @@
},
{
"path": "src-tauri/src/commands.rs",
"sha256": "8d13e54b487bf27984ed988abee126d90e63a21a72d12aa21a819796bff4c569"
"sha256": "1788dbf6f84c47399ce645989468e93b5b9d41e59cfac11639d3caa96bf3adb7"
},
{
"path": "src-tauri/src/db/encrypted.rs",
Expand Down Expand Up @@ -303,7 +303,7 @@
},
{
"path": "src-tauri/src/lib.rs",
"sha256": "5582994d94163235a885bce0d44eadc11b6d8eb9a36898610e94c2b1141efbae"
"sha256": "5a2e2c35da52a7c3b01c5810882a1eb3a9ea45fac82556b1eee1a715989585b9"
},
{
"path": "src-tauri/src/sync/coordinator.rs",
Expand Down Expand Up @@ -434,5 +434,5 @@
"sha256": "a27f294ee15e407b69fdfc73609e8708ac0509b6e6a8872daef5451fde61a8db"
}
],
"manifest_sha256": "ded2577b0f66b63d6ea65fe3d315d69dce68b0fa930f2b6d9106b7e0b52b526d"
"manifest_sha256": "225d2dfef642da0ee454a240247d616eba2ed60e19fa139ebbc5d9cff06fe0c1"
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
lamemustafa marked this conversation as resolved.
"check": "pnpm run build && pnpm run cargo:check",
"tauri": "tauri",
"tauri:dev": "tauri dev",
Expand Down
4 changes: 0 additions & 4 deletions scripts/check-dependency-inventory.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]);
Expand Down
52 changes: 51 additions & 1 deletion scripts/check-fixture-byte-integrity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ const fixtureDirectories = [
"src-tauri/crates/tally-protocol-simulator/fixtures",
"docs/tally/compatibility/fixtures",
];
Comment thread
lamemustafa marked this conversation as resolved.
const repositoryDirectories = walkDirectories(repositoryRoot)
.map((directory) => relative(repositoryRoot, directory).replaceAll("\\", "/"));
const ignoredDirectories = gitIgnoredPaths(repositoryDirectories);
Comment thread
lamemustafa marked this conversation as resolved.
const discoveredFixtureDirectories = repositoryDirectories
.filter((directory) => !ignoredDirectories.has(directory))
.filter((directory) => directory.endsWith("/fixture") || directory.endsWith("/fixtures"))
Comment thread
lamemustafa marked this conversation as resolved.
Comment thread
lamemustafa marked this conversation as resolved.
.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));
Expand Down Expand Up @@ -40,6 +57,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
Comment thread
lamemustafa marked this conversation as resolved.
Expand Down Expand Up @@ -72,9 +93,38 @@ function walkFiles(directory) {
return paths;
}

function runGit(args, { allowFailure = false } = {}) {
function walkDirectories(directory) {
const directories = [];
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (!entry.isDirectory() || [".git", "node_modules", "target"].includes(entry.name)) continue;
Comment thread
lamemustafa marked this conversation as resolved.
Comment thread
lamemustafa marked this conversation as resolved.
const path = join(directory, entry.name);
directories.push(path, ...walkDirectories(path));
}
return directories;
}

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,
});
Expand Down
55 changes: 55 additions & 0 deletions scripts/check-fixture-byte-integrity.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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"), "<fixture />\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 });
}
});

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"), "<ignored-fixture />\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 });
}
});
9 changes: 8 additions & 1 deletion scripts/client-grouping.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Loading