Skip to content
SheetForge

The Authoring Kernel — Building a Second Authoring Surface

Advanced. For asset/tool authors who want to build their own authoring UI (for example, a node-graph canvas) on top of SheetForge's engine. Game teams using the Data Studio don't need this page.

The authoring window is not the engine. The Data Studio — and the browser app beside it — are consumers of a window-agnostic authoring kernel.

Everything they do runs through public types that a third surface can drive the same way: staging, validation, reflect orchestration, undo boundaries, re-import. Two surfaces already do, which is the practical proof that the seam is real rather than aspirational.

Before building a whole surface, check whether an extension point already covers the need. A plugin can add verbs, panels, badges and cell widgets to the shipped window without owning a window at all, described as data so they render in the editor and the browser — see Plugin Authoring §4.16. This page is for the case where you want your own canvas.

A consumer-simulation test assembly (SheetForge.Tests.Consumer, with no InternalsVisibleTo access into Core or Editor) implements a virtual authoring surface end to end against the public API alone. If any member it needs were internal, that assembly would fail to compile (CS0122). So it serves as the executable specification for the surface described below.

The three-object engine

┌─────────────────────┐     ┌──────────────────────────┐     ┌───────────────┐
│  AuthoringSession   │────▶│   AuthoringDispatcher    │────▶│ BaselineStore │
│  (staging state)    │     │   .Reflect()             │     │ (round-trip   │
│                     │     │   (the full cycle)       │     │  snapshots)   │
└─────────────────────┘     └────────────┬─────────────┘     └───────────────┘
                                         │ binds
                            ┌────────────▼─────────────┐
                            │ AuthoringDispatchCallbacks│
                            │ (view concerns — YOUR UI) │
                            └──────────────────────────┘

AuthoringSession — the staging state

A [Serializable] plain class, deliberately not a ScriptableObject. Hold it in a [SerializeField] field of your EditorWindow and you get Unity-native Undo snapshots and domain-reload survival for free — the same mechanism behind the shipped window's Ctrl+Z.

It owns all staged state:

  • cell edits (Edits), new rows (NewRows), structure ops (StructOps);
  • per-tab reorders (Reorders), tab renames (TabRenames);
  • baseline anchors, isolated edits.

On top of that state it exposes the mutation/query API:

  • SetStaged(...) — stage a cell edit. Edits carry a logical address (tab · RecordId · field); the physical row ordinal is a derived cache re-resolved just before reflect.
  • ResolveBaselineEdits(provider) — re-anchor all edits against the current baseline. Resolvable edits proceed. The three unresolvable cases (external rename / external delete / key conflict) are moved to IsolatedEdits: excluded from reflect, badged, never silently dropped, never session-blocking.
  • Baseline read surface: TabNames, TryGetBaselineTable(tab, out SheetTable) — typed schema access (TypeToken, @desc, @overlap) without touching the parser yourself.
  • EffectiveStructOps() / PendingStructCount() — the composed, canonical structure-op view.
  • Remap hooks (RemapFieldName / RemapRecordId / RemapTab) keep staged state coherent across renames.
  • LastProjectionResult caches the latest projection.

AuthoringDispatcher — the reflect orchestration

var dispatcher = new AuthoringDispatcher(session, callbacks, baselineStore);
dispatcher.Reflect();   // the entire cycle, one call

Reflect() runs the whole cycle, in order:

  • pre-flight validation
  • per-source reflect — surgical writes for local, safe rewrite for Google, your own target for custom providers
  • retained-state cleanup
  • the ClearUndo confirm boundary
  • automatic re-import with report

Also:

  • BuildProjectionResult() — a side-effect-free projection of the current staged state as an ImportResult (validate-as-if-reflected). Use it for live error badges.
  • Public Session / Callbacks / Baselines — custom source providers use these to assemble their reflect targets.

AuthoringDispatchCallbacks — your UI's contract

A bundle of 13 general delegates the dispatcher calls for every view concern: ResolveSettings, confirmation dialogs (ConfirmKeyRenames, ConfirmTabRenames, …), RenderReport (an Action<ImportReport> — null-tolerated, it's observational), PushApprover, TriggerReimport, ClearUndo, Rebuild, and so on.

The 14 dialog delegates specific to the built-in Local/Google sources live in a separate opt-in BuiltInSourceDialogs bundle, which an external surface or provider never needs to bind.

The shipped window binds dialog-showing defaults; your canvas binds its own (or no-ops). The engine never draws UI itself.

Graph material

For a "node = record, edge = reference ∪ declaration" projection:

  • ReferenceScanner (Core) — the single source of truth for enumerating reference occurrences across all tables: scalars, list elements, explicit defaults. It is the same enumeration the reference validator uses, so your graph and the validation agree by construction. Scan(tables) / ScanTable / ScanField / IsReferenceField.
  • IEdgeContributor / EdgeSpec / EdgeContributorRegistry (Core) — domain plugins declare edges the scanner can't see (inside custom-type values, type-column links, record-edges with a payload record). Collect them via the Editor PluginRegistry.BuildEdgeContributors.
  • ReferenceIndex / RecordEdge (Core) — the assembled snapshot the Data Studio's own canvas runs on: Build(...) merges scanned references with contributor edges once, then OutEdges / InEdges / InCount answer in O(1) per record. Full member list in the API Reference.
  • IRecordCanvasAugmenter / CanvasAugmentBuilder (Core) — the per-tab override contract, if you want domain packs to extend your canvas the same way they extend the Studio's (virtual nodes, extra edges, layer and display hints).
  • ProjectionErrorMapper (Editor, pure) — maps a projection error's physical coordinate (tab/row/field) to a logical address (tab/RecordId/field), so you can pin error badges to nodes rather than row numbers.

Supporting pieces

TypeWhat your surface uses it for
ImportEventsTwo buses, both public contracts. ImportCompleted (ImportCompletedArgs: Tabs · BakeFolder) fires when the auto-chain has run all the way through bake, so a subscriber may read the baked assets. BaselineUpdated (BaselineUpdatedArgs: Tabs · Quarantined) fires whenever a sheet snapshot was saved — including a run that failed validation — which is what a surface subscribes to if it wants to show the failed sheets and let people fix them. Subscribe to both if your view shows sheets and baked values; unsubscribe symmetrically in OnDisable.
IPipelineObserverIf the thing that needs to know is a plugin rather than a window, this is the lighter path: register an observer and receive an immutable PipelineRunView at the end of each import cycle, with no editor dependency at all — it works in the browser host too. See Plugin Authoring §4.17.
RecordIdMinterSuggest ids for new records — prefix detection + collision-safe uniquification. A suggestion API, deliberately not automatic numbering.
EphemeralSoApplyPreview staged values onto baked SOs temporarily (re-import restores). Applies the computable subset; returns skip reasons for pending columns and parse failures. Nothing in the shipped UI drives it any more, so a surface that wants this preview owns the button for it.
KeyRenamePlannerPlan key renames (3-stage: extract / propagation / surgery), the same way the shipped window does. Tab-rename confirmations flow through the public ConfirmTabRenames callback instead.
SourceProviderRegistryResolve the active source provider the same way the settings UI does.

Ground rules the kernel enforces (and you inherit)

  • The sheet stays canonical — your surface stages and reflects; it never writes SOs.
  • Validate-then-reflectReflect() writes nothing if pre-flight fails.
  • No silent loss — unresolvable edits isolate with a reason; confirmations pass through your callbacks.
  • Undo integrates natively — keep the session in a serialized field and register undo snapshots on your window; ClearUndo marks the reflect boundary.
  • Domain-agnostic — the kernel contains zero domain vocabulary (guard-tested). Your domain arrives via the plugin contracts, not via kernel edits.