mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
`anti-slop/no-reflect-get` rejects every call to `Reflect.get`. The
reflective read bypasses ordinary property access and throws away the
type evidence the compiler would otherwise give you: the result is
`any`/`unknown` with no narrowing, so a typo in the key or a shape drift
in the source object is invisible until runtime. The rule's remedy is to
parse dynamic input into a named domain type (or narrow it with `in`)
and then read the field normally.
Baseline: 86 violations across 67 files. Now zero unsuppressed
violations under
`npx oxlint --config config/oxlint-anti-slop.json --ignore-pattern 'config/oxlint-plugins/anti-slop/**' src config tests mobile`.
Fix pattern
-----------
44 of the 86 were rewritten. The dominant shape was an `unknown` value
read through `Reflect.get` right after a `typeof === 'object'` guard;
those became `in`-narrowed property access, which TypeScript checks:
- Reflect.get(value, 'agents')
+ 'agents' in value ? value.agents : null
Two further shapes:
- `Reflect.get(Object(x), 'k')` on a possibly-primitive envelope became a
small named reader that boxes once and indexes a
`Record<string, unknown>` (`settingsField` in
mobile/src/transport/settings-read-operations.ts).
- Tests reaching into private state moved to TypeScript's checked
bracket-index escape hatch (`runtime['layoutQueues']`), or to a
documented read-only accessor on the owning class
(`SearchSubprocessLineAccumulator.retainedCapacityBytes()`,
`CodexSubagentExecutions.retentionSizes()`).
No type assertion was added anywhere: the diff contains zero net-new
`as` casts, `as any`, `as unknown as`, `@ts-ignore`, or
`@ts-expect-error`, so nothing was laundered into the sibling
assertion rules.
Suppressions
------------
42x `// oxlint-disable-next-line anti-slop/no-reflect-get` across 38
files. Every one is the default-forward branch of a `Proxy` `get` trap:
get(target, property, receiver) {
...
return Reflect.get(target, property, receiver)
}
`Reflect.get(target, property, receiver)` is the only construct that
forwards with correct `receiver` semantics; `target[property]` invokes
an accessor with the wrong `this` and silently breaks getters that read
sibling state. There is no typed alternative, so these are suppressed
rather than rewritten.
3x `// oxlint-disable-next-line typescript-eslint/consistent-type-definitions
-- declaration merging requires interface` in
tests/e2e/github-url-smart-input-transition.spec.ts,
tests/e2e/linear-url-workspace-entry.spec.ts, and
tests/e2e/worktree-active-delete-scroll-position.spec.ts. Replacing
`Reflect.get(window, 'x')` with typed `window.x` requires a
`declare global { interface Window }` block, and `interface` is
mandatory for declaration merging. Matches the existing convention at
tests/e2e/helpers/runtime-types.ts:63.
1x `// eslint-disable-next-line no-var -- main-process gate handle for
this spec` in tests/e2e/project-group-creation-visibility.spec.ts, for
the same reason a `var` global is needed to type the handle. Matches
tests/e2e/agent-session-log-tail-stability.spec.ts:24.
Also updates two source-text anchors in mobile's rpc-recording mutation
harness (mobile/src/test-support/rpc-recording/operation-mutations.ts
and recording-runner.test.ts), which pin the exact text of the rewritten
line in settings-read-operations.ts and would otherwise fail with
"Mutant anchor matched 0 sites, expected 1".
163 lines
5.9 KiB
TypeScript
163 lines
5.9 KiB
TypeScript
import {
|
|
importReleaseCheckoutModule,
|
|
materializeReleaseCheckout,
|
|
type ReleaseCheckout
|
|
} from './release-checkout'
|
|
|
|
/**
|
|
* The two things that decide whether a structured agent session exists for a given
|
|
* pairing: the capability strings a build can name, and the RPC methods it
|
|
* registers. Both are read per build, so "the old side does not have it" is a fact
|
|
* about a real release rather than a hand-written list.
|
|
*/
|
|
|
|
export const WORKING_TREE = 'working-tree' as const
|
|
|
|
/** Each build owns its own copy of the module-level host slot, so a host installed
|
|
* in current source is invisible to a release checkout's dispatcher. */
|
|
const STRUCTURED_HOST_REGISTRY =
|
|
'/src/main/native-chat/agent-session-wire/structured-agent-session-registry.ts'
|
|
|
|
export type RpcReply = {
|
|
id: string
|
|
ok: boolean
|
|
streaming?: true
|
|
result?: unknown
|
|
error?: { code: string; message: string }
|
|
}
|
|
|
|
export type RpcClientIdentity = {
|
|
clientKind?: 'mobile' | 'runtime'
|
|
clientCapabilities?: readonly string[]
|
|
updateClientCapabilities?: (capabilities: readonly string[]) => void
|
|
connectionId?: string
|
|
clientId?: string
|
|
}
|
|
|
|
export type AgentSessionDispatcher = {
|
|
dispatchStreaming: (
|
|
request: { id: string; authToken: string; method: string; params?: unknown },
|
|
reply: (message: string) => void,
|
|
options?: RpcClientIdentity
|
|
) => Promise<void>
|
|
}
|
|
|
|
export type AgentSessionWireBuild = {
|
|
/** Human label used in test names and failure messages. */
|
|
label: string
|
|
/** `working-tree` for current code, otherwise the resolved release commit. */
|
|
revision: string
|
|
/** Capability strings this build defines. A peer cannot advertise — nor a client
|
|
* ask for — a string its own source never names. */
|
|
capabilities: readonly string[]
|
|
protocolVersion: number
|
|
/** RPC method names the build registers, read from source. */
|
|
methodNames: readonly string[]
|
|
/** A dispatcher carrying a method set this build really ships, so an
|
|
* unknown-method answer is about the method and not an empty registry. */
|
|
createDispatcher: (runtime: unknown) => AgentSessionDispatcher
|
|
/** Put a host in *this* build's slot. Loaded on call so a release that predates
|
|
* the surface stays loadable, and throws rather than no-opping so a build with
|
|
* no slot cannot read as a surface that answered. */
|
|
installStructuredHost: (host: unknown) => Promise<void>
|
|
}
|
|
|
|
type DispatcherModule = {
|
|
RpcDispatcher: new (options: { runtime: unknown; methods: unknown[] }) => AgentSessionDispatcher
|
|
}
|
|
|
|
function registeredMethodNames(methods: readonly unknown[]): string[] {
|
|
return methods
|
|
.flatMap((method) => {
|
|
if (!method || typeof method !== 'object' || !('name' in method)) {
|
|
return []
|
|
}
|
|
const { name } = method
|
|
return typeof name === 'string' ? [name] : []
|
|
})
|
|
.sort()
|
|
}
|
|
|
|
function applyStructuredHost(module: Record<string, unknown>, label: string, host: unknown): void {
|
|
const install = module.setStructuredAgentSessionHost
|
|
if (typeof install !== 'function') {
|
|
throw new Error(`Build ${label} publishes no structured agent-session host registry`)
|
|
}
|
|
;(install as (next: unknown) => void)(host)
|
|
}
|
|
|
|
function capabilityStrings(module: Record<string, unknown>): readonly string[] {
|
|
const declared = module.RUNTIME_CAPABILITIES
|
|
if (!Array.isArray(declared) || declared.length === 0) {
|
|
throw new Error('Cross-version harness found no RUNTIME_CAPABILITIES to compare')
|
|
}
|
|
return declared as readonly string[]
|
|
}
|
|
|
|
async function loadWorkingTreeBuild(): Promise<AgentSessionWireBuild> {
|
|
const [protocol, dispatcher, methodRegistry] = await Promise.all([
|
|
import('../../../src/shared/protocol-version'),
|
|
import('../../../src/main/runtime/rpc/dispatcher'),
|
|
import('../../../src/main/runtime/rpc/methods')
|
|
])
|
|
const module = dispatcher as unknown as DispatcherModule
|
|
const methods = methodRegistry.ALL_RPC_METHODS as unknown[]
|
|
return {
|
|
label: WORKING_TREE,
|
|
revision: WORKING_TREE,
|
|
capabilities: capabilityStrings(protocol as unknown as Record<string, unknown>),
|
|
protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION,
|
|
methodNames: registeredMethodNames(methods),
|
|
createDispatcher: (runtime) =>
|
|
new module.RpcDispatcher({
|
|
runtime,
|
|
methods
|
|
}),
|
|
installStructuredHost: async (host) => {
|
|
const registry =
|
|
await import('../../../src/main/native-chat/agent-session-wire/structured-agent-session-registry')
|
|
applyStructuredHost(registry as unknown as Record<string, unknown>, WORKING_TREE, host)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadReleaseBuild(checkout: ReleaseCheckout): Promise<AgentSessionWireBuild> {
|
|
const [protocol, dispatcher, methodRegistry] = await Promise.all([
|
|
importReleaseCheckoutModule(checkout, '/src/shared/protocol-version.ts'),
|
|
importReleaseCheckoutModule(checkout, '/src/main/runtime/rpc/dispatcher.ts'),
|
|
importReleaseCheckoutModule(checkout, '/src/main/runtime/rpc/methods/index.ts')
|
|
])
|
|
const module = dispatcher as unknown as DispatcherModule
|
|
const methods = methodRegistry.ALL_RPC_METHODS as unknown[]
|
|
return {
|
|
label: checkout.ref,
|
|
revision: checkout.commit,
|
|
capabilities: capabilityStrings(protocol),
|
|
protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION as number,
|
|
methodNames: registeredMethodNames(methods),
|
|
createDispatcher: (runtime) =>
|
|
new module.RpcDispatcher({
|
|
runtime,
|
|
methods
|
|
}),
|
|
installStructuredHost: async (host) => {
|
|
applyStructuredHost(
|
|
await importReleaseCheckoutModule(checkout, STRUCTURED_HOST_REGISTRY),
|
|
checkout.ref,
|
|
host
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load the structured-session wire surface for one build. `WORKING_TREE` imports
|
|
* current source; any other value is a git ref extracted into a cached checkout.
|
|
*/
|
|
export async function loadAgentSessionWireBuild(ref: string): Promise<AgentSessionWireBuild> {
|
|
if (ref === WORKING_TREE) {
|
|
return loadWorkingTreeBuild()
|
|
}
|
|
return loadReleaseBuild(await materializeReleaseCheckout(ref))
|
|
}
|