从源码投射的契约

Backend port

实现必需的投影和编辑方法;可选 port 会逐步丰富同一个电子表格 UI。

此代码块在构建时从 excel/spreadsheet-ui-core/src/backend/types.ts 提取, 不是手工维护的副本。

必需方法

export interface SpreadsheetBackend {
  listSheets?(): Promise<SheetListResult>
  readVisibleProjection(request: VisibleProjectionRequest): Promise<VisibleProjectionResult>
  readRangeProjection(request: RangeProjectionRequest): Promise<RangeProjectionResult>
  exportRangeTsv?(request: RangeTsvExportRequest): Promise<RangeTsvExportResult>
  /**
   * Wave 8.4 — range screenshot. Optional capability; UI core hides the
   * Copy-as-PNG surfaces and `encodeSelectionAsImage` returns `null`
   * when this method is omitted.
   */
  exportRangeAsImage?(request: RangeImageExportRequest): Promise<RangeImageExportResult>
  consumeExportRangeTsvChunks?(
    request: RangeTsvExportRequest,
    onChunk: RangeTsvChunkConsumer,
  ): Promise<RangeTsvChunkExportResult>
  readViewportSizeProjection?(
    request: ViewportSizeProjectionRequest,
  ): Promise<ViewportSizeProjectionResult>
  /**
   * Commit one cell's raw input. Resolving a `BackendMutationResult` means
   * the value really landed — a host that cannot apply the write MUST
   * reject, never resolve a success-shaped ACK, or the user's keystrokes
   * disappear while UI core records history and bumps the revision.
   * `runEditingCommitAtom` turns the rejection into a `rejected` editing
   * lifecycle carrying the Error's `message`, so the reason belongs there
   * (any structured `code` / `detail` is host-private).
   */
  setCellInput(request: SetCellInputRequest): Promise<BackendMutationResult>
  setCellRichValue?(request: SetCellRichValueRequest): Promise<BackendMutationResult>
  importCells?(request: ImportCellsRequest): Promise<BackendMutationResult>
  importCellChunks?(request: ImportCellChunksRequest): Promise<BackendMutationResult>
  clearRange?(request: ClearRangeRequest): Promise<BackendMutationResult>
  insertRows?(request: InsertRowsRequest): Promise<BackendMutationResult>
  deleteRows?(request: DeleteRowsRequest): Promise<BackendMutationResult>
  insertColumns?(request: InsertColumnsRequest): Promise<BackendMutationResult>
  deleteColumns?(request: DeleteColumnsRequest): Promise<BackendMutationResult>
  setFormatRange?(request: SetFormatRangeRequest): Promise<BackendMutationResult>
  setRowHeight?(request: SetRowHeightRequest): Promise<BackendMutationResult>
  setColumnWidth?(request: SetColumnWidthRequest): Promise<BackendMutationResult>
  fillRange?(request: FillRangeRequest): Promise<AutoFillMutationResult>
  resolveDataEdge?(request: ResolveDataEdgeRequest): Promise<ResolveDataEdgeResult>
  addSheet?(request: AddSheetRequest): Promise<SheetMutationResult>
  renameSheet?(request: RenameSheetRequest): Promise<SheetMutationResult>
  deleteSheet?(request: DeleteSheetRequest): Promise<SheetMutationResult>
  reorderSheet?(request: ReorderSheetRequest): Promise<SheetMutationResult>
  undoTransaction?(request: UndoTransactionRequest): Promise<HistoryTransactionResult>
  redoTransaction?(request: RedoTransactionRequest): Promise<HistoryTransactionResult>
  listNamedRanges?(request: ListNamedRangesRequest): Promise<NamedRangeListResult>
  setNamedRange?(request: SetNamedRangeRequest): Promise<NamedRangeMutationResult>
  deleteNamedRange?(request: DeleteNamedRangeRequest): Promise<NamedRangeMutationResult>
  mergeRange?(request: MergeRangeRequest): Promise<BackendMutationResult>
  unmergeRange?(request: UnmergeRangeRequest): Promise<BackendMutationResult>
  readFreezeConfig?(request: ReadFreezeConfigRequest): Promise<ReadFreezeConfigResult>
  setFreezeConfig?(request: SetFreezeConfigRequest): Promise<BackendMutationResult>
  hideRows?(request: HideRowsRequest): Promise<BackendMutationResult>
  unhideRows?(request: UnhideRowsRequest): Promise<BackendMutationResult>
  // Read-back of the engine-owned hidden state (rows + filter), for hydrating
  // UI-core render caches on sheet activation / restore. Optional: a backend
  // whose engine does not OWN this state (the TS worker) omits the port and
  // UI-core keeps its own canonical view fact instead of hydrating from a
  // source that cannot answer authoritatively.
  readSheetHiddenState?(request: SheetHiddenStateRequest): Promise<SheetHiddenStateResult>
  hideColumns?(request: HideColumnsRequest): Promise<BackendMutationResult>
  unhideColumns?(request: UnhideColumnsRequest): Promise<BackendMutationResult>
  // engine hidden-row eval input (parity #23). Optional, fire-and-forget
  // whole-set REPLACE (no exact ACK / undo): mirrors the UI-core canonical
  // hidden-row VIEW fact into the engine so SUBTOTAL 101-111 can exclude
  // manually hidden data rows. A backend whose engine models no such input
  // omits the port; UI core silently skips the push and SUBTOTAL 101-111
  // degrades to "does not exclude" without disturbing any other feature.
  setEvalHiddenRows?(request: SetEvalHiddenRowsRequest): Promise<void> | void
  fillSeries?(request: FillSeriesRequest): Promise<AutoFillMutationResult>
  // comments & notes
  setNote?(request: SetNoteRequest): Promise<BackendMutationResult>
  clearNote?(request: ClearNoteRequest): Promise<BackendMutationResult>
  postComment?(request: PostCommentRequest): Promise<BackendMutationResult>
  resolveCommentThread?(request: ResolveCommentThreadRequest): Promise<BackendMutationResult>
  deleteComment?(request: DeleteCommentRequest): Promise<BackendMutationResult>
  // data validation
  setValidationRule?(request: SetValidationRuleRequest): Promise<BackendMutationResult>
  clearValidationRule?(request: ClearValidationRuleRequest): Promise<BackendMutationResult>
  // conditional formatting
  setConditionalFormatRule?(
    request: SetConditionalFormatRuleRequest,
  ): Promise<BackendMutationResult>
  removeConditionalFormatRule?(
    request: RemoveConditionalFormatRuleRequest,
  ): Promise<BackendMutationResult>
  listConditionalFormatRules?(
    request: ListConditionalFormatRulesRequest,
  ): Promise<ConditionalFormatRulesResult>
  // print config
  readPrintConfig?(request: ReadPrintConfigRequest): Promise<ReadPrintConfigResult>
  setPrintConfig?(request: SetPrintConfigRequest): Promise<SetPrintConfigResult>
  // find-replace
  /**
   * Match offsets are UTF-16 code-unit indexes into the selected target and use half-open
   * `[matchStart, matchEnd)` intervals. Every emitted span satisfies
   * `0 <= matchStart < matchEnd`; zero-width regex results are omitted.
   */
  searchRange?(request: SearchRangeRequest): Promise<SearchRangeResult>
  /**
   * Replacement spans use the same UTF-16 code-unit, half-open, non-empty contract as search
   * results. Zero-width and reversed spans are rejected before any mutation.
   */
  replaceMatches?(request: ReplaceMatchesRequest): Promise<ReplaceMatchesResponse>
  // presence
  subscribePresence?(handler: (update: PresenceUpdate) => void): SubscribePresenceUnsubscribe
  publishLocalPresence?(request: PublishLocalPresenceRequest): Promise<void>
  // filter-sort
  // Resolves the ordinary mutation ACK PLUS the filter-hidden row set the
  // rule scan produced (`SetFilterSortResult.hiddenRowIndices`). Widening the
  // result is backward compatible: a host that returns a bare
  // `BackendMutationResult` still satisfies this signature, and UI core reads
  // the missing field as "cannot compute visibility" and hides nothing.
  setFilterSort?(request: SetFilterSortRequest): Promise<SetFilterSortResult>
  // engine physical sort — design-engine-sort. Optional capability: host
  // adapters whose runtime cannot physically reorder workbook data omit
  // this port (the TS worker declares `sortRange: false`), which hides
  // the physical-sort entry through the standard degradation contract.
  // Unlike `setFilterSort` (a UI-core VIEW fact / display permutation),
  // this reorders engine DATA; undo is host-orchestrated (one bounded
  // range-snapshot transaction). Resolves an applied report OR a
  // structured not-applied result — a gated request does NOT reject the
  // promise.
  sortRange?(request: SortRangeRequest): Promise<SortRangeResult>
  // protection — UI-core canonical (#40). These ports are an optional
  // persistence hook: `setSheetProtection` / `setRangeLock` receive a
  // fire-and-forget mirror of local commits and `readSheetProtection`
  // seeds a one-shot hydration. Backends that omit them keep the full
  // protection feature; enforcement runs in the UI-core mutation gateway.
  setSheetProtection?(request: SetSheetProtectionRequest): Promise<BackendMutationResult>
  setRangeLock?(request: SetRangeLockRequest): Promise<BackendMutationResult>
  readSheetProtection?(request: ReadSheetProtectionRequest): Promise<ReadSheetProtectionResult>
  // paste-special — Wave 7.3. Optional capability: host adapters that
  // omit this method cause the menu entry + dialog to hide via
  // `pasteSpecialSupportedAtom` so the surface degrades cleanly.
  pasteRange?(request: PasteRangeRequest): Promise<PasteRangeResult>
  // Optional fail-closed subdivision of the pasteRange capability (see
  // `PasteSpecialControllerPort.pasteRangeSupportedKinds`): a backend
  // that cannot apply the format leg declares only the value-leg kinds
  // and Core blocks the rest pre-dispatch. Absent → full trust.
  readonly pasteRangeSupportedKinds?: readonly PasteSpecialKind[]
  // remove-duplicates — Wave 7.5. Optional capability; host adapters that
  // omit this method cause the Data > Remove Duplicates menu entry to hide
  // via `removeDuplicatesSupportedAtom` so the surface degrades cleanly.
  removeRows?(request: RemoveRowsRequest): Promise<RemoveRowsResult>
  // custom-formulas — Wave 8. Optional capability; host adapters that
  // omit these methods make the `customFormulaRegistryAtom` inert
  // (writes succeed but no worker side-effect runs). `source` is the
  // body of a function whose argument is bound to `args` (Array) — the
  // adapter is expected to `new Function('args', source)` it inside
  // whichever runtime owns the formula engine. Errors thrown during
  // evaluation surface as `#ERROR!` cells. Wave 8.2: pass
  // `options.isAsync` to compile through the AsyncFunction constructor —
  // the cell holds `#BUSY!` until the returned Promise settles, and the
  // result is memoized per (name, args) until the next registry change.
  registerCustomFormula?(
    name: string,
    source: string,
    options?: { isAsync?: boolean },
  ): Promise<void>
  unregisterCustomFormula?(name: string): Promise<void>
  // tables — Excel Table CRUD (parity #32, design-excel-table.md §10).
  // Optional capability family: host adapters whose engine has no Table
  // model omit these ports (the TS worker declares `structuredTables:
  // false`, the static backend implements none), which hides every Table
  // entry through the standard method-presence degradation contract. The
  // engine registry is canonical (CANONICAL_OWNERSHIP §3 #32) — these are
  // the only path UI core reads a table's geometry; it stores no second
  // copy. `createTable` resolves the engine-assigned canonical name OR a
  // structured `TableMutationRejectedResult` (name conflict / range
  // overlap / cap 256 / …) rather than rejecting the promise; rename and
  // delete follow the same applied-or-structured-reject convention.
  // Table-definition undo is NOT wired in this slice (design §11/§12
  // known gap — persistence and the snapshot primitive do not carry the
  // table registry).
  createTable?(request: CreateTableRequest): Promise<CreateTableResult>
  renameTable?(request: RenameTableRequest): Promise<TableMutationResult>
  renameTableColumn?(request: RenameTableColumnRequest): Promise<TableMutationResult>
  deleteTable?(request: DeleteTableRequest): Promise<TableMutationResult>
  listTables?(request: ListTablesRequest): Promise<ListTablesResult>
  getTable?(request: GetTableRequest): Promise<GetTableResult>
  // Totals row (parity #32 T6, design-excel-table.md §7). Optional
  // subdivision of the Table capability family — the same
  // `structuredTables` witness gates them, so a backend whose engine has no
  // Table model omits them alongside the CRUD ports and UI core hides the
  // totals toggle through the standard method-presence contract. Both
  // resolve the shared applied-or-structured-reject convention: a gated
  // request (row-below occupied → `totals-row-blocked`, function before the
  // row is enabled → `no-totals-row`, unknown aggregate id →
  // `invalid-totals-function`) resolves a `TableMutationRejectedResult`
  // rather than rejecting the promise. Not undoable in this slice (design
  // §11/§12 known gap; the SUBTOTAL cell writes are covered by the existing
  // cell snapshots, the registry geometry change is not).
  setTableTotalsRow?(request: SetTableTotalsRowRequest): Promise<TableMutationResult>
  setTableTotalFunction?(request: SetTableTotalFunctionRequest): Promise<TableMutationResult>
  // spill (dynamic arrays) — ADR 0006 阶段 3。可选能力:引擎里没有动态数组
  // 模型的宿主(静态后端)省掉这个端口,`spillRegionSupportedAtom` 转 false,
  // 溢出边框与投影格标记整体不出现 —— 端口缺席是「功能不存在」,不是错误。
  //
  // 按需查询而不是给 `DisplayCell` 加字段:溢出边框只在选区落进数组时出现,
  // 所以一次选区移动查一次就够。取舍与代价见 `src/spill/README.md`。
  // `region: null` 是明确的「这一格不在任何活动溢出区里」,与端口缺席不同。
  readSpillRegion?(request: SpillRegionRequest): Promise<SpillRegionResult>
  // content-change push — Wave 8.2. Optional capability: backends whose
  // engine can change cell content OUTSIDE a UI-initiated mutation
  // (async custom-formula settles, collaborative edits) invoke the
  // handler after such a change so the host refetches the visible
  // projection. Coarse signal, no payload — hosts must tolerate
  // spurious invocations. Backends whose content only ever changes in
  // response to their own mutation methods may omit it.
  subscribeContentChanges?(handler: () => void): () => void
}

源码中的契约说明

# backend

Defines framework-agnostic ports that host adapters implement.

This folder must not import worker, WASM, DOM, Solid, or React code. It only
defines request/response contracts used by commands in the UI core.

Backend ports are range based. A host adapter can read a visible window or an
explicit user range, and can execute compact range commands such as clear,
format, structural row/column edits, and fill handle copy. The UI core must not
ask for a workbook snapshot, formula cache, dependency graph, or sparse sheet
dump.

Clipboard-style TSV export is also range based. `exportRangeTsv` returns a TSV
body plus origin metadata for the requested range; the body must not include any
clipboard marker line. Host adapters may implement this with worker-side row
chunks or sparse snapshot chunks, but the UI core still sees only the bounded
range command result.

Clipboard paste uses `importCells` when a host adapter supports bulk import.
The UI core still owns only the parsed target coordinates and text/formula
inputs; worker-backed hosts should map those cells into chunked workbook import
sessions instead of issuing one mutation RPC per cell.

Data-navigation ports return a single coordinate, not a row/column projection.
For example, `resolveDataEdge` lets a host adapter answer Ctrl+Arrow movement
from sparse facts without materializing a full row or column in the UI layer.

Point queries return one bounded fact, not a projection. `readSpillRegion`
answers "does this one cell belong to a dynamic array" with the anchor plus the
array's bounding rectangle — the UI core keeps exactly one such answer at a
time (the active cell's). It is deliberately NOT a `DisplayCell` field: the
spill outline only appears while the selection sits inside the array, so a
per-cell wire field would be paid on every visible cell of every scroll for a
decoration that is usually not drawn. See `../spill/README.md`.

Sheet metadata ports return bounded sheet lists only. `reorderSheet` changes the
displayed sheet metadata order and must not imply any cell snapshot or sheet
content materialization.

Viewport size metadata ports are window based. `readViewportSizeProjection`
returns only sparse row heights and column widths for the visible window; resize
mutations write one row or one column at a time. UI-level autofit must measure
only the current visible DOM or an explicit finite range, then persist through
the same one-row/one-column resize mutations.

## State Decision Template

- Source atoms: none.
- Derived atoms: none.
- Commands: port calls only.
- Scale bound: request/response contracts must be window/range based.
- Backend reads: implemented by host adapters.
- Per-cell/per-row/per-col atom risk: not applicable.
- Tests: `test/backend*.test.ts`, `test/projection*.test.ts`.