Files
orca/config/scripts/check-focus-read-ratchet.mjs
T
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00

439 lines
16 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import {
diffCounts,
parseCountBaseline,
pruneCountBaseline,
readTrackedSources,
totalCount
} from './ratchet-baseline.mjs'
// Count direct Active Server reads and discovered caller-returning wrappers so changing syntax cannot hide host-misrouting growth.
// Route work by resource host; counts only decrease, and deleted helpers stay banned.
const SCAN_ROOT = 'src/renderer/src'
const BASELINE_PATH = 'config/focus-setting-read-baseline.txt'
const FORBIDDEN_HELPERS = [
'getActiveRuntimeTarget',
'legacyRouteFromSettings',
'settingsForRuntimeOwner'
]
const IMPORT_EXPORT_LIST = /\b(?:import|export)\s+(?:type\s+)?\{[^}]*\}/g
// Calls and value uses (`.map(helper)`); definitions and type queries are not routing.
const FORBIDDEN_HELPER_USE = new RegExp(
`(?<!(?:function|typeof)\\s+)\\b(?:${FORBIDDEN_HELPERS.join('|')})\\b`,
'g'
)
// Seed readers; every function that reads the setting or calls a reader joins them by discovery.
// The sanctioned readers are seeds too: their bodies reach the setting through a private helper.
const SEED_FOCUS_READERS = [
...FORBIDDEN_HELPERS,
'defaultCreationHost',
'defaultScopeHost',
'defaultScopeSource',
'rowLessSourceTarget'
]
const FOCUS_SCAN_ROOTS = [SCAN_ROOT, 'src/shared']
/** Imports and re-exports are not uses; an aliased one is reported by {@link hasFocusRoutingAlias}. */
export function countForbiddenHelperUses(sourceText) {
return sourceText.replace(IMPORT_EXPORT_LIST, '').match(FORBIDDEN_HELPER_USE)?.length ?? 0
}
// Element reads after a PascalCase name or `>` are indexed types (`GlobalSettings['…']`), not reads.
const SETTING_MEMBER_READ =
/\??\.\s*activeRuntimeEnvironmentId\b|(?<!(?:\b[A-Z][\w$]*|>)\s*)\[\s*['"]activeRuntimeEnvironmentId['"]\s*\]/g
// `const { activeRuntimeEnvironmentId } = s`, `{ activeRuntimeEnvironmentId: id }: T = s` or nested
// `{ settings: { activeRuntimeEnvironmentId } } = state`; object literals and `({ … }) =>`
// parameters are not reads of the setting.
const SETTING_DESTRUCTURE_READ =
/\{[^{}]*\bactiveRuntimeEnvironmentId\b[^{}]*\}(?:\s*\})*\s*(?::[^=;{}]*)?=(?![=>])/g
// A top-level declaration: `[export] [async] function name` or `[export] const|let name =`.
const TOP_LEVEL_DECLARATION =
/^(export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s*\*?\s*([\w$]+)|(?:const|let)\s+([\w$]+)\b)/gm
// Any new statement at column 0 ends a declaration; closing brackets and continuations do not.
const TOP_LEVEL_BOUNDARY = /^[^\s)}\]]/gm
// Member calls only, so dotted string keys (`'auto.hooks.useX.abc'`) are not uses.
const MEMBER_NAME = /\.\s*([\w$]+)\s*(?:\?\.\s*)?\(/g
function readerUsePattern(names) {
return names.size === 0
? null
: new RegExp(
`(?<!(?:function|typeof|const|let)\\s*\\*?\\s+)\\b(?:${[...names].join('|')})\\b`,
'g'
)
}
function countSettingReads(body) {
return (
(body.match(SETTING_MEMBER_READ)?.length ?? 0) +
(body.match(SETTING_DESTRUCTURE_READ)?.length ?? 0)
)
}
/** Top-level declarations with their source text, up to the next top-level statement. */
export function topLevelDeclarations(sourceText) {
const boundaries = [...sourceText.matchAll(TOP_LEVEL_BOUNDARY)].map((match) => match.index)
return [...sourceText.matchAll(TOP_LEVEL_DECLARATION)].map((match) => {
const end = boundaries.find((index) => index > match.index) ?? sourceText.length
return {
name: match[2] ?? match[3],
exported: Boolean(match[1]),
text: sourceText.slice(match.index, end)
}
})
}
// Follow returned values until bracket-free newline; skip object bags or store/slice exports pull in the whole graph.
const VALUE_STATEMENT_START = /\breturn\s+(?!\(?\s*\{)|=>\s*(?!\(?\s*\{)/g
function statementsFrom(body, start) {
const statements = []
for (const match of body.matchAll(start)) {
let depth = 0
let end = match.index + match[0].length
for (; end < body.length; end += 1) {
const char = body[end]
if ('([{'.includes(char)) {
depth += 1
} else if (')]}'.includes(char)) {
if (depth === 0) {
break
}
depth -= 1
} else if (depth === 0 && (char === '\n' || (char === ',' && match[0].startsWith('=>')))) {
break
}
}
statements.push({ name: match[1], text: body.slice(match.index, end) })
}
return statements
}
// `const id = reader(…)` counts only when `id` is then returned (assign-then-return).
const LOCAL_ASSIGNMENT_START = /\b(?:const|let)\s+([\w$]+)\s*(?::[^=\n]*)?=(?![=>])/g
function valueStatements(body) {
const returned = statementsFrom(body, VALUE_STATEMENT_START).map(({ text }) => text)
const returnedIds = new Set(returned.join('\n').match(/[\w$]+/g))
const assigned = statementsFrom(body, LOCAL_ASSIGNMENT_START)
.filter(({ name }) => returnedIds.has(name))
.map(({ text }) => text)
return [...returned, ...assigned].join('\n')
}
const IMPORT_STATEMENT =
/\bimport\s+(?:type\s+)?(?:[\w$]+\s*,\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g
const REEXPORT_STATEMENT = /\bexport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g
// `const { reader } = (await import('…'))` and `import('…').then(({ reader }) =>` bind like a
// named import; `{ reader: local }` renames like `as`.
const DYNAMIC_IMPORT_DESTRUCTURE =
/\{([^{}]*)\}\s*=\s*\(?\s*await\s+import\(\s*['"]([^'"]+)['"]\s*\)(?:\s*\))?/g
const DYNAMIC_IMPORT_THEN =
/\bimport\(\s*['"]([^'"]+)['"]\s*\)\s*\.then\(\s*(?:async\s*)?\(\s*\{([^{}]*)\}/g
const STAR_REEXPORT_STATEMENT = /\bexport\s+\*\s+from\s*['"]([^'"]+)['"]/g
const LOCAL_EXPORT_LIST = /\bexport\s+\{([^}]*)\}(?!\s*from)/g
function specifiers(list) {
return list
.split(',')
.map((part) => part.trim().replace(/^type\s+/, ''))
.filter(Boolean)
.map((part) => {
const [imported, local = imported] = part.split(/\s+as\s+|\s*:\s*/)
return { imported: imported.trim(), local: local.trim() }
})
}
function resolveModule(spec, fromRel, files) {
let base
if (spec.startsWith('@/')) {
base = `${SCAN_ROOT}/${spec.slice(2)}`
} else if (spec.startsWith('.')) {
base = path.posix.join(path.posix.dirname(fromRel), spec)
} else {
return null
}
base = base.replace(/\.(?:m?js|tsx?)$/, '')
return (
['.ts', '.tsx', '/index.ts', '/index.tsx'].map((ext) => base + ext).find((c) => files.has(c)) ??
null
)
}
function parseModule(rel, text, files) {
const imports = new Map()
for (const [, list, spec] of [
...text.matchAll(IMPORT_STATEMENT),
...text.matchAll(DYNAMIC_IMPORT_DESTRUCTURE),
...[...text.matchAll(DYNAMIC_IMPORT_THEN)].map(([all, from, names]) => [all, names, from])
]) {
const from = resolveModule(spec, rel, files)
for (const { imported, local } of specifiers(list)) {
imports.set(local, { from, name: imported })
}
}
const reexports = new Map()
for (const [, list, spec] of text.matchAll(REEXPORT_STATEMENT)) {
const from = resolveModule(spec, rel, files)
for (const { imported, local } of specifiers(list)) {
reexports.set(local, { from, name: imported })
}
}
const starFrom = [...text.matchAll(STAR_REEXPORT_STATEMENT)].map(([, spec]) =>
resolveModule(spec, rel, files)
)
const listed = new Map()
for (const [, list] of text.matchAll(LOCAL_EXPORT_LIST)) {
for (const { imported, local } of specifiers(list)) {
listed.set(imported, local)
}
}
const declarations = topLevelDeclarations(text.replace(IMPORT_EXPORT_LIST, '')).map((decl) => ({
...decl,
exportedAs: decl.exported ? decl.name : (listed.get(decl.name) ?? null)
}))
return { imports, reexports, starFrom, declarations }
}
// Discover exported setting readers and returned-reader wrappers to a fixpoint, keyed by defining file.
// Hand lists miss renamed copies; following every call instead of returned values pulls in most renderer code through the store.
export function discoverFocusReaders(sources) {
const files = new Set(sources.keys())
const modules = new Map([...sources].map(([rel, text]) => [rel, parseModule(rel, text, files)]))
const seeds = new Set(SEED_FOCUS_READERS)
const seedUse = readerUsePattern(seeds)
const readerKeys = new Set()
const resolveExport = (rel, name, depth = 0) => {
const mod = rel ? modules.get(rel) : null
if (!mod || depth > 8) {
return null
}
if (mod.declarations.some((decl) => decl.exportedAs === name)) {
return `${rel}#${name}`
}
const re = mod.reexports.get(name)
if (re) {
return resolveExport(re.from, re.name, depth + 1)
}
for (const from of mod.starFrom) {
const key = resolveExport(from, name, depth + 1)
if (key) {
return key
}
}
return null
}
const resolveLocal = (rel, name) => {
const mod = modules.get(rel)
if (mod.declarations.some((decl) => decl.exportedAs && decl.name === name)) {
return `${rel}#${mod.declarations.find((decl) => decl.name === name).exportedAs}`
}
const imported = mod.imports.get(name)
return imported ? resolveExport(imported.from, imported.name) : null
}
const isReader = (rel, name) => seeds.has(name) || readerKeys.has(resolveLocal(rel, name))
const pending = []
for (const [rel, mod] of modules) {
for (const { name, exportedAs, text } of mod.declarations) {
// Components render; a `<Pane />` that reads focus inside is not a read by its parent.
if (!exportedAs || /^[A-Z]/.test(exportedAs)) {
continue
}
seedUse.lastIndex = 0
if (countSettingReads(text) > 0 || seedUse.test(text)) {
readerKeys.add(`${rel}#${exportedAs}`)
continue
}
const statements = valueStatements(text)
const passed = new Set(statements.match(/[\w$]+/g))
passed.delete(name)
const members = [...statements.matchAll(MEMBER_NAME)].map(([, member]) => member)
pending.push({ rel, key: `${rel}#${exportedAs}`, passed: [...passed], members })
}
}
// Exported reader names, for `ns.reader(…)` through a namespace or dynamic import.
const memberNames = new Set(seeds)
const addReader = (key) => {
readerKeys.add(key)
memberNames.add(key.slice(key.indexOf('#') + 1))
}
for (const key of readerKeys) {
addReader(key)
}
let changed = true
while (changed) {
changed = false
for (let i = pending.length - 1; i >= 0; i -= 1) {
const { rel, key, passed, members } = pending[i]
if (
passed.some((id) => isReader(rel, id)) ||
members.some((member) => memberNames.has(member))
) {
addReader(key)
pending.splice(i, 1)
changed = true
}
}
}
const namesByFile = new Map()
return {
has: (name) => memberNames.has(name),
memberNames,
/** Names that refer to a reader inside `rel`: seeds, its own readers and imported ones. */
namesFor(rel) {
if (!namesByFile.has(rel)) {
const mod = modules.get(rel)
const local = [
...(mod?.declarations.map((decl) => decl.name) ?? []),
...(mod?.imports.keys() ?? [])
]
namesByFile.set(
rel,
new Set([...seeds, ...local.filter((name) => mod && isReader(rel, name))])
)
}
return namesByFile.get(rel)
}
}
}
/**
* Reads of the setting (member, element and destructuring reads) plus every use of a name that
* refers to a reader. Object-literal keys are writes and are not counted.
*/
export function countFocusSettingReads(
sourceText,
readerNames = new Set(SEED_FOCUS_READERS),
memberNames = new Set()
) {
const body = sourceText
.replace(IMPORT_EXPORT_LIST, '')
// A placeholder, so a dangling `const` does not read as a declaration of the next call.
.replace(DYNAMIC_IMPORT_DESTRUCTURE, 'dynamicImport')
.replace(DYNAMIC_IMPORT_THEN, 'dynamicImport')
const pattern = readerUsePattern(readerNames)
// `ns.reader(…)` through a namespace or dynamic import; names already counted bare are skipped.
const members = [...body.matchAll(MEMBER_NAME)].filter(
([, member]) => memberNames.has(member) && !readerNames.has(member)
).length
return countSettingReads(body) + (pattern ? (body.match(pattern)?.length ?? 0) : 0) + members
}
/** An alias hides later calls from the name-based helper count, so it is refused outright. */
export function hasFocusRoutingAlias(sourceText) {
const alias = new RegExp(`\\b(?:${SEED_FOCUS_READERS.join('|')})\\s+as\\b`)
return (sourceText.match(IMPORT_EXPORT_LIST) ?? []).some((list) => alias.test(list))
}
const BASELINE_HEADER = [
'# Renderer reads of the Active Server setting (member, element and destructuring reads of',
'# activeRuntimeEnvironmentId) plus calls of exported functions that read it, per file.',
'# This is a RATCHET: counts may only go DOWN. Only creation flows with no source row may read',
'# the default host, through defaultCreationHost. Everything else routes by the owner.',
'# Prune after removing reads: pnpm check:focus-read-ratchet --prune'
]
const ADVICE =
"Route by the resource's owner. Only a creation flow with no source row may use defaultCreationHost."
// Why test support too: harnesses and fixtures seed settings for tests; they never route host work.
const TEST_SUPPORT_PATH =
/(?:\.(?:test|spec)(?:-fixture|-support)?|-test-(?:harness|fixtures?))\.tsx?$/
export function isScannedPath(rel) {
return /\.(ts|tsx)$/.test(rel) && !TEST_SUPPORT_PATH.test(rel)
}
// One read and one discovery per root per run; `--prune` then `main` reuse them.
const scanCache = new Map()
function scanSources(root) {
if (!scanCache.has(root)) {
const sources = readTrackedSources(root, FOCUS_SCAN_ROOTS, isScannedPath)
scanCache.set(root, { sources, readers: discoverFocusReaders(sources) })
}
return scanCache.get(root)
}
export function collectCurrentCounts(root = process.cwd()) {
const { sources, readers } = scanSources(root)
const counts = new Map()
const forbidden = []
const aliased = []
for (const [rel, source] of sources) {
if (!rel.startsWith(`${SCAN_ROOT}/`)) {
continue
}
const found = countFocusSettingReads(source, readers.namesFor(rel), readers.memberNames)
if (found > 0) {
counts.set(rel, found)
}
if (countForbiddenHelperUses(source) > 0) {
forbidden.push(rel)
}
if (hasFocusRoutingAlias(source)) {
aliased.push(rel)
}
}
return { counts, forbidden, aliased }
}
export function main(root = process.cwd()) {
const baselineFile = path.join(root, BASELINE_PATH)
if (!fs.existsSync(baselineFile)) {
console.error(`::error::Missing ${BASELINE_PATH}.`)
return 1
}
const { counts: current, forbidden, aliased } = collectCurrentCounts(root)
for (const file of forbidden) {
console.error(
`::error file=${file}::${FORBIDDEN_HELPERS.join(', ')} are deleted focus-routing helpers; do not reintroduce them. ${ADVICE}`
)
}
for (const file of aliased) {
console.error(
`::error file=${file}::A focus reader is imported or exported under another name, which hides its calls from this ratchet. Use the original name.`
)
}
const { grown, shrunk } = diffCounts(
current,
parseCountBaseline(fs.readFileSync(baselineFile, 'utf8'))
)
for (const { file, now, allowed } of grown) {
console.error(
`::error file=${file}::${now} focus read(s), baseline allows ${allowed}. ${ADVICE}`
)
}
for (const { file, now, allowed } of shrunk) {
console.error(
`::error file=${file}::${now} focus read(s), baseline still allows ${allowed}. Run: pnpm check:focus-read-ratchet --prune`
)
}
if (forbidden.length + aliased.length + grown.length + shrunk.length > 0) {
return 1
}
console.log(`Focus-read ratchet OK — ${totalCount(current)} read(s).`)
return 0
}
export function prune(root = process.cwd()) {
const { counts: current } = collectCurrentCounts(root)
const kept = pruneCountBaseline(root, BASELINE_PATH, BASELINE_HEADER, current)
console.log(`Pruned ${BASELINE_PATH} to ${kept} read(s).`)
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const root = process.cwd()
if (process.argv[2] === '--prune') {
prune(root)
}
process.exit(main(root))
}