fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785)

Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear
every violation under src, config, tests and mobile.

What the rule bans
------------------
The case-insensitive substring "shape" in any JS/TS identifier: variables,
functions, parameters, types, type parameters, class members, private names,
object-literal keys and JSX identifiers. The one exemption is a statically
accessed member read owned by another value (`zodObject.shape` is fine), so
third-party APIs stay readable without a suppression.

"Shape" names a value's structure rather than its domain role. `UserShape`,
`validateArgShape` and `errorShape` all tell you the symbol is "an object
with some fields" -- which is already what a type says -- while saying
nothing about what the value is for or who owns it. The rule forces the
name to carry the domain instead.

Violations fixed
----------------
689 violations across 109 files at baseline (verified by re-running the
audit against the pre-change tree with the rule set to "error").

Fix pattern
-----------
Rename for the domain role, not the structure:

  -type FieldShape = 'list' | 'map' | 'whole'
  -const FIELD_SHAPES = { ... } satisfies Record<keyof Observation, FieldShape>
  +type FieldEncoding = 'list' | 'map' | 'whole'
  +const FIELD_ENCODINGS = { ... } satisfies Record<keyof Observation, FieldEncoding>

  -function assertGitPushTargetShape(target: unknown): void
  +function assertValidGitPushTarget(target: unknown): void

  -function describeReadDirPathShape(p: string): ReadDirPathKind
  +function classifyReadDirPath(p: string): ReadDirPathKind

Predicates became statements about the value (`isDeltaShapedProviderFrameKind`
-> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` ->
`discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` ->
`isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the
remaining name was already unambiguous (`GhGraphqlErrorShape` ->
`GhGraphqlError`).

No wire-visible name was renamed: no IPC or RPC channel, stream opcode,
request/response param, persisted field, or i18n key. The `--shape=symlink|copy`
CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged --
only the local variable holding it was renamed.

Exemptions
----------
They are file-scoped entries in config/oxlint-anti-slop.json, not inline
`oxlint-disable` comments. An inline directive naming an anti-slop rule reads
back as an UNUSED directive under the root lint scan, which does not load this
plugin -- the changed-code quality gate counts that warning, so the comment form
cannot be used for a rule that lives only in this config.

* src/renderer/src/components/browser-pane/annotate/**:
  in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow,
  rect, ellipse, highlight. That is a genuine domain noun, and it pervades
  every symbol in the module.
* repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx:
  lucide exports the icon component as `Shapes`. The name is theirs, and the
  matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the
  desktop picker -- renaming it would orphan saved repo icons.
* src/shared/onboarding-state-types.ts, src/shared/constants.ts:
  `shapedSidebar` is a persisted onboarding-checklist field and a telemetry
  enum member; renaming it would orphan saved state.
* src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape`
  property is what selects the ZodObject branch of the conditional type.

No exemption was added merely to avoid a rename. Eight symbols initially
suppressed as "a cross-module refactor outside this change" were proven to have
zero non-TypeScript references repo-wide and renamed instead.

Zod's `ZodRawShape` needed no exemption at all: `Readonly<Record<string,
z.ZodType>>` is its definition, so repo-update-params.ts and
ui-update-value-tolerance-params.ts spell it out instead. Likewise
telemetry-event-classification.ts now reads `.shape` through an `in` narrowing,
which also retires two pre-existing type assertions; three more assertions the
rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite
row read) became annotations and an explicit row mapping.

Verified
--------
* Audit reports zero violations; confirmed the rule genuinely fires by
  planting a probe violation.
* node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0.
* Vitest over src/shared, src/main/github/project-view, the annotate module,
  the repo-icon components and the Chromium SameSite electron spec: all green.
* All 66 removed "shape" identifiers grepped repo-wide across every file type;
  none survive.
* node config/scripts/generate-rpc-params-catalog.mjs --check exits 0.
* node --check on every changed .mjs; oxfmt clean on all changed files.
* `pnpm run check:code-quality:changed` reports 0 findings.

Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve
`expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated
specs. All are rename- or comment-only hunks, read in full.
This commit is contained in:
Neil
2026-09-15 02:00:27 -07:00
committed by GitHub
parent bfdec26352
commit 231e805b1e
96 changed files with 441 additions and 348 deletions
+51 -1
View File
@@ -35,7 +35,7 @@
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "error",
"anti-slop/no-runtime-typeof": "off",
"anti-slop/no-shape-in-symbol-names": "off",
"anti-slop/no-shape-in-symbol-names": "error",
"anti-slop/no-unknown-parameters": "off",
"anti-slop/no-unknown-returns": "off",
"anti-slop/no-unknown-type-aliases": "error",
@@ -55,6 +55,56 @@
"rules": {
"anti-slop/no-module-mocking": "off"
}
},
// The exemptions below are file-scoped rather than inline `oxlint-disable` comments
// because the root lint scan does not load this plugin, so an inline directive naming
// an anti-slop rule always reads back as an unused directive there.
//
// In the screenshot annotator a "shape" is the drawn geometry -- pen, arrow, rect,
// ellipse, highlight. A domain noun, and it pervades every symbol in the module.
// mobile/src/test-support/rpc-recording is the golden recorder engine. recorder-digest.ts
// hashes these files' RAW BYTES into every golden's `recorderSha256` header, so any edit
// here -- a rename or even an added comment -- invalidates all 208 recordings. The exemption
// is config-scoped for that reason: an inline directive would change the bytes it protects.
{
"files": ["**/test-support/rpc-recording/**"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
{
"files": ["**/browser-pane/annotate/**"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
// lucide exports the icon component as `Shapes`, and the matching REPO_LUCIDE_ICONS key
// is the persisted icon name shared by the desktop picker and mobile.
{
"files": [
"**/components/repo/repo-icon.tsx",
"**/worktree-list/rows/repo-header-project-actions.tsx",
"**/components/MobileRepoIcon.tsx"
],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
// `shapedSidebar` is a persisted onboarding-checklist field and a telemetry enum member;
// renaming it would orphan saved state.
{
"files": ["**/src/shared/constants.ts", "**/src/shared/onboarding-state-types.ts"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
},
// Matching zod's own literal `shape` property is what selects the ZodObject branch of
// RpcSendInput's conditional type.
{
"files": ["**/rpc-contract/rpc-send-params.ts"],
"rules": {
"anti-slop/no-shape-in-symbol-names": "off"
}
}
]
}
@@ -50,7 +50,7 @@ for (let sample = 0; sample < 500; sample++) {
}
const results = []
for (const [shape, count] of [
for (const [topology, count] of [
['flat', 1000],
['all-cycles', 1000],
['mixed-cycles', 100],
@@ -58,9 +58,9 @@ for (const [shape, count] of [
['mixed-cycles', 1000]
]) {
const rows = Array.from({ length: count }, (_, index) =>
row(index, shape === 'flat' ? undefined : index ^ 1)
row(index, topology === 'flat' ? undefined : index ^ 1)
)
if (shape === 'mixed-cycles') {
if (topology === 'mixed-cycles') {
rows.unshift(row('root', undefined))
}
assert.deepEqual(after(rows), before(rows))
@@ -83,7 +83,7 @@ for (const [shape, count] of [
samples[arm].push({ wallMs, cpuMs: (used.user + used.system) / 30_000 })
}
}
results.push({ shape, count, samples })
results.push({ topology, count, samples })
}
console.log(
JSON.stringify({ baseline, node: process.version, parityGraphs: 500, results }, null, 2)
@@ -58,15 +58,19 @@ for (let trial = 0; trial < 5000; trial += 1) {
const results = []
for (const count of [8, 32, 128, 512, 1024]) {
for (const shape of ['flat', 'fanout', 'balanced', 'chain']) {
for (const topology of ['flat', 'fanout', 'balanced', 'chain']) {
const rows = Array.from({ length: count }, (_, index) => {
const parent =
shape === 'fanout' ? 0 : shape === 'balanced' ? Math.floor((index - 1) / 4) : index - 1
topology === 'fanout'
? 0
: topology === 'balanced'
? Math.floor((index - 1) / 4)
: index - 1
return {
paneKey: `pane-${index}`,
entry: {
orchestration:
index > 0 && shape !== 'flat' ? { parentPaneKey: `pane-${parent}` } : undefined
index > 0 && topology !== 'flat' ? { parentPaneKey: `pane-${parent}` } : undefined
}
}
})
@@ -92,7 +96,7 @@ for (const count of [8, 32, 128, 512, 1024]) {
}
results.push({
count,
shape,
topology,
iterations,
meanMicrosecondsPerTree: Object.fromEntries(
Object.entries(samples).map(([arm, values]) => [
@@ -40,7 +40,7 @@ function measure(fn, input, repeats) {
return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)]
}
const results = []
for (const [shape, input] of [
for (const [inputCase, input] of [
['ordinary Markdown', '# Hello\n\n<p>Use `Array<string>` and <b>bold</b>.</p>'],
...[2048, 8192, 16384].map((length) => [
`${length} underscore collision`,
@@ -49,7 +49,7 @@ for (const [shape, input] of [
]) {
assert.equal(after(input), before(input))
results.push({
shape,
inputCase,
bytes: Buffer.byteLength(input),
beforeMs: measure(before, input, 5),
afterMs: measure(after, input, 15)
@@ -25,7 +25,7 @@ function median(fn, input, repeats) {
return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)]
}
const rows = []
for (const [shape, input] of [
for (const [label, input] of [
['8KiB blank lines', '\n'.repeat(8192)],
['16KiB blank lines', '\n'.repeat(16384)],
['32KiB blank lines', '\n'.repeat(32768)],
@@ -37,7 +37,7 @@ for (const [shape, input] of [
const beforeMs = median(before, input, 3)
const afterMs = median(redactString, input, 15)
rows.push({
shape,
label,
bytes: Buffer.byteLength(input),
beforeMs,
afterMs,
@@ -36,15 +36,15 @@ function measurePair(source) {
const results = []
for (const size of [8192, 16384, 32768]) {
for (const shape of ['no icon', 'rel without href', 'unterminated link starts']) {
for (const variant of ['no icon', 'rel without href', 'unterminated link starts']) {
const source =
shape === 'unterminated link starts'
variant === 'unterminated link starts'
? '<link '.repeat(Math.floor(size / 6))
: 'a'.repeat(size) + (shape === 'rel without href' ? ' rel:"icon"' : '')
: 'a'.repeat(size) + (variant === 'rel without href' ? ' rel:"icon"' : '')
assert.equal(extractIconHref(source), original(source))
const { beforeMs, afterMs } = measurePair(source)
results.push({
shape,
variant,
bytes: Buffer.byteLength(source),
beforeMs,
afterMs,
@@ -45,9 +45,9 @@ const arms = {
}
const results = []
for (const size of [20_000, 200_000, 600_000]) {
for (const shape of ['lines', 'long-line']) {
for (const lineLayout of ['lines', 'long-line']) {
const phrase =
shape === 'lines'
lineLayout === 'lines'
? 'Ordinary prose with a little `code`.\n'
: 'Ordinary prose with a little `code`. '
const content = phrase.repeat(Math.ceil(size / phrase.length)).slice(0, size)
@@ -73,7 +73,7 @@ for (const size of [20_000, 200_000, 600_000]) {
samples[arm].push({ ms, cpuMs: (cpu.user + cpu.system) / 20_000 })
}
}
results.push({ size, shape, samples })
results.push({ size, lineLayout, samples })
}
}
console.log(
@@ -213,13 +213,13 @@ function drain(iterate, data, meta) {
}
function describeFrames(iterate, data, meta) {
const shapes = []
const descriptions = []
for (const frame of iterate(data, meta)) {
shapes.push(
descriptions.push(
`${Buffer.from(frame.bytes).toString('base64')}|${frame.seq ?? 'u'}|${frame.opcode ?? 'u'}`
)
}
return shapes.join('\n')
return descriptions.join('\n')
}
const SURROGATE_PAIR = '\u{1f600}'
@@ -77,7 +77,7 @@ for (const input of [
]) {
assert.deepEqual(display(after, input), display(before, input))
}
for (const [shape, input] of [
for (const [caseName, input] of [
['tiny', 'ls -la'],
['100KB', 'a b\n\t'.repeat(15000)],
['1MB', 'a b\n\t'.repeat(150000)],
@@ -110,5 +110,5 @@ for (const [shape, input] of [
samples[label].push((cpu.user + cpu.system) / 20000)
}
}
console.log(JSON.stringify({ shape, samples }))
console.log(JSON.stringify({ caseName, samples }))
}
@@ -22,7 +22,7 @@ function option(name) {
const cliVersion = option('cli')
const autocrlf = option('autocrlf')
const shape = option('shape')
const placement = option('shape')
// Why: PR branch names are untrusted workflow input. Keep them out of the
// generated shell command and pass them to Node through the environment.
const source = option('source') ?? process.env.SKILL_UPDATE_SOURCE
@@ -30,7 +30,7 @@ const ref = option('ref') ?? process.env.SKILL_UPDATE_REF
if (
!cliVersion ||
(autocrlf !== 'true' && autocrlf !== 'false') ||
(shape !== 'symlink' && shape !== 'copy') ||
(placement !== 'symlink' && placement !== 'copy') ||
!source ||
!ref ||
!/^[^/\s]+\/[^/\s]+$/.test(source)
@@ -103,7 +103,7 @@ async function seedPlacement(name, tag) {
const providerRoot = path.join(home, '.claude', 'skills')
const provider = path.join(providerRoot, name)
await mkdir(providerRoot, { recursive: true })
await (shape === 'copy'
await (placement === 'copy'
? cp(canonical, provider, { recursive: true })
: symlink(canonical, provider, process.platform === 'win32' ? 'junction' : 'dir'))
}
@@ -211,20 +211,20 @@ try {
await assertCurrentCanonical(targetName)
const targetProviderAfter = await packageDigestAt(await realpath(targetProvider))
const targetProviderStat = await lstat(targetProvider)
if (shape === 'symlink' && !targetProviderStat.isSymbolicLink()) {
if (placement === 'symlink' && !targetProviderStat.isSymbolicLink()) {
throw new Error(`${targetName} provider alias was replaced with an independent copy`)
}
if (shape === 'symlink' && targetProviderAfter !== currentSkill(targetName).packageDigest) {
if (placement === 'symlink' && targetProviderAfter !== currentSkill(targetName).packageDigest) {
throw new Error(`${targetName} provider alias did not converge with the canonical update`)
}
if (
shape === 'copy' &&
placement === 'copy' &&
targetProviderAfter !== targetProviderBefore &&
targetProviderAfter !== currentSkill(targetName).packageDigest
) {
throw new Error('Independent provider copy changed to an unexpected package identity')
}
if (shape === 'copy') {
if (placement === 'copy') {
// Why: hosted 1.5.17 replaces copies with aliases while equivalent local runs
// retain the copy. Both prove this input topology must remain ineligible.
const outcome = targetProviderStat.isSymbolicLink()
@@ -241,7 +241,7 @@ try {
throw new Error('Targeted update changed the non-targeted control provider placement')
}
const controlProviderStat = await lstat(controlProvider)
if (shape === 'symlink' && !controlProviderStat.isSymbolicLink()) {
if (placement === 'symlink' && !controlProviderStat.isSymbolicLink()) {
throw new Error('Targeted update changed the non-targeted control topology')
}
} finally {
+1 -1
View File
@@ -358,7 +358,7 @@ async function main() {
sampleAggregation: BENCHMARK_SAMPLE_AGGREGATION,
injectedLoginDelayMs: options.loginDelayMs,
loginProbePreambleBytes: Buffer.byteLength(probeText.split('__ORCA_PATH__', 1)[0]),
guestProcessShape: {
guestProcessChain: {
login: 'sh -> interactive login shell -> git',
fast: 'env -> git'
},
+1
View File
@@ -16,6 +16,7 @@ import {
Palette,
Rocket,
Server,
// `Shapes` is lucide's own export name; exempted in config/oxlint-anti-slop.json.
Shapes,
Sparkles,
SquareTerminal,
@@ -243,7 +243,7 @@ function countLinearWork(run: () => void): WorkCounts {
}
}
function shape(sections: LinearIssueSection[]) {
function summarizeSections(sections: LinearIssueSection[]) {
return sections.map((section) => ({
key: section.key,
label: section.label,
@@ -268,8 +268,8 @@ describe('useMobileTasksProviderViewProjection linear sections', () => {
(linearGroupBy) => {
const projection = mount({ linearGroupBy })
expect(projection.linearBoardSections).toBe(projection.linearIssueSections)
expect(shape(projection.linearBoardSections)).toEqual(
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy }).boardSections)
expect(summarizeSections(projection.linearBoardSections)).toEqual(
summarizeSections(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy }).boardSections)
)
}
)
@@ -278,8 +278,12 @@ describe('useMobileTasksProviderViewProjection linear sections', () => {
const projection = mount({ linearGroupBy: 'none' })
const legacy = legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' })
expect(projection.linearBoardSections).not.toBe(projection.linearIssueSections)
expect(shape(projection.linearIssueSections)).toEqual(shape(legacy.listSections))
expect(shape(projection.linearBoardSections)).toEqual(shape(legacy.boardSections))
expect(summarizeSections(projection.linearIssueSections)).toEqual(
summarizeSections(legacy.listSections)
)
expect(summarizeSections(projection.linearBoardSections)).toEqual(
summarizeSections(legacy.boardSections)
)
expect(projection.linearIssueSections.map((section) => section.key)).toEqual(['all'])
expect(projection.linearBoardSections.length).toBeGreaterThan(1)
expect(projection.linearListEntries.every((entry) => entry.type === 'issue')).toBe(true)
@@ -293,8 +297,12 @@ describe('useMobileTasksProviderViewProjection linear sections', () => {
linearGroupBy,
linearOrderBy: order
})
expect(shape(projection.linearIssueSections)).toEqual(shape(legacy.listSections))
expect(shape(projection.linearBoardSections)).toEqual(shape(legacy.boardSections))
expect(summarizeSections(projection.linearIssueSections)).toEqual(
summarizeSections(legacy.listSections)
)
expect(summarizeSections(projection.linearBoardSections)).toEqual(
summarizeSections(legacy.boardSections)
)
expect(projection.linearIssuesForView.map((issue) => issue.id)).toEqual(
legacy.issuesForView.map((issue) => issue.id)
)
@@ -310,20 +318,24 @@ describe('useMobileTasksProviderViewProjection transitions', () => {
const grouped = rerender({ linearGroupBy: 'status' })
expect(grouped.linearBoardSections).toBe(grouped.linearIssueSections)
expect(shape(grouped.linearBoardSections)).toEqual(
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'status' }).boardSections)
expect(summarizeSections(grouped.linearBoardSections)).toEqual(
summarizeSections(
legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'status' }).boardSections
)
)
const assignee = rerender({ linearGroupBy: 'assignee' })
expect(assignee.linearBoardSections).toBe(assignee.linearIssueSections)
expect(shape(assignee.linearBoardSections)).toEqual(
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'assignee' }).boardSections)
expect(summarizeSections(assignee.linearBoardSections)).toEqual(
summarizeSections(
legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'assignee' }).boardSections
)
)
const none = rerender({ linearGroupBy: 'none' })
expect(none.linearBoardSections).not.toBe(none.linearIssueSections)
expect(shape(none.linearBoardSections)).toEqual(
shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' }).boardSections)
expect(summarizeSections(none.linearBoardSections)).toEqual(
summarizeSections(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' }).boardSections)
)
})
@@ -333,8 +345,8 @@ describe('useMobileTasksProviderViewProjection transitions', () => {
const next = rerender({ linearGroupBy: 'priority', linearOrderBy: 'identifier' })
expect(next.linearBoardSections).not.toBe(firstSections)
expect(next.linearBoardSections).toBe(next.linearIssueSections)
expect(shape(next.linearBoardSections)).toEqual(
shape(
expect(summarizeSections(next.linearBoardSections)).toEqual(
summarizeSections(
legacyProjection({
...DEFAULT_INPUT,
linearGroupBy: 'priority',
@@ -369,17 +381,17 @@ describe('useMobileTasksProviderViewProjection transitions', () => {
const refreshed = rerender({ linearGroupBy: 'status', items: makeItems(50) })
expect(refreshed.linearBoardSections).not.toBe(sections)
expect(refreshed.linearBoardSections).toBe(refreshed.linearIssueSections)
expect(shape(refreshed.linearBoardSections)).toEqual(shape(sections))
expect(summarizeSections(refreshed.linearBoardSections)).toEqual(summarizeSections(sections))
})
it('does not mutate the shared sections when the list entries are built', () => {
const projection = mount({ linearGroupBy: 'status' })
const before = shape(projection.linearIssueSections)
const before = summarizeSections(projection.linearIssueSections)
const entryIssueIds = projection.linearListEntries
.filter((entry) => entry.type === 'issue')
.map((entry) => (entry.type === 'issue' ? entry.issue.id : ''))
expect(entryIssueIds).toHaveLength(50)
expect(shape(projection.linearBoardSections)).toEqual(before)
expect(summarizeSections(projection.linearBoardSections)).toEqual(before)
})
})
+2 -2
View File
@@ -17,7 +17,7 @@ import {
UnsafeWindowsBatchArgumentsError,
WINDOWS_BATCH_UNSAFE_CHARACTERS_LABEL
} from '../../shared/windows-batch-spawn'
import { isSkillsCliAgentKeyShaped, toSkillsCliAgentKeys } from '../../shared/skills-cli-agent-keys'
import { isUsableSkillsCliAgentKey, toSkillsCliAgentKeys } from '../../shared/skills-cli-agent-keys'
import {
buildAgentFeatureSkillInstallArgs,
buildAgentFeatureSkillUpdateArgs
@@ -150,7 +150,7 @@ function resolveInstallAgentKeys(flags: Map<string, string | boolean>): string[]
if (keys.length === 0) {
throw new RuntimeClientError('invalid_argument', 'Missing required --agent')
}
const unusable = keys.find((key) => !isSkillsCliAgentKeyShaped(key))
const unusable = keys.find((key) => !isUsableSkillsCliAgentKey(key))
if (unusable !== undefined) {
// Why: the skills CLI drops a value starting with `-`, which leaves it with
// no target and installs into every agent it knows.
@@ -47,7 +47,7 @@ const CAN_DENY_READ = process.platform !== 'win32' && process.getuid?.() !== 0
const INTERVAL_MS = 20_000
const SESSIONS = ['aaaaaaaa', 'bbbbbbbb', 'cccccccc']
type RootShape = {
type RootLayout = {
name: string
/** Where the unreachable root's transcripts live, and where its files go. */
detachedRoot: (harness: SessionSearchIndexerHarness) => string
@@ -59,7 +59,7 @@ type RootShape = {
const OPENCLAW_SESSION_DIR = join('agents', 'main', 'sessions')
const ROOT_SHAPES: RootShape[] = [
const ROOT_LAYOUTS: RootLayout[] = [
{
name: 'roots discovery reports one per directory',
detachedRoot: (harness) => harness.roots.claudeProjectsDir ?? '',
@@ -81,7 +81,7 @@ const ROOT_SHAPES: RootShape[] = [
}
]
type UnreachableShape = {
type UnreachableMode = {
name: string
needsDeniedRead: boolean
/**
@@ -97,7 +97,7 @@ type UnreachableShape = {
attach: (root: string, transcriptDir: string, parked: string) => Promise<void>
}
const UNREACHABLE_SHAPES: UnreachableShape[] = [
const UNREACHABLE_MODES: UnreachableMode[] = [
{
name: 'the root itself is not there',
needsDeniedRead: false,
@@ -276,8 +276,8 @@ function indexedSessions(): string[] {
.sort()
}
for (const roots of ROOT_SHAPES) {
for (const unreachable of UNREACHABLE_SHAPES) {
for (const roots of ROOT_LAYOUTS) {
for (const unreachable of UNREACHABLE_MODES) {
describe.skipIf(unreachable.needsDeniedRead && !CAN_DENY_READ)(
`${roots.name}, ${unreachable.name}`,
() => {
@@ -298,7 +298,7 @@ for (const roots of ROOT_SHAPES) {
// pass, so the setup drives passes until the index has caught up.
await driveUntilIndexed(SESSIONS.length * 2)
const detachedIds = detachedPaths.map((_path, index) =>
roots === ROOT_SHAPES[0]
roots === ROOT_LAYOUTS[0]
? fullSessionId(SESSIONS[index] ?? '')
: (SESSIONS[index] ?? '')
)
@@ -17,7 +17,7 @@ const MAX_TERMS = 64
// A query that quotes something from a transcript: camelCase, SCREAMING_SNAKE,
// a dotted or snake_case name, a path, a filename, a PR number, a ticket, code
// punctuation, or an error word.
const LITERAL_SHAPE =
const LITERAL_PATTERN =
/[A-Za-z0-9_]*[a-z][A-Z][A-Za-z0-9_]*|\b[A-Z][A-Z0-9]{2,}(_[A-Z0-9]+)+\b|\b\w{2,}[._]\w{2,}\b|\b[\w.-]+\/[\w/.-]+\b|\b\w+\.(ts|tsx|js|jsx|py|rs|go|json|md|sh|yml|yaml|toml|c|cc|h|java|sql)\b|#\d{3,}|\b[A-Z]{2,6}-\d{2,}\b|[(){};=]|::|->|--\w|\b(Error|Exception|Traceback|error:|warning:)\b/
const QUOTED = /"[^"]{3,}"|'[^']{3,}'/
@@ -36,7 +36,7 @@ export type SessionSearchQueryPlan = {
}
export function isLiteralQuery(query: string): boolean {
return QUOTED.test(query) || LITERAL_SHAPE.test(query)
return QUOTED.test(query) || LITERAL_PATTERN.test(query)
}
/**
+2 -2
View File
@@ -21,7 +21,7 @@ import type { AiVaultScanOptions } from './session-scanner-types'
// Agents whose session IS the directory holding the scanned file: everything
// beside it belongs to the same session (rovo's session_context.json, grok's
// chat_history.jsonl), so the directory is the only complete delete unit.
const AI_VAULT_DIRECTORY_SHAPED_DELETE_AGENTS = new Set<AiVaultDeletableAgent>([
const AI_VAULT_WHOLE_DIRECTORY_DELETE_AGENTS = new Set<AiVaultDeletableAgent>([
'rovo',
'grok',
'cline'
@@ -109,7 +109,7 @@ function sessionDeleteRemovals(args: {
}): readonly AiVaultSessionDeleteRemoval[] | null {
const { agent, resolvedPath, matchedRoot, roots } = args
if (AI_VAULT_DIRECTORY_SHAPED_DELETE_AGENTS.has(agent)) {
if (AI_VAULT_WHOLE_DIRECTORY_DELETE_AGENTS.has(agent)) {
const sessionDir = dirname(resolvedPath)
if (sessionDir === matchedRoot || !isPathInsideOrEqual(matchedRoot, sessionDir)) {
return null
@@ -33,7 +33,7 @@ type FixtureResult = {
afterCookies: JarCookie[]
}
type SourceShape = {
type SourceCookieRow = {
name: string
samesite: number | null
is_secure: number
@@ -147,18 +147,26 @@ run().catch((error) => {
`
}
function readSourceShape(sourceDbPath: string): SourceShape[] {
function readSourceCookieRows(sourceDbPath: string): SourceCookieRow[] {
const db = new DatabaseSync(sourceDbPath, { readOnly: true })
try {
return db
.prepare('SELECT name, samesite, is_secure FROM cookies ORDER BY rowid')
.all() as SourceShape[]
.all()
.map((row) => ({
name: String(row.name),
samesite: row.samesite === null ? null : Number(row.samesite),
is_secure: Number(row.is_secure)
}))
} finally {
db.close()
}
}
async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: SourceShape[] }> {
async function runFixture(): Promise<{
fixture: FixtureResult
sourceCookieRows: SourceCookieRow[]
}> {
const root = mkdtempSync(join(tmpdir(), 'orca-samesite-enum-'))
fixtureRoots.push(root)
const bundlePath = join(root, 'cookie-import-samesite.cjs')
@@ -176,7 +184,7 @@ async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: Sour
})
)
createChromiumCookieTestDatabase(sourceDbPath, rows).close()
const sourceShape = readSourceShape(sourceDbPath)
const sourceCookieRows = readSourceCookieRows(sourceDbPath)
writeFileSync(
bundleEntryPath,
`export { importCookiesFromBrowser } from ${JSON.stringify(join(process.cwd(), 'src/main/browser/browser-cookie-import.ts'))}`
@@ -212,22 +220,23 @@ async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: Sour
const fixtureResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result'
expect(run.error).toBeUndefined()
expect(run.status, `${fixtureResult}\n${run.stdout}\n${run.stderr}`).toBe(0)
return { fixture: JSON.parse(fixtureResult) as FixtureResult, sourceShape }
const fixture: FixtureResult = JSON.parse(fixtureResult)
return { fixture, sourceCookieRows }
}
describe('Chromium SameSite storage enum import', () => {
let fixture: FixtureResult
let sourceShape: SourceShape[]
let sourceCookieRows: SourceCookieRow[]
beforeAll(async () => {
;({ fixture, sourceShape } = await runFixture())
;({ fixture, sourceCookieRows } = await runFixture())
}, 120_000)
it('runs the real Chromium import against the complete synthetic matrix', () => {
expect(fixture.step).toBe('import finished')
expect(fixture.beforeCookieCount).toBe(0)
expect(fixture.importResult.ok).toBe(true)
expect(sourceShape).toEqual(
expect(sourceCookieRows).toEqual(
[REJECTION_CONTROL, ...VALID_COMBINATIONS, NULL_CASE].map(
({ name, rawSameSite, secure }) => ({
name,
@@ -70,7 +70,7 @@ describe('ClaudeRuntimeAuthService', () => {
it('rejects wrong-shaped refreshed credentials during read-back', async () => {
const runtimeCredentialsPath = join(testState.fakeHomeDir, '.claude', '.credentials.json')
const originalCredentials = createClaudeCredentialsJson('user@example.com', 'original')
const wrongShapedRefresh = `${JSON.stringify({
const malformedRefresh = `${JSON.stringify({
claudeAiOauth: {
email: 'user@example.com',
expiresAt: Date.now() + 120_000
@@ -91,7 +91,7 @@ describe('ClaudeRuntimeAuthService', () => {
settings.activeClaudeManagedAccountId = 'account-1'
await service.syncForCurrentSelection()
writeFileSync(runtimeCredentialsPath, wrongShapedRefresh, 'utf-8')
writeFileSync(runtimeCredentialsPath, malformedRefresh, 'utf-8')
await service.syncForCurrentSelection()
expect(readManagedCredentialsForTest('account-1', managedAuthPath)).toBe(originalCredentials)
@@ -806,7 +806,7 @@ describe('codex item bodies', () => {
// Both the row label and the run header read top-level input keys only, so a
// shape whose detail sits inside `action` renders as the input's raw JSON.
const url = 'https://example.com/docs/page'
const shapes: [string, unknown, string, string][] = [
const cases: [string, unknown, string, string][] = [
['started', null, '', ''],
[
'search',
@@ -823,7 +823,7 @@ describe('codex item bodies', () => {
],
['other', { type: 'other' }, 'other', '']
]
for (const [name, action, label, brief] of shapes) {
for (const [name, action, label, brief] of cases) {
// Codex leaves the item's own `query` empty on most completed searches.
const query = name === 'search' || name === 'findInPage' ? 'a sample query' : ''
const input = toolCallInput({ type: 'webSearch', id: 'w', query, action })
+3 -3
View File
@@ -168,13 +168,13 @@ export class CursorHookService {
}
const cleaned = removeManagedCommands(definitions, isManagedCommand)
// Also strip entries with the command at the top level (Cursor schema).
const strippedCursorShape = cleaned.filter(
const strippedTopLevelCommands = cleaned.filter(
(definition) => !isManagedCommand(definition.command)
)
if (strippedCursorShape.length === 0) {
if (strippedTopLevelCommands.length === 0) {
delete nextHooks[eventName]
} else {
nextHooks[eventName] = strippedCursorShape
nextHooks[eventName] = strippedTopLevelCommands
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import type { GitPushTarget } from '../../shared/worktree/types'
import { assertGitPushTargetShape } from '../../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../../shared/git-push-target-validation'
import { gitExecFileAsync } from './runner'
import type { GitExecOptions as GitCommandExecOptions } from './command-runner/git-exec-options'
@@ -10,7 +10,7 @@ export async function validateGitPushTarget(
target: unknown,
options: GitExecOptions = {}
): Promise<GitPushTarget> {
assertGitPushTargetShape(target)
assertValidGitPushTarget(target)
await gitExecFileAsync(['check-ref-format', '--branch', target.branchName], {
cwd: repoPath,
...options
@@ -592,9 +592,9 @@ describe('GitHub GraphQL rate-limit guard', () => {
})
it.each([
{ stackShape: 'omits stack', stackField: {} },
{ stackShape: 'sets stack to null', stackField: { stack: null } }
])('keeps legacy merge when an ordinary GitHub response $stackShape', async (scenario) => {
{ stackVariant: 'omits stack', stackField: {} },
{ stackVariant: 'sets stack to null', stackField: { stack: null } }
])('keeps legacy merge when an ordinary GitHub response $stackVariant', async (scenario) => {
ghExecFileAsyncMock
.mockResolvedValueOnce({
stdout: JSON.stringify({
@@ -164,7 +164,7 @@ function primeGitExecForDefaultBranch({
})
}
type RestPRShape = {
type RestPROverrides = {
number?: number
state?: string
merged_at?: string | null
@@ -178,7 +178,7 @@ function restPR({
merged_at = null,
head_ref = 'master',
head_sha = 'stale-master-oid'
}: RestPRShape = {}): Record<string, unknown> {
}: RestPROverrides = {}): Record<string, unknown> {
return {
number,
title: 'Historical PR',
+2 -2
View File
@@ -17,7 +17,7 @@ import {
classifyProjectError,
driftError,
rateLimitedError,
type GhGraphqlErrorShape
type GhGraphqlError
} from './project-error-classification'
export {
@@ -172,7 +172,7 @@ export async function runGraphql<T>(
...(exec?.host ? { host: exec.host } : {})
})
try {
const parsed = JSON.parse(stdout) as { data?: T; errors?: GhGraphqlErrorShape[] }
const parsed: { data?: T; errors?: GhGraphqlError[] } = JSON.parse(stdout)
if (parsed.errors && parsed.errors.length > 0) {
return {
ok: false,
@@ -4,14 +4,14 @@
import type { GitHubProjectViewError } from '../../../shared/github/project-result-types'
import { githubProjectHost } from '../../../shared/github/project-identity'
export type GhGraphqlErrorShape = {
export type GhGraphqlError = {
type?: string
message?: string
path?: (string | number)[]
extensions?: { code?: string }
}
export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlErrorShape[] {
export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlError[] {
// `gh api graphql` prints the response JSON to stdout even on GraphQL
// errors, and the stderr carries a summary. Try stdout first; if parsing
// fails, fall back to stderr.
@@ -21,7 +21,7 @@ export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlE
continue
}
try {
const parsed = JSON.parse(src) as { errors?: GhGraphqlErrorShape[] }
const parsed: { errors?: GhGraphqlError[] } = JSON.parse(src)
if (parsed.errors && parsed.errors.length > 0) {
return parsed.errors
}
@@ -32,7 +32,7 @@ export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlE
return []
}
export function errorsIndicateParentField(errors: GhGraphqlErrorShape[], stderr: string): boolean {
export function errorsIndicateParentField(errors: GhGraphqlError[], stderr: string): boolean {
const lower = stderr.toLowerCase()
// Preview-header shape: gh returns a 4xx with "preview" in the message.
if (lower.includes('preview') && lower.includes('parent')) {
@@ -14,7 +14,7 @@ import {
classifyProjectError,
driftError,
rateLimitedError,
type GhGraphqlErrorShape
type GhGraphqlError
} from './project-error-classification'
import { ownerQueryRoot } from './project-view-config'
import type { RawItem } from './project-view-item-normalization'
@@ -47,7 +47,7 @@ export async function fetchItemsPageWithRaw(args: {
| {
ok: false
error: GitHubProjectViewError
rawErrors: GhGraphqlErrorShape[]
rawErrors: GhGraphqlError[]
stderr: string
}
> {
@@ -117,7 +117,7 @@ export async function fetchItemsPageWithRaw(args: {
stdout = extracted.stdout
execFailed = true
}
let parsed: { data?: Record<string, unknown>; errors?: GhGraphqlErrorShape[] } = {}
let parsed: { data?: Record<string, unknown>; errors?: GhGraphqlError[] } = {}
try {
parsed = JSON.parse(stdout)
} catch {
@@ -8,7 +8,7 @@ import {
} from '../../../providers/ssh-git-dispatch'
import { resolveRegisteredWorktreePath } from '../../registered-worktree-roots-cache'
import { getLocalGitOptionsForRegisteredWorktree } from '../../local-worktree-runtime-options'
import { assertGitPushTargetShape } from '../../../../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../../../../shared/git-push-target-validation'
import {
materializeWorktreePushTargetRemote,
materializeWorktreePushTargetRemoteSsh
@@ -35,7 +35,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl
const publish = args.publish === true
if (args.connectionId) {
if (args.pushTarget) {
assertGitPushTargetShape(args.pushTarget)
assertValidGitPushTarget(args.pushTarget)
}
const provider = getSshGitProvider(args.connectionId)
if (!provider) {
@@ -99,7 +99,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl
): Promise<void> => {
if (args.connectionId) {
if (args.pushTarget) {
assertGitPushTargetShape(args.pushTarget)
assertValidGitPushTarget(args.pushTarget)
}
const provider = getSshGitProvider(args.connectionId)
if (!provider) {
@@ -159,7 +159,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl
): Promise<void> => {
if (args.connectionId) {
if (args.pushTarget) {
assertGitPushTargetShape(args.pushTarget)
assertValidGitPushTarget(args.pushTarget)
}
const provider = getSshGitProvider(args.connectionId)
if (!provider) {
@@ -15,7 +15,7 @@ import {
} from '../../../providers/ssh-git-dispatch'
import { resolveRegisteredWorktreePath } from '../../registered-worktree-roots-cache'
import { getLocalGitOptionsForRegisteredWorktree } from '../../local-worktree-runtime-options'
import { assertGitPushTargetShape } from '../../../../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../../../../shared/git-push-target-validation'
import { validateGitForkSyncExpectedUpstream } from '../../../../shared/git-fork-sync'
import {
materializeWorktreePushTargetRemote,
@@ -34,7 +34,7 @@ export function registerGitRemoteSyncHandlers(context: FilesystemHandlerContext)
): Promise<GitUpstreamStatus> => {
if (args.connectionId) {
if (args.pushTarget) {
assertGitPushTargetShape(args.pushTarget)
assertValidGitPushTarget(args.pushTarget)
}
const provider = getSshGitProvider(args.connectionId)
if (!provider) {
@@ -65,7 +65,7 @@ export function registerGitRemoteSyncHandlers(context: FilesystemHandlerContext)
): Promise<void> => {
if (args.connectionId) {
if (args.pushTarget) {
assertGitPushTargetShape(args.pushTarget)
assertValidGitPushTarget(args.pushTarget)
}
const provider = getSshGitProvider(args.connectionId)
if (!provider) {
+18 -15
View File
@@ -1,24 +1,27 @@
import { describe, expect, it } from 'vitest'
import { buildReadDirErrorBreadcrumb, describeReadDirPathShape } from './readdir-error-diagnostics'
import { buildReadDirErrorBreadcrumb, classifyReadDirPath } from './readdir-error-diagnostics'
describe('describeReadDirPathShape', () => {
describe('classifyReadDirPath', () => {
it('classifies a WSL UNC path without leaking it', () => {
const shape = describeReadDirPathShape('\\\\wsl.localhost\\Ubuntu\\home\\u\\repo', undefined)
expect(shape).toEqual({ hasConnectionId: false, isUNC: true, isWsl: true })
const classification = classifyReadDirPath(
'\\\\wsl.localhost\\Ubuntu\\home\\u\\repo',
undefined
)
expect(classification).toEqual({ hasConnectionId: false, isUNC: true, isWsl: true })
})
it('classifies the legacy \\\\wsl$ root as WSL', () => {
expect(describeReadDirPathShape('\\\\wsl$\\Ubuntu\\home', undefined).isWsl).toBe(true)
expect(classifyReadDirPath('\\\\wsl$\\Ubuntu\\home', undefined).isWsl).toBe(true)
})
it('classifies a plain network UNC share as UNC but not WSL', () => {
const shape = describeReadDirPathShape('\\\\fileserver\\share\\dir', undefined)
expect(shape).toMatchObject({ isUNC: true, isWsl: false })
expect(shape.driveLetter).toBeUndefined()
const classification = classifyReadDirPath('\\\\fileserver\\share\\dir', undefined)
expect(classification).toMatchObject({ isUNC: true, isWsl: false })
expect(classification.driveLetter).toBeUndefined()
})
it('extracts an uppercased drive letter for mapped drives', () => {
expect(describeReadDirPathShape('z:\\projects\\repo', undefined)).toEqual({
expect(classifyReadDirPath('z:\\projects\\repo', undefined)).toEqual({
hasConnectionId: false,
isUNC: false,
isWsl: false,
@@ -27,18 +30,18 @@ describe('describeReadDirPathShape', () => {
})
it('flags the SSH connection without recording it', () => {
const shape = describeReadDirPathShape('/remote/repo', 'ssh-1')
expect(shape).toEqual({ hasConnectionId: true, isUNC: false, isWsl: false })
const classification = classifyReadDirPath('/remote/repo', 'ssh-1')
expect(classification).toEqual({ hasConnectionId: true, isUNC: false, isWsl: false })
})
it('never includes the raw path in the shape', () => {
const shape = describeReadDirPathShape('\\\\wsl.localhost\\Ubuntu\\secret\\path', 'ssh-9')
expect(JSON.stringify(shape)).not.toContain('secret')
it('never includes the raw path in the classification', () => {
const classification = classifyReadDirPath('\\\\wsl.localhost\\Ubuntu\\secret\\path', 'ssh-9')
expect(JSON.stringify(classification)).not.toContain('secret')
})
})
describe('buildReadDirErrorBreadcrumb', () => {
it('captures throw site, error code/name, and path shape', () => {
it('captures throw site, error code/name, and path classification', () => {
const breadcrumb = buildReadDirErrorBreadcrumb({
dirPath: '\\\\wsl.localhost\\Ubuntu\\home\\u\\repo',
connectionId: undefined,
+2 -2
View File
@@ -11,7 +11,7 @@ export type ReadDirThrowSite = 'ssh-provider' | 'authorize' | 'readdir'
* even though breadcrumbs are path-redacted downstream, never collecting the
* raw path is the safer default.
*/
export function describeReadDirPathShape(
export function classifyReadDirPath(
dirPath: string,
connectionId: string | undefined
): CrashReportBreadcrumbData {
@@ -52,6 +52,6 @@ export function buildReadDirErrorBreadcrumb(args: {
throwSite: args.throwSite,
errorName: args.error instanceof Error ? args.error.name : typeof args.error,
...(errorCode(args.error) ? { errorCode: errorCode(args.error)! } : {}),
...describeReadDirPathShape(args.dirPath, args.connectionId)
...classifyReadDirPath(args.dirPath, args.connectionId)
}
}
+2 -2
View File
@@ -48,7 +48,7 @@ import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
import { getHostedReviewForBranch } from '../source-control/hosted-review'
import type { ForgeProviderId } from '../source-control/forge-provider'
import { validateGitPushTarget } from '../git/push-target-validation'
import { assertGitPushTargetShape } from '../../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../../shared/git-push-target-validation'
import { gitExecFileAsync } from '../git/runner'
import type {
OrcaRuntimeService,
@@ -1277,7 +1277,7 @@ export async function prepareWorktreePushTargetSsh(
store?: WorktreePushTargetStore,
repoId?: string
): Promise<GitPushTarget> {
assertGitPushTargetShape(target)
assertValidGitPushTarget(target)
const execGit: GitRemoteExec = (args, cwd) => provider.exec(args, cwd)
const { remoteCreated: _ignoredRemoteCreated, ...sanitizedTarget } = target
await provider.exec(['check-ref-format', '--branch', target.branchName], repoPath)
@@ -63,14 +63,12 @@ function serializedLifecycleBatchFits(
fence: Number.MAX_SAFE_INTEGER,
ts: Number.MAX_SAFE_INTEGER,
settlementId,
mutations: mutations.map(lifecycleMutationRowShape)
mutations: mutations.map(toLifecycleMutationRow)
}
return Buffer.byteLength(JSON.stringify(row), 'utf8') + 1 <= MAX_JOURNAL_LIFECYCLE_BATCH_BYTES
}
function lifecycleMutationRowShape(
mutation: JournalLifecycleMutationInput
): JournalLifecycleMutation {
function toLifecycleMutationRow(mutation: JournalLifecycleMutationInput): JournalLifecycleMutation {
const itemId = agentJournalItemKey(mutation.identity)
return mutation.kind === 'item'
? {
@@ -75,14 +75,14 @@ function item(index: number, sequence: number): AgentJournalRenderItem {
}
}
/** Every sequence-run shape of `length` items, as run-length compositions. */
function* runShapes(length: number): Generator<number[]> {
/** Every run-length composition of `length` items. */
function* runLengthCompositions(length: number): Generator<number[]> {
if (length === 0) {
yield []
return
}
for (let first = 1; first <= length; first += 1) {
for (const rest of runShapes(length - first)) {
for (const rest of runLengthCompositions(length - first)) {
yield [first, ...rest]
}
}
@@ -105,7 +105,7 @@ function buildItems(runs: number[], repeatSequence: boolean): AgentJournalRender
it('matches eager grouping at every newest-window limit for every run shape', () => {
let cases = 0
for (let length = 0; length <= 7; length += 1) {
for (const runs of runShapes(length)) {
for (const runs of runLengthCompositions(length)) {
for (const repeatSequence of [false, true]) {
const items = buildItems(runs, repeatSequence)
// Every boundary, including 0, each exact group edge, and past the end.
@@ -127,7 +127,7 @@ it('matches eager byte bounding at every budget boundary in both directions', ()
let truncatedCases = 0
let partialCases = 0
for (let length = 1; length <= 6; length += 1) {
for (const runs of runShapes(length)) {
for (const runs of runLengthCompositions(length)) {
for (const repeatSequence of [false, true]) {
const items = buildItems(runs, repeatSequence)
const perItem = historyEntryBytes(items[0]!, submissionBytes)
@@ -3,7 +3,7 @@ import { CODEX_APP_SERVER_NOTIFICATION_METHODS } from '../../codex/codex-app-ser
import { CLAUDE_STREAM_JSON_FRAME_KINDS } from './claude-stream-json-frame-schema'
import {
classifyProviderFrame,
isDeltaShapedProviderFrameKind,
isDeltaProviderFrameKind,
PROVIDER_FRAME_CLASSIFICATIONS
} from './provider-frame-disposition'
import { unhandledProviderFrameJournalItem } from './unhandled-provider-frame'
@@ -25,7 +25,7 @@ describe('provider frame classification catalog', () => {
const deltaKinds = [
...Object.keys(PROVIDER_FRAME_CLASSIFICATIONS.codex),
...Object.keys(PROVIDER_FRAME_CLASSIFICATIONS.claude)
].filter(isDeltaShapedProviderFrameKind)
].filter(isDeltaProviderFrameKind)
expect(deltaKinds.length).toBeGreaterThan(0)
for (const kind of deltaKinds) {
@@ -226,7 +226,7 @@ function itemKind(kind: string): string | null {
return kind.startsWith('item:') ? kind.slice('item:'.length) : null
}
export function isDeltaShapedProviderFrameKind(kind: string): boolean {
export function isDeltaProviderFrameKind(kind: string): boolean {
return notificationKind(kind).toLowerCase().endsWith('delta')
}
@@ -260,7 +260,7 @@ export function classifyProviderFrame(
if (hasProviderError(payload)) {
return 'error-surface'
}
if (isDeltaShapedProviderFrameKind(kind)) {
if (isDeltaProviderFrameKind(kind)) {
return 'stream-into-item'
}
if (provider === 'claude' && kind === 'message:result') {
+2 -2
View File
@@ -25,7 +25,7 @@ const SECRETS = {
pem: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQ\n-----END PRIVATE KEY-----'
}
const SHAPES: { label: string; raw: string; tag: string }[] = [
const PROVIDER_KEY_CASES: { label: string; raw: string; tag: string }[] = [
{ label: 'anthropic', raw: SECRETS.anthropic, tag: 'anthropic-key' },
{ label: 'openai', raw: SECRETS.openai, tag: 'openai-key' },
{ label: 'github', raw: SECRETS.github, tag: 'github-token' },
@@ -37,7 +37,7 @@ const SHAPES: { label: string; raw: string; tag: string }[] = [
]
describe('redactor — provider-key fingerprints', () => {
for (const { label, raw, tag } of SHAPES) {
for (const { label, raw, tag } of PROVIDER_KEY_CASES) {
describe(`${label}`, () => {
it('redacts when the secret appears as an attribute value', () => {
// Bare "<secret>" without a labeled-kv keyword nearby — exercises the
@@ -138,7 +138,7 @@ function writeLegacyFile(dataFile: string): void {
/** Inverse of everything this change does, applied to a compact file: what the old serializer
* would have written for the same state. */
function reexpandToLegacyShape(state: PersistedState): PersistedState {
function reexpandToLegacySerialization(state: PersistedState): PersistedState {
const expanded = structuredClone(state)
for (const map of [expanded.worktreeMeta, expanded.worktreeMetaByIdentity]) {
for (const [key, meta] of Object.entries(map ?? {})) {
@@ -207,7 +207,7 @@ describe('persisted-state redundancy', () => {
// Apples to apples: re-expand the file we just wrote back into the old shape and compare, so
// the number is the redundancy alone and not the settings defaults a synthetic fixture lacks.
expect(Buffer.byteLength(rewritten)).toBeLessThan(
Buffer.byteLength(JSON.stringify(reexpandToLegacyShape(onDisk))) * 0.6
Buffer.byteLength(JSON.stringify(reexpandToLegacySerialization(onDisk))) * 0.6
)
// load(save(state)) deep-equals the pre-save state for every field touched.
@@ -8,7 +8,7 @@ const userDataDir = mkdtempSync(join(tmpdir(), 'orca-pi-overlay-path-userdata-')
import { PiTitlebarExtensionService } from './titlebar-extension-service'
const PATH_SHAPED_PTY_ID = [
const PATH_LIKE_PTY_ID = [
'50c010a2-bc8e-4eb1-8847-5812133ad6df',
'Users',
'dev',
@@ -45,7 +45,7 @@ describe('PiTitlebarExtensionService legacy overlay paths', () => {
const svc = new PiTitlebarExtensionService()
try {
const env = svc.buildPtyEnv(PATH_SHAPED_PTY_ID, piHome, 'pi')
const env = svc.buildPtyEnv(PATH_LIKE_PTY_ID, piHome, 'pi')
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe(piHome)
@@ -61,12 +61,12 @@ describe('PiTitlebarExtensionService legacy overlay paths', () => {
})
it('clears legacy raw path-shaped daemon overlays during teardown', () => {
const legacyOverlayDir = legacyOverlayPath('pi', PATH_SHAPED_PTY_ID)
const legacyOverlayDir = legacyOverlayPath('pi', PATH_LIKE_PTY_ID)
mkdirSync(legacyOverlayDir, { recursive: true })
writeFileSync(join(legacyOverlayDir, 'stale.txt'), 'stale overlay')
const svc = new PiTitlebarExtensionService()
svc.clearPty(PATH_SHAPED_PTY_ID)
svc.clearPty(PATH_LIKE_PTY_ID)
expect(existsSync(legacyOverlayDir)).toBe(false)
})
@@ -132,10 +132,10 @@ function* legacyIterateTerminalOutputFrameChunks(
}
}
type FrameShape = { base64: string; seq: number | 'undefined'; opcode: number | 'undefined' }
type FrameSummary = { base64: string; seq: number | 'undefined'; opcode: number | 'undefined' }
function describeFrames(frames: Iterable<TerminalOutputFrameChunk>): FrameShape[] {
const out: FrameShape[] = []
function describeFrames(frames: Iterable<TerminalOutputFrameChunk>): FrameSummary[] {
const out: FrameSummary[] = []
for (const frame of frames) {
out.push({
base64: Buffer.from(frame.bytes).toString('base64'),
@@ -170,10 +170,10 @@ const SURROGATE_EDGES = [
'\udfff\udc00'
]
// Meta shapes exercised against every fixture: no meta, seq-preserved (rawLength ===
// Meta variants exercised against every fixture: no meta, seq-preserved (rawLength ===
// data.length), the delayed-final-seq path (rawLength !== data.length -> OutputSpan),
// transformed, and cwd-only.
function metaShapesFor(data: string): { label: string; meta: TerminalOutputMeta | undefined }[] {
function metaVariantsFor(data: string): { label: string; meta: TerminalOutputMeta | undefined }[] {
return [
{ label: 'no-meta', meta: undefined },
{ label: 'seq-only', meta: { seq: 5_000_000 } },
@@ -187,8 +187,8 @@ function metaShapesFor(data: string): { label: string; meta: TerminalOutputMeta
}
function sweepAll(data: string, label: string): void {
for (const shape of metaShapesFor(data)) {
expectEquivalent(data, shape.meta, `${label} [${shape.label}]`)
for (const variant of metaVariantsFor(data)) {
expectEquivalent(data, variant.meta, `${label} [${variant.label}]`)
}
}
+3 -3
View File
@@ -32,15 +32,15 @@ export function computeTerminalTailWaitState(
partialLine: string,
preview: string
): TerminalTailWaitState {
const tailShape = inspectTerminalWaitTail(lines, partialLine)
if (!tailShape.fromTail) {
const tailInspection = inspectTerminalWaitTail(lines, partialLine)
if (!tailInspection.fromTail) {
return {
waitText: preview,
signal: findActionableTerminalWaitBlockedSignal(preview.toLowerCase()),
fromTail: false
}
}
if (!tailShape.mayContainBlockedSignal) {
if (!tailInspection.mayContainBlockedSignal) {
// Why: reads waitText only when a signal exists; avoid retaining a rebuilt 256 KiB string in the common case.
return { waitText: '', signal: null, fromTail: true }
}
+3 -3
View File
@@ -18,7 +18,7 @@ export type SkillBundleArtifacts = {
}
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/)
const snapshotShape = {
const snapshotFields = {
releaseRevision: z.number().int().positive(),
packageDigest: sha256Schema,
gitTreeSha: z.string().regex(/^[a-f0-9]{40}$/),
@@ -38,7 +38,7 @@ const snapshotShape = {
)
.min(1)
}
const knownSnapshotSchema = z.object(snapshotShape).strict()
const knownSnapshotSchema = z.object(snapshotFields).strict()
const manifestSchema = z
.object({
schemaVersion: z.literal(2),
@@ -47,7 +47,7 @@ const manifestSchema = z
.object({
name: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
sourcePath: z.string().min(1),
...snapshotShape
...snapshotFields
})
.strict()
)
+1 -1
View File
@@ -303,7 +303,7 @@ describe('a host key store written by a newer version', () => {
const storeFile = join(dir, 'ssh-host-keys.json')
const future = JSON.stringify({
version: 99,
hostKeys: [{ shape: 'we do not understand' }]
hostKeys: [{ unrecognized: 'we do not understand' }]
})
await writeFile(storeFile, future, 'utf-8')
@@ -167,9 +167,9 @@ describe('what the host publishes about a pane, read by the sweep', () => {
it('records that a backgrounded and a suspended shell are indistinguishable at tpgid/pgid', () => {
// The premise of the whole file. If this ever fails, the fixtures drifted and every verdict
// below is testing something other than the defect. Pids differ between captures, so the
// comparison is of the shell row's shape: who its parent is, whether it leads its own process
// group, whether that group owns the terminal, and its state flags.
const shellShape = (capture: { rootPid: number; table: readonly string[] }): string => {
// comparison is of the shell row's signature: who its parent is, whether it leads its own
// process group, whether that group owns the terminal, and its state flags.
const shellRowSignature = (capture: { rootPid: number; table: readonly string[] }): string => {
const row = parseStrictProcessTableRows(capture.table.join('\n')).find(
(candidate) => candidate.pid === capture.rootPid
)!
@@ -181,19 +181,21 @@ describe('what the host publishes about a pane, read by the sweep', () => {
].join(' ')
}
expect(shellShape(CAPTURES.idle)).toBe('ppid=1 leadsOwnGroup=true ownsTerminal=true stat=Ss+')
expect(shellShape(CAPTURES.background)).toBe(shellShape(CAPTURES.idle))
expect(shellShape(CAPTURES.ctrlz)).toBe(shellShape(CAPTURES.idle))
expect(shellShape(CAPTURES.foreground)).not.toBe(shellShape(CAPTURES.idle))
expect(shellRowSignature(CAPTURES.idle)).toBe(
'ppid=1 leadsOwnGroup=true ownsTerminal=true stat=Ss+'
)
expect(shellRowSignature(CAPTURES.background)).toBe(shellRowSignature(CAPTURES.idle))
expect(shellRowSignature(CAPTURES.ctrlz)).toBe(shellRowSignature(CAPTURES.idle))
expect(shellRowSignature(CAPTURES.foreground)).not.toBe(shellRowSignature(CAPTURES.idle))
// Same premise for the `set +m` captures, minus `ppid`: their harness keeps its parent alive
// rather than reparenting the shell to init, and the ppid is the one field of the shape the
// predicate never reads.
const paneShape = (capture: { rootPid: number; table: readonly string[] }): string =>
shellShape(capture).split(' ').slice(1).join(' ')
expect(paneShape(CAPTURES.setMinusMBackground)).toBe(paneShape(CAPTURES.idle))
expect(paneShape(CAPTURES.nottyGroupMember)).toBe(paneShape(CAPTURES.idle))
expect(paneShape(CAPTURES.doubleForkedGroupMember)).toBe(paneShape(CAPTURES.idle))
// rather than reparenting the shell to init, and the ppid is the one field of the signature
// the predicate never reads.
const paneRowSignature = (capture: { rootPid: number; table: readonly string[] }): string =>
shellRowSignature(capture).split(' ').slice(1).join(' ')
expect(paneRowSignature(CAPTURES.setMinusMBackground)).toBe(paneRowSignature(CAPTURES.idle))
expect(paneRowSignature(CAPTURES.nottyGroupMember)).toBe(paneRowSignature(CAPTURES.idle))
expect(paneRowSignature(CAPTURES.doubleForkedGroupMember)).toBe(paneRowSignature(CAPTURES.idle))
})
it('sweeps an idle shell', async () => {
@@ -232,9 +232,9 @@ describe('waitForSentinel', () => {
it.each(['ssh2 channel', 'system-SSH child stdio'])(
'forwards write(false), callback settlement, and drain for a %s',
async (shape) => {
async (channelKind) => {
const channel = createMockChannel()
if (shape.startsWith('system')) {
if (channelKind.startsWith('system')) {
Object.assign(channel, { _process: new EventEmitter() })
}
const callback = vi.fn()
@@ -38,7 +38,7 @@ export async function detectRemoteHostPlatform(
}
// Why: only the PowerShell probe can settle a uname the parser cannot map
// (Cygwin, say), so a refused or timed-out channel leaves it unsettled.
const windowsProbeNeverRan = windows.kind === 'failed' && isTransportShapedError(windows.error)
const windowsProbeNeverRan = windows.kind === 'failed' && isTransportFailure(windows.error)
if ((uname.kind === 'unsupported' && !windowsProbeNeverRan) || windows.kind === 'unsupported') {
const reported = uname.kind === 'unsupported' ? uname.uname : probeUname(windows)
console.warn(`[ssh-relay] Remote reported an unsupported platform: ${reported}`)
@@ -66,7 +66,7 @@ function undetectedPlatformError(
windows: PlatformProbeOutcome
): Error {
for (const outcome of [uname, windows]) {
if (outcome.kind === 'failed' && isTransportShapedError(outcome.error)) {
if (outcome.kind === 'failed' && isTransportFailure(outcome.error)) {
return wrapProbeError(outcome.error)
}
}
@@ -84,7 +84,7 @@ function undetectedPlatformError(
// Why: a refused or timed-out channel explains the failure better than the
// other probe's mundane non-zero exit (e.g. "sh: not found" on Windows).
function isTransportShapedError(error: unknown): boolean {
function isTransportFailure(error: unknown): boolean {
return (
isSshSessionLimitError(error) ||
isUnconfirmedSshCommandTermination(error) ||
@@ -327,7 +327,7 @@ describe('generateCommitMessageFromContext', () => {
'401: {"message":"slot 1:/Users/name/alt failed"}',
'Pi CLI command failed with code 1: 401: {"message":"slot 1:[path] failed"}'
]
])('redacts a %s in provider bodies', async (_shape, stderr, expected) => {
])('redacts a %s in provider bodies', async (_variant, stderr, expected) => {
const result = await generateCommitMessageFromContext(
{
branch: 'main',
@@ -55,7 +55,7 @@ describe('WSL vault intermediate-symlink reproduction', () => {
it.each([
['file-shaped', `${FIXTURE_ROOT}/linked-project/session.json`, false],
['directory-shaped', `${FIXTURE_ROOT}/linked-project/session`, true]
])('rejects a %s target before removal', async (_shape, target, recursive) => {
])('rejects a %s target before removal', async (_targetKind, target, recursive) => {
const options = { recursive, approvedRoots: [unc(FIXTURE_ROOT)] }
let rejection: unknown
+1 -1
View File
@@ -46,7 +46,7 @@ describe.skipIf(!runRealWsl)('WSL contained delete integration', () => {
it.each([
['file-shaped', 'file-link/session.json', false],
['directory-shaped', 'dir-link/session', true]
])('rejects a %s escape and preserves all outside entries', async (_shape, path, recursive) => {
])('rejects a %s escape and preserves all outside entries', async (_label, path, recursive) => {
const vaultRoot = `${fixtureRoot}/vault`
await expect(
+4 -4
View File
@@ -547,10 +547,10 @@ describe('WSL availability cache', () => {
it.each([
['wsl.exe reports WSL unusable', { status: 1 }],
['wsl.exe is not installed', { code: 'ENOENT' }]
])('holds a definitive failure far longer than a timeout when %s', (_label, errorShape) => {
])('holds a definitive failure far longer than a timeout when %s', (_label, errorFields) => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw Object.assign(new Error('definitive failure'), errorShape)
throw Object.assign(new Error('definitive failure'), errorFields)
})
execFileSyncMock.mockReturnValueOnce('')
@@ -621,10 +621,10 @@ describe('WSL availability cache', () => {
it.each([
['a definitive failure', { status: 1 }],
['a timeout', { code: 'ETIMEDOUT', status: null, signal: 'SIGTERM' }]
])('re-probes availability once a distro list succeeds after %s', (_label, errorShape) => {
])('re-probes availability once a distro list succeeds after %s', (_label, errorFields) => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw Object.assign(new Error('probe failed'), errorShape)
throw Object.assign(new Error('probe failed'), errorFields)
})
try {
+2 -2
View File
@@ -96,7 +96,7 @@ const DIFF_ALLOWED_FLAGS = new Set([
// only those two exact shapes, held to the same remote-name and URL rules the
// relay already enforces on every pushTarget-carrying RPC. Everything else --
// set-url, rename, prune, flags before the action -- stays blocked.
function isAllowedRemoteWriteShape(args: string[]): boolean {
function isAllowedRemoteWriteInvocation(args: string[]): boolean {
if (args[1] === 'add') {
return args.length === 4 && isSafeGitRemoteName(args[2]) && isSafePushTargetRemoteUrl(args[3])
}
@@ -197,7 +197,7 @@ export function validateGitExecArgs(args: string[]): void {
if (
remoteSubcmd &&
REMOTE_WRITE_SUBCOMMANDS.has(remoteSubcmd) &&
!isAllowedRemoteWriteShape(args)
!isAllowedRemoteWriteInvocation(args)
) {
throw new Error('Destructive git remote operations are not allowed via exec')
}
@@ -128,10 +128,10 @@ describe('pinned and legacy branch diff equivalence against real Git', () => {
for (const entry of compare.entries) {
// Exactly what the renderer sends: paths from the compare entry list,
// OIDs from the compare summary that produced that same list.
const callerShape = { filePath: entry.path, oldPath: entry.oldPath }
const legacy = await branchDiff(callerShape)
const callerParams = { filePath: entry.path, oldPath: entry.oldPath }
const legacy = await branchDiff(callerParams)
const pinned = await branchDiff({
...callerShape,
...callerParams,
baseRef: compare.summary.mergeBase,
headOid: compare.summary.headOid
})
@@ -5,7 +5,7 @@ import { parseBranchDiff } from './git-handler-utils'
import { parseNumstat } from '../shared/git-uncommitted-line-stats'
import { isNoUpstreamError, normalizeGitErrorMessage } from '../shared/git-remote-error'
import { upstreamOnlyCommitsArePatchEquivalent } from '../shared/git-upstream-status'
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
import { getPublishTargetStatus, type GitCommandRunner } from '../shared/git-publish-target-status'
import type { GitPushTarget } from '../shared/worktree/types'
import { getEffectiveGitUpstreamStatus } from '../shared/git-effective-upstream'
@@ -46,7 +46,7 @@ export class GitHandlerComparisonOperations extends GitHandlerOperationContext {
try {
if (params.pushTarget !== undefined) {
assertGitPushTargetShape(params.pushTarget)
assertValidGitPushTarget(params.pushTarget)
const pushTarget = params.pushTarget as GitPushTarget
await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath)
return await getPublishTargetStatus(
+2 -2
View File
@@ -1,6 +1,6 @@
import type { RequestContext } from './dispatcher'
import { GitHandlerOperationContext } from './git-handler-operation-context'
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
import type { GitPushTarget } from '../shared/worktree/types'
import { normalizeGitErrorMessage, isExecKilledError } from '../shared/git-remote-error'
import { syncForkDefaultBranch, validateGitForkSyncExpectedUpstream } from '../shared/git-fork-sync'
@@ -21,7 +21,7 @@ export class GitHandlerFetchOperations extends GitHandlerOperationContext {
try {
try {
if (params.pushTarget !== undefined) {
assertGitPushTargetShape(params.pushTarget)
assertValidGitPushTarget(params.pushTarget)
const pushTarget = params.pushTarget as GitPushTarget
await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath)
await this.git(['fetch', '--prune', pushTarget.remoteName], worktreePath)
+2 -2
View File
@@ -1,4 +1,4 @@
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
import {
resolveConfiguredGitPushTarget,
type ResolvedGitPushTarget
@@ -15,7 +15,7 @@ export async function resolveRelayPushTarget(
if (pushTarget === undefined) {
return resolveConfiguredGitPushTarget((args) => git(args, worktreePath))
}
assertGitPushTargetShape(pushTarget)
assertValidGitPushTarget(pushTarget)
const explicitTarget: GitPushTarget = pushTarget
// Why here and not in the shared resolver: an explicit target arrives over the wire,
// so the host re-validates its shape and asks Git to vet the branch name itself.
+2 -2
View File
@@ -3,7 +3,7 @@ import type { RequestContext } from './dispatcher'
import { GitHandlerOperationContext } from './git-handler-operation-context'
import { resolveRelayPushTarget } from './git-handler-push-target'
import { normalizeGitErrorMessage, runPullWithDivergenceFallback } from '../shared/git-remote-error'
import { assertGitPushTargetShape } from '../shared/git-push-target-validation'
import { assertValidGitPushTarget } from '../shared/git-push-target-validation'
import type { GitCommandRunner } from '../shared/git-publish-target-status'
import type { GitPushTarget } from '../shared/worktree/types'
import { resolveEffectiveGitUpstream } from '../shared/git-effective-upstream'
@@ -63,7 +63,7 @@ export class GitHandlerSyncOperations extends GitHandlerOperationContext {
const worktreePath = params.worktreePath as string
const runPull = async (effectiveArgs: string[]): Promise<void> => {
if (params.pushTarget !== undefined) {
assertGitPushTargetShape(params.pushTarget)
assertValidGitPushTarget(params.pushTarget)
const pushTarget = params.pushTarget as GitPushTarget
await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath)
await this.git(
@@ -149,7 +149,7 @@ function parseStructuredPayload(value: string): HtmlSuperscriptLinkSource | null
} catch {
return null
}
if (!isCitationShape(candidate)) {
if (!isCitationSource(candidate)) {
return null
}
const parsed = parseHtmlSuperscriptLinkSource(candidate.source)
@@ -197,7 +197,7 @@ function hasOnlyAttributes(element: Element, allowed: string[]): boolean {
return Array.from(element.attributes).every((attribute) => allowedSet.has(attribute.name))
}
function isCitationShape(value: unknown): value is HtmlSuperscriptLinkSource {
function isCitationSource(value: unknown): value is HtmlSuperscriptLinkSource {
if (!value || typeof value !== 'object') {
return false
}
@@ -16,6 +16,7 @@ import {
Palette,
Rocket,
Server,
// `Shapes` is lucide-react's own export name; exempted in config/oxlint-anti-slop.json.
Shapes,
Sparkles,
SquareTerminal,
@@ -13,9 +13,7 @@ export type DiscardConfirmationCopy = {
* Untracked and newly-added paths have no HEAD version to restore, so Orca's discard removes the
* working-tree file. Every surface that names the operation must say "delete" for these.
*/
export function isDeleteShapedDiscardEntry(
entry: Pick<GitStatusEntry, 'area' | 'status'>
): boolean {
export function discardDeletesEntryFile(entry: Pick<GitStatusEntry, 'area' | 'status'>): boolean {
return entry.area === 'untracked' || entry.status === 'untracked' || entry.status === 'added'
}
@@ -24,7 +22,7 @@ export function getDiscardEntryConfirmationCopy(
): DiscardConfirmationCopy {
const name = basename(entry.path)
if (isDeleteShapedDiscardEntry(entry)) {
if (discardDeletesEntryFile(entry)) {
return {
title: translate(
'auto.components.right.sidebar.source.control.discard.confirmation.96c772bee9',
@@ -61,7 +61,7 @@ describe('showSourceControlEntryFailureToast', () => {
it('says "delete" for an entry whose discard removes the file rather than restoring it', () => {
// Why: untracked and added paths have no HEAD version, so the row button and the confirmation
// dialog both say "delete" — the failure must not contradict the verb the user pressed.
show({ operation: 'discard', deleteShaped: true })
show({ operation: 'discard', deletesFile: true })
expect(lastToast().title).toBe('Failed to delete “src/app.ts”')
})
@@ -26,7 +26,7 @@ export function dismissSourceControlEntryFailureToast(worktreeId: string | null)
function entryFailureTitle(
operation: SourceControlEntryOperation,
filePath: string,
deleteShaped: boolean
deletesFile: boolean
): string {
switch (operation) {
case 'stage':
@@ -42,7 +42,7 @@ function entryFailureTitle(
{ value0: filePath }
)
case 'discard':
return deleteShaped
return deletesFile
? translate(
'auto.components.right.sidebar.SourceControl.entryDeleteFailed',
'Failed to delete “{{value0}}”',
@@ -67,7 +67,7 @@ function entryFailureTitle(
export function showSourceControlEntryFailureToast({
operation,
filePath,
deleteShaped = false,
deletesFile = false,
error,
worktreeId,
worktreeName,
@@ -76,7 +76,7 @@ export function showSourceControlEntryFailureToast({
operation: SourceControlEntryOperation
filePath: string
/** True when this discard deletes the file rather than restoring it — see `discard-confirmation`. */
deleteShaped?: boolean
deletesFile?: boolean
error: unknown
/** The worktree the failed attempt ran against. */
worktreeId: string | null
@@ -85,7 +85,7 @@ export function showSourceControlEntryFailureToast({
onRetry?: () => void
}): void {
const isActiveWorktree = useAppStore.getState().activeWorktreeId === worktreeId
const title = entryFailureTitle(operation, filePath, deleteShaped)
const title = entryFailureTitle(operation, filePath, deletesFile)
const offerRetry = Boolean(onRetry) && isActiveWorktree
entryFailureSlotOwner = { worktreeId }
toast.error(
@@ -10,7 +10,7 @@ import {
runDiscardAllForArea,
type DiscardAllArea
} from './discard-all-sequence'
import { isDeleteShapedDiscardEntry } from './discard-confirmation'
import { discardDeletesEntryFile } from './discard-confirmation'
import { readIpcErrorMessage } from '@/lib/ipc-error'
import {
dismissSourceControlEntryFailureToast,
@@ -62,7 +62,7 @@ export function useSourceControlDiscardConfirmation({
showSourceControlEntryFailureToast({
operation: 'discard',
filePath: entry.path,
deleteShaped: isDeleteShapedDiscardEntry(entry),
deletesFile: discardDeletesEntryFile(entry),
error,
worktreeId: activeWorktreeId,
worktreeName: worktreePath ? basename(worktreePath) : null
@@ -223,14 +223,14 @@ export function useDaemonActions(callbacks?: DaemonActionCallbacks): DaemonActio
}
}
type CopyShape = {
type DaemonActionCopy = {
title: string
description: React.ReactNode
confirmLabel: string
busyLabel: string
}
function getCopy(kind: DaemonActionKind): CopyShape {
function getCopy(kind: DaemonActionKind): DaemonActionCopy {
if (kind === 'restart') {
return {
title: translate(
@@ -164,11 +164,11 @@ function exactFileLink(value: string, allowSpacedRelative: boolean): ParsedTermi
if (!parsed) {
return null
}
const hasPathShape =
const looksLikePath =
ROOTED_PATH_PREFIX_PATTERN.test(parsed.pathText) ||
/[\\/]/.test(parsed.pathText) ||
/\.[\p{L}][\p{L}\p{N}\p{M}_+-]*$/u.test(parsed.pathText)
if (!hasPathShape) {
if (!looksLikePath) {
return null
}
const explicitLink = {
@@ -6,6 +6,7 @@ import {
FolderInput,
FolderTree,
Plus,
// `Shapes` is lucide-react's own export name; exempted in config/oxlint-anti-slop.json.
Shapes,
SlidersHorizontal,
Trash2
@@ -31,7 +31,7 @@ import { REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS } from './remote-runtime-pty-re
const ELECTRON_IPC_PREFIX = "Error invoking remote method 'runtimeEnvironments:call': "
/** A rejection exactly as the renderer sees it after Electron IPC strips custom props. */
function electronIpcShapedRejection(errorName: string, message: string): Error {
function electronIpcRejection(errorName: string, message: string): Error {
return new Error(`${ELECTRON_IPC_PREFIX}${errorName}: ${message}`)
}
@@ -192,7 +192,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
const { isRecoverableRemoteRuntimeConnectionError, toRemoteRuntimeClientErrorLike } =
await import('../../../../shared/remote-runtime-client-error-classification')
const rendererSide = toRemoteRuntimeClientErrorLike(
electronIpcShapedRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
electronIpcRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
)
// Electron IPC stripped the code; the fragment list still catches this one.
expect(rendererSide.code).toBeUndefined()
@@ -201,7 +201,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
// per-selector RPC queue saturated by 15s-timeout calls) is classified
// fatal even though its own code says "retry later".
const overload = toRemoteRuntimeClientErrorLike(
electronIpcShapedRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
electronIpcRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
)
expect(overload.code).toBeUndefined()
// DESIRED: transient capacity pressure during an outage is recoverable,
@@ -220,7 +220,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
runtimeCall.mockImplementation(async (request: { method: string; params?: unknown }) => {
if (request.method === 'terminal.send') {
sendRejections += 1
throw electronIpcShapedRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
throw electronIpcRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW)
}
return healthyImpl(request)
})
@@ -294,7 +294,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () =
if (request.method === 'terminal.resolvePane') {
throw Object.assign(new Error(fatalMessage), { code: 'unauthorized' })
}
throw electronIpcShapedRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
throw electronIpcRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT)
})
subscriptionCallbacks?.onClose?.()
await vi.waitFor(() => expect(onError).toHaveBeenCalled())
@@ -107,7 +107,7 @@ function openTerminalWithSearch(): SearchHarness {
* showed up for some of them, so the regression has to sweep rather than pin
* one lucky case.
*/
const CONTENT_SHAPES: readonly (readonly [string, string])[] = [
const CONTENT_LAYOUTS: readonly (readonly [string, string])[] = [
['matches on two lines', 'needle one\r\nneedle two\r\n'],
['matches on three lines', 'needle one\r\nneedle two\r\nneedle three\r\n'],
['matches on four lines', 'needle a\r\nneedle b\r\nneedle c\r\nneedle d\r\n'],
@@ -128,7 +128,7 @@ describe('terminal search decoration cleanup (STA-2707)', () => {
document.body.replaceChildren()
})
it.each(CONTENT_SHAPES)(
it.each(CONTENT_LAYOUTS)(
'leaves no highlighted cells after closing search (%s)',
async (_name, content) => {
// Sweeping the match-navigation count matters: which decoration is the
@@ -7,7 +7,7 @@ import {
getEditorExternalWatchTargetKey,
selectEditorExternalWatchTargets,
type EditorExternalWatchTarget,
type EditorExternalWatchTargetState as EditorExternalWatchTargetStateShape
type EditorExternalWatchTargetState
} from './editor-external-watch-targets'
import {
buildEditorExternalWatchEventHandler,
@@ -15,7 +15,7 @@ import {
} from './editor-external-watch-event-reconciliation'
import { verifyLatchedEditorMoveDestinations } from './editor-external-watch-disk-verification'
export type EditorExternalWatchTargetState = EditorExternalWatchTargetStateShape
export type { EditorExternalWatchTargetState }
function warnExternalWatchFailure(target: EditorExternalWatchTarget, err: unknown): void {
console.warn('[filesystem-watch] failed to watch worktree', {
@@ -11,7 +11,7 @@ import type { TuiAgent } from '../../../shared/tui-agent'
const AGENTS: readonly TuiAgent[] = ['claude', 'codex']
const SLOT_COUNT = 7
const SHAPE_COUNT = 3 ** SLOT_COUNT * 4 * 2
const COMBINATION_COUNT = 3 ** SLOT_COUNT * 4 * 2
const TITLES: readonly string[] = ['', 'zsh', 'Task - claude', 'Task - codex']
type Breakdown = Record<
@@ -124,7 +124,7 @@ describe('renderer ladder decision table', () => {
const proofFree = runDecisionTable(false)
const freshProof = runDecisionTable(true)
const result = {
shapes: SHAPE_COUNT,
combinations: COMBINATION_COUNT,
proofOmitted: proofFree,
freshProof,
flippedByAddingProof: proofFree.flipped
@@ -131,7 +131,7 @@ export type FocusedPaneCensus = {
type CountableRecord = Record<string, unknown> | null | undefined
export type TypingCensusStoreShape = {
export type TypingCensusStoreView = {
worktreesByRepo?: Record<string, WorktreeLike[]> | null
tabsByWorktree?: Record<string, unknown[]> | null
unifiedTabsByWorktree?: Record<string, unknown[]> | null
@@ -231,7 +231,7 @@ function collectWorktrees(
}
export function summarizeTypingScaleCensus(input: {
state: TypingCensusStoreShape | null
state: TypingCensusStoreView | null
appVersion: string | null
livePaneCount: number | null
instrumentedPaneCount: number
@@ -67,11 +67,11 @@ function buildCase(random: () => number): {
worktreesByRepo: Record<string, readonly OwnerRecord[]>
probeIds: string[]
} {
const shape = random()
if (shape < 0.05) {
const roll = random()
if (roll < 0.05) {
return { detectedWorktreesByRepo: undefined, worktreesByRepo: {}, probeIds: ['repo-0::absent'] }
}
if (shape < 0.1) {
if (roll < 0.1) {
return { detectedWorktreesByRepo: {}, worktreesByRepo: {}, probeIds: ['repo-0::absent'] }
}
const repoCount = 1 + Math.floor(random() * 6)
@@ -65,19 +65,19 @@ function makeEntry(index: number, overrides: Record<string, unknown> = {}): neve
}
describe('mobile agent-status projection equivalence', () => {
it('matches the whole-array serialization across shapes and cache reuse', () => {
it('matches the whole-array serialization across status maps and cache reuse', () => {
resetRuntimeMobileAgentStatusProjectionCacheForTests()
const shapes: AppState['agentStatusByPaneKey'][] = []
shapes.push({})
shapes.push({ 'tab-0:leaf-0': makeEntry(0) })
shapes.push({ 'tab-0:leaf-0': makeEntry(0, { workingMode: 'monitoring' }) })
const statusMaps: AppState['agentStatusByPaneKey'][] = []
statusMaps.push({})
statusMaps.push({ 'tab-0:leaf-0': makeEntry(0) })
statusMaps.push({ 'tab-0:leaf-0': makeEntry(0, { workingMode: 'monitoring' }) })
const many: AppState['agentStatusByPaneKey'] = {}
for (let index = 0; index < 12; index += 1) {
many[`tab-${index}:leaf-0`] = makeEntry(index)
}
shapes.push(many)
statusMaps.push(many)
// Optional fields absent entirely, which the ?? null fallbacks must cover.
shapes.push({
statusMaps.push({
'tab-9:leaf-1': makeEntry(9, {
agentType: undefined,
terminalTitle: undefined,
@@ -89,17 +89,17 @@ describe('mobile agent-status projection equivalence', () => {
})
})
// Keys deliberately out of insertion order to pin the sort.
shapes.push({
statusMaps.push({
'tab-z:leaf-0': makeEntry(2),
'tab-a:leaf-0': makeEntry(1),
'tab-m:leaf-0': makeEntry(3)
})
for (const [index, shape] of shapes.entries()) {
for (const [index, statusMap] of statusMaps.entries()) {
expect({
index,
projection: buildRuntimeMobileAgentStatusProjectionForTests(shape)
}).toEqual({ index, projection: referenceProjection(shape) })
projection: buildRuntimeMobileAgentStatusProjectionForTests(statusMap)
}).toEqual({ index, projection: referenceProjection(statusMap) })
}
// Now exercise the cache: replace one entry the way setAgentStatus does and
@@ -256,7 +256,7 @@ describe('editor draft projection on the typing path', () => {
})
it('matches the uncached projection byte for byte across draft shapes', () => {
const shapes: Record<string, string>[] = [
const draftCases: Record<string, string>[] = [
{},
{ 'file-a': '' },
{ 'file-a': 'hello' },
@@ -267,10 +267,10 @@ describe('editor draft projection on the typing path', () => {
{ 'file-a': 'hello', 'file-b': 'world', 'file-c': 'third' },
{ 'file-a': 'HELLO', 'file-c': 'third' }
]
for (const [index, shape] of shapes.entries()) {
expect({ index, projection: buildRuntimeMobileEditorDraftsProjection(shape) }).toEqual({
for (const [index, draft] of draftCases.entries()) {
expect({ index, projection: buildRuntimeMobileEditorDraftsProjection(draft) }).toEqual({
index,
projection: referenceEditorDraftsProjection(shape)
projection: referenceEditorDraftsProjection(draft)
})
}
})
@@ -402,7 +402,7 @@ describe('open-files and browser projections', () => {
})
it('matches the uncached projections byte for byte across shapes', () => {
const openFileShapes: AppState['openFiles'][] = [
const openFileCases: AppState['openFiles'][] = [
[] as unknown as AppState['openFiles'],
[makeOpenFile(0)] as unknown as AppState['openFiles'],
[makeOpenFile(0, { isDirty: true })] as unknown as AppState['openFiles'],
@@ -412,14 +412,14 @@ describe('open-files and browser projections', () => {
makeOpenFile(2, { isUntitled: true, deleteUntouchedOnClose: true, language: undefined })
] as unknown as AppState['openFiles']
]
for (const [index, shape] of openFileShapes.entries()) {
expect({ index, projection: buildRuntimeMobileOpenFilesProjection(shape) }).toEqual({
for (const [index, openFiles] of openFileCases.entries()) {
expect({ index, projection: buildRuntimeMobileOpenFilesProjection(openFiles) }).toEqual({
index,
projection: referenceOpenFilesProjection(shape)
projection: referenceOpenFilesProjection(openFiles)
})
}
const browserShapes: AppState[] = [
const browserCases: AppState[] = [
makeState({}),
makeState({ browserTabsByWorktree: { 'wt-1': [makeBrowserWorkspace(0)] } as never }),
makeState({
@@ -437,10 +437,10 @@ describe('open-files and browser projections', () => {
browserPagesByWorkspace: { 'ws-9': [makeBrowserPage(9, { url: 'a"b\\c' })] } as never
})
]
for (const [index, shape] of browserShapes.entries()) {
expect({ index, projection: buildRuntimeMobileBrowserProjection(shape) }).toEqual({
for (const [index, state] of browserCases.entries()) {
expect({ index, projection: buildRuntimeMobileBrowserProjection(state) }).toEqual({
index,
projection: referenceBrowserProjection(shape)
projection: referenceBrowserProjection(state)
})
}
})
@@ -30,13 +30,17 @@ type UsageSnapshot = {
recentSessions: object[]
}
type UsageShape<Scope extends string, Range extends string, Snapshot extends UsageSnapshot> = {
type UsageProviderTypes<
Scope extends string,
Range extends string,
Snapshot extends UsageSnapshot
> = {
scope: Scope
range: Range
snapshot: Snapshot
}
type UsageData<T extends UsageShape<string, string, UsageSnapshot>> = {
type UsageData<T extends UsageProviderTypes<string, string, UsageSnapshot>> = {
scope: T['scope']
range: T['range']
scanState: T['snapshot']['scanState'] | null
@@ -47,7 +51,7 @@ type UsageData<T extends UsageShape<string, string, UsageSnapshot>> = {
recentSessions: T['snapshot']['recentSessions']
}
type UsageApi<T extends UsageShape<string, string, UsageSnapshot>> = {
type UsageApi<T extends UsageProviderTypes<string, string, UsageSnapshot>> = {
getScanState: () => Promise<T['snapshot']['scanState']>
setEnabled: (args: { enabled: boolean }) => Promise<T['snapshot']['scanState']>
refresh: (args?: { force?: boolean }) => Promise<T['snapshot']['scanState']>
@@ -61,7 +65,7 @@ type UsageApi<T extends UsageShape<string, string, UsageSnapshot>> = {
type ProviderUsageSlice<
Prefix extends string,
Name extends string,
T extends UsageShape<string, string, UsageSnapshot>
T extends UsageProviderTypes<string, string, UsageSnapshot>
> = {
[K in keyof UsageData<T> as `${Prefix}Usage${Capitalize<K & string>}`]: UsageData<T>[K]
} & Record<`set${Name}UsageEnabled`, (enabled: boolean) => Promise<void>> &
@@ -74,7 +78,7 @@ type ProviderUsageSlice<
type UsageProviderConfig<
Prefix extends string,
Name extends string,
T extends UsageShape<string, string, UsageSnapshot>
T extends UsageProviderTypes<string, string, UsageSnapshot>
> = {
prefix: Prefix
name: Name
@@ -93,13 +97,13 @@ const usageDataFields = [
'modelBreakdown',
'projectBreakdown',
'recentSessions'
] as const satisfies readonly (keyof UsageData<UsageShape<string, string, UsageSnapshot>>)[]
] as const satisfies readonly (keyof UsageData<UsageProviderTypes<string, string, UsageSnapshot>>)[]
function usageDataKey(prefix: string, field: string): string {
return `${prefix}Usage${field[0].toUpperCase()}${field.slice(1)}`
}
function readUsageData<T extends UsageShape<string, string, UsageSnapshot>>(
function readUsageData<T extends UsageProviderTypes<string, string, UsageSnapshot>>(
state: AppState,
prefix: string
): UsageData<T> {
@@ -109,7 +113,7 @@ function readUsageData<T extends UsageShape<string, string, UsageSnapshot>>(
) as UsageData<T>
}
function createUsagePatch<T extends UsageShape<string, string, UsageSnapshot>>(
function createUsagePatch<T extends UsageProviderTypes<string, string, UsageSnapshot>>(
prefix: string,
patch: Partial<UsageData<T>>
): Partial<AppState> {
@@ -123,7 +127,7 @@ function createUsagePatch<T extends UsageShape<string, string, UsageSnapshot>>(
function createUsageProviderSlice<
Prefix extends string,
Name extends string,
T extends UsageShape<string, string, UsageSnapshot>
T extends UsageProviderTypes<string, string, UsageSnapshot>
>(
config: UsageProviderConfig<Prefix, Name, T>
): StateCreator<AppState, [], [], ProviderUsageSlice<Prefix, Name, T>> {
@@ -255,18 +259,22 @@ function createUsageProviderSlice<
}
}
type ClaudeUsageShape = UsageShape<ClaudeUsageScope, ClaudeUsageRange, ClaudeUsageSnapshot>
type CodexUsageShape = UsageShape<CodexUsageScope, CodexUsageRange, CodexUsageSnapshot>
type OpenCodeUsageShape = UsageShape<OpenCodeUsageScope, OpenCodeUsageRange, OpenCodeUsageSnapshot>
type ClaudeUsageTypes = UsageProviderTypes<ClaudeUsageScope, ClaudeUsageRange, ClaudeUsageSnapshot>
type CodexUsageTypes = UsageProviderTypes<CodexUsageScope, CodexUsageRange, CodexUsageSnapshot>
type OpenCodeUsageTypes = UsageProviderTypes<
OpenCodeUsageScope,
OpenCodeUsageRange,
OpenCodeUsageSnapshot
>
export type ClaudeUsageSlice = ProviderUsageSlice<'claude', 'Claude', ClaudeUsageShape>
export type CodexUsageSlice = ProviderUsageSlice<'codex', 'Codex', CodexUsageShape>
export type OpenCodeUsageSlice = ProviderUsageSlice<'openCode', 'OpenCode', OpenCodeUsageShape>
export type ClaudeUsageSlice = ProviderUsageSlice<'claude', 'Claude', ClaudeUsageTypes>
export type CodexUsageSlice = ProviderUsageSlice<'codex', 'Codex', CodexUsageTypes>
export type OpenCodeUsageSlice = ProviderUsageSlice<'openCode', 'OpenCode', OpenCodeUsageTypes>
export const createClaudeUsageSlice = createUsageProviderSlice<
'claude',
'Claude',
ClaudeUsageShape
ClaudeUsageTypes
>({
prefix: 'claude',
name: 'Claude',
@@ -276,7 +284,7 @@ export const createClaudeUsageSlice = createUsageProviderSlice<
hasCachedData: (state) => state.hasAnyClaudeData
})
export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex', CodexUsageShape>({
export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex', CodexUsageTypes>({
prefix: 'codex',
name: 'Codex',
initialScope: 'orca',
@@ -288,7 +296,7 @@ export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex',
export const createOpenCodeUsageSlice = createUsageProviderSlice<
'openCode',
'OpenCode',
OpenCodeUsageShape
OpenCodeUsageTypes
>({
prefix: 'openCode',
name: 'OpenCode',
+2 -2
View File
@@ -1,4 +1,4 @@
import { isSkillsCliAgentKeyShaped } from './skills-cli-agent-keys'
import { isUsableSkillsCliAgentKey } from './skills-cli-agent-keys'
export const ORCA_SKILLS_REPOSITORY_URL = 'https://github.com/stablyai/orca'
@@ -35,7 +35,7 @@ export function buildAgentFeatureSkillInstallArgs(
}
// Why: a value the skills CLI would drop leaves it with no target at all, which
// is the same all-agents install as passing no --agent.
const unusable = agents.find((agent) => !isSkillsCliAgentKeyShaped(agent))
const unusable = agents.find((agent) => !isUsableSkillsCliAgentKey(agent))
if (unusable !== undefined) {
throw new Error(`"${unusable}" is not a usable install target.`)
}
@@ -17,7 +17,7 @@ const SHELLS: { platform: NodeJS.Platform; shell: AgentStartupShell }[] = [
/** Independent selector oracle deliberately NOT the implementation's own
* predicate, so a regression that shrinks the stripped set cannot also blind
* this assertion. */
function isSelectorShapedToken(token: string): boolean {
function isResumeSelectorToken(token: string): boolean {
return (
['--resume', '--continue', '-r', '-c'].includes(token) ||
['--resume=', '--continue=', '-r=', '-c='].some((prefix) => token.startsWith(prefix))
@@ -31,7 +31,7 @@ function expectSingleAuthoritativeResume(command: string, shell: AgentStartupShe
if (!tokenized.ok) {
return
}
const selectors = tokenized.tokens.filter(isSelectorShapedToken)
const selectors = tokenized.tokens.filter(isResumeSelectorToken)
expect(selectors).toEqual(['--resume'])
const index = tokenized.tokens.indexOf('--resume')
expect(tokenized.tokens[index + 1]).toBe(SESSION_ID)
+1 -1
View File
@@ -242,7 +242,7 @@ export function isAdmissibleAgentJournalSubmission(
* never reject a row a writer in this build produced. The schemas are
* deliberately wider on open string fields, so only this direction holds. */
type Admits<T extends true> = T
export type CanonicalJournalShapesAreAdmissible = [
export type CanonicalJournalTypesAreAdmissible = [
Admits<AgentJournalItemBody extends z.input<typeof AgentJournalItemBodySchema> ? true : false>,
Admits<AgentJournalMessageItem extends z.input<typeof MessageBody> ? true : false>,
Admits<
+2 -2
View File
@@ -337,7 +337,7 @@ export function isAgentSessionRecord(value: unknown): value is AgentSessionRecor
return false
}
const record = value as Partial<AgentSessionRecord>
const shapeValid =
const fieldsValid =
record.schemaVersion === AGENT_SESSION_RECORD_SCHEMA_VERSION &&
isAgentSessionId(record.sessionId) &&
isAgentSessionExecutionLocation(record.location) &&
@@ -356,7 +356,7 @@ export function isAgentSessionRecord(value: unknown): value is AgentSessionRecor
record.lease.sessionId === record.sessionId &&
Number.isSafeInteger(record.createdAt) &&
Number.isSafeInteger(record.updatedAt)
if (!shapeValid) {
if (!fieldsValid) {
return false
}
const validated = record as AgentSessionRecord
@@ -1,19 +1,19 @@
import { describe, expect, it } from 'vitest'
import { assertGitPushTargetShape } from './git-push-target-validation'
import { assertValidGitPushTarget } from './git-push-target-validation'
describe('assertGitPushTargetShape', () => {
describe('assertValidGitPushTarget', () => {
it('accepts slash-separated git remote names', () => {
expect(() =>
assertGitPushTargetShape({ remoteName: 'foo/bar', branchName: 'feature/fix' })
assertValidGitPushTarget({ remoteName: 'foo/bar', branchName: 'feature/fix' })
).not.toThrow()
})
it('rejects remote names with empty or parent segments', () => {
expect(() =>
assertGitPushTargetShape({ remoteName: 'foo//bar', branchName: 'feature/fix' })
assertValidGitPushTarget({ remoteName: 'foo//bar', branchName: 'feature/fix' })
).toThrow('Invalid git remote name')
expect(() =>
assertGitPushTargetShape({ remoteName: 'foo/../bar', branchName: 'feature/fix' })
assertValidGitPushTarget({ remoteName: 'foo/../bar', branchName: 'feature/fix' })
).toThrow('Invalid git remote name')
})
})
+1 -1
View File
@@ -32,7 +32,7 @@ export function isSafePushTargetRemoteUrl(remoteUrl: string): boolean {
return GITHUB_CLONE_URL.test(remoteUrl) || GITHUB_SSH_URL.test(remoteUrl)
}
export function assertGitPushTargetShape(target: unknown): asserts target is GitPushTarget {
export function assertValidGitPushTarget(target: unknown): asserts target is GitPushTarget {
if (typeof target !== 'object' || target === null) {
throw new Error('Invalid PR push target.')
}
+3 -3
View File
@@ -19,7 +19,7 @@ export function registerQuestionTool(toolName: string, parser: InteractiveQuesti
QUESTION_TOOL_PARSERS.set(toolName, parser)
}
function parseQuestionsShape(input: unknown): AskPrompt | null {
function parseCanonicalQuestionsInput(input: unknown): AskPrompt | null {
if (!input || typeof input !== 'object') {
return null
}
@@ -73,12 +73,12 @@ function parseOptions(raw: unknown): AskOption[] {
}
for (const name of ['AskUserQuestion', 'ask_user_question', 'askUserQuestion']) {
QUESTION_TOOL_PARSERS.set(name, parseQuestionsShape)
QUESTION_TOOL_PARSERS.set(name, parseCanonicalQuestionsInput)
}
function parseToolInput(toolName: string | undefined, input: unknown): AskPrompt | null {
const parser = toolName ? QUESTION_TOOL_PARSERS.get(toolName) : undefined
return (parser ? parser(input) : null) ?? parseQuestionsShape(input)
return (parser ? parser(input) : null) ?? parseCanonicalQuestionsInput(input)
}
export function parseAskFromStatus(
+2
View File
@@ -9,6 +9,8 @@ export type OnboardingChecklistState = {
ranFirstAgent: boolean
ranSecondAgentOnSameTask: boolean
triedCmdJ: boolean
// Persisted field, also a telemetry enum member in ./telemetry-onboarding-foundation-schemas;
// renaming it would orphan saved state. Rule exemption: config/oxlint-anti-slop.json.
shapedSidebar: boolean
reviewedDiff: boolean
openedPr: boolean
@@ -52,8 +52,8 @@ describe('resolvePaneAgentIdentity', () => {
})
describe('run generation separates the bug from the legitimate reclaim', () => {
// Both shapes are `completed hook = A, title = B`. Ordering alone cannot tell them apart.
const shape = (hookRun: number, titleRun: number): PaneAgentEvidence[] => [
// Both cases are `completed hook = A, title = B`. Ordering alone cannot tell them apart.
const evidenceFor = (hookRun: number, titleRun: number): PaneAgentEvidence[] => [
{ source: 'completed-hook', agent: 'claude', run: { authorityId: H, incarnation: hookRun } },
{ source: 'title', agent: 'codex', run: { authorityId: H, incarnation: titleRun } }
]
@@ -61,7 +61,7 @@ describe('resolvePaneAgentIdentity', () => {
it('keeps the completed hook when both belong to the current run', () => {
// The reported bug: nothing new started, so the hook is still the truth.
const result = resolvePaneAgentIdentity({
evidence: shape(7, 7),
evidence: evidenceFor(7, 7),
currentRun: { authorityId: H, incarnation: 7 }
})
expect(result).toMatchObject({ agent: 'claude', source: 'completed-hook' })
@@ -72,7 +72,7 @@ describe('resolvePaneAgentIdentity', () => {
// The legitimate reclaim: the pane was reused, so run 7's hook describes an agent that is
// no longer there. It is ineligible, not merely outranked.
const result = resolvePaneAgentIdentity({
evidence: shape(7, 8),
evidence: evidenceFor(7, 8),
currentRun: { authorityId: H, incarnation: 8 }
})
expect(result).toMatchObject({ agent: 'codex', source: 'title' })
@@ -82,11 +82,11 @@ describe('resolvePaneAgentIdentity', () => {
it('produces opposite answers from identical evidence, given only the run ids', () => {
// The whole point, stated as one assertion.
const bug = resolvePaneAgentIdentity({
evidence: shape(7, 7),
evidence: evidenceFor(7, 7),
currentRun: { authorityId: H, incarnation: 7 }
})
const reclaim = resolvePaneAgentIdentity({
evidence: shape(7, 8),
evidence: evidenceFor(7, 8),
currentRun: { authorityId: H, incarnation: 8 }
})
expect(bug.agent).not.toBe(reclaim.agent)
@@ -5,7 +5,7 @@ import {
PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH,
PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES,
validatePluginLanguagePackCatalog,
validatePluginLanguagePackCatalogShape,
checkPluginLanguagePackCatalog,
pluginLanguageResourceId
} from './plugin-language-pack-artifact'
@@ -205,7 +205,7 @@ describe('plugin language-pack artifacts', () => {
}
})
expect(validatePluginLanguagePackCatalogShape(catalog)).toEqual({
expect(checkPluginLanguagePackCatalog(catalog)).toEqual({
ok: true,
entries: PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES
})
@@ -37,7 +37,7 @@ export function isPluginLanguagePackRegistration(
pack.resourceLanguage === pluginLanguageResourceId(pack.id as `plugin:${string}`) &&
typeof pack.pluginKey === 'string' &&
typeof pack.locale === 'string' &&
validatePluginLanguagePackCatalogShape(pack.catalog).ok
checkPluginLanguagePackCatalog(pack.catalog).ok
)
}
@@ -119,7 +119,7 @@ export function validatePluginLanguagePackCatalog(source: unknown): PluginLangua
return { ok: true, catalog: result.catalog!, entries: result.entries }
}
export function validatePluginLanguagePackCatalogShape(
export function checkPluginLanguagePackCatalog(
source: unknown
): PluginLanguagePackValidationResult {
const result = walkPluginLanguagePackCatalog(source, false)
+2 -2
View File
@@ -31,7 +31,7 @@ function isNonNegativeSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0
}
function hasValidRuntimeStatusShape(status: Record<string, unknown>): boolean {
function hasValidRuntimeStatus(status: Record<string, unknown>): boolean {
return (
typeof status.runtimeId === 'string' &&
status.runtimeId.length > 0 &&
@@ -103,7 +103,7 @@ export function verifyRemotePairingRuntimeStatus(
: 'Update Orca on the remote host before adding it.'
}
}
if (!hasValidRuntimeStatusShape(status)) {
if (!hasValidRuntimeStatus(status)) {
return {
ok: false,
kind: 'connection-interrupted',
@@ -34,12 +34,14 @@ export const RepoUpstream = z
.nullable()
.optional()
// The return type is inferred on purpose: an explicit z.ZodObject<...z.ZodRawShape>
// annotation widened `updates` to an open record, which erased all 24 named fields
// from RpcParams<'repo.update'> for every typed caller.
export function createRepoUpdateSchema<T extends z.ZodRawShape>(selectorShape: T) {
// The return type is inferred on purpose: an explicit z.ZodObject<...> annotation
// widened `updates` to an open record, which erased all 24 named fields from
// RpcParams<'repo.update'> for every typed caller.
export function createRepoUpdateSchema<T extends Readonly<Record<string, z.ZodType>>>(
selectorFields: T
) {
return z.object({
...selectorShape,
...selectorFields,
updates: z.object({
displayName: OptionalString,
badgeColor: RepoBadgeColor,
+9 -9
View File
@@ -21,15 +21,15 @@ type Prettify<T> = { [K in keyof T]: T[K] } & {}
/** zod's own input-side key-optionality rule, copied from $InferObjectInput. */
type SendOptionalSchema = { _zod: { optin: 'optional' | 'defaulted' } }
type SendShape<Shape> = Prettify<
type SendFields<Fields> = Prettify<
{
-readonly [K in keyof Shape as Shape[K] extends SendOptionalSchema ? never : K]: RpcSendInput<
Shape[K]
-readonly [K in keyof Fields as Fields[K] extends SendOptionalSchema ? never : K]: RpcSendInput<
Fields[K]
>
} & {
-readonly [K in keyof Shape as Shape[K] extends SendOptionalSchema ? K : never]?: RpcSendInput<
Shape[K]
>
-readonly [
K in keyof Fields as Fields[K] extends SendOptionalSchema ? K : never
]?: RpcSendInput<Fields[K]>
}
>
@@ -50,11 +50,11 @@ export type RpcSendInput<Schema> =
? RpcSendInput<Element>[]
: // ZodObject is the only schema carrying a `shape`, and matching on it keeps
// .strict()/.extend()/.superRefine() results in this branch.
Schema extends { shape: infer Shape }
? keyof Shape extends never
Schema extends { shape: infer Fields }
? keyof Fields extends never
? // Mirrors $InferObjectOutput: a no-field object admits no properties.
Record<string, never>
: SendShape<Shape>
: SendFields<Fields>
: // ZodDiscriminatedUnion extends ZodUnion, so both land here.
Schema extends z.ZodUnion<infer Options>
? RpcSendInput<Options[number]>
@@ -7,13 +7,15 @@ import type { z } from 'zod'
* dropped from the payload and the rest of the batch still lands. Unknown KEYS
* stay a hard rejection the parity assertions exist to catch those.
*/
export function tolerateUnknownValues<TShape extends z.ZodRawShape>(shape: TShape): TShape {
return Object.fromEntries(
Object.entries(shape).map(([key, schema]) => [
key,
(schema as z.ZodType).catch(() => undefined)
])
) as unknown as TShape
export function tolerateUnknownValues<TFields extends Readonly<Record<string, z.ZodType>>>(
fields: TFields
): TFields {
const tolerant: Record<string, z.ZodType> = {}
for (const [key, schema] of Object.entries(fields)) {
tolerant[key] = schema.catch(() => undefined)
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the loop copies every key of `fields` and only wraps its schema in `.catch()`, so the result carries exactly `TFields`' keys; Object.entries erases that key identity.
return tolerant as TFields
}
/** Drops the `undefined` entries `tolerateUnknownValues` leaves behind, so a
+3 -3
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import { TUI_AGENT_CONFIG } from './tui-agent-config'
import {
SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT,
isSkillsCliAgentKeyShaped,
isUsableSkillsCliAgentKey,
SKILLS_CLI_UNIVERSAL_AGENT_KEY,
toSkillsCliAgentKeys
} from './skills-cli-agent-keys'
@@ -117,10 +117,10 @@ describe('skills CLI agent keys', () => {
it('rejects values the skills CLI would drop, and allows the explicit wildcard', () => {
for (const bad of ['-y', '--copy', '', ' ', 'a b', 'a,b']) {
expect(isSkillsCliAgentKeyShaped(bad), bad).toBe(false)
expect(isUsableSkillsCliAgentKey(bad), bad).toBe(false)
}
for (const good of ['claude-code', 'universal', 'trae-cn', 'inference-sh', '*']) {
expect(isSkillsCliAgentKeyShaped(good), good).toBe(true)
expect(isUsableSkillsCliAgentKey(good), good).toBe(true)
}
})
+1 -1
View File
@@ -68,7 +68,7 @@ export const SKILLS_CLI_UNIVERSAL_AGENT_KEY = 'universal'
* emptiness. An unknown-but-plausible key is left to the CLI, which rejects it
* loudly with its own valid list before writing anything.
*/
export function isSkillsCliAgentKeyShaped(value: string): boolean {
export function isUsableSkillsCliAgentKey(value: string): boolean {
return /^(?:\*|[a-z0-9][a-z0-9.-]*)$/i.test(value)
}
+9 -9
View File
@@ -8,32 +8,32 @@ export type EventName = keyof EventMap
export type EventProps<N extends EventName> = EventMap[N]
// Why: non-`ZodObject` schemas have no `.shape`; return null so `key in undefined` can't throw at module load.
function eventSchemaShape(schema: z.ZodTypeAny): z.ZodRawShape | null {
// Why `object` and not zod's own field-record type: callers only ask `key in fields`.
function eventSchemaFields(schema: z.ZodTypeAny): object | null {
if (schema instanceof z.ZodObject) {
return schema.shape
}
const shapeBearingSchema = schema as { shape?: unknown }
// Why: refined object schemas may expose `.shape` even when refinement breaks `instanceof ZodObject`.
if (shapeBearingSchema.shape && typeof shapeBearingSchema.shape === 'object') {
return shapeBearingSchema.shape as z.ZodRawShape
if ('shape' in schema && typeof schema.shape === 'object' && schema.shape !== null) {
return schema.shape
}
return null
}
function eventsWithShapeKey(key: string): ReadonlySet<EventName> {
function eventsDeclaringKey(key: string): ReadonlySet<EventName> {
return new Set(
(Object.entries(eventSchemas) as [EventName, z.ZodTypeAny][])
.filter(([, schema]) => {
const shape = eventSchemaShape(schema)
return shape !== null && key in shape
const fields = eventSchemaFields(schema)
return fields !== null && key in fields
})
.map(([name]) => name)
)
}
// Cohort injection is gated on this derived set because `.strict()` schemas drop events that don't declare `nth_repo_added`.
const COHORT_EXTENDED_SET = eventsWithShapeKey('nth_repo_added')
const COHORT_EXTENDED_SET = eventsDeclaringKey('nth_repo_added')
// Compile-time roster guarding the runtime injection set against silent schema drift.
type _CohortExtendedRoster =
@@ -78,7 +78,7 @@ export function isCohortExtendedEvent(name: EventName): boolean {
}
// Events whose schema declares `cohort`: the IPC handler injects cohort only for these — a `.strict()` schema without it would reject the event.
const ONBOARDING_COHORT_SET = eventsWithShapeKey('cohort')
const ONBOARDING_COHORT_SET = eventsDeclaringKey('cohort')
// `NonNullable` strips `undefined` introduced by `cohortSchema`'s `.optional()`.
export type OnboardingCohort = NonNullable<z.infer<typeof cohortSchema>>
+5 -5
View File
@@ -20,7 +20,7 @@ const CONTAINERS: [string, () => z.ZodType, unknown][] = [
['salvagingArray', () => salvagingArray(z.string()), ['v']]
]
describe('salvaging containers used bare in an object shape', () => {
describe('salvaging containers used bare in an object schema', () => {
it.each(CONTAINERS)('%s is neither optional-in nor optional-out', (_name, build) => {
const { optin, optout } = optionalityOf(build())
expect(optin).toBeUndefined()
@@ -28,12 +28,12 @@ describe('salvaging containers used bare in an object shape', () => {
})
it.each(CONTAINERS)('%s rejects an absent key and an explicit undefined', (_name, build, ok) => {
const shape = z.object({ a: build() })
const schema = z.object({ a: build() })
expect(shape.safeParse({}).success).toBe(false)
expect(shape.safeParse({ a: undefined }).success).toBe(false)
expect(schema.safeParse({}).success).toBe(false)
expect(schema.safeParse({ a: undefined }).success).toBe(false)
// Why: a positive control, so the two rejections above cannot pass by rejecting everything.
expect(shape.safeParse({ a: ok })).toMatchObject({ success: true })
expect(schema.safeParse({ a: ok })).toMatchObject({ success: true })
})
it.each(CONTAINERS)(
@@ -48,7 +48,7 @@ const HOST_TERMINAL_SURFACE_SEPARATOR = '::'
/** Daemon session id form. Deliberately excluded from id-shape classification,
* which is why a host-created tab needs its own binding to be preserved
* a `serve-`/`ssh-` shaped id would take an already-correct path instead. */
function isDaemonShapedPtyId(ptyId: string, worktreeId: string): boolean {
function isDaemonPtyIdForm(ptyId: string, worktreeId: string): boolean {
return (
ptyId.startsWith(`${worktreeId}@@`) &&
!ptyId.startsWith('serve-') &&
@@ -156,7 +156,7 @@ export async function createHostCliTerminal(
throw new Error('Host did not report a leaf id for the CLI-created terminal')
}
expect(
isDaemonShapedPtyId(ptyId, worktreeId),
isDaemonPtyIdForm(ptyId, worktreeId),
`CLI terminal ${ptyId} must carry the daemon id shape this seam excludes from classification`
).toBe(true)
await expect
@@ -73,7 +73,7 @@ const SUBSTITUTION_GROUPS = [
* The two ways a substituted keystroke can reach the renderer. Both are real; only the second one
* regressed, and only the second one can regress, which is why running both is the point.
*/
const SUBSTITUTION_SHAPES: readonly {
const SUBSTITUTION_ROUTES: readonly {
name: string
slug: string
dispatch: (session: CDPSession, keystroke: SubstitutedKeystroke) => Promise<void>
@@ -165,9 +165,9 @@ test.describe('Terminal CJK IME committed text', () => {
}
})
for (const shape of SUBSTITUTION_SHAPES) {
for (const route of SUBSTITUTION_ROUTES) {
for (const group of SUBSTITUTION_GROUPS) {
test(`sends full-width ${group.label} and never their ASCII form when ${shape.name}`, async ({
test(`sends full-width ${group.label} and never their ASCII form when ${route.name}`, async ({
orcaPage,
testRepoPath
}, testInfo) => {
@@ -179,7 +179,7 @@ test.describe('Terminal CJK IME committed text', () => {
try {
await startTerminalImeByteReader(orcaPage, arena.ptyId, reader)
for (const keystroke of group.keystrokes) {
await shape.dispatch(arena.session, keystroke)
await route.dispatch(arena.session, keystroke)
await orcaPage.waitForTimeout(60)
}
await dispatchPlainEnter(arena.session)
@@ -200,7 +200,7 @@ test.describe('Terminal CJK IME committed text', () => {
await closeTerminalImePaneArena(
arena,
testInfo,
`full-width-${group.label}-${shape.slug}`,
`full-width-${group.label}-${route.slug}`,
!completed
)
removeTerminalImeByteReader(reader)
@@ -69,7 +69,7 @@ export function parseArgs(argv) {
function validate(opts, exePathFlagPresent, argv) {
const errors = []
errors.push(...validateArgShape(argv))
errors.push(...validateArgSyntax(argv))
if (!opts.expect) {
errors.push('Missing --expect <survival|orphaned>')
} else if (!VALID_PROFILES.has(opts.expect)) {
@@ -94,7 +94,7 @@ function validate(opts, exePathFlagPresent, argv) {
return errors
}
function validateArgShape(argv) {
function validateArgSyntax(argv) {
const errors = []
const seen = new Set()
for (let index = 0; index < argv.length; index++) {