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.
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.
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.
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
sourcestores shared state in authored Markdown and topmatter.actorstores acompletedkey 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.
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.
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.
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.
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.
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.