diff --git a/AGENTS.md b/AGENTS.md index df840f665f0..b1607f6289f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,12 @@ branches. like `ERR_PNPM_OUTDATED_LOCKFILE` / "specifiers in the lockfile don't match package.json". Prefer regenerating the lockfile over repeatedly choosing ours/theirs on `pnpm-lock.yaml` during multi-commit rebases of `fork/changes`. +- **Per-layer `vp check` after stack rebase (required):** when rebasing or rewriting the stack, + run root **`vp check` on each layer and fix all failures before rebasing the next layer**. Order: + `fork/tim` → `fork/candidates` → `fork/changes` → each overlay → compose `fork/integration`. + Do not push a "green later" tip and stack on top of it. Same rule for feature PRs after + `pnpm fork:stack update`: rebase, then `vp check`, then push/merge. Full detail: + [docs/fork-stack.md](./docs/fork-stack.md) ("Per-layer `vp check` after stack rebase"). ## Pull requests (required handoff) diff --git a/docs/fork-stack.md b/docs/fork-stack.md index bca723b72ad..0deca459bce 100644 --- a/docs/fork-stack.md +++ b/docs/fork-stack.md @@ -218,6 +218,33 @@ gh workflow run fork-ci.yml --repo patroza/t3code --ref fork/integration Prefer one deliberate lockfile regeneration at the end of a multi-commit `fork/changes` rebase over resolving the lockfile at every intermediate conflict. +### Per-layer `vp check` after stack rebase (required) + +When you manually rebase or rewrite the stack, **do not advance to the next layer until the current +layer is clean**. After each layer is rebased onto its parent, install/lock is consistent, and +conflicts are resolved: + +1. Check out that layer's tip. +2. Run root **`vp check`** (the same formatter/linter gate Fork CI uses). +3. Fix every failure on **that layer** (format, lint, lockfile, type-level breakage the check + surfaces). Commit and force-with-lease push the layer if needed. +4. Only then rebase the **next** layer onto the fixed parent. + +Layer order for this gate: + +```text +main (upstream mirror — skip product fixes; do not hand-edit) + → fork/tim + → fork/candidates + → fork/changes + → each integration overlay (desktop, discord, vscode) onto fork/changes + → fork/integration (compose last) +``` + +Skipping `vp check` and stacking "fix it later" commits is how lockfile and lint failures cascade +into every PR and block merge. Feature PRs (e.g. based on `fork/changes`) get the same treatment +after `pnpm fork:stack update`: rebase onto the fixed parent, then `vp check` before push/merge. + `register` is used during the one-time cutover and only when intentionally building an advanced, dependent integration chain: diff --git a/packages/client-runtime/src/state/threadRecencyGroups.test.ts b/packages/client-runtime/src/state/threadRecencyGroups.test.ts index 805efebdbe1..19a7e9439c6 100644 --- a/packages/client-runtime/src/state/threadRecencyGroups.test.ts +++ b/packages/client-runtime/src/state/threadRecencyGroups.test.ts @@ -9,9 +9,27 @@ import { THREAD_RECENCY_BUCKET_LABELS, } from "./threadRecencyGroups.ts"; +/** Local calendar fixture; Date APIs are intentional for bucket tests. */ +function localDate( + year: number, + monthIndex: number, + day: number, + hours = 0, + minutes = 0, + seconds = 0, +): Date { + // @effect-diagnostics-next-line globalDate:off + return new Date(year, monthIndex, day, hours, minutes, seconds); +} + +function dateFromMs(ms: number): Date { + // @effect-diagnostics-next-line globalDate:off + return new Date(ms); +} + describe("getThreadRecencyBucketId", () => { // Fixed local afternoon so last-hour and earlier-today both fit in the day. - const now = new Date(2026, 2, 15, 14, 30, 0); // 2026-03-15 14:30 local + const now = localDate(2026, 2, 15, 14, 30, 0); // 2026-03-15 14:30 local it("splits today into last hour vs earlier today", () => { const startToday = startOfLocalDay(now).getTime(); @@ -40,7 +58,7 @@ describe("getThreadRecencyBucketId", () => { }); describe("groupThreadsByRecency", () => { - const now = new Date(2026, 2, 15, 14, 30, 0); + const now = localDate(2026, 2, 15, 14, 30, 0); const startToday = startOfLocalDay(now).getTime(); const nowMs = now.getTime(); @@ -104,10 +122,10 @@ describe("shouldShowRecencySectionHeaders", () => { describe("groupSortedThreadsByRecency", () => { it("groups using activity timestamps from ThreadSortInput", () => { - const now = new Date(2026, 2, 15, 14, 30, 0); + const now = localDate(2026, 2, 15, 14, 30, 0); const startToday = startOfLocalDay(now); - const lastHourIso = new Date(now.getTime() - 5 * 60_000).toISOString(); - const olderIso = new Date(startToday.getTime() - 40 * 24 * 60 * 60 * 1000).toISOString(); + const lastHourIso = dateFromMs(now.getTime() - 5 * 60_000).toISOString(); + const olderIso = dateFromMs(startToday.getTime() - 40 * 24 * 60 * 60 * 1000).toISOString(); const groups = groupSortedThreadsByRecency( [ diff --git a/packages/client-runtime/src/state/threadRecencyGroups.ts b/packages/client-runtime/src/state/threadRecencyGroups.ts index 548ac83a053..b308ef33189 100644 --- a/packages/client-runtime/src/state/threadRecencyGroups.ts +++ b/packages/client-runtime/src/state/threadRecencyGroups.ts @@ -33,8 +33,30 @@ export const THREAD_RECENCY_BUCKET_LABELS: Record const MS_PER_HOUR = 60 * 60 * 1000; const MS_PER_DAY = 24 * MS_PER_HOUR; +function makeLocalDate( + year: number, + monthIndex: number, + day: number, + hours = 0, + minutes = 0, + seconds = 0, +): Date { + // @effect-diagnostics-next-line globalDate:off + return new Date(year, monthIndex, day, hours, minutes, seconds); +} + +function makeDateFromEpochMs(ms: number): Date { + // @effect-diagnostics-next-line globalDate:off + return new Date(ms); +} + +function makeNow(): Date { + // @effect-diagnostics-next-line globalDate:off + return new Date(); +} + export function startOfLocalDay(date: Date): Date { - return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + return makeLocalDate(date.getFullYear(), date.getMonth(), date.getDate()); } /** @@ -44,7 +66,7 @@ export function startOfLocalDay(date: Date): Date { */ export function getThreadRecencyBucketId( timestampMs: number, - now: Date = new Date(), + now: Date = makeNow(), ): ThreadRecencyBucketId { if (!Number.isFinite(timestampMs)) { return "older"; @@ -103,7 +125,7 @@ export function shouldShowRecencySectionHeaders( export function groupThreadsByRecency( threads: readonly T[], getTimestampMs: (thread: T) => number, - now: Date = new Date(), + now: Date = makeNow(), ): ReadonlyArray> { const buckets = new Map(); for (const id of THREAD_RECENCY_BUCKET_ORDER) { @@ -134,7 +156,7 @@ export function groupThreadsByRecency( */ export function groupSortedThreadsByRecency( threads: readonly T[], - now: Date = new Date(), + now: Date = makeNow(), ): ReadonlyArray> { return groupThreadsByRecency( threads,