mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
chore(ratchet): layout-writer ratchets for main and renderer (#27431)
* chore(ratchet): layout-writer ratchets for main and renderer Two per-file ratchets that may only go down: main-process callers of the Store's layout setters plus direct edits of saved session partitions, and renderer writes of the window's layout and per-view selection fields (including computed-key and spread writes, counted where the key or object is built). The frozen-mirror writers are exempt. Wired next to the focus-read ratchet in lint and the PR workflow. * chore(ratchet): count only membership writes of editor and browser entries A .map over the existing openFiles or browser tab list keeps its entries and order, so it is a content update (draft, git status, URL) and is no longer counted by the renderer layout-writer ratchet. * chore(ratchet): ignore variable keys and pass-through shorthands A variable key such as exitCodes[ptyId] names no field, so only string keys count. A shorthand key counts only in an object handed to set, setState or a Store writer; elsewhere it passes a value on. * chore(ratchet): prune renderer baseline after main moved
This commit is contained in:
@@ -336,6 +336,10 @@ jobs:
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: pnpm run check:focus-read-ratchet
|
||||
|
||||
- name: Enforce layout-writer ratchets
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: pnpm run check:layout-writer-ratchets
|
||||
|
||||
- name: Check Node runtime pin
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: pnpm run check:node-runtime-pin
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Main-process writers of saved workspace layout, per file: calls of the Store layout
|
||||
# setters, plus assignments, deletes and object keys that edit session partitions directly.
|
||||
# This is a RATCHET: counts may only go DOWN. Layout moves to one runtime owner; do not add
|
||||
# writers. Prune after removing writers: pnpm check:layout-writer-ratchets --prune
|
||||
1 src/main/ipc/pty/ipc/leaf-move.ts
|
||||
2 src/main/ipc/pty/ipc/spawn-commit-persist.ts
|
||||
1 src/main/ipc/pty/pane/pane-owner-replacement.ts
|
||||
3 src/main/ipc/pty/pane/stable-owner.ts
|
||||
2 src/main/ipc/pty/runtime/spawn-commit.ts
|
||||
1 src/main/ipc/renderer-shutdown-checkpoint.ts
|
||||
1 src/main/ipc/runtime-environment-removal-cleanup.ts
|
||||
1 src/main/ipc/runtime-environment-session-reconcile.ts
|
||||
3 src/main/ipc/session.ts
|
||||
6 src/main/orca-profiles/profile-project-session-state.ts
|
||||
4 src/main/orca-profiles/profile-project-source-removal.ts
|
||||
2 src/main/orca-profiles/profile-project-state-file.ts
|
||||
4 src/main/orca-profiles/profile-project-transfer-payload.ts
|
||||
2 src/main/orca-profiles/profile-project-transfer.ts
|
||||
9 src/main/orca-profiles/profile-session-owner-transfer.ts
|
||||
3 src/main/persistence/leasing-ssh-ptys/ssh-pty-binding-cleanup.ts
|
||||
1 src/main/persistence/leasing-ssh-ptys/ssh-pty-lease-operations.ts
|
||||
1 src/main/persistence/loading-store/loaded-state-parsing.ts
|
||||
1 src/main/persistence/loading-store/metadata-lineage-operations.ts
|
||||
1 src/main/persistence/loading-store/mobile-tab-selection-persistence.ts
|
||||
1 src/main/persistence/loading-store/normalize-loaded-profile-state.ts
|
||||
10 src/main/persistence/loading-store/pty-binding-persistence.ts
|
||||
10 src/main/persistence/loading-store/pty-binding-session-update.ts
|
||||
11 src/main/persistence/loading-store/repo-lifecycle-operations.ts
|
||||
10 src/main/persistence/loading-store/session-host-partitions.ts
|
||||
9 src/main/persistence/loading-store/session-snapshot-operations.ts
|
||||
3 src/main/persistence/loading-store/state-serialization-secret-handling.ts
|
||||
4 src/main/persistence/loading-store/workspace-session-snapshot-publication.ts
|
||||
4 src/main/persistence/loading-store/workspace-session-terminal-binding-replay.ts
|
||||
2 src/main/persistence/loading-store/workspace-session-write-listeners.ts
|
||||
2 src/main/persistence/migrating-orcad-catalog/orcad-destination-client-state.ts
|
||||
1 src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.ts
|
||||
3 src/main/persistence/migrating-orcad-catalog/orcad-dormant-state-records.ts
|
||||
2 src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.ts
|
||||
1 src/main/persistence/migrating-orcad-catalog/orcad-source-dormant-state.ts
|
||||
1 src/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.ts
|
||||
1 src/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session-eligibility.ts
|
||||
3 src/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session-layout.ts
|
||||
4 src/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session-subtraction.ts
|
||||
1 src/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session.ts
|
||||
1 src/main/persistence/restoring-sessions/folder-workspace-operations.ts
|
||||
5 src/main/persistence/restoring-sessions/session-owner-fields.ts
|
||||
12 src/main/persistence/restoring-sessions/session-owner-removal.ts
|
||||
2 src/main/persistence/restoring-sessions/workspace-pane-normalization.ts
|
||||
4 src/main/persistence/terminal-topology/terminal-topology-membership.ts
|
||||
1 src/main/persistence/tracking-repos/project-group-operations.ts
|
||||
4 src/main/persistence/tracking-repos/repo-worktree-pruning.ts
|
||||
3 src/main/persistence/tracking-repos/worktree-identity-migration.ts
|
||||
1 src/main/runtime/client-hosted-browser-page-persistence.ts
|
||||
1 src/main/runtime/mobile-session-editor-projection.ts
|
||||
3 src/main/runtime/mobile-session-layout-projection.ts
|
||||
2 src/main/runtime/orca-runtime-adopt-terminal-orphans-from-inventory.ts
|
||||
2 src/main/runtime/orca-runtime-apply-mobile-session-tab-navigation.ts
|
||||
2 src/main/runtime/orca-runtime-attach-window.ts
|
||||
2 src/main/runtime/orca-runtime-build-headless-mobile-session-browser-tabs.ts
|
||||
1 src/main/runtime/orca-runtime-get-runtime-id.ts
|
||||
2 src/main/runtime/orca-runtime-move-headless-mobile-session-tab.ts
|
||||
6 src/main/runtime/orca-runtime-persist-headless-session-tab-props.ts
|
||||
2 src/main/runtime/orca-runtime-persist-headless-terminal-title.ts
|
||||
1 src/main/runtime/orca-runtime-persist-terminal-surface-retirements.ts
|
||||
1 src/main/runtime/orca-runtime-pty-foreground-process-reads.ts
|
||||
3 src/main/runtime/orca-runtime-stop-terminals-for-worktree.ts
|
||||
4 src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.ts
|
||||
2 src/main/runtime/runtime-pane-admission.ts
|
||||
7 src/main/runtime/runtime-terminal-orphan-session-adoption.ts
|
||||
1 src/main/runtime/runtime-workspace-session-controller.ts
|
||||
4 src/main/runtime/workspace-session-worktree-id.ts
|
||||
1 src/main/ssh/ssh-target-id-migration.ts
|
||||
2 src/main/terminal-scrollback-snapshots.ts
|
||||
@@ -0,0 +1,125 @@
|
||||
# Renderer writes of the window layout and per-view selection fields, per file: assignments,
|
||||
# deletes, object keys and string keys (computed-key writes). Same-name copies are reads.
|
||||
# This is a RATCHET: counts may only GO DOWN. Layout comes from the runtime; send a command
|
||||
# instead. Prune after removing writes: pnpm check:layout-writer-ratchets --prune
|
||||
3 src/renderer/src/attention/notification-subject-owner.ts
|
||||
1 src/renderer/src/components/activity/use-agent-pane-threads.ts
|
||||
3 src/renderer/src/components/cmd-j/palette-live-status.tsx
|
||||
2 src/renderer/src/components/dashboard/worktree-agent-rows-cache.ts
|
||||
1 src/renderer/src/components/sidebar/use-worktree-activity-status.ts
|
||||
1 src/renderer/src/components/sidebar/use-worktree-activity-statuses.ts
|
||||
4 src/renderer/src/components/sidebar/worktree-card-send-target-inputs.ts
|
||||
3 src/renderer/src/components/sidebar/worktree-list-lineage-store-state.ts
|
||||
3 src/renderer/src/components/sidebar/worktree-list-pinned-store-state.ts
|
||||
4 src/renderer/src/components/sidebar/worktree-list/listing/use-agent-send-target.ts
|
||||
1 src/renderer/src/components/status-bar/workspace-space-decision-inputs.ts
|
||||
2 src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts
|
||||
3 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.ts
|
||||
23 src/renderer/src/components/tab-group/tab-drag-preview-activation.ts
|
||||
1 src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
|
||||
1 src/renderer/src/components/terminal-pane/pty-connection-test-deps.ts
|
||||
4 src/renderer/src/components/terminal-pane/terminal-recovery-ledger-test-store.ts
|
||||
3 src/renderer/src/components/worktree-jump-palette-status-inputs.ts
|
||||
9 src/renderer/src/lib/agent-background-session-test-state.ts
|
||||
1 src/renderer/src/lib/agent-hibernation-coordinator.ts
|
||||
4 src/renderer/src/lib/floating-workspace-terminal-actions.ts
|
||||
1 src/renderer/src/lib/pane-manager/client-hosted-browser-row-state.ts
|
||||
4 src/renderer/src/lib/session-write-subscriber.ts
|
||||
1 src/renderer/src/lib/workspace-session-browser-tabs.ts
|
||||
7 src/renderer/src/lib/workspace-session-host-hydration.ts
|
||||
7 src/renderer/src/lib/workspace-session-hydration-keys.ts
|
||||
25 src/renderer/src/lib/workspace-session-patch.ts
|
||||
3 src/renderer/src/lib/workspace-session-staged-browser-tabs.ts
|
||||
17 src/renderer/src/lib/workspace-session.ts
|
||||
21 src/renderer/src/lib/worktree-activation-created-agent-test-state.ts
|
||||
3 src/renderer/src/runtime/local-structured-session-tabs-sync/snapshot-projection.ts
|
||||
3 src/renderer/src/runtime/structured-agent-session-tab-retirement.ts
|
||||
10 src/renderer/src/runtime/sync-runtime-graph/mobile-session-group-projection.ts
|
||||
2 src/renderer/src/runtime/sync-runtime-graph/mobile-session-snapshots.ts
|
||||
2 src/renderer/src/runtime/sync-runtime-graph/sync-key.ts
|
||||
13 src/renderer/src/runtime/web-runtime-browser-creation-placement-test-rig.ts
|
||||
1 src/renderer/src/runtime/web-runtime-session-snapshot.ts
|
||||
3 src/renderer/src/runtime/web-session-client-owned-tab-placement.ts
|
||||
1 src/renderer/src/runtime/web-session-tabs-sync/agent-status-primitives.ts
|
||||
8 src/renderer/src/runtime/web-session-tabs-sync/apply-active-state.ts
|
||||
18 src/renderer/src/runtime/web-session-tabs-sync/apply-final-patch.ts
|
||||
2 src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-groups.ts
|
||||
5 src/renderer/src/runtime/web-session-tabs-sync/apply-terminal-records.ts
|
||||
4 src/renderer/src/runtime/web-session-tabs-sync/apply-worktree-records.ts
|
||||
1 src/renderer/src/runtime/web-session-tabs-sync/mirrored-editor-file-identity.ts
|
||||
2 src/renderer/src/runtime/web-session-tabs-sync/session-tabs-inventory-absence.ts
|
||||
1 src/renderer/src/runtime/web-session-terminal-orphan-recovery-surface.ts
|
||||
2 src/renderer/src/runtime/web-session-terminal-orphan-topology.ts
|
||||
3 src/renderer/src/runtime/web-session-terminal-recovery-snapshot-validation.ts
|
||||
9 src/renderer/src/store/repos/repo-removal.ts
|
||||
10 src/renderer/src/store/slices/browser/browser-close-actions.ts
|
||||
6 src/renderer/src/store/slices/browser/browser-hydration-actions.ts
|
||||
5 src/renderer/src/store/slices/browser/browser-page-focus-actions.ts
|
||||
2 src/renderer/src/store/slices/browser/browser-page-state-actions.ts
|
||||
6 src/renderer/src/store/slices/browser/browser-tab-actions.ts
|
||||
4 src/renderer/src/store/slices/browser/browser-tab-focus-actions.ts
|
||||
2 src/renderer/src/store/slices/browser/create-browser-slice.ts
|
||||
1 src/renderer/src/store/slices/direct-ssh-terminal-recovery.ts
|
||||
1 src/renderer/src/store/slices/editor/actions/chat-visual-tab-actions.ts
|
||||
9 src/renderer/src/store/slices/editor/actions/check-run-details-actions.ts
|
||||
7 src/renderer/src/store/slices/editor/actions/close-file-action.ts
|
||||
1 src/renderer/src/store/slices/editor/actions/git-status-actions.ts
|
||||
5 src/renderer/src/store/slices/editor/actions/hydrate-editor-session.ts
|
||||
3 src/renderer/src/store/slices/editor/actions/markdown-preview-actions.ts
|
||||
27 src/renderer/src/store/slices/editor/actions/open-combined-diff.ts
|
||||
13 src/renderer/src/store/slices/editor/actions/open-conflict-file.ts
|
||||
14 src/renderer/src/store/slices/editor/actions/open-conflict-review.ts
|
||||
4 src/renderer/src/store/slices/editor/actions/open-file-apply.ts
|
||||
5 src/renderer/src/store/slices/editor/actions/open-file-mutations.ts
|
||||
7 src/renderer/src/store/slices/editor/actions/open-file-state.ts
|
||||
26 src/renderer/src/store/slices/editor/actions/open-history-diff.ts
|
||||
1 src/renderer/src/store/slices/editor/actions/open-unstaged-diff.ts
|
||||
10 src/renderer/src/store/slices/editor/actions/recently-closed-editor-tabs.ts
|
||||
4 src/renderer/src/store/slices/editor/actions/rekey-open-files-action.ts
|
||||
5 src/renderer/src/store/slices/editor/actions/restored-editor-owner-in-place.ts
|
||||
10 src/renderer/src/store/slices/editor/actions/restored-editor-owner-transition.ts
|
||||
3 src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-ids.ts
|
||||
2 src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-selection.ts
|
||||
1 src/renderer/src/store/slices/editor/file-ids/open-file-path-rekey.ts
|
||||
4 src/renderer/src/store/slices/editor/tabs/editor-open-target-group.ts
|
||||
2 src/renderer/src/store/slices/ssh-target-cleanup.ts
|
||||
2 src/renderer/src/store/slices/tab-group-reference-repair.ts
|
||||
4 src/renderer/src/store/slices/tab-group-state.ts
|
||||
2 src/renderer/src/store/slices/tabs-hydration.ts
|
||||
4 src/renderer/src/store/slices/tabs/create-tabs-slice.ts
|
||||
17 src/renderer/src/store/slices/tabs/tabs-close-actions.ts
|
||||
14 src/renderer/src/store/slices/tabs/tabs-create-actions.ts
|
||||
11 src/renderer/src/store/slices/tabs/tabs-drop-actions.ts
|
||||
4 src/renderer/src/store/slices/tabs/tabs-focus-actions.ts
|
||||
13 src/renderer/src/store/slices/tabs/tabs-group-actions.ts
|
||||
1 src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts
|
||||
8 src/renderer/src/store/slices/tabs/tabs-label-actions.ts
|
||||
10 src/renderer/src/store/slices/tabs/tabs-move-actions.ts
|
||||
9 src/renderer/src/store/slices/tabs/tabs-reconciliation.ts
|
||||
1 src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts
|
||||
4 src/renderer/src/store/slices/tabs/tabs-session-actions.ts
|
||||
5 src/renderer/src/store/slices/tabs/tabs-surface.ts
|
||||
6 src/renderer/src/store/slices/terminal-orphan-helpers.ts
|
||||
4 src/renderer/src/store/slices/terminal-tab-title-batch.ts
|
||||
6 src/renderer/src/store/slices/terminals.ts
|
||||
5 src/renderer/src/store/slices/worktrees/session/set-active-folder-workspace.ts
|
||||
7 src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts
|
||||
15 src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts
|
||||
2 src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts
|
||||
18 src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts
|
||||
18 src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
|
||||
5 src/renderer/src/store/terminals/terminal-layout-state.ts
|
||||
2 src/renderer/src/store/terminals/terminal-pane-hibernation.ts
|
||||
3 src/renderer/src/store/terminals/terminal-pty-bindings.ts
|
||||
2 src/renderer/src/store/terminals/terminal-pty-release.ts
|
||||
1 src/renderer/src/store/terminals/terminal-shutdown-guards.ts
|
||||
3 src/renderer/src/store/terminals/terminal-shutdown-state.ts
|
||||
1 src/renderer/src/store/terminals/terminal-tab-agent-launch-pane.ts
|
||||
2 src/renderer/src/store/terminals/terminal-tab-attention.ts
|
||||
6 src/renderer/src/store/terminals/terminal-tab-close.ts
|
||||
10 src/renderer/src/store/terminals/terminal-tab-creation.ts
|
||||
7 src/renderer/src/store/terminals/terminal-tab-navigation.ts
|
||||
3 src/renderer/src/store/terminals/terminal-tab-presentation.ts
|
||||
5 src/renderer/src/store/terminals/workspace-terminal-hydration-patch.ts
|
||||
2 src/renderer/src/store/terminals/workspace-terminal-hydration.ts
|
||||
2 src/renderer/src/store/terminals/workspace-terminal-reconnect.ts
|
||||
@@ -0,0 +1,504 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript-api'
|
||||
|
||||
import {
|
||||
diffCounts,
|
||||
formatBaseline,
|
||||
parseBaseline,
|
||||
parseBaselineNotes
|
||||
} from './check-focus-read-ratchet.mjs'
|
||||
|
||||
// Two ratchets for writers of workspace tab layout, which is moving to one owner in the runtime.
|
||||
// Per-file counts may only go down. Deleted symbols are left to the compiler.
|
||||
// - Main: callers of the Store's layout setters, plus writes that edit saved session partitions
|
||||
// directly (the Store-internal session writers included).
|
||||
// - Renderer: writes of the window's layout and per-view selection fields, including computed
|
||||
// keys (counted at the string key) and spreads (counted where the spread object is built).
|
||||
// Content updates of existing editor and browser entries are not membership and are not counted.
|
||||
// Shorthand keys count only in an object handed to `set`/`setState` or a Store writer.
|
||||
|
||||
// Partition fields of the saved workspace session (census class 1).
|
||||
const SESSION_PARTITION_FIELDS = [
|
||||
'tabsByWorktree',
|
||||
'terminalLayoutsByTabId',
|
||||
'unifiedTabs',
|
||||
'tabGroups',
|
||||
'tabGroupLayouts',
|
||||
'activeGroupIdByWorktree',
|
||||
'activeTabIdByWorktree',
|
||||
'activeWorktreeId',
|
||||
'openFilesByWorktree',
|
||||
'browserTabsByWorktree',
|
||||
'activeBrowserTabIdByWorktree',
|
||||
'activeFileIdByWorktree',
|
||||
'activeTabTypeByWorktree',
|
||||
'tabBarOrderByWorktree',
|
||||
'sleepingAgentSessionsByPaneKey',
|
||||
'terminalPtyIncarnationsByPaneKey',
|
||||
'defaultTerminalTabsAppliedByWorktreeId',
|
||||
'lastVisitedAtByWorktreeId',
|
||||
'browserUrlHistory'
|
||||
]
|
||||
const SESSION_FIELDS = ['workspaceSession', 'workspaceSessionsByHostId']
|
||||
// Tab and pane-layout fields edited in place inside a partition (census class 1, third search).
|
||||
const SESSION_TAB_FIELDS = [
|
||||
'ptyId',
|
||||
'ptyIdsByLeafId',
|
||||
'customTitle',
|
||||
'titlesByLeafId',
|
||||
'buffersByLeafId',
|
||||
'activeLeafId'
|
||||
]
|
||||
|
||||
// Window layout fields of acceptance 1 and the per-view selection fields (census class 2).
|
||||
const RENDERER_LAYOUT_FIELDS = new Set([
|
||||
'tabsByWorktree',
|
||||
'terminalLayoutsByTabId',
|
||||
'tabBarOrderByWorktree',
|
||||
'unifiedTabsByWorktree',
|
||||
'groupsByWorktree',
|
||||
'layoutByWorktree',
|
||||
'openFiles',
|
||||
'browserTabsByWorktree',
|
||||
'activeGroupIdByWorktree',
|
||||
'activeTabIdByWorktree',
|
||||
'activeTabId',
|
||||
'activeFileIdByWorktree',
|
||||
'activeFileId',
|
||||
'activeBrowserTabIdByWorktree',
|
||||
'activeBrowserTabId',
|
||||
'activeTabTypeByWorktree',
|
||||
'activeTabType',
|
||||
'ptyIdsByTabId'
|
||||
])
|
||||
|
||||
export const RATCHETS = [
|
||||
{
|
||||
id: 'main',
|
||||
label: 'main-process layout writer(s)',
|
||||
scanRoot: 'src/main',
|
||||
baselinePath: 'config/main-layout-writer-baseline.txt',
|
||||
// Store layout setters and the Store-internal helpers that edit partitions (census class 1
|
||||
// entry points). Every call, and every method value (`store.setWorkspaceSession.bind`), counts.
|
||||
writerNames: new Set([
|
||||
'setWorkspaceSession',
|
||||
'patchWorkspaceSession',
|
||||
'stageWorkspaceSessionBeforeUnload',
|
||||
'setWorkspaceSessionForWorktree',
|
||||
'setForWorktree',
|
||||
'persistPtyBinding',
|
||||
'retirePtyBinding',
|
||||
'moveTerminalLeafToNewTab',
|
||||
'admitTerminalPane',
|
||||
'withdrawTerminalPane',
|
||||
'removeWorkspaceSessionHost',
|
||||
'removeWorkspaceSessionStateForWorktree',
|
||||
'setHostWorkspaceSession',
|
||||
'setLocalWorkspaceSession',
|
||||
'applyPtyBinding',
|
||||
'removeWorkspaceSessionOwner',
|
||||
'removeWorkspaceSessionOwners',
|
||||
'removeWorkspaceSessionOwnerEverywhere',
|
||||
'removeWorkspaceSessionOwnersEverywhere',
|
||||
'removeWorkspaceSessionOwnerInPartition',
|
||||
'deleteScannedSessionFieldsForOwners',
|
||||
'removeRepoFromWorkspaceSession',
|
||||
'removeRepoFromHostWorkspaceSessions',
|
||||
'applyPreparedOrcadMigrationWorkspaceSession',
|
||||
'applyPayloadToTarget',
|
||||
'removeSourceRepo',
|
||||
'clearSshRemotePtyBindingsForTarget',
|
||||
'clearSshRemotePtyBindingsForLeases'
|
||||
]),
|
||||
assignedFields: new Set([
|
||||
...SESSION_FIELDS,
|
||||
...SESSION_PARTITION_FIELDS,
|
||||
...SESSION_TAB_FIELDS,
|
||||
'mobileClientTabSelectionsByDeviceId'
|
||||
]),
|
||||
// These names also key unrelated main objects, so only the session itself counts as a key.
|
||||
keyedFields: new Set(SESSION_FIELDS),
|
||||
entryContentFields: new Set(),
|
||||
allowed: () => false,
|
||||
header: [
|
||||
'# Main-process writers of saved workspace layout, per file: calls of the Store layout',
|
||||
'# setters, plus assignments, deletes and object keys that edit session partitions directly.',
|
||||
'# This is a RATCHET: counts may only go DOWN. Layout moves to one runtime owner; do not add',
|
||||
'# writers. Prune after removing writers: pnpm check:layout-writer-ratchets --prune'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'renderer',
|
||||
label: 'renderer layout write(s)',
|
||||
scanRoot: 'src/renderer/src',
|
||||
baselinePath: 'config/renderer-layout-writer-baseline.txt',
|
||||
writerNames: new Set(),
|
||||
assignedFields: RENDERER_LAYOUT_FIELDS,
|
||||
keyedFields: RENDERER_LAYOUT_FIELDS,
|
||||
// Entries here also carry content (drafts, git status, URL). A `.map(…)` over the existing
|
||||
// list keeps membership and order, so it is a content update and is not counted.
|
||||
entryContentFields: new Set(['openFiles', 'browserTabsByWorktree']),
|
||||
// The three writers the plan allows: applyFromRuntime, the frozen mirror's store patch and
|
||||
// the frozen setters for remote-server workspaces.
|
||||
allowed: (rel) =>
|
||||
rel === 'src/renderer/src/store/workspace-layout-cache.ts' ||
|
||||
rel === 'src/renderer/src/runtime/web-session-tabs-sync/store-patch.ts' ||
|
||||
rel.startsWith('src/renderer/src/runtime/remote-workspace-frozen/'),
|
||||
header: [
|
||||
'# Renderer writes of the window layout and per-view selection fields, per file: assignments,',
|
||||
'# deletes, object keys and string keys (computed-key writes). Same-name copies are reads.',
|
||||
'# This is a RATCHET: counts may only GO DOWN. Layout comes from the runtime; send a command',
|
||||
'# instead. Prune after removing writes: pnpm check:layout-writer-ratchets --prune'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Census test filter: test, fixture and harness files are not product writers.
|
||||
export function isScannedPath(rel) {
|
||||
return (
|
||||
/\.(ts|tsx)$/.test(rel) &&
|
||||
!/\.(test|spec)\.|\/tests?\/|__tests__|test-support|fixture|harness|e2e\//.test(rel)
|
||||
)
|
||||
}
|
||||
|
||||
const ASSIGNMENT_OPERATORS = new Set([
|
||||
ts.SyntaxKind.EqualsToken,
|
||||
ts.SyntaxKind.QuestionQuestionEqualsToken,
|
||||
ts.SyntaxKind.BarBarEqualsToken,
|
||||
ts.SyntaxKind.AmpersandAmpersandEqualsToken
|
||||
])
|
||||
|
||||
/** A string-literal key; a variable key (`exitCodes[ptyId]`) names no field. */
|
||||
function literalKey(node) {
|
||||
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null
|
||||
}
|
||||
|
||||
function staticName(node) {
|
||||
if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) {
|
||||
return node.text
|
||||
}
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||
return node.text
|
||||
}
|
||||
if (ts.isComputedPropertyName(node)) {
|
||||
return literalKey(node.expression)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** The field a write target (`a.b[c]!.d`) goes through, if any; root identifiers are locals. */
|
||||
function targetedField(node, fields) {
|
||||
let current = node
|
||||
while (current) {
|
||||
if (
|
||||
ts.isParenthesizedExpression(current) ||
|
||||
ts.isNonNullExpression(current) ||
|
||||
ts.isAsExpression(current) ||
|
||||
ts.isSatisfiesExpression(current)
|
||||
) {
|
||||
current = current.expression
|
||||
} else if (ts.isPropertyAccessExpression(current)) {
|
||||
if (fields.has(current.name.text)) {
|
||||
return current.name.text
|
||||
}
|
||||
current = current.expression
|
||||
} else if (ts.isElementAccessExpression(current)) {
|
||||
const key = literalKey(current.argumentExpression)
|
||||
if (key !== null && fields.has(key)) {
|
||||
return key
|
||||
}
|
||||
current = current.expression
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function unwrap(node) {
|
||||
let current = node
|
||||
while (
|
||||
ts.isParenthesizedExpression(current) ||
|
||||
ts.isNonNullExpression(current) ||
|
||||
ts.isAsExpression(current) ||
|
||||
ts.isSatisfiesExpression(current)
|
||||
) {
|
||||
current = current.expression
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/** An existing value read as-is: `s.openFiles`, `s.tabs[id] ?? []`. */
|
||||
function isPlainRead(node) {
|
||||
const current = unwrap(node)
|
||||
if (ts.isIdentifier(current) || current.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
return true
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
|
||||
return isPlainRead(current.expression)
|
||||
}
|
||||
if (
|
||||
ts.isBinaryExpression(current) &&
|
||||
(current.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
current.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
||||
) {
|
||||
const right = unwrap(current.right)
|
||||
const emptyLiteral =
|
||||
(ts.isArrayLiteralExpression(right) && right.elements.length === 0) ||
|
||||
(ts.isObjectLiteralExpression(right) && right.properties.length === 0)
|
||||
return isPlainRead(current.left) && (emptyLiteral || isPlainRead(right))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** `list.map(…)`, or `{ ...map, [key]: list.map(…) }`: same entries in the same order. */
|
||||
function keepsMembership(node) {
|
||||
const value = unwrap(node)
|
||||
if (ts.isCallExpression(value)) {
|
||||
const callee = unwrap(value.expression)
|
||||
return (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
callee.name.text === 'map' &&
|
||||
isPlainRead(callee.expression)
|
||||
)
|
||||
}
|
||||
if (ts.isObjectLiteralExpression(value)) {
|
||||
return value.properties.every(
|
||||
(property) =>
|
||||
(ts.isSpreadAssignment(property) && isPlainRead(property.expression)) ||
|
||||
(ts.isPropertyAssignment(property) && keepsMembership(property.initializer))
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const STORE_SET_CALLS = new Set(['set', 'setState'])
|
||||
|
||||
/** Whether an object literal is a store write: `set({ … })`, `set((s) => ({ … }))` or a writer call. */
|
||||
function isPassedToWriter(objectLiteral, writerNames) {
|
||||
let node = objectLiteral
|
||||
for (;;) {
|
||||
const parent = node.parent
|
||||
if (
|
||||
ts.isParenthesizedExpression(parent) ||
|
||||
ts.isAsExpression(parent) ||
|
||||
ts.isSatisfiesExpression(parent) ||
|
||||
ts.isConditionalExpression(parent)
|
||||
) {
|
||||
node = parent
|
||||
} else if (ts.isArrowFunction(parent) && parent.body === node) {
|
||||
node = parent
|
||||
} else if (ts.isReturnStatement(parent)) {
|
||||
let fn = parent.parent
|
||||
while (fn && !ts.isFunctionLike(fn)) {
|
||||
fn = fn.parent
|
||||
}
|
||||
if (!fn) {
|
||||
return false
|
||||
}
|
||||
node = fn
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
const call = node.parent
|
||||
if (!ts.isCallExpression(call) || !call.arguments.includes(node)) {
|
||||
return false
|
||||
}
|
||||
const callee = unwrap(call.expression)
|
||||
const name = ts.isIdentifier(callee)
|
||||
? callee.text
|
||||
: ts.isPropertyAccessExpression(callee)
|
||||
? callee.name.text
|
||||
: null
|
||||
return name !== null && (STORE_SET_CALLS.has(name) || writerNames.has(name))
|
||||
}
|
||||
|
||||
/** `{ tabsByWorktree: s.tabsByWorktree }` copies a value under its own name: a selector read. */
|
||||
function isSameNameCopy(property, name) {
|
||||
let value = property.initializer
|
||||
while (ts.isParenthesizedExpression(value) || ts.isNonNullExpression(value)) {
|
||||
value = value.expression
|
||||
}
|
||||
return ts.isPropertyAccessExpression(value) && value.name.text === name
|
||||
}
|
||||
|
||||
/** Layout writes in one file under one ratchet's rules. */
|
||||
export function countLayoutWrites(sourceText, ratchet, fileName = 'file.tsx') {
|
||||
const names = [...ratchet.writerNames, ...ratchet.assignedFields]
|
||||
if (!names.some((name) => sourceText.includes(name))) {
|
||||
return 0
|
||||
}
|
||||
const sourceFile = ts.createSourceFile(
|
||||
fileName,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
||||
)
|
||||
let count = 0
|
||||
const visit = (node) => {
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(node) && ratchet.writerNames.has(node.name.text)) ||
|
||||
(ts.isCallExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
ratchet.writerNames.has(node.expression.text))
|
||||
) {
|
||||
count += 1
|
||||
} else if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
ASSIGNMENT_OPERATORS.has(node.operatorToken.kind) &&
|
||||
targetedField(node.left, ratchet.assignedFields) !== null
|
||||
) {
|
||||
const field = targetedField(node.left, ratchet.assignedFields)
|
||||
if (!(ratchet.entryContentFields.has(field) && keepsMembership(node.right))) {
|
||||
count += 1
|
||||
}
|
||||
} else if (
|
||||
ts.isDeleteExpression(node) &&
|
||||
targetedField(node.expression, ratchet.assignedFields) !== null
|
||||
) {
|
||||
count += 1
|
||||
} else if (
|
||||
ts.isShorthandPropertyAssignment(node) &&
|
||||
ratchet.keyedFields.has(node.name.text) &&
|
||||
// A shorthand elsewhere passes a value on (`return { activeTabId, tabsByWorktree }`).
|
||||
isPassedToWriter(node.parent, ratchet.writerNames)
|
||||
) {
|
||||
count += 1
|
||||
} else if (ts.isPropertyAssignment(node)) {
|
||||
const name = staticName(node.name)
|
||||
if (
|
||||
name !== null &&
|
||||
ratchet.keyedFields.has(name) &&
|
||||
!isSameNameCopy(node, name) &&
|
||||
!(ratchet.entryContentFields.has(name) && keepsMembership(node.initializer))
|
||||
) {
|
||||
count += 1
|
||||
}
|
||||
} else if (
|
||||
(ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) &&
|
||||
ratchet.keyedFields.has(node.text) &&
|
||||
// Object keys and element reads are judged above; `'tabsByWorktree'` elsewhere feeds a
|
||||
// computed-key write (`patch[key] = …`), which cannot be resolved statically.
|
||||
!(ts.isPropertyAssignment(node.parent) && node.parent.name === node) &&
|
||||
!ts.isComputedPropertyName(node.parent) &&
|
||||
!(ts.isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) &&
|
||||
!ts.isLiteralTypeNode(node.parent)
|
||||
) {
|
||||
count += 1
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(sourceFile)
|
||||
return count
|
||||
}
|
||||
|
||||
function trackedFiles(root, scanRoot) {
|
||||
return execFileSync('git', ['ls-files', scanRoot], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 64 * 1024 * 1024
|
||||
})
|
||||
.split('\n')
|
||||
.filter((rel) => rel && isScannedPath(rel))
|
||||
}
|
||||
|
||||
export function collectCurrentCounts(ratchet, root = process.cwd()) {
|
||||
const counts = new Map()
|
||||
for (const rel of trackedFiles(root, ratchet.scanRoot)) {
|
||||
if (ratchet.allowed(rel)) {
|
||||
continue
|
||||
}
|
||||
let text
|
||||
try {
|
||||
text = fs.readFileSync(path.join(root, rel), 'utf8')
|
||||
} catch {
|
||||
// Deleted in the working tree but still tracked.
|
||||
continue
|
||||
}
|
||||
const found = countLayoutWrites(text, ratchet, rel)
|
||||
if (found > 0) {
|
||||
counts.set(rel, found)
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
function total(counts) {
|
||||
return [...counts.values()].reduce((sum, count) => sum + count, 0)
|
||||
}
|
||||
|
||||
const ADVICE =
|
||||
'Workspace layout is moving to one runtime owner; send a layout command instead of writing it here.'
|
||||
|
||||
export function main(root = process.cwd()) {
|
||||
let failed = false
|
||||
for (const ratchet of RATCHETS) {
|
||||
const baselineFile = path.join(root, ratchet.baselinePath)
|
||||
if (!fs.existsSync(baselineFile)) {
|
||||
console.error(`::error::Missing ${ratchet.baselinePath}.`)
|
||||
failed = true
|
||||
continue
|
||||
}
|
||||
const current = collectCurrentCounts(ratchet, root)
|
||||
const { grown, shrunk } = diffCounts(
|
||||
current,
|
||||
parseBaseline(fs.readFileSync(baselineFile, 'utf8'))
|
||||
)
|
||||
for (const { file, now, allowed } of grown) {
|
||||
console.error(
|
||||
`::error file=${file}::${now} ${ratchet.label}, baseline allows ${allowed}. ${ADVICE}`
|
||||
)
|
||||
}
|
||||
for (const { file, now, allowed } of shrunk) {
|
||||
console.error(
|
||||
`::error file=${file}::${now} ${ratchet.label}, baseline still allows ${allowed}. Run: pnpm check:layout-writer-ratchets --prune`
|
||||
)
|
||||
}
|
||||
if (grown.length + shrunk.length > 0) {
|
||||
failed = true
|
||||
} else {
|
||||
console.log(`Layout-writer ratchet (${ratchet.id}) OK — ${total(current)} ${ratchet.label}.`)
|
||||
}
|
||||
}
|
||||
return failed ? 1 : 0
|
||||
}
|
||||
|
||||
export function prune(root = process.cwd()) {
|
||||
for (const ratchet of RATCHETS) {
|
||||
const baselineFile = path.join(root, ratchet.baselinePath)
|
||||
const text = fs.existsSync(baselineFile) ? fs.readFileSync(baselineFile, 'utf8') : ''
|
||||
const current = collectCurrentCounts(ratchet, root)
|
||||
// Lowers entries only; growth still has to be fixed in the code.
|
||||
const pruned = new Map(
|
||||
[...parseBaseline(text)].map(([file, allowed]) => [
|
||||
file,
|
||||
Math.min(allowed, current.get(file) ?? 0)
|
||||
])
|
||||
)
|
||||
fs.writeFileSync(baselineFile, formatBaseline(pruned, ratchet.header, parseBaselineNotes(text)))
|
||||
console.log(`Pruned ${ratchet.baselinePath} to ${total(pruned)} ${ratchet.label}.`)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const root = process.cwd()
|
||||
if (process.argv[2] === '--init') {
|
||||
// One-time bootstrap of the baselines from today's tree.
|
||||
for (const ratchet of RATCHETS) {
|
||||
const counts = collectCurrentCounts(ratchet, root)
|
||||
fs.writeFileSync(
|
||||
path.join(root, ratchet.baselinePath),
|
||||
formatBaseline(counts, ratchet.header)
|
||||
)
|
||||
}
|
||||
} else if (process.argv[2] === '--prune') {
|
||||
prune(root)
|
||||
}
|
||||
process.exit(main(root))
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { countLayoutWrites, isScannedPath, RATCHETS } from './check-layout-writer-ratchets.mjs'
|
||||
|
||||
const [mainRatchet, rendererRatchet] = RATCHETS
|
||||
|
||||
describe('main layout writers', () => {
|
||||
it('counts Store setter calls, partition edits and session keys, not reads or types', () => {
|
||||
const src = [
|
||||
'store.setWorkspaceSession(next, hostId)',
|
||||
'const save = store.persistPtyBinding.bind(store)',
|
||||
'removeWorkspaceSessionOwners(state, ids)',
|
||||
'this.state.workspaceSession = merged',
|
||||
'session.tabsByWorktree[id] = tabs',
|
||||
'delete partition.terminalLayoutsByTabId![tabId]',
|
||||
'tab.ptyId = null',
|
||||
'const next = { ...state, workspaceSession: merged }',
|
||||
'const tabs = session.tabsByWorktree[id]',
|
||||
'type T = { workspaceSession: Session }',
|
||||
'const reply = { tabsByWorktree: tabs }',
|
||||
'exitCodes[ptyId] = code',
|
||||
'const byPty = { [ptyId]: code }'
|
||||
].join('\n')
|
||||
expect(countLayoutWrites(src, mainRatchet, 'a.ts')).toBe(8)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderer layout writes', () => {
|
||||
it('counts assignments, object, shorthand and string keys, not same-name copies or types', () => {
|
||||
const src = [
|
||||
'set({ tabsByWorktree: next, activeTabId })',
|
||||
'patch.tabsByWorktree![id] = tabs',
|
||||
"const KEYS = ['groupsByWorktree', 'layoutByWorktree'] as const",
|
||||
'patch[key] = value',
|
||||
'const picked = useAppStore((s) => ({ openFiles: s.openFiles }))',
|
||||
'const useTabs = () => ({ activeTabId, tabsByWorktree })',
|
||||
"type Field = Pick<AppState, 'tabsByWorktree'>",
|
||||
"const read = state['unifiedTabsByWorktree']",
|
||||
'const { tabsByWorktree } = state'
|
||||
].join('\n')
|
||||
expect(countLayoutWrites(src, rendererRatchet, 'a.tsx')).toBe(5)
|
||||
})
|
||||
|
||||
it('skips content updates of existing editor and browser entries, not membership changes', () => {
|
||||
const src = [
|
||||
'set({ openFiles: s.openFiles.map((f) => (f.id === id ? { ...f, isDirty: true } : f)) })',
|
||||
'set({ browserTabsByWorktree: { ...s.browserTabsByWorktree, [wt]: (s.browserTabsByWorktree[wt] ?? []).map(withUrl) } })',
|
||||
'set({ openFiles: [...s.openFiles, file] })',
|
||||
'set({ openFiles: s.openFiles.filter((f) => f.id !== id) })',
|
||||
'set({ tabsByWorktree: s.tabsByWorktree.map(rename) })'
|
||||
].join('\n')
|
||||
expect(countLayoutWrites(src, rendererRatchet, 'a.tsx')).toBe(3)
|
||||
})
|
||||
|
||||
it('allows only the frozen mirror writers', () => {
|
||||
expect(rendererRatchet.allowed('src/renderer/src/store/workspace-layout-cache.ts')).toBe(true)
|
||||
expect(
|
||||
rendererRatchet.allowed('src/renderer/src/runtime/remote-workspace-frozen/setters.ts')
|
||||
).toBe(true)
|
||||
expect(
|
||||
rendererRatchet.allowed('src/renderer/src/store/slices/tabs/tabs-create-actions.ts')
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isScannedPath', () => {
|
||||
it('skips tests, fixtures and harnesses', () => {
|
||||
expect(isScannedPath('src/main/ipc/session.ts')).toBe(true)
|
||||
expect(isScannedPath('src/main/ipc/session.test.ts')).toBe(false)
|
||||
expect(isScannedPath('src/main/runtime/terminal-retirement-fixture.ts')).toBe(false)
|
||||
expect(isScannedPath('src/main/ipc/session.md')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -224,6 +224,7 @@ it('pins every foreground and background step to its selected phase', () => {
|
||||
['Enforce ts-nocheck ratchet', staticPhase],
|
||||
['Enforce runtime Electron-import ratchet', staticPhase],
|
||||
['Enforce focus-read ratchet', staticPhase],
|
||||
['Enforce layout-writer ratchets', staticPhase],
|
||||
['Check Node runtime pin', staticPhase],
|
||||
['Boot orcad and round-trip a terminal', staticPhase],
|
||||
['Verify the generated RPC params catalog', staticPhase],
|
||||
|
||||
+2
-1
@@ -14,7 +14,7 @@
|
||||
"audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src",
|
||||
"test:perf:contracts": "vitest run --config config/vitest.performance.config.ts",
|
||||
"format": "oxfmt --write .",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports",
|
||||
"audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor",
|
||||
"audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings",
|
||||
"audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings",
|
||||
@@ -40,6 +40,7 @@
|
||||
"check:ts-nocheck-ratchet": "node config/scripts/check-ts-nocheck-ratchet.mjs",
|
||||
"check:runtime-electron-ratchet": "node config/scripts/check-runtime-electron-ratchet.mjs",
|
||||
"check:focus-read-ratchet": "node config/scripts/check-focus-read-ratchet.mjs",
|
||||
"check:layout-writer-ratchets": "node config/scripts/check-layout-writer-ratchets.mjs",
|
||||
"check:readme-local-links": "node config/scripts/check-readme-local-links.mjs",
|
||||
"check:daemon-protocol-crossing": "node config/scripts/check-daemon-protocol-crossing.mjs",
|
||||
"check:runtime-launcher-protocol-ratchet": "node config/scripts/check-runtime-launcher-protocol-ratchet.mjs",
|
||||
|
||||
Reference in New Issue
Block a user