From 56330092f05f02a773ecf03e6addf769e0e5daf9 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 17 May 2026 15:40:43 +0800 Subject: [PATCH 1/2] feat: add frontend LOC ratchet warnings --- .github/frontend-architecture-manifest.md | 51 +++++------ .github/workflows/ci.yml | 48 ++++++++++ .../opencode/test/github/ci-workflow.test.ts | 46 +++++++++- .../test/github/frontend-inventory.test.ts | 54 +++++++++++ script/frontend-inventory.mjs | 90 ++++++++++++++++++- 5 files changed, 259 insertions(+), 30 deletions(-) create mode 100644 packages/opencode/test/github/frontend-inventory.test.ts diff --git a/.github/frontend-architecture-manifest.md b/.github/frontend-architecture-manifest.md index 9c6f4a362..4f721d91b 100644 --- a/.github/frontend-architecture-manifest.md +++ b/.github/frontend-architecture-manifest.md @@ -6,8 +6,8 @@ This manifest covers git-tracked, hand-written frontend files under `packages/ap ## Current State -- Manifest status: governance baseline. -- Baseline commit: `5da4d3d61` (`fix: harden frontend inventory classification`). +- Manifest status: LOC warn-only CI slice. +- Baseline commit: `c3e931935` (`chore(release): bump desktop version to v2026.5.17`). - Default branch: `dev`. - Master owner: [#599 UI rewrite v2](https://github.com/Astro-Han/pawwork/issues/599). - Historical issue [#440](https://github.com/Astro-Han/pawwork/issues/440) is closed and must not be used as a new work entry. @@ -20,10 +20,11 @@ The repeatable report command is: ```sh bun run frontend:inventory node script/frontend-inventory.mjs --format json +node script/frontend-inventory.mjs --check-baseline --base origin/dev --head HEAD bun run frontend:inventory -- --format markdown --max-rows 120 ``` -The script is warn-only in this governance phase. It prints threshold warnings but exits successfully. +The script is warn-only in this governance phase. It prints threshold warnings for touched production frontend files above the `>500` and `>200` LOC lines, but exits successfully unless inventory generation or git diff fails. ## Report Schema @@ -60,17 +61,17 @@ If this work changes from physical LOC to logical LOC or another metric, add sch ## Baseline Summary -Generated with `node script/frontend-inventory.mjs --format json` at baseline commit `5da4d3d61`. -This replaces the earlier `eb80bed96` draft baseline because the review fix made the report include root-level `src/*.ts(x)` files and stopped classifying logic-bearing `index.ts` / pure-config candidates as visibility-only inventory. +Generated with `node script/frontend-inventory.mjs --format json` at baseline commit `c3e931935`. +This replaces the earlier `5da4d3d61` baseline after the merged UI governance and owner-extraction queue changed the tracked frontend file set. | Metric | Count | | --- | ---: | -| Tracked `.ts` / `.tsx` files | 682 | -| Production ratchet set | 356 | -| Visibility-only inventory | 326 | +| Tracked `.ts` / `.tsx` files | 727 | +| Production ratchet set | 388 | +| Visibility-only inventory | 339 | | Approved exceptions | 0 | -| Production files `>500` LOC | 26 | -| Production files `>200` LOC | 80 | +| Production files `>500` LOC | 19 | +| Production files `>200` LOC | 81 | | Visibility-only files `>500` LOC | 15 | | Visibility-only files `>200` LOC | 56 | @@ -78,14 +79,14 @@ Production by owner lane: | Owner lane | Files | `>200` | `>500` | | --- | ---: | ---: | ---: | -| other/deferred | 130 | 24 | 5 | -| #638 interface audit | 7 | 1 | 0 | -| #604 settings | 21 | 9 | 3 | +| other/deferred | 136 | 24 | 5 | +| #638 interface audit | 8 | 1 | 0 | +| #604 settings | 35 | 9 | 0 | | #599 mainline | 2 | 0 | 0 | | #606 final shell | 45 | 14 | 6 | -| #601 message flow | 56 | 9 | 5 | +| #601 message flow | 66 | 10 | 1 | | #605 visual shell | 84 | 18 | 6 | -| #595/#615 scroll-perf | 11 | 5 | 1 | +| #595/#615 scroll-perf | 12 | 5 | 1 | ## Owner Lanes @@ -106,9 +107,9 @@ Closed area references are allowed as background only. [#602](https://github.com | Stage | Rule | | --- | --- | -| 1. Governance | Warn only. Establish baseline, schema, owner map, report command, and exception format. | -| 2. New `>500` guard | Do not add or modify an unexplained production file above 500 LOC. | -| 3. New `>200` guard | Do not add or modify a production file above 200 LOC without owner lane and manifest entry. | +| 1. Governance | Warn only. Establish baseline, schema, owner map, report command, CI inventory job, and exception format. | +| 2. New `>500` guard | Warn when a PR adds or modifies a production file above 500 LOC. Do not hard fail in the current slice. | +| 3. New `>200` guard | Warn when a PR adds or modifies a production file above 200 LOC. Do not hard fail in the current slice. | | 4. Tighter ratchet | Only after the launch path is stable and current exceptions are reviewed. | ## Exception Schema @@ -134,28 +135,21 @@ Current approved exceptions: none. | LOC | Owner Lane | Status | Path | | ---: | --- | --- | --- | | 2463 | #606 final shell | needs-over-500-resolution | `packages/app/src/pages/layout.tsx` | -| 1217 | #601 message flow | needs-over-500-resolution | `packages/app/src/pages/session/message-timeline.tsx` | | 1129 | #605 visual shell | needs-over-500-resolution | `packages/ui/src/components/file.tsx` | | 1119 | #606 final shell | needs-over-500-resolution | `packages/app/src/context/layout.tsx` | -| 753 | #601 message flow | needs-over-500-resolution | `packages/ui/src/components/session-turn.tsx` | -| 733 | #604 settings | needs-over-500-resolution | `packages/app/src/components/settings-general.tsx` | -| 691 | #601 message flow | needs-over-500-resolution | `packages/app/src/pages/session.tsx` | | 673 | other/deferred | needs-over-500-resolution | `packages/app/src/components/terminal.tsx` | | 665 | #606 final shell | needs-over-500-resolution | `packages/app/src/context/sync.tsx` | | 661 | #605 visual shell | needs-over-500-resolution | `packages/ui/src/components/session-review.tsx` | -| 654 | #604 settings | needs-over-500-resolution | `packages/app/src/components/dialog-connect-provider.tsx` | -| 649 | #604 settings | needs-over-500-resolution | `packages/app/src/components/dialog-select-server.tsx` | | 634 | other/deferred | needs-over-500-resolution | `packages/app/src/addons/serialize.ts` | -| 618 | #601 message flow | needs-over-500-resolution | `packages/app/src/pages/session/use-session-commands.tsx` | -| 598 | #601 message flow | needs-over-500-resolution | `packages/ui/src/components/markdown.tsx` | | 596 | #605 visual shell | needs-over-500-resolution | `packages/ui/src/components/line-comment-annotations.tsx` | | 595 | #595/#615 scroll-perf | needs-over-500-resolution | `packages/app/src/pages/session/session-timeline-scroll-controller.ts` | | 588 | #605 visual shell | needs-over-500-resolution | `packages/ui/src/components/file-icon.tsx` | | 568 | other/deferred | needs-over-500-resolution | `packages/app/src/utils/persist.ts` | | 560 | #606 final shell | needs-over-500-resolution | `packages/app/src/context/local.tsx` | +| 556 | #605 visual shell | needs-over-500-resolution | `packages/ui/src/theme/resolve.ts` | | 553 | other/deferred | needs-over-500-resolution | `packages/app/src/pages/session/session-side-panel.tsx` | +| 548 | #601 message flow | needs-over-500-resolution | `packages/app/src/pages/session/use-session-commands.tsx` | | 543 | #606 final shell | needs-over-500-resolution | `packages/app/src/context/global-sync.tsx` | -| 540 | #605 visual shell | needs-over-500-resolution | `packages/ui/src/theme/resolve.ts` | | 527 | #605 visual shell | needs-over-500-resolution | `packages/ui/src/context/marked.tsx` | | 511 | #606 final shell | needs-over-500-resolution | `packages/app/src/context/terminal.tsx` | | 507 | other/deferred | needs-over-500-resolution | `packages/app/src/components/file-tree.tsx` | @@ -170,7 +164,8 @@ bun run frontend:inventory -- --format markdown --max-rows 120 | PR | Owner lane | Base | Depends on | Boundary | Architecture effect | Verification | Status | Public write status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| Governance PR | #599 mainline / governance | `dev` | None | Manifest, schema, owner map, warn-only script, baseline report command | boundary created, owner map established, ratchet command added | `bun run frontend:inventory`, `node script/frontend-inventory.mjs --format json`, `bun run frontend:inventory -- --format markdown --max-rows 120` | in progress | PR body only | +| Governance PR | #599 mainline / governance | `dev` | None | Manifest, schema, owner map, warn-only script, baseline report command | boundary created, owner map established, ratchet command added | `bun run frontend:inventory`, `node script/frontend-inventory.mjs --format json`, `bun run frontend:inventory -- --format markdown --max-rows 120` | complete | PR body only | +| LOC warn-only CI PR | #688 governance | `dev` | None | Add CI inventory output and touched-file LOC warnings for `>500` / `>200` production frontend files | LOC governance is executable in CI; current slice warns only and does not enforce hard failure | `node script/frontend-inventory.mjs --format json`, `node script/frontend-inventory.mjs --check-baseline --base origin/dev --head HEAD`, workflow/script contract tests | in progress | PR body only | | Contract PR | #638 interface audit | Governance branch or post-merge `dev` | Governance PR | Public contract/import boundary and compatibility checks | public contract stabilized, private import risk surfaced | typecheck plus contract-specific compatibility check | planned | PR body only | | Message-flow PR stack | #601 message flow | post-governance `dev` unless stacked | Governance PR, maybe Contract PR if public imports move | Current launch-path message flow files only | owner extracted, LOC reduced, verification added | typecheck, unit/e2e, #600 perf gate, visual smoke | planned | PR body only | | [#670](https://github.com/Astro-Han/pawwork/pull/670) | #601 message flow | `dev` | #667, #669 | Extract `createTimelineStaging` from `MessageTimeline` into `session-timeline-staging.ts` with browser-condition staging tests | timeline staging owner isolated; active-session message growth remains staged instead of popping to full render | focused staging/history/scroll tests, typecheck, diff check, PR CI | in review | PR body + manifest | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9956ba967..605ac0a3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,6 +165,50 @@ jobs: - name: lint run: bun run lint:ci + frontend-architecture: + needs: changes + if: needs.changes.outputs.docs_only != 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + # Load-bearing: `bun install` runs `trustedDependencies` + # postinstalls from the root package.json (electron, node-pty, + # esbuild, tree-sitter at time of writing), which call `node` + # explicitly. Do not drop without first patching those scripts. + # Source of truth: grep `trustedDependencies` in package.json. (#70) + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # actions/setup-node@v6.4.0 + with: + node-version: "24" + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # oven-sh/setup-bun@v2 + with: + bun-version: "1.3.13" + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # actions/cache@v5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - run: bun install --frozen-lockfile + + - name: frontend inventory + run: | + mkdir -p .artifacts/frontend-architecture + node script/frontend-inventory.mjs --format json > .artifacts/frontend-architecture/frontend-inventory.json + + - name: LOC ratchet warnings + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.sha }} + run: node script/frontend-inventory.mjs --check-baseline --base "$BASE_SHA" --head "$HEAD_SHA" + unit-app: needs: changes if: needs.changes.outputs.docs_only != 'true' @@ -430,6 +474,7 @@ jobs: needs: - changes - typecheck + - frontend-architecture - unit-ui-focused - unit-app - unit-opencode @@ -440,6 +485,7 @@ jobs: env: DOCS_ONLY: ${{ needs.changes.outputs.docs_only }} TYPECHECK_RESULT: ${{ needs.typecheck.result }} + FRONTEND_ARCHITECTURE_RESULT: ${{ needs['frontend-architecture'].result }} UNIT_UI_FOCUSED_RESULT: ${{ needs['unit-ui-focused'].result }} UNIT_APP_RESULT: ${{ needs['unit-app'].result }} UNIT_OPENCODE_RESULT: ${{ needs['unit-opencode'].result }} @@ -453,11 +499,13 @@ jobs: fi if [ "$TYPECHECK_RESULT" != "success" ] || + [ "$FRONTEND_ARCHITECTURE_RESULT" != "success" ] || [ "$UNIT_UI_FOCUSED_RESULT" != "success" ] || [ "$UNIT_APP_RESULT" != "success" ] || [ "$UNIT_OPENCODE_RESULT" != "success" ] || [ "$UNIT_DESKTOP_RESULT" != "success" ]; then echo "typecheck=$TYPECHECK_RESULT" + echo "frontend-architecture=$FRONTEND_ARCHITECTURE_RESULT" echo "unit-ui-focused=$UNIT_UI_FOCUSED_RESULT" echo "unit-app=$UNIT_APP_RESULT" echo "unit-opencode=$UNIT_OPENCODE_RESULT" diff --git a/packages/opencode/test/github/ci-workflow.test.ts b/packages/opencode/test/github/ci-workflow.test.ts index 119056774..70af1ef1f 100644 --- a/packages/opencode/test/github/ci-workflow.test.ts +++ b/packages/opencode/test/github/ci-workflow.test.ts @@ -20,6 +20,7 @@ const pinned = { const runAttempt = "${{ github.run_attempt }}" const githubSha = "${{ github.sha }}" const lintJobName = "lint" +const frontendArchitectureJobName = "frontend-architecture" const windowsUnitJobName = "unit-windows" // Suffixes drive readable job and artifact names; commands use package.json names verbatim. @@ -243,6 +244,39 @@ describe("ci workflow", () => { expect(checkNeeds).not.toContain(lintJobName) }) + test("runs frontend architecture inventory as a warn-only blocking infrastructure check", () => { + const parsed = parseWorkflow(ciWorkflowPath) + const job = parsed.jobs?.[frontendArchitectureJobName] + const check = parsed.jobs?.check + const checkNeeds = Array.isArray(check?.needs) ? check.needs : [] + const inventory = stepByName(frontendArchitectureJobName, "frontend inventory") + const ratchet = stepByName(frontendArchitectureJobName, "LOC ratchet warnings") + + expect(job?.needs).toBe("changes") + expect(job?.if).toBe("needs.changes.outputs.docs_only != 'true'") + expect(job?.["runs-on"]).toBe("ubuntu-latest") + expect(job?.["timeout-minutes"]).toBe(10) + expect(job?.["continue-on-error"]).toBeUndefined() + expect(job?.permissions).toBeUndefined() + expect(checkoutStep(frontendArchitectureJobName)?.uses).toBe(pinned.checkout) + expect(checkoutStep(frontendArchitectureJobName)?.with?.["fetch-depth"]).toBe(0) + expect(steps(frontendArchitectureJobName).find((step) => step.uses?.startsWith("actions/setup-node@"))?.uses).toBe( + pinned.setupNode, + ) + expect(steps(frontendArchitectureJobName).find((step) => step.uses?.startsWith("oven-sh/setup-bun@"))?.uses).toBe( + pinned.setupBun, + ) + expect(steps(frontendArchitectureJobName).filter((step) => step.uses?.startsWith("actions/cache@")).map((step) => step.uses)).toEqual([ + pinned.cache, + ]) + expect(inventory?.run).toContain("node script/frontend-inventory.mjs --format json") + expect(inventory?.run).toContain(".artifacts/frontend-architecture/frontend-inventory.json") + expect(ratchet?.env?.BASE_SHA).toBe("${{ github.event.pull_request.base.sha || github.event.before }}") + expect(ratchet?.env?.HEAD_SHA).toBe("${{ github.sha }}") + expect(ratchet?.run).toBe("node script/frontend-inventory.mjs --check-baseline --base \"$BASE_SHA\" --head \"$HEAD_SHA\"") + expect(checkNeeds).toContain(frontendArchitectureJobName) + }) + test("splits required Linux unit jobs by package while preserving Turbo dependency semantics", () => { const parsed = parseWorkflow(ciWorkflowPath) @@ -432,7 +466,15 @@ describe("ci workflow", () => { const validate = stepByName("check", "Validate CI result") expect(check?.if).toBe("always()") - expect(needs).toEqual(["changes", "typecheck", "unit-ui-focused", "unit-app", "unit-opencode", "unit-desktop"]) + expect(needs).toEqual([ + "changes", + "typecheck", + frontendArchitectureJobName, + "unit-ui-focused", + "unit-app", + "unit-opencode", + "unit-desktop", + ]) expect(needs).not.toContain(lintJobName) expect(needs).not.toContain("unit-windows") expect(needs).not.toContain("unit-windows-app") @@ -443,11 +485,13 @@ describe("ci workflow", () => { expect(needs).not.toContain("unit-windows-opencode-server-tools") expect(validate?.env?.DOCS_ONLY).toBe("${{ needs.changes.outputs.docs_only }}") expect(validate?.env?.TYPECHECK_RESULT).toBe("${{ needs.typecheck.result }}") + expect(validate?.env?.FRONTEND_ARCHITECTURE_RESULT).toBe("${{ needs['frontend-architecture'].result }}") expect(validate?.env?.UNIT_UI_FOCUSED_RESULT).toBe("${{ needs['unit-ui-focused'].result }}") expect(validate?.env?.UNIT_APP_RESULT).toBe("${{ needs['unit-app'].result }}") expect(validate?.env?.UNIT_OPENCODE_RESULT).toBe("${{ needs['unit-opencode'].result }}") expect(validate?.env?.UNIT_DESKTOP_RESULT).toBe("${{ needs['unit-desktop'].result }}") expect(validate?.run).toContain("Docs-only change, daily CI skipped.") + expect(validate?.run).toContain("FRONTEND_ARCHITECTURE_RESULT") expect(validate?.run).toContain("UNIT_UI_FOCUSED_RESULT") expect(validate?.run).toContain("UNIT_APP_RESULT") expect(validate?.run).toContain("UNIT_OPENCODE_RESULT") diff --git a/packages/opencode/test/github/frontend-inventory.test.ts b/packages/opencode/test/github/frontend-inventory.test.ts new file mode 100644 index 000000000..d0074b6b2 --- /dev/null +++ b/packages/opencode/test/github/frontend-inventory.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test" +import { execFileSync, spawnSync } from "node:child_process" +import { cpSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import path from "node:path" + +const repoRoot = path.join(import.meta.dir, "../../../..") +const inventoryScript = path.join(repoRoot, "script", "frontend-inventory.mjs") + +function git(cwd: string, args: string[]) { + return execFileSync("git", args, { cwd, encoding: "utf8" }) +} + +function writeLines(filePath: string, count: number) { + writeFileSync( + filePath, + Array.from({ length: count }, (_, index) => `export function value${index}() { return ${index} }`).join("\n") + "\n", + ) +} + +describe("frontend inventory", () => { + test("reports touched oversized production frontend files as warn-only baseline output", () => { + const workspace = mkdtempSync(path.join(tmpdir(), "pawwork-frontend-inventory-")) + cpSync(inventoryScript, path.join(workspace, "frontend-inventory.mjs")) + mkdirSync(path.join(workspace, "packages", "app", "src"), { recursive: true }) + + git(workspace, ["init"]) + git(workspace, ["config", "user.email", "codex@example.com"]) + git(workspace, ["config", "user.name", "Codex"]) + + const largeFile = path.join(workspace, "packages", "app", "src", "large-view.ts") + writeLines(largeFile, 205) + git(workspace, ["add", "."]) + git(workspace, ["commit", "-m", "test: add baseline file"]) + const base = git(workspace, ["rev-parse", "HEAD"]).trim() + + writeLines(largeFile, 206) + git(workspace, ["add", "."]) + git(workspace, ["commit", "-m", "test: modify large file"]) + const head = git(workspace, ["rev-parse", "HEAD"]).trim() + + const result = spawnSync("node", ["frontend-inventory.mjs", "--check-baseline", "--base", base, "--head", head], { + cwd: workspace, + encoding: "utf8", + }) + + expect(result.status).toBe(0) + expect(result.stdout).toContain("Frontend LOC ratchet warnings") + expect(result.stdout).toContain("warn-only") + expect(result.stderr).toContain("::warning") + expect(result.stderr).toContain("packages/app/src/large-view.ts") + expect(result.stderr).toContain(">200 LOC") + }) +}) diff --git a/script/frontend-inventory.mjs b/script/frontend-inventory.mjs index 59282dec4..84d119d9e 100644 --- a/script/frontend-inventory.mjs +++ b/script/frontend-inventory.mjs @@ -141,12 +141,29 @@ const CLOSED_AREA_HINTS = [ function parseArgs(argv) { const out = { + base: null, + checkBaseline: false, format: "summary", + head: "HEAD", maxRows: 80, } for (let index = 0; index < argv.length; index += 1) { const arg = argv[index] + if (arg === "--check-baseline") { + out.checkBaseline = true + continue + } + if (arg === "--base" || arg === "--base-ref") { + out.base = argv[index + 1] ?? out.base + index += 1 + continue + } + if (arg === "--head" || arg === "--head-ref") { + out.head = argv[index + 1] ?? out.head + index += 1 + continue + } if (arg === "--format") { out.format = argv[index + 1] ?? out.format index += 1 @@ -401,6 +418,38 @@ function listFrontendFiles() { return stdout ? stdout.split("\n").filter(Boolean).sort() : [] } +function listChangedFrontendFiles(base, head) { + if (!base || /^0{40}$/.test(base)) { + return { + changedPaths: [], + skippedReason: "No comparable base ref was provided; inventory generated without touched-file warnings.", + } + } + + let stdout = "" + try { + stdout = execFileSync( + "git", + ["diff", "--name-status", "--find-renames", "--find-copies", base, head, "--", ...FRONTEND_PATHSPECS], + { encoding: "utf8" }, + ).trim() + } catch (error) { + exitWithInventoryError(`git diff failed for frontend inventory baseline check (${base}..${head}).`, error) + } + + const changedPaths = new Set() + for (const line of stdout ? stdout.split("\n") : []) { + const [status, path1, path2] = line.split("\t") + if (!status || status.startsWith("D")) continue + changedPaths.add(status.startsWith("R") || status.startsWith("C") ? path2 : path1) + } + + return { + changedPaths: [...changedPaths].filter(Boolean).sort(), + skippedReason: null, + } +} + function readFrontendFile(path) { try { return readFileSync(path, "utf8") @@ -409,6 +458,43 @@ function readFrontendFile(path) { } } +function githubWarning(record, threshold) { + const title = encodeURIComponent("Frontend LOC ratchet") + return `::warning file=${record.path},title=${title}::${record.path} is ${record.loc} LOC (${threshold}); warn-only for #688 LOC ratchet. Keep an owner lane or split before promoting this gate.` +} + +function checkBaseline(inventory, { base, head }) { + const { changedPaths, skippedReason } = listChangedFrontendFiles(base, head) + const recordsByPath = new Map(inventory.records.map((record) => [record.path, record])) + const touchedProduction = changedPaths + .map((path) => recordsByPath.get(path)) + .filter((record) => record?.setType === "production ratchet set") + const over500 = touchedProduction.filter((record) => record.loc > 500) + const over200 = touchedProduction.filter((record) => record.loc > 200) + const warnRows = touchedProduction + .filter((record) => record.loc > 200) + .sort((a, b) => b.loc - a.loc) + + console.log("Frontend LOC ratchet warnings") + console.log("Mode: warn-only") + console.log(`Base: ${base ?? "(not provided)"}`) + console.log(`Head: ${head}`) + console.log(`Changed frontend files: ${changedPaths.length}`) + console.log(`Touched production files: ${touchedProduction.length}`) + console.log(`Touched production files >500 LOC: ${over500.length}`) + console.log(`Touched production files >200 LOC: ${over200.length}`) + + if (skippedReason) { + console.log(skippedReason) + return + } + + for (const record of warnRows) { + const threshold = record.loc > 500 ? ">500 LOC" : ">200 LOC" + console.error(githubWarning(record, threshold)) + } +} + function exitWithInventoryError(message, error) { console.error(`error: ${message}`) if (error instanceof Error && error.message) console.error(error.message) @@ -518,7 +604,9 @@ function printMarkdown(inventory, maxRows) { const args = parseArgs(process.argv.slice(2)) const inventory = buildInventory() -if (args.format === "json") { +if (args.checkBaseline) { + checkBaseline(inventory, args) +} else if (args.format === "json") { console.log(JSON.stringify(inventory, null, 2)) } else if (args.format === "markdown") { printMarkdown(inventory, args.maxRows) From 4e94c6dcc505e5191ed5d74e6f8629d124ba0541 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 17 May 2026 15:45:47 +0800 Subject: [PATCH 2/2] test: use shared tmpdir fixture for inventory test --- .../opencode/test/github/frontend-inventory.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/opencode/test/github/frontend-inventory.test.ts b/packages/opencode/test/github/frontend-inventory.test.ts index d0074b6b2..5ebc034f9 100644 --- a/packages/opencode/test/github/frontend-inventory.test.ts +++ b/packages/opencode/test/github/frontend-inventory.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test" import { execFileSync, spawnSync } from "node:child_process" -import { cpSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs" -import { tmpdir } from "node:os" +import { cpSync, mkdirSync, writeFileSync } from "node:fs" import path from "node:path" +import { tmpdir } from "../fixture/fixture" const repoRoot = path.join(import.meta.dir, "../../../..") const inventoryScript = path.join(repoRoot, "script", "frontend-inventory.mjs") @@ -19,15 +19,12 @@ function writeLines(filePath: string, count: number) { } describe("frontend inventory", () => { - test("reports touched oversized production frontend files as warn-only baseline output", () => { - const workspace = mkdtempSync(path.join(tmpdir(), "pawwork-frontend-inventory-")) + test("reports touched oversized production frontend files as warn-only baseline output", async () => { + await using tmp = await tmpdir({ git: true }) + const workspace = tmp.path cpSync(inventoryScript, path.join(workspace, "frontend-inventory.mjs")) mkdirSync(path.join(workspace, "packages", "app", "src"), { recursive: true }) - git(workspace, ["init"]) - git(workspace, ["config", "user.email", "codex@example.com"]) - git(workspace, ["config", "user.name", "Codex"]) - const largeFile = path.join(workspace, "packages", "app", "src", "large-view.ts") writeLines(largeFile, 205) git(workspace, ["add", "."])