Task board
Stable task ids (
task-001, …) for PR titles and changelog entries. Milestone themes: Roadmap.
Legend
| Status | Symbol | Meaning |
|---|---|---|
| proposed | [ ] | Captured, not yet committed. |
| accepted | [~] | In a milestone, ready to start. |
| in-progress | [/] | PR open or partially landed on main. |
| done | [x] | Merged and verified. |
Size estimates: S (< 1 day), M (1–3 days), L (≥ 1 week).
Milestone 0.2 — API surface alignment
task-001 Export every slice hook from the primary entry · [x] · S
- Why.
useGristStatus,useGristSelection,useGristWrites,useGristThemeare documented and must be exported frompackages/core/src/sdk/index.ts. - Design. /api/slice-hooks
- Acceptance.
import { useGristStatus } from "grist-widget-sdk"type-checks inapps/playground.- Each slice hook has a unit test under
packages/core/tests/sdk/. - /api/slice-hooks matches reality.
- Status. Landed — see
packages/core/tests/sdk/public-api.test.tsand slice hook test files.
task-002 Add currentTableId to useGristStatus · [x] · S
- Why. Docs state
useGristStatuscarriescurrentTableId; the status hook is the natural home for table context. - Design. /design/open-questions — Pending API tightenings
- Acceptance.
useGristStatus()return type includescurrentTableId,currentTableLoading,refreshCurrentTable,tableError.- Test verifies
currentTableIdupdates when emulator switches tables.
- Status. Landed —
packages/core/tests/sdk/slice-status.test.tsx.
task-003 Reverse-map allowMultiple with alert surface · [x] · M
- Why.
mapBackmust not silently skipallowMultiplecolumns. - Design. /design/open-questions
- Acceptance.
mapBackkeeps current behavior (skip).- Skipped columns exposed as
mapBackSkippedonuseGrist()(and slice where applicable). getGristSdkAlertDescriptors(w)returns akind: "warning"descriptor when present.- Documented in /guide/column-mapping.
- Status. Landed —
packages/core/tests/sdk/map-back-allow-multiple.test.tsx.
task-004 Normalise safeParse argument across reads · [x] · S
- Why. All entry points should accept
safeParse: boolean | GristSafeParseOptions. - Acceptance.
GristFetchTableRowsOptions.safeParseunion.- Same union on
safeParseGristValues,safeParseGristTableData,useGristRowsFromTable. - Tests cover both forms.
- Status. Landed in helpers; extend tests if gaps remain when closing 0.2.
task-005 Complete action builders for schema mutations · [x] · S
- Why. Table/document action builders shipped in 0.1.
- Acceptance met. See /api/helpers.
task-006 Stable test presets owned by the SDK · [x] · M
- Why. Shared fixtures instead of per-widget minimal documents.
- Acceptance.
presetsexported fromgrist-widget-sdk/emulatorand/emulator/testing.blank,todoList,contactsfactories inpackages/core/src/emulator/templates.ts.- Used by playground tests and widgets.
- Status. Landed. (
presets.defaultTemplate()also exists for neutral demos.)
task-007 Tighten safeParse defaults · [x] · M
- Why. With
columnsprovided butsafeParseomitted, types are decoded — document this contract explicitly. - Acceptance met.
/guide/reading-dataColumns vs safeParse section;grist-table-data.test.tscovers plain decode vs safe-parse cells.
task-008 Audit packages/core/src/sdk/index.ts against design · [x] · S
- Why. Public exports must match docs; internals must not leak.
- Acceptance met.
public-api.test.tspins exports;/design/api-surface.mddeleted;tests/docs/structure.test.tsenforces retirement; Pending API tightenings cleared for shipped 0.2 items.
Milestone 0.3 — Testing & emulator hardening
task-010 Re-export testing-library surface · [x] · S
- Why. Single import path for widget tests.
- Acceptance.
grist-widget-sdk/emulator/testingre-exportsscreen,fireEvent,waitFor,act,cleanup.- Documented in /api/emulator.
- Status. Landed — asserted in
public-api.test.ts.
task-011 waitForEvent reliability + tests · [x] · M
- Why. Helper exists; needs a dedicated contract test suite.
- Acceptance met. Next-event-only wait (no history scan); kind overload + predicate form; timeout lists recent bus types;
wait-for-event.test.tscovers all kinds.
task-012 Snapshot tests for useGristSchema() output · [x] · M
- Why. Schema replica is a primary surface; guard
buildReplicaDocumentFromDocApiregressions (includes preset document shapes used in tests). - Acceptance met.
use-grist-schema.snapshot.test.tsx— 9 snapshots (3 presets × 3 row modes);stableSchemaSnapshotpinsgeneratedAt/ column order.
Milestone 0.4 — Performance & re-render budget
task-020 Bench harness · [x] · M
- Acceptance.
pnpm --filter grist-widget-sdk benchruns a vitest + React profiler harness.- Reports renders/operation for: full-record subscription, slice subscription, write, schema fetch.
- Output committed to
packages/core/bench/results.jsonfor diffing.
task-021 Slice-hook identity stability · [x] · M
- Acceptance.
- Memoized child receiving each callable as a prop asserts zero re-renders across irrelevant slice updates.
task-022 Document render budget per slice · [x] · S
- Acceptance.
- /guide/performance updated with measured renders / operation from the bench.
Milestone 0.5 — Replica & schema upgrade
task-030 Add references to GristReplicaColumn · [ ] · S
- Design. /design/open-questions
- Acceptance.
GristReplicaColumn.references?: stringforRef:*/RefList:*.parseDocumentvalidatesreferencesagainsttables.
task-031 Type GristReplicaColumnWidgetOptions · [ ] · M
- Acceptance.
- Type with
choices?,numericFormat?,dateFormat?,alignment?, plus index signature. - /api/use-grist-schema updated.
- Type with
task-032 Include selection in useGristSchema() output · [ ] · S
- Acceptance.
- Always present with at least
mode: "empty".
- Always present with at least
task-033 Migrate fixtures to .replica.json · [ ] · S
- Acceptance.
- Repo-wide rename from
.grist.jsonwhere applicable. - VS Code association in
apps/docs/files/conventions.md.
- Repo-wide rename from
task-034 replica:dump CLI · [ ] · L
- Acceptance.
npx grist-widget-sdk replica:dump --doc <id>prints / writes JSON.- Auth via
GRIST_API_KEY.
Milestone 0.6 — Examples & templates
task-040 Reorg apps/playground by Guide page · [ ] · M
- Acceptance.
- One route per
/guide/page (e.g./playground/column-mapping). - Each route demonstrates exactly one concept end-to-end.
- One route per
task-041 Cross-link Guide → playground · [ ] · S
- Acceptance.
- Every
/guide/*page ends with "Live example:/playground/<page>".
- Every
task-042 create-grist-widget scaffolder · [ ] · L
- Acceptance.
npm create grist-widget@latest my-widgetworks.- Vite template with SDK preinstalled.
Milestone 0.7 — Pre-1.0 freeze
task-050 Close every open question · [ ] · M
- Acceptance.
- /design/open-questions empty or repurposed; resolutions merged into guide/api/design pages.
task-051 Bundle-size budget · [ ] · M
- Acceptance.
size-limit: < 30 KB gzipped primary, < 10 KB/advanced, < 25 KB/emulator.- CI fails on regression.
task-052 Peer-dep widening · [ ] · S
- Acceptance.
react@^18 || ^19 || ^20inpeerDependencies.- Build matrix verifies each.
task-053 Error-message audit · [ ] · M
- Acceptance.
- Every
throw new Error(...)includes method name + one-line hint; doc URL where useful.
- Every
task-054 Write MIGRATION.md · [ ] · S
- Acceptance.
packages/core/MIGRATION.mdlinked from docs workflow pages.
Milestone 0.9 — Handshake state machine (SOTA)
Foundational refactor to standardise the widget ↔ Grist handshake and synchronisation state across initialisation and runtime, with explicit robustness guarantees under degraded networks.
task-060 Pure handshake state machine — foundation · [x] · M
- Why. A single source of truth for handshake/sync state. Replaces the ad-hoc
useStateladder with a reducer + projection. - Design. Five orthogonal axes (
LIFECYCLE,LINK,AUTHZ,CONFIG,SYNC) feeding a pure reducer + status projection + capability derivation. - Acceptance.
packages/core/src/sdk/internal/handshake/ships:types,actions,mapping-resolver,reducer,derive,invariants,access,index.gristReducer(snapshot, action, ctx)is pure (no I/O, no globals).MappingResolveringests payloads in any order and produces a deterministic resolution by static source priority.deriveStatus()reproduces the legacy 4-state status;mergeGristStatusroutes throughderiveStatus().deriveCapabilities()exposescanRead/canRender/canWriteRecords/canWriteSchema/canFetchTable/hasFreshSelection/ mapping lists.- Test coverage:
handshake-mapping-resolver.test.ts— 24-permutation determinism + priority + generation drop + reset/bump.handshake-reducer.test.ts— lifecycle, link (stale/lost), generation drop, idempotence, current-table local-error containment, mapping invalidation.handshake-derive.test.ts— status projection + capability gates.
- Status. Landed — 50 new unit tests; full suite 209/209.
task-061 Effects layer (detect / negotiate / subscriptions / mappings) · [x] · M
- Why. Wire the pure machine to real Grist runtime: backoff-based detection, abortable negotiation, deterministic mapping ingestion, restart().
- Landed.
internal/handshake/environment.ts—GristEnvironmentabstraction (browser adapter + virtual-clockcreateTestEnvironment).internal/handshake/effects/detect.ts— exponential-backoff polling witheffectiveType-adaptive budget; emitsDETECT_TICK/DETECT_AVAILABLE/DETECT_TIMEOUT/NOT_EMBEDDED.internal/handshake/effects/negotiate.ts— issuesgrist.readywith timeout + AbortSignal; emitsNEGOTIATE_*actions.internal/handshake/effects/subscriptions.ts— pluggable binder (default singletons;noopSubscriptionsBinderfor tests) that feedsSTREAM_RECEIVEDwith deterministic payload hashes.internal/handshake/effects/mappings.ts— declare +sectionApi.mappings()- stream-payload ingestion +
MAPPING_TIMEOUT.
- stream-payload ingestion +
internal/handshake/manager.ts—GristHandshakeManager(snapshot store, dispatcher, listeners, per-generationAbortController,reload(),restart()).
- Tests. 14 manager unit tests with virtual time (detect timeout/backoff, negotiate timeout/failure, generation drop, capability transitions, invariants).
task-062 Public useGristHandshake() + useGristCapabilities() hooks · [x] · S
- Why. Expose
GristWidgetSnapshot+GristCapabilitiesto widgets that want to brand on fine-grained state (link lost banners, mapping prompts). - Landed.
low-level-hooks/use-grist-handshake.ts— React adapter onGristHandshakeManagerviauseSyncExternalStore; returns{ snapshot, status, error, capabilities, reload, restart }.- Exports added to
/advanced(hooks + every snapshot type).
- Tests. 3 emulator-driven integration tests in
tests/sdk/handshake-react.test.tsx.
task-063 Heartbeat (RPC coalesced) · [x] · S
- Why. Detect dead link before the user notices. Coalesce probes with any successful RPC so we never burn a request when natural traffic proves the link.
- Landed.
internal/handshake/effects/heartbeat.ts— interval probe (default 30 s) callinggrist.docApi.getDocName(), with per-probe timeout (default 10 s), visibility-aware pause,onlineevent resume, andrecordRpcSuccess()/recordRpcFailure()API for external coalescing.- Wired into
GristHandshakeManager— starts ononlinetransition, cancels on any transition away.heartbeat: falsedisables entirely. - Public
manager.recordRpcSuccess()/manager.recordRpcFailure()for future integration with the existinguseGristActions/ writes hooks.
- Tests. 11 heartbeat unit tests + 5 manager-integration tests under virtual time (stale → lost degradation, coalesce, visibility, online event).
task-064 Public <GristHandshakeProvider> + context hooks · [x] · S
- Why. Single shared manager per app tree so multiple consumers share one snapshot (vs spawning a manager per
useGristHandshake()call). - Landed.
components/grist-handshake-provider.tsx— provider + context.useGristHandshakeContext()(throws when missing) +useGristHandshakeContextOptional()(returnsnull).- Coexists with
<GristWidgetProvider>; no breaking change to legacy.
- Tests. 4 emulator-driven tests covering shared context, optional hook semantics, and the throwing variant.
task-065 Migrate <GristWidgetProvider> to the handshake manager · [x] · M
- Why. Until this task,
<GristWidgetProvider>still drove its slice composer via the pre-FSMuseGristAvailability+useGristReadychain. We want every existing widget to transparently benefit from the FSM's generation discipline, mapping resolver, and heartbeat — without changing the public API. - Landed.
internal/handshake-manager-context.ts— private React context that holds the manager owned by<GristWidgetProvider>. Not exported from the public barrel.<GristWidgetProvider>allocates a singleGristHandshakeManageron first mount (viauseRef), starts it inuseEffect, and wraps its children inGristHandshakeManagerContext.Provider. Manager options are derived from the publicUseGristOptions(requiredAccess, columns, allowSelectBy, hasCustomOptions, onEditOptions, availability tuning →detect.{pollSequenceMs, budgetMs}).- The negotiate effect now accepts an optional
readyImpl. The provider threadsensureGristReadyso a standaloneuseGristReady()mounted elsewhere on the page coalesces with the manager's ready call. manager.reload()gains anonBeforeReloadhook; the provider wires it toresetGristReadySingleton()so soft reloads actually re-issuegrist.ready(otherwise the cached promise would short-circuit).useGristCoreconsumesGristHandshakeManagerContextviauseSyncExternalStoreand projects the snapshot into the legacy{ status, isAvailable, isReady, error, docApi, reload }shape. Outside the provider, it falls back to the pre-FSM hooks byte-for-byte (no behaviour change for escape-hatch users).resetGristReadySingleton()exported alongside the existingresetGristReadyCacheForTestsalias.
- Tests. All 259 tests pass on 10/10 consecutive runs after the migration. Two pre-existing flaky integration tests (
tests/sdk/sdk-react.test.tsx,tests/sdk/column-mapping-pending.test.tsx) were tightened to wait on the fully-settled state in a singlewaitForblock, becausependingColumnMappingStatus.okistrue(nomissingcolumns reported yet) and the original single-shot assertion was racing against the transient pending-but-ok window. No regressions in slice tests, property-based tests, or the heartbeat suite. - Follow-ups. Real-widget validation (the developer will exercise the bundled
world-map,v0-minimal-demo, andcreate-email-draftwidgets before publishing). Documentation refresh tracked undertask-068. Auto-coalescence of heartbeat withuseGristActions/ writes tracked undertask-066.
task-066 Heartbeat auto-coalescence via slice hooks · [x] · M
- Why. The handshake manager runs a heartbeat that probes
grist.docApi.getDocName()every 30 s. Chatty widgets (write-heavy / read-heavy) already prove the link healthy on every RPC; the heartbeat shouldn't burn extra requests in that case. Conversely, when a real RPC fails, the link signal should degrade immediately rather than waiting for the interval. - Landed.
internal/hooks/use-rpc-heartbeat-coalesce.ts— new helper hookuseRpcHeartbeatCoalesce()that returns a stable wrapper<T>(fn: () => Promise<T>) => Promise<T>. On resolve it callsmanager.recordRpcSuccess(), on rejectmanager.recordRpcFailure()and re-throws. Reads the manager from the internal handshake context — outside a provider it's a zero-cost passthrough.- All slice hooks wired through
coalesceRpc: writes (useGristTableOpsBound:applyActions,table.*,getTable(…).*), reads (useGristReads:fetchTable,fetchTableRows,fetchRow,listTables,getDocName,buildReplicaDocumentFromDocApi,fetchSelectedTable,fetchSelectedRecord), widget options (useGristWidgetOptionsApi: all six), linking (useGristLinking:setCursorPosition,setLinkedRowSelection), section (useGristSectionApi:configure,refreshMappings), attachments (useGristAttachmentsRest:getAttachmentUrl,fetchAttachmentBase64,fetchAttachmentBlob,getAccessToken,fetchWithAuth), current table (useGristCurrentTable.refreshCurrentTable), and the mid-leveluseGristActions.apply. <GristWidgetProvider>was restructured into a two-layer component (GristWidgetContextTreeunder the manager) so the manager context is mounted before the slice composer runs. Without this,useRpcHeartbeatCoalesce()inside the slices resolved tonulland coalescence silently no-op'd. The integration testtests/sdk/handshake-rpc-coalesce.test.tsxpins that contract (writes / reads / linking / failures all reported to the manager).- Defensive
typeof grist === "undefined"guards added touseGristCurrentTableanduseGristSelectionso late passive effects (firing after a sibling test'semulator.dispose()) no longer surfaceReferenceError: grist is not defined. Pre-existing flake; fully eliminated in the 10/10 stability loop. - Unit tests (
tests/unit/handshake-rpc-heartbeat-coalesce.test.tsx) cover the success/failure/no-manager/concurrent code paths of the coalesce helper itself.
- Tests. 269 tests pass on 10/10 consecutive runs.
tsc --noEmitclean. SDK build clean. Docs build clean.
task-067 Property-based tests (fast-check) · [x] · S
- Why. Lock in race-condition invariants of the FSM with shrinkable counter-examples — beyond the 24 hand-crafted permutations, we now cover thousands of random traces per CI run.
- Landed.
packages/core/tests/unit/handshake-properties.test.ts— 12 property tests across 5 families:- Mapping resolver determinism — resolution depends only on the SET of payloads, not their arrival order; verified across 200 runs of auto-generated
(declared × payloads)pairs. - Generation gate — stale-generation payloads are never reflected (200 runs).
- Reducer idempotence on
STREAM_RECEIVED(100 runs). terminatedis absorbing under any random follow-up action (100 runs).- Fuzz / chaos — 200-run sequences of random actions never throw; status is always projectable; generation is monotonically non-decreasing;
link.statestays in domain. Plus pure derivation contracts:deriveStatustotality (300 runs), capability conjunctivitycanWriteSchema ⇒ canWriteRecords ⇒ canRender ⇒ canRead(300 runs), andincompletemappings never escalating to globalerror(100 runs).
- Mapping resolver determinism — resolution depends only on the SET of payloads, not their arrival order; verified across 200 runs of auto-generated
- Dev dep.
fast-check@^4.8.0added topackages/core.
task-068 Public docs for the handshake module · [x] · M
- Why. The new
useGristHandshake()/useGristCapabilities()/<GristHandshakeProvider>hooks landed in 0.2.0 with no companion documentation. The existing guide pages also still described the olduseGristAvailability+useGristReadychain as the FSM's implementation. Withtask-065making the FSM the default backing for<GristWidgetProvider>andtask-066cabling auto-coalescence end-to-end, the public docs needed a refresh. - Landed.
apps/docs/api/handshake.md— new API reference page. Covers when to use the handshake module vs the default surface, the provider + standalone hooks, the snapshot shape (lifecycle / link / authz / config / sync), capabilities + the conjunctive chain, heartbeat coalescence (auto via slice hooks + manual viarecordRpcSuccess/recordRpcFailure), disabling the heartbeat,reload()vsrestart(), and generation discipline.apps/docs/guide/handshake.md— new conceptual guide. Tells the five-axis story, walks through the capability chain with a small example, explains link states + heartbeat coalescence, mapping states beyondcolumnMappingStatus, generation as a discipline for custom effect code, and a "choosing your provider" decision table.apps/docs/.vitepress/config.mts— handshake guide listed under Guide → Advanced topics; handshake API ref listed in the unified Reference sidebar.- Cross-links from
apps/docs/api/index.md,apps/docs/api/provider-boundary.md,apps/docs/guide/concepts.md,apps/docs/guide/error-handling.mdto the new pages. provider-boundary.mdupdated to describe the manager-backed implementation of<GristWidgetProvider>(was previously described in terms ofuseGristAvailability+useGristReady).
- Tests.
pnpm --filter docs buildclean.
task-069 Full SOTA — remove legacy connectivity path · [x] · M
- Why. After task-065/066 the provider ran the FSM internally but
useGristCorestill fell back touseGristAvailability+useGristReadyoutside the provider; compose still appliedmergeGristStatus()(which could override FSMderiveStatusontableError); mappings were tracked twice (FSM +useGristSelectionbootstrap);CURRENT_TABLE_*actions were never dispatched from production hooks. - Landed.
useGristManager()— single entry point (provider context or embedded ref-counted manager).useGristCorealways projects the FSM snapshot.internal/build-manager-options.ts,internal/embedded-handshake-manager.ts.- Compose uses
useGristCorestatus directly (nomergeGristStatus). useGristSelection—deriveColumnMappingStatus/extractResolvedMappingsfrom FSM; narrow mapping subscription (stable slice identity on unrelated link/heartbeat updates).useGristCurrentTable— dispatchesCURRENT_TABLE_*, reads sync axis.useGristReady/useGristAvailability— thinuseGristCorewrappers.- Mid-level
useGristTableOps/useGristRowsFromTable—useRpcHeartbeatCoalesce. - Test resets:
resetEmbeddedHandshakeManagerForTestsin unit setup +renderWithGristcleanup.
- Tests. 267/267;
tsc+ build clean.
task-070 Handshake-aware boundary + alert shell · [x] · M
- Why. Blocking chrome (
<GristBoundary>) and non-blocking callouts (getGristSdkAlertDescriptors) should reflect FSM phase / link / mapping / current-table state, not only the legacy four-statestatus. - Landed.
deriveBoundaryView/deriveBoundaryBootLabel(helpers/grist-boundary-view.ts).- Extended
getGristSdkAlertDescriptors+useGristSdkAlertDescriptors(mapping-pending, mapping-unreported, link-stale, current-table-error;title/severityon descriptors). <GristBoundary>— phase-aware boot label;gate="canRender"+preparingFallback.- Template + widgets + playground
GristSdkAlertsshadcn shell updated. - Docs:
error-handling.md,provider-boundary.md.
Milestone 0.8 — Optionals
Nice-to-have work that improves rigour or DX but is not required for 1.0 exit criteria. Pick these up when the critical path (0.4–0.7) is clear.
task-013 Emulator parity audit · [ ] · L
- Why. Emulator should implement every
grist.*method the SDK calls (or fail loudly). - Acceptance.
tests/unit/emulator-coverage.test.tsiterates the documented method set.- Unimplemented methods throw a descriptive error in the emulator.
Cross-cutting / continuous
task-090 Keep CHANGELOG.md current · ongoing
- Every PR adds an entry under "Unreleased".
task-091 Lint setup · [ ] · M
- Acceptance.
pnpm --filter grist-widget-sdk lintpasses.- CI gate added.
task-092 GitHub Actions release workflow · [ ] · M
- Acceptance.
release.ymlon tag push; npm publish viaNPM_TOKEN; GitHub release body from changelog.
Backlog (proposed, no milestone)
task-100 Server-rendered status snapshot
For Next.js hosts, support a server-rendered "booting" frame that hydrates client-side.
task-101 "Form" hook
Out-of-scope per /design/principles. Tracked to acknowledge requests.
task-102 Realtime cross-table subscription
Plugin API limitation — tracked in case Grist adds row-change events for non-section tables.
How to update this file
- Add a task with
[ ] proposed, unique id, "why", design link, acceptance criteria. - Move to a milestone when accepting.
- Bump status when work starts / finishes (
[/]→[x]). - Don't delete done tasks until the minor release ships; then summarize in the changelog.