ci: relax file-size ceilings by surface (#6485)

## Summary

- raise Desktop Rust's differential file-size ceiling from 1,000 to
1,500 lines
- raise Desktop frontend and Mobile ceilings from 1,000 to 1,200 lines
- keep Web at 1,000 lines and preserve the existing no-growth ratchet
above each ceiling

## Why

The flat 1,000-line limit is forcing mechanical trimming in Desktop and
Mobile even for small cohesive changes. Surface-specific ceilings
relieve that pressure without granting Web or every authored component a
blanket 2,000-line budget.

## Testing

- `just file-size-check`
- `cd desktop && pnpm exec biome check scripts/check-file-sizes.mjs`
- `node --check mobile/scripts/check-file-sizes.mjs`
- `git diff HEAD^ --check`

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-09-01 13:14:15 -06:00
committed by GitHub
parent b270437a62
commit 4794a5cecf
33 changed files with 368 additions and 132 deletions

View File

@@ -674,11 +674,13 @@ The mobile app lives in `mobile/` — a Flutter app using Riverpod + Hooks.
over raw `Theme.of(context)` calls.
- **Keep widgets small and composable.** One public widget per file; push
private sub-widgets (`_Foo`) into sibling `part` files under a
`<page>/` folder rather than growing the page file. Hard ceiling:
**1000 lines/file**, enforced across Desktop, Web, and Mobile by the
`<page>/` folder rather than growing the page file. Mobile's hard ceiling is
**1200 lines/file**, enforced with the other surface-specific limits by the
repository-level `just file-size-check` gate (`just check`, CI, and every
pre-push). If the guard trips, **split the file — never bump the limit or add
an override to slip under it.**
pre-push). If an individual file trips the guard, **split the file — never
bump a surface limit or add an override merely to admit that file.**
Deliberate repository-wide policy revisions must update the enforced rules,
tests, and guidance together.
- Feature modules must not import from other feature modules — only from
`shared/`.
- Use `Grid` tokens for spacing, `Radii` for border radius.

View File

@@ -1,61 +1,21 @@
import { realpathSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runFileSizeCheck } from "../../scripts/check-file-sizes-core.mjs";
import { rules } from "./file-size-policy.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const scriptPath = realpathSync(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(path.dirname(scriptPath), "..");
const MAX_LINES = 1000;
const rules = [
{ root: "src-tauri/src", extensions: new Set([".rs"]), maxLines: MAX_LINES },
// Workspace member crates. Without this the ratchet's only Rust root is
// `src-tauri/src`, and a crate under `src-tauri/crates/` is born outside the
// repo's one size discipline -- silently, since the check still exits 0.
{
root: "src-tauri/crates",
extensions: new Set([".rs"]),
maxLines: MAX_LINES,
},
{
root: "src/app",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/features",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/shared/api",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/shared/context",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/shared/lib",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/shared/ui",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/shared/styles",
extensions: new Set([".css"]),
maxLines: MAX_LINES,
},
];
await runFileSizeCheck({
export const policy = {
projectRoot,
rules,
label: "Desktop",
});
};
if (
process.argv[1] &&
realpathSync(path.resolve(process.argv[1])) === scriptPath
) {
await runFileSizeCheck(policy);
}

View File

@@ -0,0 +1,53 @@
const DESKTOP_FRONTEND_MAX_LINES = 1200;
const DESKTOP_RUST_MAX_LINES = 1500;
export const rules = [
{
root: "src-tauri/src",
extensions: new Set([".rs"]),
maxLines: DESKTOP_RUST_MAX_LINES,
},
// Workspace member crates. Without this the ratchet's only Rust root is
// `src-tauri/src`, and a crate under `src-tauri/crates/` is born outside the
// repo's one size discipline -- silently, since the check still exits 0.
{
root: "src-tauri/crates",
extensions: new Set([".rs"]),
maxLines: DESKTOP_RUST_MAX_LINES,
},
{
root: "src/app",
extensions: new Set([".ts", ".tsx"]),
maxLines: DESKTOP_FRONTEND_MAX_LINES,
},
{
root: "src/features",
extensions: new Set([".ts", ".tsx"]),
maxLines: DESKTOP_FRONTEND_MAX_LINES,
},
{
root: "src/shared/api",
extensions: new Set([".ts", ".tsx"]),
maxLines: DESKTOP_FRONTEND_MAX_LINES,
},
{
root: "src/shared/context",
extensions: new Set([".ts", ".tsx"]),
maxLines: DESKTOP_FRONTEND_MAX_LINES,
},
{
root: "src/shared/lib",
extensions: new Set([".ts", ".tsx"]),
maxLines: DESKTOP_FRONTEND_MAX_LINES,
},
{
root: "src/shared/ui",
extensions: new Set([".ts", ".tsx"]),
maxLines: DESKTOP_FRONTEND_MAX_LINES,
},
{
root: "src/shared/styles",
extensions: new Set([".css"]),
maxLines: DESKTOP_FRONTEND_MAX_LINES,
},
];

View File

@@ -9,7 +9,7 @@
//! via [`AgentMetricIndexRow::from_payload`].
//!
//! Kept in a sibling file (not `store.rs`) to keep that file under the
//! 1000-line gate, per the existing `pipeline.rs` precedent.
//! 1500-line gate, per the existing `pipeline.rs` precedent.
use rusqlite::{params, Connection, OptionalExtension};

View File

@@ -1,7 +1,7 @@
//! Kind-44200 (NIP-AM agent turn metric) archive and `get_agent_usage_series`
//! integration tests for `archive/mod.rs`.
//!
//! Kept in a sibling file so `mod_tests.rs` stays under the 1000-line gate;
//! Kept in a sibling file so `mod_tests.rs` stays under the 1500-line gate;
//! `#[path]`-included from there so the shared fixtures (`in_memory`,
//! `add_sub`, `candidate`, `make_observer_frame`, `run_batch_sync_with_keys`)
//! stay private to `mod_tests`.

View File

@@ -1,6 +1,6 @@
//! Unit and integration tests for `archive/mod.rs`.
//!
//! Kept in a sibling file so `mod.rs` stays under the 1000-line gate;
//! Kept in a sibling file so `mod.rs` stays under the 1500-line gate;
//! `#[path]`-included from there.
use super::pipeline::BucketWithResult;
@@ -622,7 +622,7 @@ fn test_commit_archive_rolls_back_when_scope_write_would_fail() {
}
// Kind-44200 agent-turn-metric coverage lives in a sibling file to keep this
// one under the 1000-line gate; nested here (not in `mod.rs`) so it inherits
// one under the 1500-line gate; nested here (not in `mod.rs`) so it inherits
// the shared fixtures above through `use super::*`.
#[path = "mod_agent_metric_tests.rs"]
mod agent_metric;

View File

@@ -1,6 +1,6 @@
//! Archive pipeline — three-phase plan/query/commit split.
//!
//! Separated from `mod.rs` to keep that file under the 1000-line gate.
//! Separated from `mod.rs` to keep that file under the 1500-line gate.
//!
//! # Send-safety
//!

View File

@@ -10,7 +10,7 @@
//! Phase-2 prune scan, the get/set accessors for the observer window, and the
//! PRAGMA-based size readout. The prune worker itself lands in Phase 2.
//!
//! Kept in a sibling file (not `store.rs`) to respect the 1000-line gate, per
//! Kept in a sibling file (not `store.rs`) to respect the 1500-line gate, per
//! the existing `metric_store.rs` / `pipeline.rs` / `store_migrations.rs`
//! precedent.

View File

@@ -1,7 +1,7 @@
//! Behavior tests for the observer-retention setting, the size readout, and the
//! M4 migration.
//!
//! Kept in a sibling file so `retention.rs` stays under the 1000-line gate;
//! Kept in a sibling file so `retention.rs` stays under the 1500-line gate;
//! `#[path]`-included from there. `super::*` brings the retention API (and its
//! `rusqlite::{params, Connection}` imports) into scope; `super::super::store`
//! reaches the neighbouring subscription mutators and the base `SCHEMA`.

View File

@@ -1,6 +1,6 @@
//! Migration tests for `archive/store.rs` — M1: harness column.
//!
//! Kept in a sibling file so `store_tests.rs` stays under the 1000-line gate;
//! Kept in a sibling file so `store_tests.rs` stays under the 1500-line gate;
//! `#[path]`-included from `store.rs`.
use super::*;

View File

@@ -4,7 +4,7 @@
//! `archive_migrations`, so a migration that already ran is a no-op.
//!
//! Kept in a sibling file (not `store.rs`) to keep that file under the
//! 1000-line gate, per the existing `metric_store.rs` / `pipeline.rs`
//! 1500-line gate, per the existing `metric_store.rs` / `pipeline.rs`
//! precedent.
use rusqlite::{params, Connection};

View File

@@ -1,6 +1,6 @@
//! Unit tests for `archive/store.rs`.
//!
//! Kept in a sibling file so `store.rs` stays under the 1000-line gate;
//! Kept in a sibling file so `store.rs` stays under the 1500-line gate;
//! `#[path]`-included from there.
use super::*;

View File

@@ -1,5 +1,5 @@
//! Unit tests for `commands/agent_config.rs` (split to keep `agent_config.rs`
//! under the 1000-line file-size ratchet).
//! under the 1500-line file-size ratchet).
//!
//! Included via `#[path = "agent_config_tests.rs"] mod tests;` at the bottom of
//! `agent_config.rs`, so `use super::*` gives access to all items in that module.

View File

@@ -1,5 +1,5 @@
//! Unit tests for `card.rs` — split into a child module file so the parent
//! stays under the 1000-line gate (same layout as `snapshot/tests.rs`).
//! stays under the 1500-line gate (same layout as `snapshot/tests.rs`).
use super::*;
use std::collections::BTreeMap;

View File

@@ -2,7 +2,7 @@
//! and their supporting helpers.
//!
//! Import-side commands and helpers live in `snapshot::import` to keep this
//! file under the 1000-line gate.
//! file under the 1500-line gate.
//!
//! Split from `personas/mod.rs` to keep that file under the line-count gate.

View File

@@ -1,6 +1,6 @@
//! Import-side helpers for `buzz-agent-snapshot v1`.
//!
//! Extracted from `snapshot.rs` to keep that file under the 1000-line gate.
//! Extracted from `snapshot.rs` to keep that file under the 1500-line gate.
//! The Tauri commands here (`preview_agent_snapshot_import`,
//! `confirm_agent_snapshot_import`) are re-exported from `snapshot.rs` and
//! registered in `lib.rs` through the same `personas::` path as the export

View File

@@ -1,7 +1,7 @@
//! Export-size guard tests for `validate_snapshot_encode_size`.
//!
//! Kept in a sibling file so `snapshot/tests.rs` stays under the
//! 1000-line gate; `#[path]`-included from there as a child module,
//! 1500-line gate; `#[path]`-included from there as a child module,
//! so `super::*` still resolves to the shared test imports.
//!
//! Tests call `validate_snapshot_encode_size` directly so they prove the

View File

@@ -1,7 +1,7 @@
//! Locked-card import tests for `decode_snapshot_for_import`.
//!
//! Kept in a sibling file so `snapshot/tests.rs` stays under the
//! 1000-line gate; `#[path]`-included from there as a child module,
//! 1500-line gate; `#[path]`-included from there as a child module,
//! so `super::*` still resolves to the shared test helpers.
use super::*;

View File

@@ -1,6 +1,6 @@
//! Tests for `memory_entries_from_listing` — the shared level → entries
//! selection used by both snapshot export and card minting. Split from
//! `tests.rs` to keep that file under the 1000-line gate; `#[path]`-included
//! `tests.rs` to keep that file under the 1500-line gate; `#[path]`-included
//! from there as a child module, so `super::*` resolves to `tests`'s parent
//! scope re-exports.

View File

@@ -1,7 +1,7 @@
//! Unit tests for `managed_agents/agent_snapshot.rs`.
//!
//! Kept in a sibling file so `agent_snapshot.rs` stays under the
//! 1000-line gate; `#[path]`-included from there.
//! 1500-line gate; `#[path]`-included from there.
use super::*;
use crate::managed_agents::types::{BackendKind, ManagedAgentRecord, RespondTo};

View File

@@ -1,5 +1,5 @@
//! Unit tests for `config_bridge/reader.rs` (kept in a sibling file so
//! `reader.rs` stays under the 1000-line budget; `#[path]`-included from
//! `reader.rs` stays under the 1500-line budget; `#[path]`-included from
//! there).
use std::{collections::BTreeMap, path::Path, sync::Mutex};

View File

@@ -1,5 +1,5 @@
//! Additional tests for `config_bridge/reader.rs` — split out to keep
//! `reader_tests.rs` under the 1000-line file-size ratchet.
//! `reader_tests.rs` under the 1500-line file-size ratchet.
//!
//! Included as `mod ext` inside `reader_tests.rs`, so `use super::*` gives
//! access to all helpers and types from that module.

View File

@@ -1,5 +1,5 @@
//! B5 effort lifecycle tests split out of `spawn_snapshot/tests.rs` to hold
//! that file under the 1000-line file-size ratchet.
//! that file under the 1500-line file-size ratchet.
//!
//! Included as `mod ext` inside `tests.rs`, so `use super::*` gives access to
//! its `record`, `snap`, and `record_with_env_effort` helpers.

View File

@@ -1,6 +1,6 @@
//! Unit tests for `managed_agents/storage.rs`.
//!
//! Kept in a sibling file so `storage.rs` stays closer to the 1000-line gate;
//! Kept in a sibling file so `storage.rs` stays closer to the 1500-line gate;
//! `#[path]`-included from there.
use std::cell::RefCell;

View File

@@ -1,6 +1,6 @@
//! Unit tests for `managed_agents/teams.rs`.
//!
//! Kept in a sibling file so `teams.rs` stays under the 1000-line gate;
//! Kept in a sibling file so `teams.rs` stays under the 1500-line gate;
//! `#[path]`-included from there.
use super::{

View File

@@ -153,8 +153,8 @@ with a TypeScript lookup table or an id comparison in a component.
place that resolves it for dialog surfaces and publishes it through
`ui/AgentRunLocationContext.tsx`; the field reads that context and lets an
explicit `runLocation` prop win. Do **not** thread the value as a prop
through `AgentDefinitionDialog` / `AgentInstanceEditDialog` — both are
already over the 1000-line ceiling, and neither uses the value itself.
through `AgentDefinitionDialog` / `AgentInstanceEditDialog` — neither uses
the value itself, and the shared context keeps the dialog boundary stable.
Surfaces rendered outside `AgentDialog` (e.g. `EditRespondToDialog`) pass the
prop directly. Local names "your
computer, including files, accounts, and connected tools"; remote names "the
@@ -216,9 +216,9 @@ with a TypeScript lookup table or an id comparison in a component.
cosmetic — the Rust command rejects non-local backends because remote effort
is set at deploy time via `policy_env`. Because it reads its inputs from the
config surface the dialog already fetches (`useAgentConfigSurface`) and owns
its own mutation, it does **not** thread new props through the over-1000-line
dialog (see rule 11): keep effort state inside the section component, never
as dialog-level props. The read-only display is the `thinkingEffort`
its own mutation, it does **not** thread new props through the dialog (see
rule 11): keep effort state inside the section component, never as
dialog-level props. The read-only display is the `thinkingEffort`
normalized field rendered by `AgentConfigPanel` via `NormalizedRow`, which
already shows both facts — `field.value` (canonical, the effort the next
spawn will launch with) and, when a running ACP session differs,

View File

@@ -150,7 +150,7 @@ class _ChannelRefreshFence {
/// nothing awaits it, so a throw would only surface as an unhandled error.
///
/// An extension in this part file rather than a method on the notifier because
/// `channels_provider.dart` sits against the repository-wide 1000-line file
/// `channels_provider.dart` sits against the repository-wide 1200-line file
/// ceiling enforced by `just file-size-check`.
extension _CatchUpFencing on ChannelsNotifier {
bool _isCatchUpRetired(
@@ -188,7 +188,7 @@ Future<T> _fenced<T>(_ChannelRefreshFence fence, Future<T> future) async {
/// profiles in one round-trip. Returns lowercase pubkey to label.
///
/// Lives in this part file because `channels_provider.dart` sits against the
/// repository-wide 1000-line file ceiling enforced by `just file-size-check`.
/// repository-wide 1200-line file ceiling enforced by `just file-size-check`.
Future<Map<String, String>> _resolveDmDisplayNames(
RelaySessionNotifier session,
_ChannelRefreshFence fence,
@@ -271,7 +271,7 @@ Future<List<NostrEvent>> _fetchHuddleStarts(
/// Counts distinct `p`-tagged members per channel from kind:39002 events.
///
/// Lives in this part file to keep `channels_provider.dart` under the
/// repository-wide 1000-line ceiling enforced by `just file-size-check`.
/// repository-wide 1200-line ceiling enforced by `just file-size-check`.
Map<String, int> _memberCountsByChannelId(Iterable<NostrEvent> memberEvents) {
final memberCounts = <String, int>{};
for (final event in memberEvents) {
@@ -297,7 +297,7 @@ Map<String, int> _memberCountsByChannelId(Iterable<NostrEvent> memberEvents) {
/// filter, so a retired response is discarded rather than merged.
///
/// Lives in this part file because `channels_provider.dart` sits against the
/// repository-wide 1000-line file ceiling enforced by `just file-size-check`.
/// repository-wide 1200-line file ceiling enforced by `just file-size-check`.
class _ChannelRefreshCoordinator {
/// Resolves the relay-and-identity scope that is active right now.
final String Function() currentScope;
@@ -429,7 +429,7 @@ Future<List<NostrEvent>> _fetchPaginatedChannelEvents(
/// Thread-interest and unread helpers shared by [ChannelsNotifier].
///
/// Lives in this part file because `channels_provider.dart` sits against the
/// repository-wide 1000-line file ceiling enforced by `just file-size-check`.
/// repository-wide 1200-line file ceiling enforced by `just file-size-check`.
String? _observedUnreadRootId(NostrEvent event) =>
_isBroadcastReply(event) ? null : event.threadReference.rootId;
@@ -456,7 +456,7 @@ String _encodeRootIdSet(Set<String> values) => jsonEncode(values.toList());
/// Records one observed unread event for a channel's badge state.
///
/// An extension in this part file rather than a method on the notifier because
/// `channels_provider.dart` sits against the repository-wide 1000-line file
/// `channels_provider.dart` sits against the repository-wide 1200-line file
/// ceiling enforced by `just file-size-check`. Private members stay reachable:
/// a part shares its parent's library.
extension _ObservedUnreadRecording on ChannelsNotifier {

View File

@@ -233,7 +233,7 @@ class ChannelsNotifier extends AsyncNotifier<List<Channel>> {
final dedupedMetas = latestMetaPerId.values.toList();
// Resolve DM participant display names. Extracted into the part file so
// `channels_provider.dart` stays under the 1000-line ceiling enforced by
// `channels_provider.dart` stays under the 1200-line ceiling enforced by
// `just file-size-check`.
final displayNames = await _resolveDmDisplayNames(
session,

View File

@@ -1,22 +1,21 @@
import { realpathSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runFileSizeCheck } from "../../scripts/check-file-sizes-core.mjs";
import { rules } from "./file-size-policy.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const scriptPath = realpathSync(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(path.dirname(scriptPath), "..");
const MAX_LINES = 1000;
const rules = [
{
root: "lib",
extensions: new Set([".dart"]),
maxLines: MAX_LINES,
},
];
await runFileSizeCheck({
export const policy = {
projectRoot,
rules,
label: "Mobile",
});
};
if (
process.argv[1] &&
realpathSync(path.resolve(process.argv[1])) === scriptPath
) {
await runFileSizeCheck(policy);
}

View File

@@ -0,0 +1,7 @@
export const rules = [
{
root: "lib",
extensions: new Set([".dart"]),
maxLines: 1200,
},
];

View File

@@ -1,9 +1,19 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { execFileSync, spawnSync } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
realpathSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { policy as desktopPolicy } from "../desktop/scripts/check-file-sizes.mjs";
import { policy as mobilePolicy } from "../mobile/scripts/check-file-sizes.mjs";
import { policy as webPolicy } from "../web/scripts/check-file-sizes.mjs";
import {
allowedLineCount,
countLines,
@@ -12,6 +22,8 @@ import {
resolveBaseRef,
} from "./check-file-sizes-core.mjs";
const repoRoot = path.resolve(import.meta.dirname, "..");
function git(repo, ...args) {
// These fixture repositories inherit both hook configuration and Git's
// repository-local environment when this test runs from pre-push. Isolate
@@ -26,6 +38,62 @@ function git(repo, ...args) {
}).trim();
}
function createEntrypointFixture({
surface,
files,
lineDelta = 1,
symlinkEntrypoint = false,
}) {
const repo = realpathSync(
mkdtempSync(path.join(tmpdir(), `file-size-${surface}-`)),
);
const scriptsDir = path.join(repo, "scripts");
const surfaceScriptsDir = path.join(repo, surface, "scripts");
mkdirSync(scriptsDir, { recursive: true });
mkdirSync(surfaceScriptsDir, { recursive: true });
copyFileSync(
path.join(repoRoot, "scripts/check-file-sizes-core.mjs"),
path.join(scriptsDir, "check-file-sizes-core.mjs"),
);
for (const fileName of ["check-file-sizes.mjs", "file-size-policy.mjs"]) {
copyFileSync(
path.join(repoRoot, surface, "scripts", fileName),
path.join(surfaceScriptsDir, fileName),
);
}
git(repo, "init", "-b", "main");
git(repo, "config", "user.name", "Test");
git(repo, "config", "user.email", "test@example.com");
git(repo, "add", ".");
git(repo, "commit", "-m", "base");
const base = git(repo, "rev-parse", "HEAD");
git(repo, "switch", "-c", "feature");
for (const { relativeFile, maxLines } of files) {
const governedFile = path.join(repo, surface, relativeFile);
const lineCount = maxLines + lineDelta;
mkdirSync(path.dirname(governedFile), { recursive: true });
writeFileSync(governedFile, `${"line\n".repeat(lineCount - 1)}line`);
}
const realEntrypointPath = path.join(
surfaceScriptsDir,
"check-file-sizes.mjs",
);
let entrypointPath = realEntrypointPath;
if (symlinkEntrypoint) {
entrypointPath = path.join(repo, `${surface}-file-size-check.mjs`);
symlinkSync(realEntrypointPath, entrypointPath);
}
const result = spawnSync(realpathSync(process.execPath), [entrypointPath], {
cwd: repo,
encoding: "utf8",
env: { ...process.env, CHECK_FILE_SIZES_BASE: base },
});
return { result, relativeFiles: files.map(({ relativeFile }) => relativeFile) };
}
test("local base resolution uses the branch merge-base and fails without origin/main", () => {
const repo = mkdtempSync(path.join(tmpdir(), "file-size-base-"));
git(repo, "init", "-b", "main");
@@ -47,12 +115,151 @@ test("local base resolution uses the branch merge-base and fails without origin/
);
});
const entrypointCases = [
{
surface: "desktop",
files: [
{ relativeFile: "src-tauri/src/oversized.rs", maxLines: 1500 },
{ relativeFile: "src-tauri/crates/oversized.rs", maxLines: 1500 },
{ relativeFile: "src/app/oversized.ts", maxLines: 1200 },
{ relativeFile: "src/features/oversized.tsx", maxLines: 1200 },
{ relativeFile: "src/shared/api/oversized.ts", maxLines: 1200 },
{ relativeFile: "src/shared/context/oversized.tsx", maxLines: 1200 },
{ relativeFile: "src/shared/lib/oversized.ts", maxLines: 1200 },
{ relativeFile: "src/shared/ui/oversized.tsx", maxLines: 1200 },
{ relativeFile: "src/shared/styles/oversized.css", maxLines: 1200 },
],
},
{
surface: "mobile",
files: [{ relativeFile: "lib/oversized.dart", maxLines: 1200 }],
},
{
surface: "web",
files: [
{ relativeFile: "src/app/oversized.ts", maxLines: 1000 },
{ relativeFile: "src/features/oversized.tsx", maxLines: 1000 },
{ relativeFile: "src/shared/api/oversized.ts", maxLines: 1000 },
],
},
];
test("surface entrypoints execute every production rule", () => {
for (const fixture of entrypointCases) {
const { result, relativeFiles } = createEntrypointFixture(fixture);
assert.equal(
result.status,
1,
`${fixture.surface} should reject every ceiling + 1: ${result.stderr || result.stdout}`,
);
for (const relativeFile of relativeFiles) {
assert.ok(
result.stderr.includes(relativeFile),
`${fixture.surface} should report ${relativeFile}: ${result.stderr}`,
);
}
}
});
test("surface entrypoints execute through symlinked paths", () => {
for (const fixture of entrypointCases) {
const { result, relativeFiles } = createEntrypointFixture({
...fixture,
symlinkEntrypoint: true,
});
assert.equal(
result.status,
1,
`${fixture.surface} symlink should reject ceiling + 1: ${result.stderr || result.stdout}`,
);
for (const relativeFile of relativeFiles) {
assert.ok(
result.stderr.includes(relativeFile),
`${fixture.surface} symlink should report ${relativeFile}: ${result.stderr}`,
);
}
}
});
test("surface entrypoints allow every production rule at its ceiling", () => {
for (const fixture of entrypointCases) {
const { result } = createEntrypointFixture({ ...fixture, lineDelta: 0 });
assert.equal(
result.status,
0,
`${fixture.surface} should allow every ceiling: ${result.stderr || result.stdout}`,
);
}
});
test("counts empty, LF, and CRLF content with the existing semantics", () => {
assert.equal(countLines(""), 0);
assert.equal(countLines("one\n"), 2);
assert.equal(countLines("one\r\ntwo"), 2);
});
test("surface entrypoints expose the exact ordered production policies", () => {
const policies = [
[
desktopPolicy,
[
["src-tauri/src", [".rs"], 1500],
["src-tauri/crates", [".rs"], 1500],
["src/app", [".ts", ".tsx"], 1200],
["src/features", [".ts", ".tsx"], 1200],
["src/shared/api", [".ts", ".tsx"], 1200],
["src/shared/context", [".ts", ".tsx"], 1200],
["src/shared/lib", [".ts", ".tsx"], 1200],
["src/shared/ui", [".ts", ".tsx"], 1200],
["src/shared/styles", [".css"], 1200],
],
],
[mobilePolicy, [["lib", [".dart"], 1200]]],
[
webPolicy,
[
["src/app", [".ts", ".tsx"], 1000],
["src/features", [".ts", ".tsx"], 1000],
["src/shared/api", [".ts", ".tsx"], 1000],
],
],
];
for (const [policy, expectedRules] of policies) {
const actualRules = policy.rules.map((rule) => [
rule.root,
[...rule.extensions],
rule.maxLines,
]);
assert.deepEqual(
actualRules,
expectedRules,
`${policy.label} production rules`,
);
for (const rule of policy.rules) {
assert.equal(
evaluateFileSize({
baseLines: null,
candidateLines: rule.maxLines,
maxLines: rule.maxLines,
}).violates,
false,
`${policy.label} ${rule.root} should allow the ceiling`,
);
assert.equal(
evaluateFileSize({
baseLines: null,
candidateLines: rule.maxLines + 1,
maxLines: rule.maxLines,
}).violates,
true,
`${policy.label} ${rule.root} should reject ceiling + 1`,
);
}
}
});
test("new files use the configured ceiling", () => {
assert.equal(allowedLineCount(null, 1000), 1000);
assert.deepEqual(

View File

@@ -1,32 +1,21 @@
import { realpathSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runFileSizeCheck } from "../../scripts/check-file-sizes-core.mjs";
import { rules } from "./file-size-policy.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, "..");
const scriptPath = realpathSync(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(path.dirname(scriptPath), "..");
const MAX_LINES = 1000;
const rules = [
{
root: "src/app",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/features",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/shared/api",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
];
await runFileSizeCheck({
export const policy = {
projectRoot,
rules,
label: "Web",
});
};
if (
process.argv[1] &&
realpathSync(path.resolve(process.argv[1])) === scriptPath
) {
await runFileSizeCheck(policy);
}

View File

@@ -0,0 +1,19 @@
const MAX_LINES = 1000;
export const rules = [
{
root: "src/app",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/features",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
{
root: "src/shared/api",
extensions: new Set([".ts", ".tsx"]),
maxLines: MAX_LINES,
},
];