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 toIsolatedEdits: 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. LastProjectionResultcaches the latest projection.
AuthoringDispatcher — the reflect orchestration
var dispatcher = new AuthoringDispatcher(session, callbacks, baselineStore);
dispatcher.Reflect(); // the entire cycle, one callReflect() 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
ClearUndoconfirm boundary - automatic re-import with report
Also:
BuildProjectionResult()— a side-effect-free projection of the current staged state as anImportResult(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 EditorPluginRegistry.BuildEdgeContributors.ReferenceIndex/RecordEdge(Core) — the assembled snapshot the Data Studio's own canvas runs on:Build(...)merges scanned references with contributor edges once, thenOutEdges/InEdges/InCountanswer 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
| Type | What your surface uses it for |
|---|---|
ImportEvents | Two 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. |
IPipelineObserver | If 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. |
RecordIdMinter | Suggest ids for new records — prefix detection + collision-safe uniquification. A suggestion API, deliberately not automatic numbering. |
EphemeralSoApply | Preview 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. |
KeyRenamePlanner | Plan key renames (3-stage: extract / propagation / surgery), the same way the shipped window does. Tab-rename confirmations flow through the public ConfirmTabRenames callback instead. |
SourceProviderRegistry | Resolve 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-reflect —
Reflect()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;
ClearUndomarks the reflect boundary. - Domain-agnostic — the kernel contains zero domain vocabulary (guard-tested). Your domain arrives via the plugin contracts, not via kernel edits.
Related pages
- API Reference — signatures for everything named here
- Plugin Authoring — the contracts your domain uses alongside the kernel
- Data Studio — the behavior your surface replicates or replaces