Files
orca/config/scripts/check-workspace-layout-rules.mjs
T
Jinwoo Hong 9021133929 Add the inert workspace layout model, Loader and Serializer (#26748)
* feat(layout): add the inert workspace layout model, Loader and Serializer

One model of a workspace's tab layout (all tab kinds, groups, split trees,
pane bindings, titles/colors/pin, sleeping records) in src/shared/workspace-layout,
with a Loader from today's stored session partition and a Serializer back to it.
The structural rules check and the pure pane/group tree helpers move into the
same folder so the module can run them; their callers import the new paths.
Nothing in the app calls the new module yet.

* fix(layout): avoid toSorted in shared code and drop an unused helper

The mobile bundle includes src/shared and Hermes lacks Array#toSorted.

* fix(layout): one stored home per layout fact; Loader reports every rule it applies

The row's terminal id, preview flag, worktree id and execution host are now
derived on save instead of kept a second time. Groups never stay empty. The
Loader reports row/tab-bar disagreements, re-minted ids, worktree id and host
disagreements, keeps a row stored in two workspaces where the tab bar names it,
and gives a pane id repeated in a second tab a new unbound pane. The rules check
runs on what the Serializer writes. Fixtures become test fixtures, and a test
lists every model field with the one fact it holds.

* fix(layout): derive each tab's host from its owner and keep one preview fact per editor tab

An editor tab names its file's owner (an SSH target or paired runtime), every
other tab its partition. Where the stored tab and file preview flags disagree
the tab loads permanent, so a draft can never be replaced. A row terminal that
differs from the one derived from its focused pane is now reported.

* fix(layout): one binding kind, one title, one host rule; records follow a reassigned pane

A legacy row with no pane layout now loads as a one-pane tab bound to its
terminal (reusing the leaf its records name), so the null-panes branches and
legacyPtyId are gone. A pane given a new id because two tabs shared it takes
its sleeping and incarnation records, scrollback and focus with it. A terminal's
live title is one fact (the tab-bar label wins on load). Every tab-bar entry
names its execution host by one rule. The fact-location test is now keyed by
on-disk field and checked by mutating every source.

* fix(layout): one change report by construction; records without copies

The Loader now picks values by fixed precedence only and reports one
field-by-field diff of the stored session against what the next save
writes, over the disk-field tables (now in workspace-layout-disk-fields.ts,
with session fields derived from SESSION_FIELD_OWNERS). Sleeping and
closed-tab records live in their workspace without the pane key, tab and
worktree id their key and workspace already give; the Serializer fills them.
Tab-bar entries are written in the one tab order.

* fix(layout): name the change report's key-list parameter's type

* refactor(layout): key pane data by leaf, check the model itself, one legacy record

- Each pane's data (terminal, incarnation, title, sleeping agent) is one leaf record keyed by
  leaf id, and scrollback beside it is keyed the same way. Pane keys are derived only when
  saving. The Loader gives a repeated leaf id a new id before attaching records, so
  rekeyPaneRecords is gone; records naming no pane are carried as stored.
- The rules check runs on the model directly (ungrouped tab, empty group, group tree that
  disagrees with the groups, pane data without a pane); the disk-format rules stay separate.
- The row-ownership marker and topology revision move into one legacy record outside
  WorkspaceLayout, updated by updateLegacyPersistence after each apply.
- Fixtures include a terminal tab whose tab-bar id and terminal id differ.

* fix(layout): join the group ids the tree-mismatch detail names

* fix(layout): leave the stored revision map as is when no repo advances
2026-10-09 15:05:22 -04:00

53 lines
2.0 KiB
JavaScript

#!/usr/bin/env node
// Checks a profile's saved workspace layout against the structural layout rules
// (src/shared/workspace-layout/workspace-layout-rules.ts). Works on a running dev app's
// profile too. Usage: node config/scripts/check-workspace-layout-rules.mjs <userDataDir>
// Exits 1 when any rule is broken, printing one line per breach.
import { build } from 'esbuild'
import { mkdir, mkdtemp, rm } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { join, resolve } from 'node:path'
const userDataDir = process.argv[2]
if (!userDataDir) {
console.error('usage: check-workspace-layout-rules.mjs <userDataDir>')
process.exit(2)
}
const root = resolve(import.meta.dirname, '..', '..')
await mkdir(join(root, '.tmp'), { recursive: true })
const scratch = await mkdtemp(join(root, '.tmp', 'layout-rules-'))
try {
const outfile = join(scratch, 'check.cjs')
await build({
stdin: {
contents: `
export { readPersistedProfileState } from './tests/e2e/helpers/persisted-profile-state'
export { partitionsFromProfileRoot } from './tests/e2e/helpers/workspace-layout-oracle-views'
export { checkWorkspaceLayoutRules } from './src/shared/workspace-layout/workspace-layout-rules'
`,
resolveDir: root,
loader: 'ts'
},
bundle: true,
platform: 'node',
format: 'cjs',
outfile,
external: ['electron', '@stablyai/playwright-test'],
logLevel: 'silent'
})
const { readPersistedProfileState, partitionsFromProfileRoot, checkWorkspaceLayoutRules } =
createRequire(import.meta.url)(outfile)
const violations = checkWorkspaceLayoutRules(
partitionsFromProfileRoot(readPersistedProfileState(resolve(userDataDir)))
)
for (const violation of violations) {
console.log(`${violation.rule}: ${violation.detail}`)
}
console.log(`${violations.length} layout rule breach(es) in ${userDataDir}`)
process.exitCode = violations.length > 0 ? 1 : 0
} finally {
await rm(scratch, { recursive: true, force: true })
}