Skip to content

Core / engine public API contracts

This guide defines the shared contracts used by framework adapters. The documented public APIs follow semantic versioning from 3.0.0; breaking changes require a new major version. Application authors normally use a framework package; adapter authors can use the engine and core interfaces described here. See the package READMEs for installation and the core testing guide for behavioral validation.

Engine supplies syntax, tree transformations, incremental algorithms, reference registries, and streaming controllers. Core supplies reusable sessions, block planning, contribution preparation/publication, aggregate footnotes, and turn-taking coordination. React and Vue explicitly depend on both core and engine; UI integrations depend on their corresponding framework adapter.

Core does not re-export the entire engine API or accept ReactNode, VNode, DOM elements, or framework lifecycles. Adapters own tree-to-component conversion, subscription/unmount timing, SSR hydration, slots/context, and cursor measurement. See the Vue README for implementation and usage.

SurfaceDecisionReason
Engine root export *Replace all star exports with explicit exportsAdding a source helper must not silently expand the supported API
PipelineSession / PipelineTreesAdd named types; factories explicitly return PipelineSessionFix the parse/reset contract without letting declarations grow with implementation objects
ContributionSessionAdd a named publication interfaceConsumers receive only the commit capability
BlockPlannerAdd a named function typeDistinguish the stateful factory from its per-frame planning call
isEnginePluginAdd a public configuration guardReact/Vue can validate catalog objects without reading internal stages
getEnginePluginInternals / EnginePluginInternals / EnginePluginStageRemove from the root; retain internallyStages are pipeline implementation details; the replacement exposes a boolean decision
codePointSnapshotsRemove from the root; development stories import sourceTest/demo frame generation is not a production adapter contract
attributeHastChildrenRemove from the root; retain inside the algorithmIncremental parsing owns HAST attribution
SENTINEL_FN_CONTENT / SENTINEL_LINK_URLRemove from the rootAdapters should not construct or match phantom protocol constants
Extra preprocessor array in preprocessAIMDContentAccept readonly arraysThe implementation only iterates over inputs and need not require mutability
Block digest/fingerprint helpersRetain as advanced APIsReact’s cache uses them; validity still depends on tree and reference policy
computeFreezeBoundary and stage timingRetain as advanced diagnosticsDevelopment tools use them; no promise of fixed performance values or log bytes

These changes were made during the prerelease series and are part of the stable 3.0.0 API. When upgrading from beta.1, use the documented public entries rather than private dist paths. React component, hook, plugin, and CSS public paths retain their existing shape. The existing brand string inside plugin objects remains unchanged so an organization rename alone does not change the semantics of deployed configurations.

CapabilityInputs and outputState and release
createPipelineSessionPipelineFrameOptions → PipelineTrees; synchronous parsingOne session per chunk; reset discards retained state; dropping the host reference releases it without timers or global registration
createBlockPlannerFull MDAST/HAST, preprocessed source, phantom hints → planRetains one consumer’s previous frame; replacing the planner resets its cache; retained trees must not be mutated
derivePhantomTargetsOwn definitions, registry labels, source → missing targetsPure preparation; previous is only a snapshot-identity hint for this consumer
deriveCoordinationPolicyCoordination, registration, orphan policy → handler/harvest policyNeither registers nor publishes
buildContributionChainPipeline configuration and prefix → identity tupleCovers parse policy; source equality alone cannot establish contribution validity
createContributionSession().commitCommitted trees, labels, symbol, write capability, identity tupleCall only after host commit; the host owns registration/release
buildAggregateTreeRegistry, prefix, orphan policy → footer HAST or nullRender only for the last eligible chunk; never write results into shared bodies
cloneHastForRenderHAST node → structural cloneClones nodes/children/properties/data and originalUrls; other nested plugin data remains shared
createSmoothCoordinatorOptional onEmpty → SmoothCoordinatorPair register/release; defer cleanup to a microtask; done stays sticky within a registration lifetime
deriveTailSignalMDAST and actual preprocessed source length → source-tail factsDoes not read DOM; the adapter chooses measurement and presentation

Treat parse/preparation results as borrowed readonly values even where underlying HAST types contain mutable arrays. Clone before changing trees or properties. The shared layer neither deep-freezes every node nor promises fresh identity for every object; adapters must not communicate by mutating snapshots.

An incremental-path failure clears retained state and attempts a full parse. If the full pipeline also fails, the error still propagates synchronously; fallback does not swallow all invalid configurations. incrementalParse=false also clears retained state and suits one-shot SSR. The server does not need browser detection to make this choice.

Block keys match logical positions; they do not guarantee cache validity. URL policy, registry, component, and tree-identity changes may still require reconversion. Some scanning/planning remains O(blocks) or O(document); updates are not guaranteed to cost only in proportion to newly appended characters.

createRegistry() returns RegistryController. Its read side, Registry, exposes versions, readonly index snapshots, global/per-label subscriptions, and resolution selectors. The write side provides paired registration/release and contribution methods. Internal reference counts, subscriber containers, and notification functions do not appear in the public return type.

Every registerChunk(chunkId, ...) requires one releaseSymbol(chunkId). A chunk may register again before deferred cleanup, but pairing is still mandatory. onEmpty executes in the final-release microtask. A containing map must check that its current entry still points to that registry before deleting it, avoiding removal of a newer instance.

Call receiver-dependent methods as registry.method(); wrap or bind them when passing callbacks. Unmount must unsubscribe. Notifications may coalesce changes with no net effect, so consumers read the current snapshot rather than deriving semantics from callback counts. A URL selector alone cannot observe every aggregate-body change.

ContributionRegistry deliberately exposes only contributeChunkData; the shared publisher cannot take over host lifecycle management. Keep ASTs, registries, and coordinators in shallow Vue references. Deep reactive proxy identity is not the engine’s original node identity.

Adapters create one provenance value per session and pass it to both rehype verification and cross-chunk handlers. Forged engine tags originating in Markdown must not be interpreted directly as internal framework components.

Apply buildTransform’s final URL policy to ordinary HAST before framework conversion. Cross-chunk references acquire their destinations later and must pass through resolveCrossChunkReference for sanitization, hash rebasing, and per-attribute urlTransform. Simply attaching the registry’s raw URL is insufficient. keepChildren distinguishes sanitizer unwrapping from removal of an element together with its children.

sanitizeSchema is a shared readonly default; extendSanitizeSchema creates an independent draft. New object/function identity signals configuration changes to adapters. In-place mutation of a schema already in use is unsupported. Custom framework components remain application code: the Markdown sanitizer does not inspect their output again.

The engine controller manages the visible prefix of one source; the core coordinator decides when a chunk receives its turn.

  • Initial frames and source replacements snap; confirmed appends reveal by grapheme.
  • Finish confirms the final grapheme and drains backlog; later updates can resume streaming.
  • Flush still respects active-stream grapheme hold-back.
  • The controller options object deliberately provides live configuration. Adapters update pacing while retaining object identity.
  • Schedulers must invoke callbacks asynchronously and return cancellation functions. Synchronous invocation violates the contract.
  • Dispose cancels frames and clears subscriptions, but later calls can reactivate the controller. Do not call it after actual unmount.
  • Coordinator completion is sticky. Use a new registration identity when subsequent messages must queue again.

After building, run pnpm check:public-api. The script compares complete declarations, with comments removed, against the engine, core, React, React plugins, Mantine, and Vue snapshots. It rejects private registry/coordinator types, local node_modules paths, and framework dependencies in shared layers, and checks that root entries have no star exports. Review signature changes before running node scripts/check-public-api.mjs --update.

Snapshots are not semantic proofs. Unit tests cover lifecycle and immutability contracts; real browsers cover Vue’s three complete adapter paths. Packed consumers install tarballs outside the workspace and separately exercise ESM/CJS, dev/prod, SSR, and TypeScript. Engine-impacting releases require validated local soak evidence and manual release approval; see soak coverage for the impact policy and evidence reuse rules. Engine soak coverage must not be mistaken for core or adapter lifecycle coverage.