Skip to content

docs: plugin materialization / system-plugin replaceability design#289

Open
Stvad wants to merge 2 commits into
masterfrom
claude/plugin-materialization-design-2t71dk
Open

docs: plugin materialization / system-plugin replaceability design#289
Stvad wants to merge 2 commits into
masterfrom
claude/plugin-materialization-design-2t71dk

Conversation

@Stvad

@Stvad Stvad commented Jul 1, 2026

Copy link
Copy Markdown
Owner

What

A design proposal (docs/plugin-materialization.md) for the question from the voice notes: how does somebody replace a system plugin with their own version, such that everything that relied on the original keeps relying on the replacement? — and whether to materialize system plugins into the DB (all up front vs on demand).

Core argument

The two hypothesized abstraction layers (module system with override vs materialize-into-DB) are separable, and most of both already exist:

  • User extensions are already materialized (type:'extension' blocks → Babel → blob import), with synced intent + device-local approval, and the importmap ("@/": "./src/") + preserveModules build means a materialized copy imports the same live module instances the app uses — no walled garden.
  • The runtime substrate is already string-keyed (actions, facet buckets by facet.id, mutators/queries, renderer ids, type membership, deterministic block ids) — those references retarget for free. The irreducible residue is static cross-plugin imports of behavior (daily-notes has ~10 importers of getOrCreateDailyNote et al.), which produce split-brain no materialization scheme fixes.

Proposal (staged)

  1. Eject & replace on a restaged boot — kernel → materialize → assemble. Materialize a plugin's authored source (recoverable from shipped sourcemaps' sourcesContent, zero build changes) into an extension block carrying extension:replaces: 'system:<id>'. Boot is restaged so stage 1 instantiates enabled+approved extension blocks from their pinned compiled output (no Babel on the boot path — approval pre-compiles), and stage 2 assembles ONE unified plugin set with substitutions applied at an id-keyed manifest before anything runs: the replaced original never executes, and materialized plugins get full schema-contribution parity (types/schemas/processors/localSchema from the same lifecycle position as static plugins). Substitution requires enabled + approved + instantiated on this device — so unapproved devices, instantiation failures, and safe mode all fall back to the built-in instead of running neither. Default eject output is a wrapper (recompose the original's imported parts) with progressive inlining toward a fork.
  2. Seam hardening — promote cross-plugin behavior imports to string-keyed seams (mutators/queries/verbs/variants), ranked by importer count; forks then win by the existing last-wins registry order (verified: runtime buckets append after static contributions). Includes retiring composition-by-import of other plugins' data extensions in favor of manifest requires edges.
  3. Gated: SW module-URL override — byte-level replacement of any /src/** module, extending Stable /module/<blockId> URLs for user extensions (enables cross-block plugin imports) #253; new boot-path bytes-at-rest problem documented; explicit trigger discipline, not built now.
  4. Parallel: declaration materialization — seed block-type/property-schema blocks from system definitions via the existing definition-block projector (Data-defined contributions over facets — projector-shim pattern #90); shadowing works today by construction.

Plus: upgrade/divergence tracking via base-version/base-hash provenance, trust-model analysis (no new execution paths; a synced malicious replaces block can't run or suppress anything without device approval), rejected alternatives (materialize-all, blessed auto-approval tier, importmap remapping), an implementation sketch (manifest unification and boot restage as independently landable first steps), and open questions (toggle-id stability as public API, boot-blocking policy for additive extensions, base-source storage, #253 trigger).

Notes

  • Doc carries the standard status banner, verified against code 2026-07-02.
  • Revision history: the first draft accepted a flash-of-original and a "forks must keep data extensions identical" constraint (replacement delivered via the async dynamic-resolve swap); review feedback replaced that with the restaged boot above, which dissolves both.
  • One stale-claim observation from research: docs/extension-seam-gaps.md (2026-06-21) states defineVerbFacet is unbuilt, but src/facets/verbFacet.ts + the paste/navigation/action-dispatch verbs have since shipped and are exported from api.ts — that doc's headline finding is now partially outdated (not touched in this PR).

🤖 Generated with Claude Code

https://claude.ai/code/session_01DRfy4BX6EUBSoQS6rG8vUE

Proposes how a user replaces a built-in plugin with their own version
while dependents keep working: on-demand "eject" of plugin source into
the existing extension-block pipeline, a first-class replaces relation
against system toggle ids (position-preserving substitution, built-in
fallback on unapproved devices / compile failure), staged seam
hardening for cross-plugin behavior imports, and a gated SW module-URL
override stage extending #253. Parallel track materializes system
type/property definitions via the existing definition-block projector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRfy4BX6EUBSoQS6rG8vUE
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

…ssemble

Per review: the v1 model (static paints first, replacement swaps in via
the async dynamic resolve) made ejection second-class — the original ran
before being displaced, and forks were barred from changing their data
extensions because staticDataExtensions installs pre-React.

Revised model: stage 0 loads the kernel floor (DB, approval store,
overrides), stage 1 instantiates enabled+approved extension blocks from
their pinned compiled output (no Babel on the boot path — approval
pre-compiles), stage 2 assembles ONE unified plugin set with
substitutions applied at an id-keyed manifest before anything runs.
Materialized plugins gain full schema-contribution parity; the
flash-of-original and frozen-data-extension caveats disappear, replaced
by narrower constraints (idempotent localSchema, dragged-composition
pruning, per-workspace scope, boot-blocking policy). Manifest
unification is promoted from deferred to Stage-1 prerequisite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRfy4BX6EUBSoQS6rG8vUE
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.

2 participants