Completion

Declare completion targets and show progress for Markdown Sources.

Completion owns target membership and flat progress UI. Path, Input, Assessment, and Work keep their own hierarchy and policy.

Before saving personal work

Saving a response, progress, or an attachment requires an admitted Actor and a writable Home. Use the local account setup for development, then configure credentials and persistent writable storage for shared hosting. A browser publication provides a local Actor and writable Home on one device without sign-in; its saved files do not synchronize to a server. An ordinary static export does not provide a writable Source host. Disposable drafts and device settings are separate from committed work.

The Home directory's canonical index Source owns its privacy claim with access: { type: home, owner: /canonical/actor.id }. The Actor Source itself does not carry a home field. State writers resolve the current claim through the installed access policy, so a missing or Home-free Actor cannot create personal State accidentally.

Save a personal checklist

Start with the local Ada account and writable Home in an initialized project. Install the package:

bun add @pathmx/completion@beta

Create plugins/completion.plugin.ts:

import { createCompletionPlugin } from "@pathmx/completion"

export default createCompletionPlugin({ storage: "actor" }).plugin

Create paths/preparation.page.md:

# Prepare your workshop

- [ ] Choose a topic {#topic}
- [ ] Write a worked example {#example}
- [ ] Rehearse the lesson {#rehearse}

<x-completion-progress />
<x-completion-outline />

Run bun run dev, sign in as Ada, and open /preparation.page. The initial progress is 0/3. Check “Choose a topic”: it becomes 1/3. Reload and confirm that progress remains. Uncheck it to return to 0/3. Ada's Home stores the record; the authored checklist stays unchanged. Signed-out readers cannot change her progress.

For an offline browser publication, keep the same Plugin and configure its local Actor and writable Home. An ordinary static export has no writable Source host. If controls are read-only, check admission and storage before trying to change the checklist.

Configure storage

Install @pathmx/completion@beta in the project root with bun add. Completion requires one explicit storage policy. To switch the preceding example to shared authored progress, replace its Plugin file with the following and grant the acting reader write access to the checklist Source through the Source access policy:

// plugins/completion.plugin.ts
import { createCompletionPlugin } from "@pathmx/completion"

export default createCompletionPlugin({ storage: "source" }).plugin
  • source stores shared state in authored Markdown and topmatter.
  • actor stores a completed key list below each Actor Home.

Both policies expose the same asynchronous View-bound reader. A browser Source host uses storage: "actor" with its local Actor and writable Home; progress is stored in ordinary State Sources.

Author targets

Every Markdown task is a completion target; an id is optional:

- [ ] Install dependencies
- [ ] [Review the guide](./guide.md)
- [ ] Ship the release {#ship}

Identity uses an explicit {#id}, then one exact local Source/fragment link, then normalized visible text. Duplicate identities are lint errors.

Removing a target leaves its saved Actor completion key as history. Lint reports completion/state.target-missing as a warning; current progress counts only declared targets, and new completion writes to a removed target are rejected. You do not need to edit learner records to remove an obsolete task. Reusing the same key reconnects its saved completion, so give a different task a new id. Malformed declarations, invalid saved state and missing origin Sources remain errors.

The presence of completion declares a Source target:

---
completion:
  label: Finish this lesson
---

It declares a Block target when that Block has an authored id:

<!--
id: practice
completion:
  label: Finish the practice
-->

Source storage changes task [ ]/[x] or the target's complete field. Actor storage does not rewrite authored content.

Progress UI

Layouts and Sources can render the represented Source's flat projection:

<x-completion-progress />
<x-completion-outline />

Progress renders a short native meter with a compact count such as 2/3. Outlines use aligned status indicators; authorized manual requirements have checkboxes, while derived and read-only requirements remain indicators. Manual controls submit existing Completion Actions. Without JavaScript, a labeled submit button provides the same action.

Both components omit visible headings by default. Supply label="Requirements" to show a label (progress) or heading (outline). Progress retains an accessible count and outlines retain an accessible name when label is omitted. Sources with no requirements render neither component.

Presentation uses Core's .pmx-progress, .pmx-progress-count, .pmx-task-row, and .pmx-task-status styles. Existing .pmx-completion-* classes and status attributes remain available for local layout overrides.

Target another Source or Block

Both components accept a source reference to another Source or an exact Block:

<x-completion-progress source="./project.project.md#practice" />
<x-completion-outline source="#practice" />

Omitting source uses the represented page, including in a layout. An explicit reference resolves beside the authoring file. A fragment must identify a Block, not an arbitrary heading. Cross-Source outline links and manual Actions point at the selected Source and respect its permissions.

Block summaries count requirements associated with that Block. Source summaries count all requirements once, including requirements without a Block. Neither summary adds a target or stores separate rollup state. completion: on a Source or Block still declares an independent manual requirement, not rollup settings. Provider block values must identify existing Blocks. Single-Block Input responses carry their Block; a response spanning Blocks remains Source-scoped.

Read progress in a Plugin

The same reader supports Block scope through a canonical reference:

await completion.reader.summary(view, { id: "/project.project#practice" })

To present the requirements themselves, read their items:

const items = await completion.reader.items(view, { id: "/project.project?iid=retry" })
// [{ ref: { source: "/project.project?iid=retry", key: "task/submit" },
//    label: "Submit your project", block: "block-0",
//    interaction: "manual", complete: false, settable: true }]

items() accepts the same Source, instance and Block references as summary() and returns the leaf requirements in authored order. settable is true when this View may change the item with completion.set. Derived items and unreadable Sources are never settable; Source storage cannot set a named instance, because its instances share the authored file. A control posts the item's ref as _target and key to /actions/completion.set.

Custom components read this same View-bound summary during rendering. Readers admit their target and backing State Sources through the active View, so contextual restrictions and query invalidation also apply to derived completion. The normal View refresh updates their markup after Actions and live changes.

Authored task checkboxes are also projected as read-only or actionable controls for the current View. Derived targets remain read-only and transition through their owning UI.

Composition

Use this composition in place of the preceding Completion Plugin when Input submission should also contribute progress. view and source in the reader calls come from your host or component render context; the calls illustrate that API and are not additional top-level project setup. Manual and derived domains use separate seams:

import { createInputPlugin } from "@pathmx/core/plugins"
import { createCompletionPlugin } from "@pathmx/completion"

const input = createInputPlugin()
const completion = createCompletionPlugin({
  storage: "actor",
  derived: [input.completion],
})

await completion.reader.isComplete(view, {
  source: "/lessons/setup.page",
  key: "task/install",
})

await completion.reader.summary(view, source)
// { status: "in-progress", complete: 2, incomplete: 1, total: 3 }

targets adds manual declarations for Actor storage. Source storage takes its manual declarations from authored tasks and Source/Block completion props so each target has a direct mutation representation. derived works with either storage policy and adds declarations plus isComplete(view, ref), which may return a boolean or a Promise, while its owner retains the backing state.

summary() projects authored tasks, Source/Block completion, manual provider targets, and derived provider targets through the same reader. It returns status: "none" when a Source declares no targets; an empty target set is not implicitly complete. Contributors should declare leaf requirements rather than rollups that double-count their own children.

reader.manualTarget(view, { id }) returns the unfinished, directly authored Source/Block acknowledgment when all other requirements in that scope are complete. It returns no target for derived requirements or ordinary task-only Sources. The returned { source, key } retains instance identity. Before presenting a control, find that item in items() and check settable.