Formats and Contracts

Everything another program has to agree with: the editor component’s public API and events, the Studio SDK, the callout-* class token, the authoring file layout and its sidecars, content normalization and hashing, and the components catalog. Verified against POC build 213 (20260910T123348).

Contents

Component public API

Registered synchronously on mount, so the host can drive the component immediately.

Method Purpose
getContent() / setContent(s) Read/replace the source. setContent resets history and closes any find session
undo() / redo() Programmatic history navigation
openFind() Open the find bar (same guards as Ctrl/Cmd+F)
cut() / copy() / paste() Clipboard operations on the selection
replaceRange(start, end, text) Replace a span — one undo step
insertAfter(pos, text) / insertBefore(pos, text) Block insertion relative to the line containing pos — one undo step
wsEdits(text) Whole-document tidy edit spans. See below
setStudentMode() / setInstructorMode() Switch view modes
openGlossaryDefine(term, definition) Open the glossary define dialog, prefilled
getPreviewScroll() / setPreviewScroll(y) Preview scroll persistence (used by glossary back-to-reading)
scrollPreviewToTerm(text) Scroll the preview to a <dt> or heading matching text (whitespace-collapsed, case-insensitive) and flash it; returns whether one was found

Two registration names

root.ayodeMystComponent = { … };
root.ayodeComponent = root.ayodeMystComponent;

ayodeComponent is the uniform name shared by all component types and is what the canvas prefers; ayodeMystComponent is retained as a fallback for published builds that predate the alias. New host code should use ayodeComponent.

wsEdits contract

Returns [{start, end, text}] edit spans, and its three-state return is the contract:

Return Meaning
null The AST bundle has not loaded — protected spans are unknowable. Callers must skip, never guess
[] Already normalized
spans Apply right-to-left; they never overlap by construction

It is whole-document only; selection-scoped tidying stays internal to the menu command. This is the single shared oracle behind the menu command, the marker layer and the host’s normalization pipeline — which is why they can never disagree. Its scope is wider than its name: see what the tidy pass actually removes.

Component events

Both bubble.

Event Fired
contentchange On every content path — typing, menu commands, undo/redo, AI results, programmatic edits. Carries detail.source
componentrendered After each completed preview render cycle
aitunerequest When an Intelligence operation is chosen. See ĀYŌDÈ Intelligence Operations

contentchange being the single funnel for every content path is what keeps the find bar’s match counter in step with programmatic edits.

The Studio SDK

window.AyodeStudioSDK is the standardized interface the shell exposes to every mounted component. Components must consume only this surface, never shell internals; hosts without it — Student Lab, published lessons — degrade gracefully.

Four members at version: 1:

glossary

Member Purpose
defineTerm(term, definition) Create/update an entry; the first term auto-creates the Glossary panel, then term-ifies all content panels
linkTerms() Re-run term-ification. Resolves true iff any panel source changed. Also runs on content-panel focus loss and is awaited before every save and publish
getTerm(term) Definition lookup; null when absent
getTerms() [{term, definition}]
subscribe(cb) Change notification; returns an unsubscribe function
openTerm(ref, {previewScroll}) Navigate to an entry, remembering where the reader came from
goBack() Back-to-reading: originating panel plus exact preview scroll

assessment

Member Purpose
hasAssessmentChild() Whether the active panel already has a child panel titled assessment

challengeShell

Member Purpose
listChallenges() [{ref, title, program}] for the current lesson

getAccessToken

How mounted components resolve their JWT. The Merlin Terminal’s fallback chain tries root.__ayodeAssemblyContext first, then this hook, then vendor/dataset sources.

Both hasAssessmentChild() and listChallenges() are called synchronously, at context-menu-build time — they decide a menu label’s verb and an entry’s enabled state respectively. An implementation that cannot answer immediately will produce a wrong menu, not a late one.

Design invariants behind the glossary surface

The callout class token

callout-<slug> on an admonition’s :class: is the single contract for styling, cursor detection and menu state, across the editor preview, Student Lab and published lessons.

Consequences that follow from it being the sole predicate:

Authoring file layout

Authoring is file-only: the complete lesson, including every assessment, is reconstructable from this tree alone, with no database round-trip. Each panel is a self-contained subdirectory.

<baseDir>/<Lesson>/
  <Lesson>-authoring.json                     panel tree + order + identity
  panels/<Panel>/
    <Panel>-authoring.json                    panel manifest
    components/<Panel>-authoring.md           content (myst / mission panels)
    <Panel>-assessment-authoring.json         challenge panels — the sidecar
    <Panel>-grader-authoring.json             program challenges — generator source

Panel directory names are uniquified case-insensitively (Panel, Panel-2, …), so two panels with the same title cannot collide.

The lesson manifest carries {version: "1", name}, plus:

Field Contents
identity lessonEID, lessonVariantEID, missionEID, lessonRealmEID — the realm asserted for lesson-domain calls
lastPublication The publication record the assign wizard targets

Sidecars, and what publishing never emits

Two authoring-only files sit beside a challenge’s manifest, and both are structurally excluded from publishing — the publish walker never reads them.

Sidecar Holds
<Panel>-assessment-authoring.json The question MyST, accepted answers / evaluation config, and assessment metadata (name, required flag)
<Panel>-grader-authoring.json The generator files verbatim, plus runtime, comparator and submission settings

This is a security property, not a packaging convenience. Because the publish walker never emits these files, questions, answer keys and generator sources cannot be reached through published files at all. Students receive challenge content only through the gated assignment-challenge delivery endpoints, and a generator reaches the grading job only as an uploaded, digest-addressed bundle.

The sidecar’s variant block is shape-identical to a generated-challenges assessments[].variant, so publish packaging is a projection rather than a transformation — one less place for the two shapes to drift.

Panel manifests

{version: "2", name}, with kind present only for non-myst panels, plus a components array.

For a challenge panel:

{ "version": "2", "name": "…", "kind": "challenge", "challengeRef": "…",
  "components": [
    { "name": "assessment-1", "type": "assessment-authoring", "assessmentRef": "…" },
    { "name": "grader-1", "type": "grader-authoring", "graderRef": "…" }
  ] }

For a content or mission panel the single component is {name: "myst-1", type: "myst-editor", sourceURI, contentRef, fullPanel: true, zIndex: 1, position: {left: 0, top: 0}, size: null}.

challengeRef is authored and minted once, never regenerated — published artifacts and workbench directories are addressed by it.

Content normalization and hashing

Challenge dirty detection hashes a canonical tuple. Two layers:

MyST normalization — deliberately conservative, and the same rules the backend applies server-side, so local and server hashes agree:

Nothing touches interior whitespace, because indentation is significant inside MyST code blocks and directives.

Canonical JSON — object keys sorted at every level; arrays keep their order, because order is meaningful.

SHA-256 over that canonical tuple (normalized question, strategy, locale and the evaluation configuration) gives the local hash.

The server’s contentHash is authoritative. The local hash only powers the cheap dirty gate that avoids pointless variant creation; a local/server mismatch after a variant create is surfaced as a canonicalization-drift warning rather than silently accepted.

Note the interaction with the tidy oracle: saving normalizes panel sources first, so paragraph unwrapping and HTML stripping change the bytes that are then hashed. That is intended.

Assembly URIs and the catalog

urn:ayode:asset:{realmEID}:{userEID}:{assemblyPath}/assembly.json@{version}

The three assemblies the Studio resolves by name:

Catalog name Path fragment
Merlin MyST Editor assembly-components/merlin/content/myst-editor
Merlin Challenge Config …/challenge-config
Merlin Terminal resolved by name for preview mounts

PUBLISHED_CHALLENGE_BODY is a shared cross-POC contract; asset write responses carry the stored version in a flat body ({basename, extension, realmEID, version}).