mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
Merge origin/main (v1.4.127-rc.0) into orca-performance
46 commits from main. Conflict resolutions (branch structure wins, main semantics grafted): - pending-backlog cap (#7630): main's capPendingPtyData/droppedBacklog 2MB tail-trim EXCISED — this branch's droppedOutput sentinel mechanism (O(1) drop + query salvage + settings-scaled cap + restore marker) already bounds the unsent backlog and strictly supersedes it; carrying both would race two cap policies over one buffer. Kept main's makePtyDataPayload helper and exit-time pending flush (adapted to the sentinel). Main's duplicate never-ACK cap test removed (covered by the existing sentinel cap tests); its reload-gate tests kept with the runtime mock extended. - replay guard (#7661): main's blind REPLAY_GUARD_RELEASE_FALLBACK_MS release dropped — this branch's probe-certified stall release covers the same cold-restore reattach lockout without the auto-reply leak a timed release can cause in agent TUIs. Main's three regression tests KEPT, adapted to probe timing (explicit stallCheckMs). - hidden-pressure scenario: kept this branch's STRICT 1500ms restore budget (the background keep-tail global budget earns it; main relaxed to 4s for CI drain overhead) + grafted main's timer-drift gate const. - ssh-docker-relay-perf: kept this branch's ACK-stalled background PTY test AND grafted main's new file-streams+git-churn test (4 tests). - preload/renderer: droppedBacklog threading excised with the mechanism; main's unrelated changes (ssh importConfig reAdopt, etc.) kept. Gates: typecheck clean; ipc+daemon+terminal-pane+pane-manager suites 4,396 passed / 14 skipped. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
// Ratchet gate for the oxlint `max-lines` rule.
|
||||
//
|
||||
// oxlint already fails any file that exceeds max-lines WITHOUT a suppression, so
|
||||
// the only way a file grows past the budget is by adding an `eslint/oxlint-disable
|
||||
// max-lines` comment or a per-file `max-lines` bump in mobile/.oxlintrc.json. This
|
||||
// check freezes the set of files currently allowed to do that (the baseline) and
|
||||
// fails CI when a NEW bypass appears — the existing over-limit files are
|
||||
// grandfathered; new ones must split instead. The baseline may only shrink.
|
||||
|
||||
const BASELINE_PATH = 'config/max-lines-baseline.txt'
|
||||
const MOBILE_CONFIG_PATH = 'mobile/.oxlintrc.json'
|
||||
// These two files legitimately contain the directive text as data (regex, fixtures),
|
||||
// so scanning them would self-flag. The ratchet does not police itself.
|
||||
const SELF_FILES = new Set([
|
||||
'config/scripts/check-max-lines-ratchet.mjs',
|
||||
'config/scripts/check-max-lines-ratchet.test.mjs'
|
||||
])
|
||||
|
||||
// Default max-lines budgets from .oxlintrc.json (counted lines).
|
||||
export function defaultLimitForPath(p) {
|
||||
if (/\.(test|spec)\.(ts|tsx)$/.test(p)) {
|
||||
return 800
|
||||
}
|
||||
if (p.endsWith('.tsx')) {
|
||||
return 400
|
||||
}
|
||||
if (p.endsWith('.mjs')) {
|
||||
return 600
|
||||
}
|
||||
return 300
|
||||
}
|
||||
|
||||
// True if the source contains an eslint/oxlint disable directive listing `max-lines`
|
||||
// (block or line, bare or compound, with or without a `-- Why:` reason).
|
||||
export function hasMaxLinesDisable(sourceText) {
|
||||
const re = /(?:eslint|oxlint)-disable(?:-next-line|-line)?\b([^\n]*)/g
|
||||
let m
|
||||
while ((m = re.exec(sourceText)) !== null) {
|
||||
let rules = m[1]
|
||||
rules = rules.split('--')[0] // strip the reason
|
||||
const close = rules.indexOf('*/')
|
||||
if (close !== -1) {
|
||||
rules = rules.slice(0, close) // strip block-comment tail
|
||||
}
|
||||
if (/\bmax-lines\b/.test(rules)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Per-file `max-lines` bumps in mobile/.oxlintrc.json whose `max` exceeds the
|
||||
// default for that glob (a lower `max` is stricter, not a bypass).
|
||||
export function collectMobileBumps(configText) {
|
||||
const cfg = JSON.parse(configText)
|
||||
const bumps = []
|
||||
for (const override of cfg.overrides ?? []) {
|
||||
const rule = override.rules?.['max-lines']
|
||||
if (!Array.isArray(rule) || typeof rule[1]?.max !== 'number') {
|
||||
continue
|
||||
}
|
||||
for (const glob of override.files ?? []) {
|
||||
if (rule[1].max > defaultLimitForPath(glob)) {
|
||||
bumps.push(`mobile-config ${glob}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return bumps
|
||||
}
|
||||
|
||||
export function parseBaseline(text) {
|
||||
return new Set(
|
||||
text
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.startsWith('#'))
|
||||
)
|
||||
}
|
||||
|
||||
export function diffBaseline(current, baseline) {
|
||||
const cur = new Set(current)
|
||||
const base = baseline instanceof Set ? baseline : new Set(baseline)
|
||||
const added = [...cur].filter((e) => !base.has(e)).sort()
|
||||
const stale = [...base].filter((e) => !cur.has(e)).sort()
|
||||
return { added, stale }
|
||||
}
|
||||
|
||||
// Collect every current suppression entry from the tracked tree.
|
||||
export function collectCurrentSuppressions(root = process.cwd()) {
|
||||
const tracked = execFileSync('git', ['ls-files', '*.ts', '*.tsx', '*.mjs'], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 64 * 1024 * 1024
|
||||
})
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.filter((f) => !SELF_FILES.has(f))
|
||||
|
||||
const entries = []
|
||||
for (const rel of tracked) {
|
||||
let src
|
||||
try {
|
||||
src = fs.readFileSync(path.join(root, rel), 'utf8')
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (hasMaxLinesDisable(src)) {
|
||||
entries.push(`inline ${rel}`)
|
||||
}
|
||||
}
|
||||
|
||||
const mobileCfgPath = path.join(root, MOBILE_CONFIG_PATH)
|
||||
if (fs.existsSync(mobileCfgPath)) {
|
||||
entries.push(...collectMobileBumps(fs.readFileSync(mobileCfgPath, 'utf8')))
|
||||
}
|
||||
|
||||
return entries.sort()
|
||||
}
|
||||
|
||||
function printAddedFailure(added) {
|
||||
for (const entry of added) {
|
||||
console.error(`::error::New max-lines bypass not allowed: ${entry}`)
|
||||
}
|
||||
console.error('')
|
||||
console.error('╭────────────────────────────────────────────────────────────────────────────╮')
|
||||
console.error('│ ❌ max-lines ratchet failed — a NEW file is trying to exceed the line cap. │')
|
||||
console.error('╰────────────────────────────────────────────────────────────────────────────╯')
|
||||
console.error('')
|
||||
console.error(` ${added.length} file(s)/glob(s) newly bypass the oxlint \`max-lines\` rule:`)
|
||||
console.error('')
|
||||
for (const entry of added) {
|
||||
const [kind, ...rest] = entry.split(' ')
|
||||
const target = rest.join(' ')
|
||||
const how =
|
||||
kind === 'inline'
|
||||
? 'added an eslint/oxlint-disable max-lines comment'
|
||||
: 'added a per-file max-lines bump in mobile/.oxlintrc.json'
|
||||
console.error(` • ${target}\n ↳ ${how}`)
|
||||
}
|
||||
console.error('')
|
||||
console.error(' Orca caps file size (300 .ts / 400 .tsx / 600 .mjs / 800 test — non-blank,')
|
||||
console.error(
|
||||
' non-comment lines). Existing oversized files are grandfathered; NEW ones are not.'
|
||||
)
|
||||
console.error('')
|
||||
console.error(' ✅ Fix it: SPLIT the file into focused modules — do NOT suppress the rule.')
|
||||
console.error(' See AGENTS.md → "Lint Rules: Do Not Disable Max Lines".')
|
||||
console.error('')
|
||||
console.error(' (If you are intentionally, with reviewer sign-off, adding an unavoidable')
|
||||
console.error(` exception, add the exact line(s) above to ${BASELINE_PATH}.)`)
|
||||
console.error('')
|
||||
}
|
||||
|
||||
function printStaleFailure(stale) {
|
||||
for (const entry of stale) {
|
||||
console.error(`::error::Stale max-lines baseline entry (prune it): ${entry}`)
|
||||
}
|
||||
console.error('')
|
||||
console.error('╭────────────────────────────────────────────────────────────────────────────╮')
|
||||
console.error('│ ⚠️ max-lines baseline is out of date — nice work removing a bypass! │')
|
||||
console.error('╰────────────────────────────────────────────────────────────────────────────╯')
|
||||
console.error('')
|
||||
console.error(` ${stale.length} baseline entr(y/ies) no longer have a max-lines suppression.`)
|
||||
console.error(
|
||||
' The baseline may only shrink, so these must be removed to keep re-adding blocked:'
|
||||
)
|
||||
console.error('')
|
||||
for (const entry of stale) {
|
||||
console.error(` • ${entry}`)
|
||||
}
|
||||
console.error('')
|
||||
console.error(` ✅ Fix it (one command): pnpm check:max-lines-ratchet --prune`)
|
||||
console.error('')
|
||||
}
|
||||
|
||||
export function main(root = process.cwd()) {
|
||||
const baselineFile = path.join(root, BASELINE_PATH)
|
||||
if (!fs.existsSync(baselineFile)) {
|
||||
console.error(
|
||||
`::error::Missing ${BASELINE_PATH}. Generate it with: node config/scripts/check-max-lines-ratchet.mjs --init`
|
||||
)
|
||||
return 1
|
||||
}
|
||||
const baseline = parseBaseline(fs.readFileSync(baselineFile, 'utf8'))
|
||||
const current = collectCurrentSuppressions(root)
|
||||
const { added, stale } = diffBaseline(current, baseline)
|
||||
|
||||
if (added.length > 0) {
|
||||
printAddedFailure(added)
|
||||
if (stale.length > 0) {
|
||||
console.error(
|
||||
` (Also: ${stale.length} stale baseline entr(y/ies) can be pruned — see below.)`
|
||||
)
|
||||
printStaleFailure(stale)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if (stale.length > 0) {
|
||||
printStaleFailure(stale)
|
||||
return 1
|
||||
}
|
||||
console.log(
|
||||
`max-lines ratchet OK — ${current.length} grandfathered suppression(s), no new bypasses.`
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
function writeBaseline(root, entries) {
|
||||
const header = [
|
||||
'# Files/globs currently allowed to exceed the oxlint `max-lines` budget.',
|
||||
'# This is a RATCHET: the list may only SHRINK. Do NOT add entries to get CI green —',
|
||||
'# split the oversized file instead (AGENTS.md → "Do Not Disable Max Lines").',
|
||||
'# Regenerate/prune: pnpm check:max-lines-ratchet --prune (removes stale entries only)',
|
||||
''
|
||||
].join('\n')
|
||||
fs.writeFileSync(path.join(root, BASELINE_PATH), `${header}${entries.join('\n')}\n`)
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const root = process.cwd()
|
||||
const arg = process.argv[2]
|
||||
if (arg === '--init') {
|
||||
// One-time bootstrap: capture the current suppression set as the baseline.
|
||||
const entries = collectCurrentSuppressions(root)
|
||||
writeBaseline(root, entries)
|
||||
console.log(`Wrote ${BASELINE_PATH} with ${entries.length} entries.`)
|
||||
process.exit(0)
|
||||
}
|
||||
if (arg === '--prune') {
|
||||
// Remove baseline entries whose suppression is gone (shrink only; never adds).
|
||||
const current = new Set(collectCurrentSuppressions(root))
|
||||
const baseline = parseBaseline(fs.readFileSync(path.join(root, BASELINE_PATH), 'utf8'))
|
||||
const kept = [...baseline].filter((e) => current.has(e)).sort()
|
||||
const newlyAdded = [...current].filter((e) => !baseline.has(e))
|
||||
writeBaseline(root, kept)
|
||||
console.log(
|
||||
`Pruned baseline to ${kept.length} entries (removed ${baseline.size - kept.length}).`
|
||||
)
|
||||
if (newlyAdded.length > 0) {
|
||||
console.error(
|
||||
`::error::--prune does not add entries; ${newlyAdded.length} new bypass(es) remain — split those files.`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
process.exit(main(root))
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
collectMobileBumps,
|
||||
defaultLimitForPath,
|
||||
diffBaseline,
|
||||
hasMaxLinesDisable,
|
||||
parseBaseline
|
||||
} from './check-max-lines-ratchet.mjs'
|
||||
|
||||
describe('hasMaxLinesDisable', () => {
|
||||
it('detects a bare block disable', () => {
|
||||
expect(hasMaxLinesDisable('/* eslint-disable max-lines */\nexport const a = 1\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects the oxlint spelling', () => {
|
||||
expect(hasMaxLinesDisable('/* oxlint-disable max-lines */\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects a disable with a -- Why reason', () => {
|
||||
expect(hasMaxLinesDisable('/* eslint-disable max-lines -- Why: one owner. */\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects a multi-line block where the reason wraps', () => {
|
||||
const src =
|
||||
'/* eslint-disable max-lines -- Why: this contract is\n * intentionally centralized. */\nimport x from "y"\n'
|
||||
expect(hasMaxLinesDisable(src)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects max-lines inside a compound rule list', () => {
|
||||
expect(hasMaxLinesDisable('/* eslint-disable no-control-regex, max-lines -- Why: x */\n')).toBe(
|
||||
true
|
||||
)
|
||||
expect(hasMaxLinesDisable('/* eslint-disable max-lines, no-control-regex */\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects a line-scoped disable', () => {
|
||||
expect(hasMaxLinesDisable('const a = 1 // eslint-disable-line max-lines\n')).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores a disable for an unrelated rule', () => {
|
||||
expect(hasMaxLinesDisable('/* eslint-disable no-console */\n')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not treat "max-lines" appearing only in the reason text as a suppression', () => {
|
||||
// max-lines is after the `--`, so it is prose, not a suppressed rule.
|
||||
expect(
|
||||
hasMaxLinesDisable('/* eslint-disable no-console -- we could hit max-lines later */\n')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for ordinary source', () => {
|
||||
expect(hasMaxLinesDisable('export function f() {\n return 42\n}\n')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('defaultLimitForPath', () => {
|
||||
it('uses 800 for tests, 400 for tsx, 600 for mjs, 300 otherwise', () => {
|
||||
expect(defaultLimitForPath('a/b.test.ts')).toBe(800)
|
||||
expect(defaultLimitForPath('a/b.spec.tsx')).toBe(800)
|
||||
expect(defaultLimitForPath('a/b.tsx')).toBe(400)
|
||||
expect(defaultLimitForPath('a/b.mjs')).toBe(600)
|
||||
expect(defaultLimitForPath('a/b.ts')).toBe(300)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collectMobileBumps', () => {
|
||||
it('captures only overrides whose max exceeds the default for the glob', () => {
|
||||
const cfg = JSON.stringify({
|
||||
overrides: [
|
||||
{ files: ['app/h/*/tasks.tsx'], rules: { 'max-lines': ['error', { max: 14682 }] } }, // bump (>400)
|
||||
{
|
||||
files: ['src/terminal/TerminalWebView.tsx'],
|
||||
rules: { 'max-lines': ['error', { max: 379 }] }
|
||||
}, // stricter (<400), skip
|
||||
{ files: ['scripts/mock-server.ts'], rules: { 'max-lines': ['error', { max: 407 }] } } // bump (>300)
|
||||
]
|
||||
})
|
||||
expect(collectMobileBumps(cfg)).toEqual([
|
||||
'mobile-config app/h/*/tasks.tsx',
|
||||
'mobile-config scripts/mock-server.ts'
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores overrides without a max-lines rule', () => {
|
||||
const cfg = JSON.stringify({
|
||||
overrides: [{ files: ['a.tsx'], rules: { 'no-console': 'off' } }]
|
||||
})
|
||||
expect(collectMobileBumps(cfg)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseBaseline', () => {
|
||||
it('drops comments and blank lines', () => {
|
||||
const b = parseBaseline('# header\n\ninline a.ts\nmobile-config x/*.tsx\n')
|
||||
expect(b).toEqual(new Set(['inline a.ts', 'mobile-config x/*.tsx']))
|
||||
})
|
||||
})
|
||||
|
||||
describe('diffBaseline', () => {
|
||||
it('reports added and stale entries', () => {
|
||||
const { added, stale } = diffBaseline(
|
||||
['inline b.ts', 'inline c.ts'],
|
||||
new Set(['inline a.ts', 'inline b.ts'])
|
||||
)
|
||||
expect(added).toEqual(['inline c.ts']) // new bypass
|
||||
expect(stale).toEqual(['inline a.ts']) // suppression removed
|
||||
})
|
||||
|
||||
it('is clean when current matches baseline', () => {
|
||||
const { added, stale } = diffBaseline(['inline a.ts'], new Set(['inline a.ts']))
|
||||
expect(added).toEqual([])
|
||||
expect(stale).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user