mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
* [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>
439 lines
16 KiB
JavaScript
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))
|
|
}
|