[@layout]: ../docs.layout.md

# Core API

Use the Bun adapter to host a PathMX repository programmatically. For ordinary
site authoring, the [CLI](../cli.reference.md) supplies this setup.

Choose the job you need:

- [Run a host](#run-a-host) or provide a [portable environment](#portable-composition).
- [Connect a browser](#view-connections) and handle reads, Actions, and live updates.
- [Export static routes](#static-publication-artifacts) or [publish a writable browser app](#browser-publications).
- [Write Plugins](./plugins.guide.md), [configure packages](./plugin-configuration.reference.md), and generate [derived assets](#source-owned-derived-assets).
- [Set Source access](#choose-the-source-access-policy) or narrow a [contextual View](./views.reference.md).

---

## Run a host

Use an initialized [Quickstart project](../start/first-site.guide.md) with its
welcome page. Create `host.ts` in the project root:

```ts
import { PathMX, handleRequest } from "@pathmx/core/bun"

const app = await PathMX({ root: "./paths" })
const server = Bun.serve({
  hostname: "127.0.0.1",
  port: 3000,
  fetch: (request) => handleRequest(app, request),
})
console.log(`Open ${server.url}`)

const stop = async () => {
  server.stop()
  await app.close()
  process.exit(0)
}
process.once("SIGINT", stop)
process.once("SIGTERM", stop)
```

Run `bun run host.ts` and open `http://localhost:3000/`: it serves the authored
welcome page. `curl -I http://localhost:3000/api/runtime/health` returns `204` for
a healthy repository. Stop with Ctrl+C to close the application. Choose another
port if 3000 is occupied. Installed production Auth requires its configured
credentials; see [Authentication](../accounts/auth.reference.md).

`PathMX()` supplies the Bun environment, default plugins, and repository plugin
discovery. Host lifecycle owns shutdown and resource cleanup.
Expected authored Source errors are collected by default; inspect
`app.sourceDiagnostics()` for their details. Invalid Sources and their affected
consumers remain unavailable while unrelated content works. See
[Source recovery](../authoring/sources.reference#invalid-sources-and-recovery).
Use `PathMX({ root: "./paths", diagnostics: "throw" })` when a validation caller
needs startup to fail on the first authoring diagnostic. Storage and host failures
still throw in either mode.
Define validated domain operations with the [Action API](./actions.reference.md).
Trusted plugin contexts also provide `app.view(session)`; invoke Actions through
that admitted View. Server routes use `ctx.view(await ctx.session())`. They can
[serve a mounted contextual View](./views.reference.md#bind-context-in-the-trusted-host)
with `ctx.serve(view, { basePath })`. Trusted hosts
can select a [domain context](./views.reference.md) with
`app.view(session, { context: sourceId })`.

---

## Portable composition

The package root exposes runtime-neutral contracts. A host supplies the environment
and selects plugins explicitly:

```ts
import { createApp } from "@pathmx/core"
import { bunEnvironment } from "@pathmx/core/bun"
import { defaultPlugins } from "@pathmx/core/plugins"

const app = await createApp({
  root: "./paths",
  environment: bunEnvironment,
  plugins: defaultPlugins,
})
// Await app.close() when this host is finished.
```

The Bun environment is one adapter. Keep runtime-specific setup at the host
boundary. The core foundation supplies Source access, metadata, schema, queries,
and Blocks; explicit plugins add behavior. Plugin code uses the provided contexts
rather than importing internal engine or registry modules.

---

## Atomic Source storage

A custom `PathMXEnvironment` can supply `storage.commit` to persist all Source
changes from one Action atomically:

```ts
import type { PathMXEnvironment } from "@pathmx/core"

type Commit = NonNullable<PathMXEnvironment["storage"]["commit"]>
// commit([{ path: "/reader/notes.md", before: "# Notes\n", after: "# Updated\n" }])
```

Each change contains an
adapter storage `path`, expected `before` text, and replacement `after` text.
`null` means the file is absent; an empty string is a present file. Core validates
the full Source change set and supplies each path once, with its final contents.

The adapter must compare every expected value and apply every replacement in
one atomic operation. Reject on a conflict or storage failure with all files
unchanged. Resolve only after the complete batch is durable. Core then refreshes
the graph and continues the Action lifecycle. Plugins keep using the existing
[Source transaction API](./actions.reference.md).

Without this optional capability, core uses the individual storage write/delete
methods. The Bun filesystem adapter uses that sequential behavior: an error can
leave a partially persisted batch. A method that merely loops over file writes
does not satisfy the atomic `commit` contract.

---

## Folder move storage

`storage.moveTree(move, changes, options?)` supports semantic folder moves.
`move` contains adapter paths `root`, `from`, and `to`, plus a `files` inventory
relative to `from`. `changes` has the same `path`, `before`, and `after` text
shape as `storage.commit`. Preconditions describe storage before the move;
replacements describe the final tree. Sources being relocated appear as an old
path deletion and new path creation. Assets travel with the tree.

```ts
import type { PathMXEnvironment } from "@pathmx/core"

async function previewFolderMove({ storage }: PathMXEnvironment) {
  if (!storage.moveTree) throw new Error("Folder moves are unavailable.")
  await storage.moveTree(
    {
      root: "/project/paths",
      from: "/project/paths/old",
      to: "/project/paths/new",
      files: ["a.md", "portrait.jpg"],
    },
    [
      { path: "/project/paths/old/a.md", before: "# A\n", after: null },
      { path: "/project/paths/new/a.md", before: null, after: "# A\n" },
      {
        path: "/project/paths/index.md",
        before: "[A](./old/a.md)\n",
        after: "[A](./new/a.md)\n",
      },
    ],
    { dryRun: true },
  )
}
```

The adapter must validate the Source-root boundary, exact inventory, absent
destination, and expected text before mutations. With `dryRun: true`, it performs
validation only. Apply must either complete the move and text changes or restore
the original tree and texts on a handled write failure; an incomplete recovery
must fail explicitly. Binary data must retain its bytes. Planning requests
`storage.list(root, { includeHidden: true })` to inventory all files without changing normal Source discovery. Native directories may also
implement `storage.isDirectory(path)` so planning recognizes empty folders.

The Bun adapter uses a directory rename with text rollback; the browser adapter
uses an IndexedDB transaction. Bun's guarantee covers handled I/O failures, not
power loss or edits by another process during the operation. Ordinary Source
Actions continue using the separate atomic `storage.commit` capability described
above. Adapters without `moveTree` reject folder moves before writing.

Individual assets use optional `storage.moveFile(move, changes, options?)` with
the same validation and handled-failure rollback contract. The move contains
`root`, `from`, `to`, and the planned bytes' SHA-256 hex `digest`. The adapter
checks those bytes, an absent destination, and all reference-edit preconditions
before mutating. Binary content must remain exact. The Bun adapter implements
this capability; adapters without it reject individual asset moves.

Core's durable Bun host additionally journals the asset identity and digest with
its Source edits. Recovery accepts either the original asset or its renamed
counterpart with matching bytes, then completes the reference edits. Unexpected
bytes or both locations being occupied stop recovery; the journal does not guess
which external edits to discard.

---

## View connections

A `ViewConnection` provides authorized requests, page reads, invalidation subscriptions, and
Actions without tying the UI to a transport. The host binds identity and context;
callers do not pass an Actor with each operation.

```ts
import { createHTTPConnection } from "@pathmx/core/connection"

const connection = createHTTPConnection(location.origin)
const href = "/lessons/check-in.assessment"
const unsubscribe = connection.subscribe(href, () => {
  // Re-read and reconcile through your UI's existing renderer.
})
const response = await connection.read(href)
if (!response.ok) throw new Error("Page is unavailable.")
const document = await response.text()

const receipt = await connection.actions.submit("input.submit", [
  ["_target", href],
  ["_response", "assessment"],
  ["contrast", "strong"],
])
// The receipt confirms persistence. Refresh the page separately.

unsubscribe()
connection.close()
```

`read(href)` returns a standard Response containing the authorized HTML document
and its existing document assets. A `.md` href returns Source Markdown. Subscribe
before the initial read. Notifications are invalidation hints, not document payloads;
reads always recheck access. Apply updates with the normal document renderer so
focused controls and unsent edits retain their continuity.

`request(href, init?)` sends a same-site request and returns a standard `Response`.
Plugin routes receive the host-admitted `HostSession` from `ctx.session()`; request
headers cannot select a different identity on a local connection.

```ts
const response = await connection.request("/api/search?q=contrast", {
  headers: { Accept: "application/json" },
})
if (!response.ok) throw new Error("Search is unavailable.")
const { documents } = await response.json()
```

Browser plugin clients use `getViewReader()` from `@pathmx/core/connection` for
`request` and `read`. It selects the active host connection, including a worker.
Static publications supply a reader that permits only GET and HEAD.
`getViewConnection()` supplies the full connection when Actions are available.

Worker requests preserve methods, headers, status, and finite binary or multipart
bodies. Bodies are buffered across worker messages; this transport does not stream
responses or report upload progress. An `AbortSignal` cancels the caller's wait and
signals the host handler. Cancellation does not roll back a mutation already applied.
Scoped Views expose only their admitted core routes; arbitrary plugin routes are
not redispatched through a broader View. Credential endpoints require an HTTP host.

`createHTTPConnection(baseURL)` uses same-origin credentials and the host's Action
and live endpoints. Supply the mount URL for a contextual View. Root-relative
canonical hrefs resolve within that mount; absolute URLs outside it are rejected.
Authored same-site `download` links read through the active reader, so locally
stored files can be downloaded offline. Static readers use the publication's read-only runtime and do not ship mutation
or live-server transport code.

`createWorkerConnection(worker)` and `serveWorkerConnection(connectionPromise, port)`
from the same entrypoint implement the identical contract over worker messages.
A trusted host creates the underlying connection with
`createViewConnection(app, session, viewOptions?)` from `@pathmx/core`, using its
own environment and admitted `HostSession`. A local owner uses
`{ type: "local", actor: "/people/sam.person" }`; this grants no repository
administration or remote authentication. These adapters do not supply
identity admission, publication loading, or durable storage for the host.

`getViewConnection()` from `@pathmx/core/connection` returns the active connection
installed by the document runtime. Independently bundled clients share this same
host admission. It rejects when no connection is installed, including read-only
static publications.

`actions.run(name, input, { target, request })` matches the View Action runner.
`actions.submit(name, fields, { request })` accepts ordered text pairs, consumes `_target`,
and invokes the Action's form decoder. Binary transfer uses a plugin route
through `request`; it is not converted into this text-form contract.
Closing a connection releases its subscriptions and pending transport operations,
without shutting down a shared host. A mutation interrupted before acknowledgment
has an uncertain outcome.

The optional `request` identifies one invocation across retries. HTTP connections
generate it automatically and retry transient failures with the same payload;
explicit identifiers can be retained across closed connections. See
[retry a Save](./actions.reference.md#retry-a-save) for receipt replay and scope.

A successful Action receipt confirms the complete persistence operation, including
a successful no-op or replay. Reading the updated representation is a separate
operation: a failed refresh never requires repeating the Action. The host repairs
failed projections from Sources before another mutation. Connection reads request
that repair and fail if the host cannot recover.

`ActionFailure` exposes `outcome`: `rejected` means no writes are confirmed to have
occurred; `partial` means a sequential adapter confirms an incomplete batch;
`uncertain` means the result cannot be confirmed. Preserve the form on rejection.
`applied` records the number of confirmed Source changes before failure; HTTP and
worker connections preserve that count. Zero does not prove that an uncertain
operation made no changes.
For partial or uncertain outcomes, reconcile before another submission. Generic
Actions do not promise exactly-once execution or receipt recovery after a crash.

---

## Static publication artifacts

`@pathmx/core/publish` provides `createPublication` for hosts that consume a
publication directly. Create the application with a static runtime target:

```ts
import { PathMX } from "@pathmx/core/bun"
import { createPublication } from "@pathmx/core/publish"

const app = await PathMX({
  root: "./paths",
  runtime: { target: "static" },
})
try {
  const bundle = await createPublication(app, {
    origin: "https://example.com",
  })
  const route = bundle.manifest.routes.find((route) => route.pathname === "/")
  const representation = route?.representations.find((item) => item.default)
  const artifact = bundle.artifacts.find(
    (item) => item.path === representation?.artifact,
  )
  if (!artifact) throw new Error("Home page has no default artifact.")
  const html = await (await artifact.read()).text()
  console.log(bundle.manifest.build, html)
} finally {
  await app.close()
}
```

The bundle separates public routes from stored artifact paths. Each route lists
its HTML, Markdown, or file representations with content types and any response
headers. Resolve requests through that route contract; an artifact filename is
not necessarily the reader-facing URL. The manifest records the build identity,
origin, mount `basePath`, and artifact size and cache identity. Optional `basePath`
relocates a publication beneath an origin, such as `/course/`.

Status representations use a route with `type: "status"` and a numeric `status`.
For an unmatched request, a host selects the route whose `status` is `404`,
negotiates its HTML or Markdown representation, and serves it with that HTTP
status without redirecting. The same status applies when its `/404.html` route
is requested directly. See [missing pages](../start/publish.guide.md#missing-pages)
for authored overrides and directory-host configuration.

The builder reads anonymous public content, collects Plugin publication routes,
and validates the completed publication. Unsupported server-dependent behavior
or invalid publication routes reject with `PublicationError`. Static output has
no server Action or live-update endpoints. Use [browser publications](#browser-publications)
when local personal writes are required.

By default, artifact bytes stay in memory. A host can supply a `PublicationStore`
through `store`; its `put(path, blob)` returns the stored size and a reusable
asynchronous `read()` function. Keep staging private until the whole build
succeeds, and keep stored bytes available for as long as the bundle is consumed.
Artifact readers do not depend on the running application. For a ready-to-serve
directory, use [the export command](../start/publish.guide.md) instead of writing
a storage and routing adapter.

---

## Browser publications

`publishBrowserProject` from `@pathmx/core/bun` builds an ordinary project into a
browser-hosted application. The output contains public Sources, explicitly authored
starter files, the selected plugins, their browser clients, and local assets.
Package activation and repository plugin configuration use the normal
[project setup](../packages.reference.md#install-and-activate).

Create `publish-browser.ts` in the project root and run it with
`bun run publish-browser.ts` after supplying the starter files below:

```ts
import { publishBrowserProject } from "@pathmx/core/bun"

await publishBrowserProject("./paths", {
  out: "./dist",
  origin: "https://example.com",
  local: {
    name: "my-course",
    session: { type: "local", actor: "/people/local.user" },
    writableRoot: "/people/local/",
    seedSources: "./starter",
  },
})
```

The starter folder mirrors repository-relative paths. It may contain the declared
local Actor Source and files inside `writableRoot`:

```text
starter/
  people/
    local.user.md
    local/
      my.bookmarks.md
```

Declare the Actor in `starter/people/local.user.md`:

```md
---
actor: true
---

# Local reader
```

Declare the Home on `starter/people/local/index.md`:

```md
---
access:
  type: home
  owner: /people/local.user
---

# Local reader's files
```

`starter/people/local/my.bookmarks.md` can contain `# My bookmarks`. Starter files
are deliberately published templates. Existing private Sources in `paths` are
excluded; they do not become starter files automatically. The Actor declaration
is read-only, while files inside the writable subtree seed IndexedDB only when
that named repository is first created. Keep the repository name, Actor, and Home
stable when updating a site; saved Sources and binary uploads remain intact.

Serve `dist` at the root of an HTTPS origin, or localhost during development.
Serve existing files normally and use `index.html` as the fallback for page
navigation. The output includes a service worker at `/pathmx-browser-sw.js` and
requires access to the `/.pmx` directory. No Action server is required.
`origin` records the canonical HTTP(S) origin; subpath hosting is not supported by
this builder.

The service worker caches and verifies the complete publication before the first
opening creates a personal repository. Once installed, the site can reopen offline at a deep link.
Updates wait until tabs using the previous build close, then replace its cached
publication. Incomplete or corrupted updates leave the installed build available.
These caches are separate from the saved Source repository.

Plugins must run with the browser host's capabilities. Clients and package-relative
assets are prebuilt automatically; runtime module loading and SQL are unsupported.
HTML rewrites operate on complete buffered documents, including text nodes. Import reusable code through repository plugins.
Externally hosted media and services still require their own network access.
Include `<x-browser-files-tools />` for personal-file export, atomic import, and reset.
See [saved files](./state.reference.md#export-and-restore-saved-files) for archive limits.

---

## Browser hosts and document runtime

`createBrowserHost` from `@pathmx/core/browser` opens an IndexedDB repository and
returns a `ViewConnection`. Its writable subtree overlays the supplied read-only
Source snapshot. Snapshot values may be Markdown strings or binary Blobs. Personal binary assets retain their bytes and media types as
IndexedDB Blobs. Atomic Source batches compare expected bytes and commit together;
separate asset writes are not part of a Source batch. Web Locks serialize refresh,
Actions, and route requests across tabs; BroadcastChannel invalidations
cause subscribed pages to read the saved Sources again. Closing this connection
also closes its owned repository and storage.

A small worker can host a personal Source repository:

```ts
import { createBrowserHost } from "@pathmx/core/browser"
import { serveWorkerConnection } from "@pathmx/core/connection"
import { SourceAccessPlugin } from "@pathmx/core/plugins"

const connection = createBrowserHost({
  name: "reading-notes-v1",
  writableRoot: "/people/sam/",
  session: { type: "local", actor: "/people/sam.person" },
  sources: {
    "/index.md": "# Reading notes\n\nWelcome to your notebook.",
    "/people/sam.person.md": "---\nactor: true\n---\n# Sam",
    "/people/sam/index.md":
      "---\naccess: {type: home, owner: /people/sam.person}\n---\n# My files",
    "/people/sam/notes.md": "# My notes\n",
  },
  plugins: [SourceAccessPlugin],
  resources: {
    async loadAsset(url) {
      throw new Error(`Asset is not prebuilt: ${url}`)
    },
    async bundle(entry) {
      throw new Error(`Client is not prebuilt: ${entry}`)
    },
    async loadModule(name) {
      throw new Error(`Module is not installed: ${name}`)
    },
  },
})
serveWorkerConnection(connection, self)
```

The example uses only Markdown and Source access, so its resource handlers reject
unused capabilities. Supply prebuilt assets, bundles, and installed modules when
plugins require them. The browser environment provides Markdown, YAML, and buffered
HTML and text-node rewriting; SQL is not supported by this
adapter. `createBrowserStorage` and `createBrowserEnvironment` are available from
the same entrypoint for hosts that assemble their own lifecycle.

A `bundle` resource returns `{ entrypoint, styles, assets, dependencies }`.
`entrypoint` and `styles` are output-relative paths into `assets`; `styles` contains
only the entry’s initial CSS. `assets` retains the complete module graph, including
lazy chunks and binary files, and `dependencies` lists local input URLs used for
invalidation. A CSS-only bundle has `entrypoint: null`. For an already published
bundle, return absolute publication paths with an empty `assets` array. Core emits
only the selected entry and initial styles; supporting artifacts remain available
for later imports.

Source snapshots must contain only data appropriate for that browser. Personal
Sources seed the database on first creation; reopening preserves saved bytes and
rejects malformed stored content. Use `publishBrowserProject` for publication
filtering, resource packaging, and offline startup. Hosts assembled directly with
`createBrowserHost` supply their own snapshot loading. Database migration and
backups remain application concerns.

`startViewRuntime` from `@pathmx/core/runtime` binds canonical PathMX documents to
any View connection. It installs navigation, native Action forms, and live
reconciliation with focus and draft continuity:

```ts
import { createWorkerConnection } from "@pathmx/core/connection"
import { startViewRuntime } from "@pathmx/core/runtime"

const connection = createWorkerConnection(
  new Worker("/worker.js", { type: "module" }),
)
await startViewRuntime(connection, { initialRead: true })
```

Load this client from a shell containing one `[data-pmx-view]` element and an empty
`<div data-pmx-augmentations></div>` directly under `body`. `initialRead` renders the
current URL through the connection before installing navigation. Serve that shell
for authored page routes so reloads can reopen them. Rendered documents must include
their required prebuilt client and style assets. For an already rendered HTTP page,
pass `createHTTPConnection(location.origin)` and omit `initialRead`.

---

## HTTP representations

`@pathmx/core/http` shares representation handling between the Bun server and
publication adapters. It depends only on web-standard `Request`, `Response`,
`Headers`, and `URL`:

```ts
import { prefersMarkdown, withMarkdownAlternate } from "@pathmx/core/http"

export default {
  fetch(request: Request) {
    const path = new URL(request.url).pathname
    if (path !== "/hello.page" && path !== "/hello.page.md") {
      return new Response("Not found", { status: 404 })
    }
    const markdown = path.endsWith(".md") || prefersMarkdown(request)
    const content = markdown ? "# Hello\n" : "<h1>Hello</h1>"
    const response = new Response(content, {
      headers: {
        "Content-Type": markdown ? "text/markdown" : "text/html",
      },
    })
    return withMarkdownAlternate(response, "/hello.page.md")
  },
}
```

`prefersMarkdown` compares accepted quality and specificity, preferring HTML on
a tie. `withMarkdownAlternate` adds `Vary: Accept` and a Markdown alternate `Link`
to successful responses. `appendVary(headers, name)` adds a case-insensitive
header name without duplicating existing entries. Hosts own routing and Source
access before selecting a representation.

---

## Plugin declarations

PathMX uses Zod 4 for validation and derives JSON Schema for field discovery.
Plugins may use Zod or Zod Mini:

```ts
import { defineEvent, definePlugin } from "@pathmx/core"
import * as z from "zod/mini"

const Submitted = defineEvent({
  data: z.object({ response: z.string() }),
})

export default definePlugin({
  id: "course",
  schema: {
    sources: { course: { label: "Course" } },
  },
  events: { submitted: Submitted },
})
```

Plugins declare `actions`, `queries`, and `events` directly, alongside `components`
and `routes`. Each map uses local names; the plugin id supplies the namespace, so
this Event is `course.submitted`. Registration validates these declarations in the
same installation pass. Server emission, Action provenance, bounded subscriptions,
and browser observation declarations are documented in [Events](./events.reference).

`schema` holds `sources`, `blocks`, `props`, and `settings`. `defineSchema` can extract
that data contract into a reusable fragment. Query metadata remains discoverable
through `app.schema.query(id)`; executable query definitions belong in
`plugin.queries`. Keep browser bindings and optional deployment adapters
outside the portable import surface.

---

## Render an isolated theme

`@pathmx/core/theme` provides the theme CSS and attributes for a standalone
HTML document, such as a generated share image. It uses the same normalization
and presets as the Theme plugin without applying reader preferences.

```ts
import { renderTheme } from "@pathmx/core/theme"

const theme = renderTheme({
  root: { preset: "paper", color: { accent: "#7545a0" } },
  page: { font: { heading: "Georgia, serif" } },
  scheme: "light",
})

const html = `<!doctype html>
<html data-pmx-theme="${theme.preset}" data-pmx-scheme="${theme.scheme}">
<head><style>${theme.css}</style></head>
<body style="background:var(--pmx-color-bg);color:var(--pmx-color-fg);font-family:var(--pmx-font-heading)">
  <h1>Learning to see type</h1>
</body></html>`
```

Page values override root values. `scheme` is a fallback: an authored scheme or
fixed preset scheme takes priority. The result contains `css`, `preset`, and
`scheme`. This entry point is portable; capturing a PNG belongs to a host plugin.

---

## Source-owned derived assets

Plugin contexts expose `app.assets.read(path)` for local authored binary inputs
and `app.assets.publishDerived(sourcePath, name, blob)` for generated variants.
The source must exist. Returned content-addressed URLs stay beside the original
and use its access boundary; static publication includes referenced variants.
The default Bun Image plugin uses these methods for responsive image files.

Use `app.assets.read(path)` and `app.assets.publishDerived(path, name, blob)`
from a Plugin host operation to produce a transformed download. Check the
embedding boundary before reading the original. Derived bytes retain that
original's access boundary and receive content-addressed URLs; do not author
those generated hashes.

Keep the transformation in tracked authored preparation or a domain operation,
then pass the resulting URL as plain data to a native component. A portable
presenter should not open files or perform the transformation on every render.
See [Plugin assets](./plugins.reference.md#assets-transforms-and-publication).

---

## Parse directive labels

Use `parseDirectiveLabel` when a plugin needs to interpret an authored directive
label, such as the text of a rendered link:

```ts
import { parseDirectiveLabel } from "@pathmx/core"

const result = parseDirectiveLabel(
  '@video(thumbnail="./preview.jpg" width=640) Motion study',
)

if (result.issue) throw new Error(result.issue)

// result.label: "Motion study"
// result.directive: {
//   name: "video",
//   params: { thumbnail: "./preview.jpg", width: 640 },
//   qualifiers: [],
// }
```

The result contains `label`, `directive`, and `issue` when applicable. Ordinary
text produces a label without a directive. Parameters contain scalar strings,
numbers, or booleans. The parser checks shared syntax; each plugin validates its
accepted names, values, and combinations. Check `issue` before consuming a
recognized directive.

See [Images](../authoring/images.reference.md) and
[Video](../authoring/video.reference.md) for authored parameter vocabularies.

---

## Choose the Source access policy

Bare `createApp` has no implicit public-read or personal-folder policy. It still
enforces current Actor identity, private Actor Sources, View restrictions, and
bounded Action effects. Install a base policy to choose the application's ordinary
Source permissions:

```ts
import { createApp } from "@pathmx/core"
import { bunEnvironment } from "@pathmx/core/bun"
import { SourceAccessPlugin } from "@pathmx/core/plugins"

const app = await createApp({
  root: "./paths",
  environment: bunEnvironment,
  plugins: [SourceAccessPlugin],
})
```

`SourceAccessPlugin` owns the standard public, explicit Home, shared Source, and
attachment rules, including authoring validation, mutation checks, embedding, and
permission explanations. `defaultPlugins` includes it, so ordinary `PathMX()`
applications retain their standard behavior. Supplying a replacement `plugins`
list replaces the defaults; include this policy explicitly when you need its rules.

A base policy is supplied by `Plugin.access(app)` once, before Source indexing.
Only one base policy may be installed. Other plugins add capabilities with `grants`
or narrow contextual Views; they do not silently replace the base policy.

This complete alternative publishes only Sources with `public: true`:

```ts
import { definePlugin } from "@pathmx/core"

export default definePlugin({
  id: "publication-policy",
  access(app) {
    const published = (id: string) => app.sources.get(id)?.data.public === true
    return {
      resolve(_principal, target) {
        const id = typeof target === "string" ? target : target.id
        return published(id)
          ? { visibility: "public", capabilities: ["read"] }
          : { visibility: "denied", capabilities: [] }
      },
      authorize() {
        throw new Error("Edit these files through the host.")
      },
      canEmbed(container, dependency) {
        return published(container) && published(dependency)
      },
    }
  },
})
```

Install this plugin instead of `SourceAccessPlugin`. Its authoring has no Home or
shared-Source convention. It may optionally supply `home(actorId)` and
`homeActorId(path)` for personal State and `~/` paths. Trusted consumers use
`app.access.home(actorId)`; Actor identity itself contains no Home.

`resolve` returns `visibility` (`public`, `private`, or `denied`) and a capability
array. `blocked: true` prevents supplemental grants from opening an invalid or
protected target. An optional `reason` explains permission inspection; `target`
may identify the Source owning an attachment. Inconsistent or malformed decisions
fail closed. `authorize(principal, change)` validates mutations; an undefined
principal denotes an already bounded Action effect. `canEmbed` checks whether a
compiled dependency may be included in its container. The policy may provide
`diagnostics()` and `validate()` for its authoring. Cross-Source invariants belong
in the owning Plugin's `validateSources(prepared, app)` hook, which sees parsed
changes and the proposed final repository before durable writes. Its Source hook
runs before other Source consumers, after Actor indexing.

Core independently rejects ordinary changes that create/delete an Actor, promote a
Source into an Actor, or change an untyped file's derived Source id. Plugins such as
Auth add their own authority-field invariants. Repository-admin authoring remains the
explicit management boundary for those changes.

Policy changes must use normal Source commits and indexing. Retained Views and
query caches observe the new revision, and asynchronous delivery discards stale
results. `~/` resolution is unavailable when the policy supplies no Home.

The standard policy reads a Home claim from the directory's canonical index Source:

```md
---
access:
  type: home
  owner: /people/sam.person
---
```

The claim protects that directory and every descendant Source or asset. It remains
reserved if its Actor is missing, including after restart and during static export.
Duplicate, overlapping, or case-aliased claims fail closed. Ordinary Actor and
Action mutations cannot create, remove, retarget, or relocate Home policy; trusted
repository changes validate the complete final claim set before writing. Moving or
removing a claim is rejected while any Source or asset remains in the old namespace.

A canonical directory index with `access.type: shared` similarly protects itself
and every descendant Source and asset. The nearest nested shared index or an
explicit non-index shared policy may narrow access for its subtree or Source, but
cannot grant capabilities excluded by an ancestor boundary. Inheritance is based
on storage location, not links. A shared subtree inside a Home can deliberately
admit collaborators; Home ownership and shared-directory policy remain separate.
Removing a shared index is rejected while retained descendants would become public.
Permission inspection reports the index Source from which access was inherited.

---

## Private files and attachments

The default `SourceAccessPlugin` reserves `/private/` for files that must remain
unavailable without explicit access. It requires private attachment bindings to
use this namespace, an existing protected Home, or a shared directory boundary.
Public file locations remain
ordinary public files; private bindings outside protected storage are invalid.
Protected namespaces also deny case aliases, so a URL such as `/PRIVATE/file.png`
cannot bypass protection on a case-insensitive filesystem.

A `repository-admin` View does not override a policy-blocked `/private/` Source.
Its `readSource` returns no Source and its queries omit that Source, even though
the same authority can inspect Homes. Trusted host reads through `app.sources`
are a different boundary from reads through a View.

For example, a shared document can bind an image without giving its Actor a Home:

```md
---
access:
  type: shared
  owner: /people/alice.person
  members:
    - actor: /people/alice.person
      can: [read, write]
attachments:
  - /private/sketch.png
---

# Team sketch

![Sketch](/private/sketch.png)
```

The owner is an Actor Source with `actor: true`. The binding grants the document's
readers access to the file and its derived images. A public Source can explicitly
share a file from `/private/` or a Home. A shared directory's allowlist remains a
ceiling: an attachment binding cannot grant access excluded by the file's enclosing
shared indexes. Unclaimed files inherit those indexes directly. Direct reads and
embedding checks use the same access resolver; contextual View restrictions still apply.

Actor-authored mutations can add a binding only when the Actor already has read
access to the file. Owning a document cannot claim someone else's private file or
an unbound `/private/` file. Trusted repository authoring and bounded service
Actions can establish new bindings, as upload handling already does.

Removing a binding or deleting its Source does not delete file bytes. Unbound files
under `/private/` remain denied after restart and excluded from anonymous static
export. Files in a Home return to that Home's access rules. This is a convention
of the default access plugin, not a new core storage API.

---

## Add domain Source grants

An Actor is a Source with `actor: true`. Core does not require a role, email, or
Home. With `SourceAccessPlugin`, a Home is declared by `access.type: home` and
`owner` on that directory's index Source. An Actor may own zero or one Home. Plugins
own role meaning and credential admission. `@pathmx/auth` supplies User admission
and its staff-review convention.

A plugin's synchronous `grants` hook adds Source capabilities from current indexed
state. This complete example lets an event host edit badges for that event:

```ts
import { definePlugin } from "@pathmx/core"

export default definePlugin({
  id: "event-access",
  grants(target, { principal }, app) {
    if (principal.type !== "actor") return
    if (typeof target === "string" || target.type !== "badge") return
    if (typeof target.data.event !== "string") return
    const event = app.sources.get(target.data.event)
    if (event?.data.host === principal.actor) return ["read", "write"]
  },
})
```

The badge authors `event: /saturday.event`; that Event authors
`host: /people/sam.participant`. No core role or personal folder is needed.
A general badge-editing Action can use `available: ({ can }) => can("write")`.

Grant contributions combine with the installed base policy. The standard policy
provides public, explicit Home, and shared-Source permissions. Contextual View restrictions still apply to every resulting capability.
Grants cannot resurrect stale Actors, expose Actor Sources, override malformed
shared access, or unlock reserved orphaned Home paths. Private Home/shared access requires
a host-admitted Actor. Explicit repository administration retains its host-owned
bypass. Attachment reads resolve through their owning Source.

Return an array of `read`, `write`, and/or `execute`, or `undefined` to decline.
An empty array adds nothing. Malformed or throwing providers deny access. Providers
must use indexed Sources and synchronous policy state; do not call `app.access`
recursively from a grant hook. Mutate policy through normal Source changes so
indexing, query caches, and asynchronous freshness checks update together.

Grant capabilities describe ordinary Source access. Actions that perform a narrower
domain operation still declare finite prepared effects; this hook does not replace
Action availability or expand those effects. See [Actions](./actions.reference.md)
and [Views](./views.reference.md).
---

## Related

- [Components](../authoring/components.reference.md) — Markdown and plugin rendering.
- [React](./react.reference.md) — application bindings.
- [Packages](../packages.reference.md) — package boundaries and activation.
- [CLI](../cli.reference.md) — command behavior and plugin lint contributions.

---

## Discover reachable pages

`await app.sources.discoverable(view)` returns canonical page Sources reachable from
`/` through Sources that the supplied View can read. Layout, navigation, and
other authored Source references participate in traversal. Authored queries also
connect their returned internal `href` references, evaluated in the supplied View
with the same filters, sorting, and limit as the page query. Supporting Sources
can connect pages without appearing as page results. This does not change
`app.sources.all()`, general Source queries, or direct URL access.

```ts
import { route, type Plugin } from "@pathmx/core"

export default function PageIndexPlugin(): Plugin {
  return {
    id: "page-index",
    routes: [
      route.get("", async (ctx) => {
        const view = ctx.view(await ctx.session())
        const pages = await ctx.sources.discoverable(view)
        return Response.json(
          pages.map((source) => ({
            id: source.id,
            markdown: source.path,
          })),
        )
      }),
    ],
  }
}
```

For an anonymous index, use `ctx.view({ type: "anonymous", signIn: null })`.
The result is a read-only list; copy it before sorting. Discovery uses the
indexed graph and View-aware query relations; it does not compile or render pages.

---

## Shared control surfaces

Browser plugins register navigation, transport, or utility controls into the
framework's augmentation dock. Core owns spacing, safe-area placement, and
utility overflow; plugins own their content and availability.

```ts
import { surfaces } from "@pathmx/core/controls/browser"

const control = surfaces.register({
  id: "path.navigation",
  element: navigation,
  group: "navigation",
  presentation: { document: "full", play: "compact" },
})
// Dispose when the authored feature host becomes unavailable.
control.dispose()
```

Register each id once per Document. Keep an authored availability host in the
View, and move only its control element. Registration moves the element without
remounting its state. Disposal restores its original position when available,
or removes it. React consumers can render through a portal into a registered
host. Embedded Source documents have their own scoped registry.

The presentation host calls `surfaces.setContext("play", { visible })` with its
interaction visibility and restores `"document"` on exit. The entire dock fades
together; hover, keyboard focus, and expanded utility controls keep it visible.
Plugins retain ownership of availability, including development-only controls.
Compact surfaces share `--pmx-augmented-control-size` (3rem). Use the shared
`pmx-button` floating/icon variants for individual actions.
Call a utility registration’s `reveal()` before returning focus to it from a
dialog, so its overflow group is available. Contributions receive
`data-pmx-surface-presentation="full"` or `"compact"` according to their declared
mapping. Utility controls use **More controls** in Play and at narrow widths.
Navigation and transport stay directly available; disabled navigation controls
should retain their space. Dialogs and popovers keep their normal focus and
accessibility semantics.

For a custom action flow, `executeBrowserAction({ action, request, pending })`
uses the mounted View’s action runner and reconciliation. `request` accepts
`{ kind: "form", fields }` or `{ kind: "input", value }`. Check the returned
`reconciliation.status` before navigation: only `"applied"` confirms that the
current mount received the result; `"failed"` and `"superseded"` do not. Prefer
the normal authored action form unless custom post-action behavior is needed.
