Skip to content

feat: pluginize charts/maps/canvas via registries + editor skew safety (0205) - #204

Merged
crs48 merged 10 commits into
mainfrom
claude/pluginize-registries
Jun 19, 2026
Merged

feat: pluginize charts/maps/canvas via registries + editor skew safety (0205)#204
crs48 merged 10 commits into
mainfrom
claude/pluginize-registries

Conversation

@crs48

@crs48 crs48 commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary

Implements exploration 0205 — Decomposing the app into plugins. The thesis: pursue "as many plugins as is healthy, not as many as possible." This PR lands the safe, high-leverage slice — the Tier-2 registry seams (the cleanest answer to "can charts/canvas/maps be plugins?"), the editor schema/behavior tiering that makes "the editor as plugins" safe, a schema-skew guard, a dogfooded bundled plugin, and author docs — while explicitly not pluginizing the hot path (SQLite engine, Yjs sync loop, editor schema nodes).

Also includes the exploration document itself.

What changed

Area Change
charts ChartTypeRegistryChartKind union + buildChartOption switch → registry. resolveChartOption dispatches; unknown kinds get a graceful fallback (XChart shows a text fallback).
maps BasemapRegistryBASEMAP_PRESETS → registry. resolveBasemapStyle falls back to the offline blank basemap; LayerPanel lists the registry.
canvas ShapeRegistry — extracted pure shape-paths.ts; ShapeType switch → registry. Picker + node render via resolveShapePath/shapeTypes (fallback to rectangle).
editor extension-tiers.ts — drift-proof schema-vs-behavior classifier (reads TipTap extension.type), partitionExtensions, schemaSkewRisks, REQUIRED_SCHEMA_NODES. Dev warning when a plugin contributes a schema node/mark.
plugins editor-schema-safety.tsPluginRegistry warns in dev when a plugin's editor contribution adds persisted schema (silent Yjs content-loss vector across version skew).
web ChartsExtraPlugin — first-party dogfood registering donut + hbar via chartTypeRegistry, proving a new chart type ships as a plugin with no core change. Catalogued in registry/first-party.json (drift-guarded).
docs docs/guides/extend-with-registries.md (add a chart type / basemap / shape, the editor skew rule, the storage-adapter seam).

All registries share one shape (mirroring the existing WidgetRegistry/ViewRegistry): lazy, import-order-safe built-ins that re-populate after clear(), Disposable unregister, onChange listeners.

Why this scope

Every system that succeeded at "almost everything is a plugin" (VS Code, Obsidian, Figma, Logseq) kept rendering, the editor model, and the data engine in a hardened core. This PR pluginizes at the registry/contribution seam and keeps:

  • the database as port/adapter-shaped (already swappable) rather than a plugin (the SQLite4 trap);
  • editor schema nodes bundled — lazy-loading them silently corrupts collaborative documents under Yjs version skew.

Deferred follow-ups (documented in the exploration)

Pluggable top-level routes; re-shipping Tasks/Experiments as a FeatureModule; canvas tool/layout registries; comment inspector contribution; the full all-peers-have-schema sync gate. These are genuinely multi-PR and higher risk.

Testing

  • New unit tests: charts (6), maps basemap (6), canvas shape (5), editor tiers (6), plugins schema-safety (5) — plus existing suites still green (e.g. canvas rich-nodes 55, charts spec 6, first-party-registry drift 2). 100 tests pass across touched files.
  • tsc --noEmit clean for charts, maps, canvas, editor, plugins, and apps/web (0 errors).
  • eslint clean on all changed files; prettier --check clean on packages/** + apps/**.

🤖 Generated with Claude Code

xNet Test and others added 9 commits June 18, 2026 19:08
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert the hardcoded ChartKind union + buildChartOption switch into a
runtime registry mirroring WidgetRegistry/ViewRegistry. Built-in kinds
(bar/line/area/pie) register lazily and import-order-safe; plugins can now
contribute new kinds (gantt, scatter…) with no core change. resolveChartOption
dispatches through the registry and renders a graceful fallback for unknown
kinds (XChart shows a text fallback). ChartSpec.kind widened to an open string.

Exploration 0205, Tier 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hardcoded BASEMAP_PRESETS array as the source of truth with a
runtime BasemapRegistry. Built-in basemaps (protomaps-light/dark/blank)
register lazily; plugins can contribute new basemaps (satellite, topo, custom
tilesets) with no core change. resolveBasemapStyle dispatches through the
registry and falls back to the always-offline blank basemap for unknown ids;
MapCanvas derives pmtiles-protocol registration from the definition; LayerPanel
lists registry presets (reactive to plugin registrations).

Exploration 0205, Tier 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the pure path geometry into shape-paths.ts and add a ShapeRegistry so
the hardcoded ShapeType union + createShapePath switch becomes an extension
seam. Built-in shapes register lazily; plugins can contribute new shapes
(pentagon, cross…) with no core change. ShapeNodeComponent + ShapePicker render
via resolveShapePath/shapeTypes (reactive to plugin registration, fallback to
rectangle for unknown). createShapePath/SHAPE_TYPES re-exported for back-compat.

Exploration 0205, Tier 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The editor is already a TipTap plugin architecture; the load-bearing line is
schema (Node/Mark) vs behavior extensions. Add extension-tiers.ts: a drift-proof
classifier (inspects TipTap's own extension.type), partitionExtensions (bundle
schema statically, lazy-load behavior), schemaSkewRisks, and REQUIRED_SCHEMA_NODES.
useEditorExtensions now warns in dev when a plugin contributes a schema-defining
extension (silent Yjs content-loss vector across version skew).

Exploration 0205, Tier 2 + skew-safety.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add editor-schema-safety: classify a plugin's editor contributions by whether
their TipTap extension is a Node/Mark (persisted schema) vs behavior-only. The
PluginRegistry now warns in dev when a contribution adds schema, since a node a
collaborator lacks causes ProseMirror to silently drop content under Yjs collab.
No editor dependency needed (classifies by extension.type).

Exploration 0205, schema-skew guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dogfood the chart-type registry: ChartsExtraPlugin registers donut + horizontal
bar kinds into chartTypeRegistry on activation — proving a new chart type ships
as a plugin with no core change. Both reuse echarts components XChart already
loads (pie/bar), so the bundle doesn't grow. Catalogued in
registry/first-party.json (drift-guarded) and added to BUNDLED_PLUGINS.

Exploration 0205, Tier 3 (dogfood).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author guide docs/guides/extend-with-registries.md (add a chart type / basemap /
shape, the editor Yjs skew rule, the storage-adapter seam) and update the
exploration's implementation/validation checklists to reflect what landed vs
the deferred follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the (string & {}) open-union idiom with (string & NonNullable<unknown>)
in charts/canvas/maps to clear @typescript-eslint/ban-types, and auto-fix
import/order in the editor hook + plugins files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-204 June 19, 2026 02:38 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

#204)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-204 June 19, 2026 02:39 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #204.

github-actions Bot added a commit that referenced this pull request Jun 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Interactions

🎬 Open a canvas

Open a canvas

▶ Watch MP4

🎬 Create a page and use the editor

Create a page and use the editor

▶ Watch MP4

Auto-captured by CI · run. Informational — not a blocking check.

github-actions Bot added a commit that referenced this pull request Jun 19, 2026
@crs48
crs48 merged commit 3ef5114 into main Jun 19, 2026
17 checks passed
@crs48
crs48 deleted the claude/pluginize-registries branch June 19, 2026 02:46
github-actions Bot added a commit that referenced this pull request Jun 19, 2026
crs48 added a commit that referenced this pull request Jun 19, 2026
…205)

## Summary

The public **/plugins** page (and the in-app marketplace) still showed
only 3 plugins after [#204](#204),
even though that PR added `fyi.xnet.charts-extra` to
`registry/first-party.json`.

**Root cause:** the site reads the committed flat index
`registry/registry.json` (via `site/src/data/plugins.ts`), which is
regenerated from `first-party.json` + `community.json` by
`scripts/build-plugin-index.mjs`. The `Plugins Registry` workflow's
`rebuild` job fired on the merge to main and produced the correct index
— but its `git push` was **rejected by branch protection** (`GH013:
Changes must be made through a pull request`; `github-actions[bot]` is
not a ruleset bypass actor). So `registry.json` was never updated.

This PR lands the regenerated index through a normal PR.

## Change

- `registry/registry.json` — regenerated: **4 built-in + 0 community**
(adds the Extra Charts entry). `node scripts/build-plugin-index.mjs
--check` passes. `revoked.json` unchanged.

After deploy, https://xnet.fyi/plugins/ will list 4 plugins (a new
"visualization" category filter appears).

## Note on the systemic gap

This will recur for every future `first-party.json` change: the
auto-rebuild bot can't push to `main`. Fixing it properly means making
the rebuild job's committer a ruleset **bypass actor** (the same fix
applied to the changelog PR-stamping App in exploration 0203), or having
the job open a PR instead of pushing. Out of scope here — flagged for
follow-up.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant