Skip to content

Commit e76cf6b

Browse files
committed
improvement(audits): lint tracked uploads source, reject any/! suppressions, name-check root scripts
Anchor biome's build/out/uploads ignores to the real output and runtime dirs so apps/sim/lib/uploads and the uploads API routes are linted and counted by check:explicit-any (baseline grows only under those paths). check:explicit-any fails on biome-ignore comments for its two rules. check:file-names scans root scripts/ and vitest.shared.ts, allows Next.js interception segments and dot-prefixed names, ignores the old path of an unstaged mv, and points tool-mandated names at its allowlist. All three ratchets print a rename hint instead of only the shrink instruction.
1 parent ca7717c commit e76cf6b

6 files changed

Lines changed: 106 additions & 22 deletions

File tree

‎apps/sim/lib/uploads/contexts/workspace/workspace-file-manager-errors.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing'
2-
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { afterAll, beforeEach, describe, expect, it } from 'vitest'
33
import { listWorkspaceFiles } from './workspace-file-manager'
44

55
afterAll(resetDbChainMock)

‎apps/sim/lib/uploads/server/markdown-export.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('Markdown export image rewriting', () => {
4040

4141
it('returns large documents verbatim before parsing or fetching assets', async () => {
4242
const content = Buffer.from(
43-
'![image](/api/files/view/image-1)\n' + 'a'.repeat(MAX_EXPORT_MARKDOWN_PARSE_BYTES)
43+
`![image](/api/files/view/image-1)\n${'a'.repeat(MAX_EXPORT_MARKDOWN_PARSE_BYTES)}`
4444
)
4545
const result = await createMarkdownExport({
4646
content,

‎biome.json‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
"!**/.next",
99
"!**/.next",
1010
"!**/next-env.d.ts",
11-
"!**/out",
11+
"!out",
12+
"!apps/*/out",
13+
"!packages/*/out",
1214
"!**/dist",
13-
"!**/build",
15+
"!build",
16+
"!apps/*/build",
17+
"!packages/*/build",
1418
"!**/node_modules",
1519
"!**/.bun",
1620
"!**/.cache",
@@ -32,7 +36,8 @@
3236
"!**/apps/desktop/release",
3337
"!**/venv",
3438
"!**/.venv",
35-
"!**/uploads",
39+
"!uploads",
40+
"!apps/*/uploads",
3641
"!**/apps/sim/lib/execution/sandbox/bundles/*.cjs",
3742
"!**/test-results",
3843
"!**/playwright-report"

‎scripts/check-explicit-any.baseline.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@
251251
"apps/sim/lib/table/llm/enrichment.ts": 2,
252252
"apps/sim/lib/table/snapshot-cache.test.ts": 1,
253253
"apps/sim/lib/tokenization/utils.ts": 1,
254+
"apps/sim/lib/uploads/archive.test.ts": 1,
254255
"apps/sim/lib/webhooks/providers/sendblue.test.ts": 2,
255256
"apps/sim/lib/webhooks/providers/twilio.test.ts": 5,
256257
"apps/sim/lib/webhooks/providers/vercel.test.ts": 2,
@@ -1690,6 +1691,13 @@
16901691
"apps/sim/lib/table/validation.ts": 3,
16911692
"apps/sim/lib/table/workflow-groups/service.ts": 2,
16921693
"apps/sim/lib/tokenization/accurate.ts": 1,
1694+
"apps/sim/lib/uploads/archive.test.ts": 1,
1695+
"apps/sim/lib/uploads/client/download.test.ts": 1,
1696+
"apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.test.ts": 1,
1697+
"apps/sim/lib/uploads/contexts/organization-logo/application.integration.ts": 1,
1698+
"apps/sim/lib/uploads/contexts/workspace/workspace-file-manager-page.test.ts": 1,
1699+
"apps/sim/lib/uploads/server/markdown-export.test.ts": 1,
1700+
"apps/sim/lib/uploads/server/markdown-export.ts": 3,
16931701
"apps/sim/lib/users/application/authorized-apps.test.ts": 4,
16941702
"apps/sim/lib/webhooks/pending-verification.test.ts": 2,
16951703
"apps/sim/lib/webhooks/polling/google-calendar.ts": 1,

‎scripts/check-explicit-any.ts‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
* `suspicious/noExplicitAny` and `style/noNonNullAssertion` are off repo-wide because thousands of
77
* existing hits predate the rule. With the rules off nothing stops a new one, and an agent copying
88
* a nearby `as any` has no signal it is wrong. This check runs exactly those two Biome rules (the
9-
* same parser, ignore list, and `any` forms Biome knows: `: any`, `as any`, `<any>`, `any[]`) and
10-
* compares per-file counts with `scripts/check-explicit-any.baseline.json`.
9+
* same parser and `any` forms Biome knows: `: any`, `as any`, `<any>`, `any[]`) and compares
10+
* per-file counts with `scripts/check-explicit-any.baseline.json`. It uses Biome's file list, so it
11+
* inherits every ignore in `biome.json`.
1112
*
1213
* - A file whose count rises, or a file that gains its first hit, fails.
1314
* - A file whose count drops fails until the baseline is rewritten, so the baseline only shrinks
1415
* and a fix cannot be silently spent on a new `any` elsewhere in the same file.
16+
* - A `biome-ignore` comment for either rule fails outright: it would hide a hit from the count.
1517
*
1618
* Regenerate after removing hits: `bun run scripts/check-explicit-any.ts --update`.
1719
*
@@ -75,6 +77,21 @@ function collect(): Baseline {
7577
return counts
7678
}
7779

80+
/** `biome-ignore` comments for either rule, which would hide a hit from Biome's count. */
81+
function suppressions(): string[] {
82+
const pattern = `biome-ignore(-all|-start)?[[:space:]]+(${Object.values(METRICS).join('|')})`
83+
const result = Bun.spawnSync(
84+
['git', 'grep', '-nE', '--untracked', pattern, '--', 'apps', 'packages', 'scripts'],
85+
{ cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }
86+
)
87+
// git grep exits 1 when nothing matches.
88+
if (result.exitCode > 1) {
89+
console.error(`git grep failed:\n${result.stderr.toString()}`)
90+
process.exit(1)
91+
}
92+
return result.stdout.toString().split('\n').filter(Boolean)
93+
}
94+
7895
function sorted(counts: Counts): Counts {
7996
return Object.fromEntries(
8097
Object.keys(counts)
@@ -141,6 +158,17 @@ if (process.argv.includes('--update')) {
141158

142159
let regressed = 0
143160
let stale = 0
161+
/** A vanished baselined file next to a new file with no more hits: likely a rename. */
162+
const renames = new Set<string>()
163+
164+
const suppressed = suppressions()
165+
if (suppressed.length) {
166+
console.error(
167+
`✗ ${suppressed.length} biome-ignore comment(s) hide an \`any\` or \`!\` from this check:`
168+
)
169+
for (const line of suppressed) console.error(` ${line}`)
170+
console.error(' Delete the suppression and fix the type instead.\n')
171+
}
144172

145173
for (const metric of Object.keys(METRICS) as Metric[]) {
146174
const before = baseline[metric] ?? {}
@@ -153,6 +181,13 @@ for (const metric of Object.keys(METRICS) as Metric[]) {
153181
const shrunk = Object.entries(before)
154182
.filter(([file, count]) => (after[file] ?? 0) < count)
155183
.map(([file, count]) => ` ${file}: ${after[file] ?? 0} (baseline ${count})`)
184+
for (const [oldFile, oldCount] of Object.entries(before)) {
185+
if (oldFile in after) continue
186+
const renamed = Object.entries(after).find(
187+
([file, count]) => !(file in before) && count <= oldCount
188+
)
189+
if (renamed) renames.add(`${oldFile} → ${renamed[0]}`)
190+
}
156191
if (regressions.length) {
157192
console.error(`✗ ${metric}: ${regressions.length} file(s) gained ${METRICS[metric]} hits`)
158193
console.error(regressions.sort().join('\n'))
@@ -169,14 +204,20 @@ for (const metric of Object.keys(METRICS) as Metric[]) {
169204
stale += shrunk.length
170205
}
171206

172-
if (regressed || stale) {
207+
if (regressed || stale || suppressed.length) {
173208
if (stale) {
174209
console.error(
175210
'\nCounts dropped — shrink the baseline so they cannot creep back: ' +
176211
'bun run scripts/check-explicit-any.ts --update'
177212
)
178213
}
179214
if (regressed) console.error('\nNever raise the baseline to make a new `any` or `!` pass.')
215+
for (const rename of renames) {
216+
console.error(
217+
`\nLooks like a rename: ${rename}. Move its baseline entries to the new path in ` +
218+
`${path.relative(ROOT, BASELINE)} (debt carries over; it may not grow).`
219+
)
220+
}
180221
process.exit(1)
181222
}
182223

‎scripts/check-file-names.ts‎

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
#!/usr/bin/env bun
22
/**
3-
* Enforces the file-naming conventions in `CLAUDE.md` ("files kebab-case") and
4-
* `.claude/rules/sim-architecture.md`, so an agent can predict a module's path from its role and
5-
* never has to guess between `workflowList.ts`, `workflow_list.ts`, and `workflow-list.ts`.
3+
* Enforces the file-naming conventions in `CLAUDE.md` "Naming" ("files kebab-case"), so an agent
4+
* can predict a module's path from its role and never has to guess between `workflowList.ts`,
5+
* `workflow_list.ts`, and `workflow-list.ts`.
66
*
7-
* Rules, over every JS/TS source file under `apps/` and `packages/` (tracked or untracked, never
8-
* gitignored, so a file is checked before it is staged):
7+
* Rules, over every JS/TS source file under `apps/`, `packages/`, root `scripts/`, and
8+
* `vitest.shared.ts` (tracked or untracked, never gitignored, so a file is checked before it is
9+
* staged). Dot-prefixed folders and files (`.well-known`, `.eslintrc.cjs`) are exempt: their names
10+
* are mandated by the tool that reads them.
911
*
1012
* - `kebab-case`: every path segment below the workspace root is kebab-case. Dotted suffixes
1113
* (`.test.ts`, `.server.ts`, `.d.ts`, `.config.ts`) are kebab segments too. Allowed exceptions,
1214
* each forced by something outside our control:
1315
* - Next.js routing segments: `[param]`, `[...slug]`, `[[...slug]]`, `(group)`, `@slot`,
14-
* `_private` folders, and metadata route folders named after their URL (`robots.txt`).
16+
* interception routes (`(.)x`, `(..)x`, `(..)(..)x`, `(...)x`), `_private` folders, and
17+
* metadata route folders named after their URL (`robots.txt`).
1518
* - Vitest/fixture folders wrapped in double underscores (`__integration__`, `__fixtures__`).
16-
* - `.well-known`, and the SCIM v2 resource folders (`Users`, `Groups`, …) whose casing is
17-
* fixed by RFC 7644.
19+
* - The SCIM v2 resource folders (`Users`, `Groups`, …) whose casing is fixed by RFC 7644.
1820
* - Integration folders (`apps/sim/{tools,triggers}/**`, `apps/sim/blocks/blocks/*`) may be
1921
* snake_case: a tool file and its service folder are named after the snake_case tool or
2022
* block id (`tools/google_sheets/append_row.ts`), and that id is the integration's identity.
@@ -45,6 +47,7 @@ const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
4547
const KEBAB_OR_SNAKE = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/
4648
const NEXT_DYNAMIC = /^\[{1,2}(?:\.\.\.)?[A-Za-z][A-Za-z0-9]*\]{1,2}$/
4749
const NEXT_GROUP = /^\([a-z0-9]+(?:-[a-z0-9]+)*\)$/
50+
const NEXT_INTERCEPT = /^(?:\(\.{1,3}\)|(?:\(\.\.\))+)[a-z0-9]+(?:-[a-z0-9]+)*$/
4851
const NEXT_SLOT = /^@[a-z0-9]+(?:-[a-z0-9]+)*$/
4952
const NEXT_PRIVATE = /^_[a-z0-9]+(?:-[a-z0-9]+)*$/
5053
const DUNDER = /^__[a-z0-9]+(?:-[a-z0-9]+)*__$/
@@ -81,10 +84,24 @@ interface Violation {
8184
function sourceFiles(): string[] {
8285
const output = execFileSync(
8386
'git',
84-
['ls-files', '--cached', '--others', '--exclude-standard', '-z', 'apps', 'packages'],
87+
[
88+
'ls-files',
89+
'--cached',
90+
'--others',
91+
'--exclude-standard',
92+
'-z',
93+
'apps',
94+
'packages',
95+
'scripts',
96+
'vitest.shared.ts',
97+
],
8598
{ cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }
8699
)
87-
return [...new Set(output.split('\0'))].filter((file) => SOURCE_FILE.test(file)).sort()
100+
// existsSync drops the old path of an unstaged `mv`, which `--cached` still lists.
101+
return [...new Set(output.split('\0'))]
102+
.filter((file) => SOURCE_FILE.test(file) && !path.basename(file).startsWith('.'))
103+
.filter((file) => existsSync(path.join(ROOT, file)))
104+
.sort()
88105
}
89106

90107
function toKebab(name: string): string {
@@ -101,10 +118,11 @@ function isAllowedFolder(segment: string, file: string, inIntegration: boolean):
101118
if (
102119
NEXT_DYNAMIC.test(segment) ||
103120
NEXT_GROUP.test(segment) ||
121+
NEXT_INTERCEPT.test(segment) ||
104122
NEXT_SLOT.test(segment) ||
105123
NEXT_PRIVATE.test(segment) ||
106124
DUNDER.test(segment) ||
107-
segment === '.well-known'
125+
segment.startsWith('.')
108126
) {
109127
return true
110128
}
@@ -132,7 +150,8 @@ function check(file: string): Violation[] {
132150
const violations: Violation[] = []
133151
const segments = file.split('/')
134152
const name = segments[segments.length - 1]
135-
const folders = segments.slice(2, -1)
153+
// Root `scripts/` belongs to the root workspace; apps/<name>/ and packages/<name>/ are roots.
154+
const folders = segments.slice(segments[0] === 'scripts' ? 1 : 2, -1)
136155
const inIntegration = INTEGRATION_PREFIXES.some((prefix) => file.startsWith(prefix))
137156

138157
if (!SCRIPT_MIGRATION.test(file)) {
@@ -184,7 +203,8 @@ const HOW_TO_FIX: Record<Rule, string> = {
184203
'redundant-suffix':
185204
'The utils/ or helpers/ folder already names the role; drop the suffix from the file name.',
186205
stutter:
187-
'The parent folder already names the domain; drop the repeated prefix (lib/logs/log-views.ts → lib/logs/views.ts). If the short name collides, pick a more specific one.',
206+
'The parent folder already names the domain; drop the repeated prefix (lib/logs/log-views.ts → lib/logs/views.ts). If the short name collides, pick a more specific one. ' +
207+
'Existing `handlers/<x>/<x>-handler.ts` files are baselined debt, not a convention to copy.',
188208
}
189209

190210
function key(violation: Violation): string {
@@ -239,7 +259,8 @@ if (added.length || stale.length) {
239259
if (added.length) {
240260
console.error(
241261
`\n${added.length} new file-name violation(s). Rename the file and update its importers; ` +
242-
'never add a new file to the baseline.'
262+
'never add a new file to the baseline. A name mandated by an outside tool goes in the ' +
263+
'allowlist at the top of scripts/check-file-names.ts with a comment saying why.'
243264
)
244265
}
245266
if (stale.length) {
@@ -249,6 +270,15 @@ if (added.length || stale.length) {
249270
'baseline: bun run scripts/check-file-names.ts --update'
250271
)
251272
}
273+
for (const entry of added) {
274+
const old = stale.find((s) => s.split('\t')[0] === entry.split('\t')[0])
275+
if (old) {
276+
console.error(
277+
`\nLooks like a rename: ${old.split('\t')[1]} → ${entry.split('\t')[1]}. Move its baseline ` +
278+
`entry to the new path in ${path.relative(ROOT, BASELINE)} (debt carries over; it may not grow).`
279+
)
280+
}
281+
}
252282
process.exit(1)
253283
}
254284

0 commit comments

Comments
 (0)