Publish and Assign

The publish wizard and its pre-publish checks, the staged publish pipeline, media internalization, component mirroring and version pinning, conflict resolution, and the assign wizard. Verified against POC build 213 (20260910T123348).

Contents

Why Publish is sometimes disabled

File ▸ Publish… is disabled while the lesson is unsaved or modified since the last save, with a tooltip explaining why. The reason is not tidiness: the catalog entry pins a specific manifest version, and that version is only knowable from a completed write. Publishing a dirty document would pin a version that does not describe what is being published.

The opened lesson’s manifest version is read from the directory listing rather than obtained by re-saving, so opening and immediately publishing does not mint a pointless version.

The publish wizard

Every decision is collected up front — space, course, catalog lesson — so the pipeline afterwards runs uninterrupted. The only reactive dialog left in the whole flow is challenge-name conflict resolution.

The model is a single realm tree: the chosen space is both the publish target and the catalog scope. That is forced by two server-side rules — the lesson listing shows lessons whose course realm the asserted realm contains, and creating a publication requires the published realm to contain the course realm.

Page Collects
1 Publish space (realm tree)
2 Course, and whether to create a new catalog lesson or bind to an existing one
3 The catalog lesson itself
4 Pre-publish checks, with detail and a one-click name auto-fix that renames and re-saves

A lesson already registered — its identity remembered in the manifest — collapses the wizard to page 1.

Page 4’s checks are the same validators that run during authoring: challenge names, choice pools, program challenges, and workbench bundles. Hazards are shown but do not block.

The publish pipeline

Seven stages, each idempotent, narrated in a progress modal that names the current stage and its detail. The stage is logged once per stage rather than per progress tick.

# Stage What happens
1 Preflight checks The page-4 validators, re-run
2 Localize media to the CDN External images, then authoring-zone images
3 Write published lesson tree Per-file progress; the tree is written before catalog registration
4 Register in course catalog Create the entry, or bind to an existing one
5 Create/update assessments Generator, comparator and workbench bundle uploads, then generated-challenges
6 Record publication One publication record binding challenges in authored document order
7 Save authoring state Media rewrites, lesson/mission binding, publication log

Details that matter to an implementer:

The wait animation suspends while any decision dialog is open, so the animation never covers a question.

Media internalization and localization

Two passes, both in stage 2:

Same-apex and relative URLs are recognized and left alone. Content-delivery URLs resolve against the front-door domain rather than being left relative.

Component mirroring and pinning

A published lesson must keep rendering even if the components catalog moves on. So publishing mirrors the myst-editor assembly into the publish realm — per realm and per publishing user — and re-pins it, with the lesson’s sourceURI naming the mirror’s explicit zone rather than the shared catalog entry.

Component assemblies are version-pinned at load time generally: the mounted component must be the version the catalog entry pins, not whatever manifest happens to be newest at that path. An unpinned assetURI degrades to latest.

Assembly URIs take the form urn:ayode:asset:{realmEID}:{userEID}:{assemblyPath}/assembly.json@{version}. Components live under the publisher’s user zone, not ~ — which is why the catalog carries both the realm and the publisher EID.

The components catalog itself is GET /v1/assemblies/components and is unauthenticated; only the payloads are JWT-protected.

Challenge-name conflict resolution

Submitting the assessment package can return 409 name conflicts. Rather than failing the publish, the Studio surfaces each conflict for an explicit decision — overwrite the existing variant, or add as new — proposes the next free name, applies any renames to the panels and sidecars, rebuilds the package and resubmits.

It retries a bounded number of times and then gives up with “Assessment creation kept conflicting after several attempts — resolve challenge names and retry”, rather than looping.

Assign wizard

File ▸ Assign… creates an assignment from the lesson’s last publication, recorded by the publish pipeline and restored with the authoring save.

Two gates, both explained in the refusal rather than as a greyed control:

Gate Message
No publication “Assigning targets a published record — publish this lesson first (Lesson → Publish…), then Assign.”
Published with no challenges An assignment delivers the publication’s challenge set, so there is nothing to assign

The second gate mirrors the server: createAssignmentV2 rejects content-only publications (400111 — no mission-backed challenge set). The gate reads the challenge count the publish pipeline recorded, falling back to the open document for saves that predate that field.

Step Collects
1 of 3 Space
2 of 3 Section — scoped to the publication’s course, since assignment creation requires the section’s course to match
3 of 3 Name (what students see), Available from, Accept until (optional late window), and Visibility

Visibility is Published — students see it immediately or Draft — hidden from students, and the wizard defaults to published: students discover an assignment through the sections listing’s nextDueAssignment, which only surfaces published assignments, so a draft is invisible by design.

Datetime fields are local-time datetime-local values.