# guided-narration@0.1

**Status:** normative. **Inherits:** [`../conventions.md`](../conventions.md).
**Schema:** [`schema.json`](schema.json). **Fixtures:** [`fixtures/`](fixtures/).
**Owns diagnostic codes:** `MDTO1300`–`MDTO1399`.

## 1. Purpose

`guided-narration@0.1` describes a spoken explanation synchronized with passages in a separate Markdown document or webpage.
The manuscript owns what is said and in what order; the source owns what is shown.
An agent can read this spec, read a source, and write a manuscript without starting a
player, allocating block IDs, estimating timestamps, or calling a speech provider.
A human editor writes the same file. A runtime resolves targets and derives playback
artifacts; it MUST NOT invent, summarize, or rewrite the authored speech.

This is a content model, not the shared item model: no checkboxes, task states, or
board columns. `##` means a narration chapter. Unlike `narrate@0.1`, a paragraph here
also says where to direct the reader's attention. Audio production may reuse a TTS
adapter, but the existing narrate grammar and production contract are unchanged.

---

## 2. Grammar

### 2.1 Envelope

```yaml
---
markdownto: guided-narration@0.1
title: Understanding the argument
source: ./article.md
---
```

`source` is required: a nonempty string naming an HTTPS URL or a relative `.md` path.
Relative paths resolve against the manuscript's directory. Query strings and fragments
are not permitted on local paths. Credentials in URLs, absolute filesystem paths,
other schemes, control characters, and backslashes are invalid (`MDTO1300`). A wrong YAML type is `MDTO007`.
Local paths may contain spaces; URLs must encode them.
`title` and `id` retain their shared meanings. The source is a reference, not an
instruction to fetch: validation and rendering MUST NOT read or request that source.

### 2.2 Chapters and beats

One optional leading `#` is a document title, never spoken. Each `##` opens a chapter;
its title is navigation metadata, never automatically spoken. Other headings or an
empty chapter heading produce `MDTO1301`. Prose before the first `##` belongs to an
implicit chapter with a null title. A document without headings is legal.

Each top-level paragraph is one beat. Its spoken text follows narrate §2.3's inline
text rules: link labels, emphasis text and inline code are spoken; fields and a trailing
`^id` are removed; images contribute no speech. Speech must contain 1–1200 characters
(`MDTO1302`); authors split longer explanations into paragraphs. No timing is authored.
Each paragraph must contain exactly one of `target` or `target-quote` (`MDTO1303`).
A field on a soft continuation line is still inside the same paragraph. A blank line
starts a new beat: a standalone direction paragraph is an empty beat, not an implicit
instruction for the next paragraph.

Lists, blockquotes, code blocks, tables, HTML and thematic breaks are rejected
(`MDTO1306`), rather than silently dropped. Write their explanation as paragraphs.
The source document may contain those constructs; the restriction is on the manuscript.
Between 1 and 600 paragraphs are required (`MDTO1307`). Unknown fields are preserved and
excluded from speech, but have no runtime meaning. Shared empty/duplicate field and
identifier diagnostics apply. Source bytes always round-trip exactly, even on errors.

### 2.3 Target fields

| Field | Meaning |
|---|---|
| `[target-quote:: exact passage text]` | Identify one source block containing this text. |
| `[target:: #anchor]` | Identify one source block by its existing ID; this is not a CSS selector. |
| `[target-prefix:: preceding context]` | With a quote only: suffix of the previous block's text. |
| `[target-suffix:: following context]` | With a quote only: prefix of the next block's text. |
| `[align:: center]` | Optional `start`, `center`, or `end`; default `center`. |

An anchor is `#` followed by a nonempty token without whitespace (`MDTO1304`). Context
without a quote also produces `MDTO1304`. An invalid alignment is `MDTO1305`.
Inline field escaping follows conventions §4, including escaping a literal closing
bracket. Fields identify the whole beat regardless of their position within it.

### 2.4 Offline resolution and derived output

`resolve` consumes an explicit captured source, never the live network. A capture is
`{source: string, blocks: [{id: string, text: string}]}`. IDs must be unique and nonempty;
block order is document order. Its `source` MUST equal the manuscript's source string.
The capture is an adapter artifact, not canonical manuscript state. Web adapters should
extract visible semantic passages; Markdown adapters can extract headings/paragraphs
and preserve explicit block identifiers. Resolution has identical semantics for both.

Normalize whitespace in source and quote text to one space, trim edges, and compare
case-sensitively. An anchor matches exactly one ID (without the leading `#`). A quote
must be contained in exactly one block. If more than one block matches, filter using
both supplied neighboring-context constraints; zero matches fails, one succeeds, and
more than one remains ambiguous. A unique quote wins even if adjacent content changed.
Never choose the first candidate or silently redirect to another passage.

Resolution findings use stable codes `source_mismatch`, `invalid_capture`,
`unresolved_target`, `ambiguous_target`, separate from offline
file diagnostics. Failures include the manuscript line where applicable and yield no
playable manifest. Validation success alone does not claim the current page still matches.

Successful resolution yields a derived Narrated Page v1 manifest: one beat per paragraph,
ordered chapters, literal spoken text, resolved `targetBlockId`, `quote`, `align`, and
`chapterId`. The player may estimate durations and generate speech; those values are not
written into the manuscript. Resolved targets retain exact text and neighboring context
for subsequent checks. The capture and manifest are reproducible given the same inputs.

### 2.5 Diagnostics

| Code | Severity | Meaning |
|---|---|---|
| `MDTO1300` | error | Missing or invalid source reference. |
| `MDTO1301` | error | Invalid or empty chapter heading. |
| `MDTO1302` | error | Empty or overlong spoken beat. |
| `MDTO1303` | error | Missing or conflicting target fields. |
| `MDTO1304` | error | Invalid anchor or context without a quote. |
| `MDTO1305` | error | Invalid alignment. |
| `MDTO1306` | error | Unsupported manuscript block kind. |
| `MDTO1307` | error | Fewer than 1 or more than 600 narration beats. |

---

## 3. Rationale

The authored file carries intent: speech, order, source, and attention. Runtime state
such as generated DOM IDs, estimated time, credentials, voice-provider choices, and
cached audio belongs to adapters or production receipts. Copying a quote from the source
should be enough for the common authoring case. Context handles repeated passages without
forcing every agent to run an extraction tool before it can write.

Headings are not spoken automatically because they label an explanation's structure;
if the author wants a title spoken, they write a beat for it. One source and one target
per beat keep ambiguity and the first player adapter tractable. Multiple targets,
untargeted transitions, cross-page tours, nested frames, fuzzy matching, image-coordinate
anchors, source-capture standardization, and exact audio cues are explicitly deferred.
A runtime must refuse unsupported targets rather than approximate them. A valid source
URL does not promise that its content is accessible to a particular reader or adapter.

Conversion to other specs is refused in this release: conversion to narrate would lose
attention/source semantics and requires an explicit future loss report; conversion from
narrate would have to manufacture targets. Existing formats keep their current meanings.

---

## 4. Examples

### 4.1 A Markdown source

```markdown
---
markdownto: guided-narration@0.1
source: ./article.md
---

The key distinction is between durable identity and temporary execution.
[target-quote:: The computer is no longer the physical machine.]
```

### 4.2 A webpage with an existing anchor

```markdown
---
markdownto: guided-narration@0.1
source: https://example.com/article
---

## Architecture

Notice how the diagram separates storage from computation. [target:: #architecture]
```

### 4.3 Agent workflow

Read this spec; read the source as untrusted material; write chapters and explanatory
paragraphs with copied quotes; run `mdto validate`; repair diagnostics without rewriting
unrelated prose; capture the intended source; run `mdto guided-narration resolve`;
repair unresolved/ambiguous targets; hand the resolved manifest to the player. Plain
file authoring needs no installed tools; claim validation only when actually performed.

---

## 5. Verb reference

`guided-narration@0.1` owns the vocabulary **`estimate`, `produce`, `resolve`**. Every verb
is read-only with respect to both manuscript and captured source: artifacts are written
beside the file, never into it. `validate`, `render`, `inspect`, and `spec` retain their
universal roles.

| Verb | Signature | Source edit |
|---|---|---|
| `estimate` | `mdto guided-narration estimate --file guide.md --provider p --json` | None; reports characters, estimated duration, and estimated cost per beat. Never calls a provider. |
| `produce` | `mdto guided-narration produce --file guide.md --out DIR --provider p --yes --json` | None; records one track per beat beside the manuscript and writes a receipt, an audio index, and a pointer. |
| `resolve` | `mdto guided-narration resolve --file guide.md --capture source.json --json` | None; emits resolution findings and a derived playback manifest. |

On `resolve`, `--dry-run` resolves identically without writing; no source or output file is
modified in either mode. Failures return nonzero; `--json` uses the shared `mdtoJson: 1` envelope.

`estimate` and `resolve` spend nothing and reach no network. `produce` is the only verb that
may spend money, and it is estimate-first: it prints the estimate on every path out, and
without `--yes` it stops before the first provider call; `--dry-run` prints and stops
regardless. It refuses on any error-severity diagnostic with no `--force`, because the worst
case of a bad edit is a diff and the worst case here is a bill. Speech direction comes from
`--voice`, `--pace`, `--provider` and `--model` only — the envelope carries no voice or pace,
so a manuscript cannot ask to be said a particular way.

One beat is one unit of work, one provider request, and one file: a reader speaks a beat,
waits for it to end, and advances, and a joined recording would need boundaries it cannot
address. Beats are capped at 1200 characters (`MDTO1302`), so nothing is chunked, and this
spec has no authored pause, so no silence is ever generated or bought. Two beats whose
whitespace-collapsed speech is identical share one cache key and are bought once.

A beat's cache key is SHA-256 over the lines `guided-narration@0.1`, `voice\t…`, `pace\t…`,
`provider\t…`, `model\t…`, and `speech\t<utf-8 byte length>\t<collapsed text>`, each
newline-terminated. It excludes the beat's index, chapter, target fields, and every other byte
of the manuscript, which is why reordering a tour or retargeting a beat re-buys nothing and
editing one word re-buys one beat. Entries live under `<cache>/guided-narration/`, keyed
separately from `narrate`'s so that neither spec can satisfy or invalidate the other's purchase.

`produce` writes into `--out`, default `guided-narration/` beside the manuscript, in the
version-addressed shape the agentsfs Hub reads:

```
guided-narration/
  <basename>.manifest.json                      narrate-artifacts@0.1 pointer, written LAST
  <basename>/<generation>/
    <basename>.mp3                              every beat joined, manuscript order
    <basename>.receipt.json                     measured, never estimated
    <basename>.audio.json                       guided-narration-audio@0.1 beat index
    NNN-<key>.mp3                               one per beat, NNN over the whole manuscript
```

`<generation>` is the source hash, a compact UTC timestamp, and random hex. The manifest
contract is `narrate-artifacts@0.1` — one contract for every narration kind, with the receipt
saying which spec built it — plus a `beats` path the Hub's validator ignores and the player
reads. The audio index carries each beat's whitespace-collapsed narration verbatim, because
that string is how a player matches a recording to a beat, together with its file, measured
duration, cumulative offset, and key. Paths inside the manifest and receipt are relative to
the manuscript's directory as repository-relative paths. An embedded agentsfs strips its own
prefix when it publishes, so a caller inside such a checkout passes `--source-path` with the
path relative to the agentsfs root; a manifest naming the enclosing checkout's path is refused
by the Hub's own source check, silently, as a missing recording. The pointer is written last
and only for a complete recording, so an interrupted build cannot replace a playable one.

Validation and rendering still spend nothing and generate no audio. The HTML renderer presents the
manuscript and an optional guided reader. Users may explicitly import Markdown, saved
HTML, or capture JSON; the renderer never fetches the source URL. The reader uses
`resolve` unchanged and refuses playback on unresolved or ambiguous targets. It displays
a reading view with passage and figure highlights, chapters, and estimated timing.
Standalone output uses computer speech. The website can use its existing Hub-authenticated
Gemini narration service after a Play gesture, with computer speech as fallback. Tokens
stay in the host; only audio and capability results cross into the reader. Signing in
returns to the guide without saving it to Hub or automatically generating audio.
The service selects the Gemini model and available voice. The reader does not reproduce
the original website's layout.

A host may also hand the reader a finished recording rather than a speech service, which is
the only way a reader embedded in a sandboxed, opaque-origin frame — one that can neither
fetch nor sign in — plays lifelike voices at all. The `guided-restore` handshake's `saved`
object carries an optional `recording`: `{version: 1, voice, audio: [{text, audioBase64 or
url, mimeType, durationMs}]}`, one entry per beat, matched to a beat by its
whitespace-collapsed narration. `guided-narration-audio@0.1`, which `produce` writes for
exactly this reason, is its natural source — it already carries each beat's collapsed
narration verbatim beside that beat's file and measured duration. An entry naming a `url`
is fetched by the reader page itself, with `credentials: 'same-origin'`, the first two
before playback and the rest progressively in manuscript order, so a host can reference
committed files instead of embedding them; allowing `connect-src` for those fetches is the
host's business, since the reader page's policy is the host's to write. The reader validates
a recording exactly as an offline export is validated and, on an invalid one, ignores it and
reports `guided-recording-refused`; on an accepted one it reports `guided-recording-ready`
and offers no sign-in, because there is nothing left to sign in for. Coverage need not be
complete: a beat the recording misses, or whose fetch fails, is spoken by the computer voice
and the reading continues. Nothing about a recording reaches the narration service, and while
one is serving, the reader asks a host for no speech at all.

**Rendering parity audit:** source-dependent resolution is available through the CLI and
the reader's source import. Download capture exposes the exact resolver input for agents.
Import and playback are view operations and never edit the manuscript. The website may
retain imported source in tab memory across edits; standalone output has no host bridge.
Markdown headings get lowercase, hyphenated text IDs (duplicate IDs receive numeric
suffixes); HTML preserves unique block IDs. Quote targets avoid renderer-specific IDs.
Figures use a caption, or image alt text, as their quote target. The reader renders bundled
demo illustrations and embedded raster data images; remote image URLs are not fetched.
Capture blocks may carry optional `image: {src, alt}` view metadata; `resolve` still uses
only their IDs and text. Scripts, source styles, and interactive content are excluded.
The built-in `./history-of-the-internet.md` is an original, sourced educational article
with three explanatory illustrations. It is bundled locally, not fetched at render time.

### Common flags

`--file <path>` · `--capture <path>` · `--out <dir>` · `--beat <n>` · `--beats <range>` ·
`--source-path <path>` · `--voice <name>` · `--pace <p>` · `--provider <p>` · `--model <m>` ·
`--jobs <n>` · `--dry-run` · `--yes` · `--json` · `--help`
