mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
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:
10
AGENTS.md
10
AGENTS.md
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
53
desktop/scripts/file-size-policy.mjs
Normal file
53
desktop/scripts/file-size-policy.mjs
Normal 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,
|
||||
},
|
||||
];
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
//!
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
7
mobile/scripts/file-size-policy.mjs
Normal file
7
mobile/scripts/file-size-policy.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
export const rules = [
|
||||
{
|
||||
root: "lib",
|
||||
extensions: new Set([".dart"]),
|
||||
maxLines: 1200,
|
||||
},
|
||||
];
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
19
web/scripts/file-size-policy.mjs
Normal file
19
web/scripts/file-size-policy.mjs
Normal 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,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user