The Lolly document model, draft-2026-09-24: every chapter of docs/spec/document-model/ in reading order.

# Constitution

> This chapter is a draft for review, dated 2026-09-24.  

This chapter states what a Lolly document is, which four records it is made of, the words the rest of the specification uses and the thirteen invariants every later chapter is measured against. It settles no serialisation and freezes no type. The other chapters carry the detail: [records](records.html), [source rows and patches](source-and-patches.html), [operations](operations.html), [values and time](values-and-time.html), [evaluation](evaluation.html), [policy](policy.html), [conformance](conformance.html), [extensions](extensions.html), [packaging](packaging.html), [proof cases](proof-cases.html) and [status](status.html).

## Thesis

A Lolly document represents a complete tool (D1). It carries the tool's typed interface, its authored content, its dependencies, its rules, its behaviour and the operations it offers. An operation may render, transform, extract, inspect, present or perform an explicitly authorised action (D1). A tool may produce a document, and a document may be used by another tool.

Operations are the foundation. Compositions, timelines and recorded sessions are optional (D11). A valid tool must not be required to have a canvas, a scene graph or a visual export (D11). The plan's evidence folder counts thirteen tool directories that declare an on-device transform, where a file goes in and bytes come out, so the operation is the whole tool. Unpack, Prepare, Batch and Verify are shell routes with no manifest at all, which is why R13 gives them operation adapters rather than tool definitions. The community tool `community/countdown-timer/tool.json` declares no inputs and exports only HTML.

An authored instance and one evaluation of it keep separate identities (R4). Changing a dataset in one instance must never mutate the definition that instance reuses, and evaluating an instance under a new organisation policy must never rewrite the authored instance (R4, R6).

Governance supplies the applicable rules and the acceptance authority (D6, D12). It never defines what a render means. Measured conformance and contextual acceptance stay separate answers for that reason (R8).

Generative or model-driven assets may be inputs or declared computations. Their dependency identity, repeatability and provenance are explicit claims in the evaluation record, never consequences assumed from a seed (R15).

The model is meant to produce five outcomes, and each later chapter answers to one of them:

- One model for complete tools, covering authored graphics, parameterised templates, data visualisations, timed media, interactive experiences and operations with no visual output (D1, D11).
- Local evaluation with explicit dependencies and declared repeatability, where recorded inputs and receipts are available when supported and permitted, never mandatory for a utility (D11, R15).
- Programmatic inspection, editing, patching and component reuse with stable identity and explicit constraint handling, with the MCP surface as the first consumer because it already edits by stable id (R1).
- A small conforming core with separately specified suites for particular media, outputs and interactions (R8, R12).
- Incremental adoption through adapters and native producers, with no catalog rewrite (R13).

## The four records

The four records already exist in this repository across eleven contracts with no shared naming (R4). The model gives them one set of names and states how they relate. It does not fork them.

![The four records of the document model - definition, instance, evaluation and artifact - each labelled with the existing contracts that hold it today](/info/diagrams/document-model/four-records.svg)

- **Definition.** What a tool is: identity, typed inputs, operations, resources, tokens, authored rules, components, output targets and extensions. It is held today by a manifest plus its template and hooks (`schemas/tool.schema.json`), by `DesignToolDefinitionV1` for tools compiled from Design (`packages/core/src/design-tool-v1.ts`, described in `docs/design-tool-contract.md`) and by the `.lolly` bundle of kind `tool`, which `shells/web/src/lib/lolly-pack.ts` writes and `packages/node-shell/src/lolly-file.ts` reads.
- **Instance.** One person's authored state against a definition revision, held today by the saved-session record at format 4 (`engine/src/session-record.ts`), by the `.lolly` bundle of kind `session` or by a URL (`engine/src/url-mode.ts`).
- **Evaluation.** What one run did and under which facts, recorded as a receipt: the C2PA manifest, the attribution receipt (`packages/core/src/rights-v1.ts`) and the proposed `EvaluationReceiptV1` (R15).
- **Artifact.** The produced bytes and their identity, content-addressed where a package carries them (`shells/web/src/lib/lolly-pack.ts`).

`CompiledDocument` is a transient compile result and must never be persisted as a document (R4, `engine/src/document-api.ts`). The same rule covers every other derived form: component expansion, chart lowering, text shaping and timeline sampling produce evaluation graphs that are never edited and never saved as the source (R1).

The records may travel in one package and must not be required to share one version number (R11). Each record must carry its own `schemaVersion`, a container must carry a minimum reader version and a definition must carry an engine version range (R11). Only the Design tool definition does so today (`packages/core/src/design-tool-v1.ts`); the session record carries `formatVersion` 4 (`engine/src/session-record.ts`) and the `.lolly` container carries `minReader` (`shells/web/src/lib/lolly-pack.ts`).

draft shape

```text
    Definition   id + schemaVersion + engineVersion + revision digest
    Instance     schemaVersion + definition binding (id, version, revision) + authored content
    Evaluation   schemaVersion + definition revision + instance revision + facts of the run
    Artifact     bytes + digest + the evaluation that produced them
```

This is a draft shape for review, not a type. Nothing is added to `packages/core` or `schemas/` in this wave, and the plan's section 18 orders the counterexample fixtures before any of it is frozen.

## Vocabulary

The glossary (`docs/glossary.md`) already defines engine, shell, host, tool, manifest, template, hooks, input model, URL mode, capabilities and requires, status, isolate, compose, content profile, catalog, asset, design tokens, session, user template, utility, batch, collab, project, export, Content Credentials, watermark and ship gate. Those words keep their meanings here.

One word carries three senses in this tree, and each is written in full every time. Two of them are a content profile and a user profile (`docs/glossary.md`). The third is the colour handling named by the reserved export parameter `profile` (`engine/src/url-mode.ts`). A conformance suite and an execution class carry the two senses an earlier draft would have added to this word, so no fourth or fifth sense is minted. Feature and output target are named for the same reason: each says what it is.

| Term | Meaning | Not |
|---|---|---|
| Record | One of four semantic units: definition, instance, evaluation, artifact. | A file format. Several records may share one package. |
| Operation | What a tool can do: render, transform, extract, inspect, present, act. A tool may expose several. | A closed enum; namespaces may add shapes. |
| Outcome | The typed result of one operation run: state, effects, findings, optional receipt. | A boolean. |
| Output target | A format plus size, units, DPI, colour handling and paging for one render. | The ICC `profile=` parameter, which is one field of it. |
| Execution class | The trust and enforcement context code runs in: `trusted-realm`, `isolated-worker`, `strict-worker`, `node-worker` and later `vm` for an interpreter in WebAssembly or a hardened compartment. | A capability flag. |
| Conformance suite | An immutable, versioned set of fixtures, references, a comparator and a reference environment, such as `still-2d/1`. | A content profile or a colour profile. |
| Feature | A named capability inside a suite, marked core or extended. | A host API in `requires`. |
| Acceptance | An authority's recorded decision about a measured result, including exceptions. | A pass. |
| Effective policy | The composed, attributed rule set one evaluation ran under. | The manifest. |

## The thirteen invariants

The plan lists these as candidates. This chapter states them as rules so reviewers can argue with them; the [status chapter](status.html) records when they bind. Each one cites the resolution or the repository file it rests on.

The numbers are fixed, because other chapters cite invariants by number. Read by what each one governs, the thirteen fall into four groups:

- What the source holds: 1, 7, 8 and 9.
- What code may do before and while it runs: 2 and 10.
- What a run must report, and how it fails: 3, 4, 5 and 6.
- What the model must stay open to: 11, 12 and 13.

The table after the list says where each invariant holds today, what is not met yet and which proof cases test it.

1. **Intent over implementation.** The source must record meaning, parameters, dependencies and permitted changes, and renderer internals must live in declared extensions (D1, R10). A document that stores one renderer's internals cannot be read by a second renderer.
2. **Inspection before execution.** A host must be able to read declared inputs, operations, rules, code, required powers and known dependencies without running any document code (D4, `schemas/tool.schema.json`). Inputs are declared in the manifest and never inferred from the template, and one module owns what an input means (`engine/src/inputs.ts`), so the declared surface can be read before anything runs. The declared dependency envelope and the dependencies discovered during execution must be reported separately (R15).
3. **Explicit evaluation context.** A result must identify the definition and instance revisions, the resolved resources, the engine and suite versions, the execution class, the effective policy, the time, the seed and the capabilities granted for that operation (R15). A receipt that omits the font files it resolved cannot support a fidelity claim about the file it describes.
4. **Output-based conformance, in three parts.** A claim must be tested against reference artifacts with a declared fuzz band, against region-scoped structural checks and against semantic checks on the produced artifact (R9, D3). One whole-image number cannot separate a vanished caption from a half-pixel shift, which is why the evidence appears in the [conformance chapter](conformance.html) and nowhere else. Byte identity across platforms is a separate and stronger claim with a known price: the emoji treatment's colour core uses no transcendental function, because V8 and JavaScriptCore agree bit for bit on add, subtract, multiply, divide and square root but not on pow, cbrt, exp or log (`engine/src/emoji-treatment.ts`).
5. **Repeatability is declared.** Rendering, reconstruction, capture and retention are four independent properties, and an operation may succeed with no persisted recording (D11, R15). A utility that strips metadata from a person's own file has done its job without retaining anything.
6. **Safe failure.** A missing capability, resource, grant or compatible version must produce a structured failure at the affected operation, and an optional feature must have an explicit fallback with the loss reported (R5, R10). A host must never claim to have rendered a feature it does not support. The single-file rule that an unsuccessful operation claims no outputs stays inside the adapter that owns it and must not be generalised to every operation (C3, `packages/core/src/file-operation-v1.ts`).
7. **Stable identity.** Tools, instances, rows, components and assets must keep permanent identities, a small edit must produce a small diff on the source rows, and a patch must state the revision it was made against, which a merge must still be validated after, because two patches valid against the same base can converge into an invalid document (R1, C2, `packages/core/src/canvas-op-v1.ts`). Identity by position breaks the moment two people edit at once.
8. **Typed authorable values.** A property must have a declared type and property-specific permissions for literals, tokens, bindings, conditionals, expressions and timed values, and a structured value must travel as a typed payload record referenced by id, never as an ad hoc string (R2, R14, C1). The collaboration row permits only strings, numbers, booleans and null, so a chart cannot be spread across scalar fields.
9. **Separate concerns.** Definitions, authored state, evaluation results, artifacts, tokens, data and policy have distinct semantics, and editor selection and panel state are separate metadata (R4, `shells/web/src/lib/editor-state.ts`). A selection that travelled as document content would make two identical designs compare as different.
10. **Least authority for strict execution.** Code must never acquire a device power because the host happens to support it, and trusted compatibility execution must be classified as such and must never inherit a strict claim (R3, C6, `engine/src/hook-worker-core.ts`). In-realm hooks can reach page globals, which `docs/constraints.md` already states as the limit of that path.
11. **Accessible intent.** Roles, descriptions, reading and focus order and input alternatives must be available where they are meaningful, with checks specific to each output (D1; the plan's section 6.6 re-homes the accessibility chapter, and its findings take the one finding shape R5 gives every outcome). Roles and reading order that survive to the screen but not into PDF or video are lost at the export boundary, and each output needs a check of its own.
12. **Media and operation independence.** A new domain or output kind must be addable without forcing every tool into image or scene semantics (D5, D11). Extraction has no rotation, and a transform has no canvas.
13. **Existing tools remain usable.** Adoption must be incremental, and an opaque adapter must state which guarantees and editing surfaces it preserves (R13, D11). Every mounted tool keeps working while the model is adopted, or the model is not adopted.

## Where each invariant holds today

Each row gives the place in the tree that already holds part of the invariant, the gap the model still has to close and the proof cases that would fail if the invariant broke. Each gap is work the model requires, taken from the chapter that specifies it. The case numbers are this table's own mapping onto the [proof cases](proof-cases.html), which key each case to a resolution rather than to an invariant.

| Invariant | Holds today at | Not met yet | Proof cases |
|---|---|---|---|
| 1. Intent over implementation | Inputs are declared with their meaning in a closed manifest schema that refuses unknown keys (`schemas/tool.schema.json`). | No extension map exists for renderer internals to live in (R10), and the Design `3d` row carries the 3D Studio's own settings as a link query (`community/design/tool.json`). | 3, 8, 21 |
| 2. Inspection before execution | `validateDocument` answers through `validateManifest` without running the tool (`engine/src/validate.ts`), and `missingRequires` refuses a mount before any hook runs (`packages/core/src/host-v1/apis.ts`). | The `requires` list comes from a regular expression that `host["text"]` and a local alias evade (`scripts/tool-requires.ts`, C6), and declared and discovered dependencies are not reported apart. | 8, 20 |
| 3. Explicit evaluation context | Run facts are recorded in six forms that nothing joins, among them the C2PA manifest (`engine/src/c2pa.ts`) and the `.lolly` manifest's engine version and fonts by digest (`shells/web/src/lib/lolly-pack.ts`). | No evaluation receipt exists. A live-text fallback reaches standard error and the `--json` warnings, never a receipt (`shells/cli/src/output.ts`), and no record says which execution class ran. | 4, 18, 19, 23 |
| 4. Output-based conformance | `compareSources` answers `undetermined` rather than equal when a budget cuts it short (`engine/src/compare.ts`), and `lolly smoke` never counts a skip as a pass (`shells/cli/src/smoke.ts`). | No suite, no fixture and no three-check comparator, and no band is calibrated (R9, C8). | 9, 10, 22 |
| 5. Repeatability is declared | An on-device utility's output carries no provenance and no watermark, so its success never depends on a record of the run (`engine/src/runtime.ts`). | Nothing declares capture, retention or replay, and Q4 is open. | 7, 25 |
| 6. Safe failure | An engine range that cannot be parsed fails closed (`engine/src/loader.ts`), a missing host API is refused by name before any hook runs, and an export that needs a layout check the host cannot perform fails with `NEEDS_BROWSER` rather than substituting (`shells/cli/src/exit-codes.ts`). | Refusal is scoped to the mount rather than the operation, no shared outcome type exists, and `resume()` drops the record of files already written when an input changes (`packages/node-shell/src/rebrand-run-manifest.ts`). | 5, 6, 8, 11, 16, 23, 24 |
| 7. Stable identity | Tool and asset ids are permanent, the same version with different bytes is refused (`shells/web/src/lib/installed-tools.ts`), and `validate:catalog` holds the blocks wire order append-only (`schemas/blocks-wire-order.json`). | No patch carries a base revision (`services/mcp/src/tools.ts`), nothing validates a merge after convergence (C2), and Design paints rows in array position with no per-row order key. | 2, 15, 17, 21 |
| 8. Typed authorable values | Collaboration rows accept scalars only (`schemas/canvas-op.schema.json`), and `validateDesignTool` refuses a second writer on one property (`packages/core/src/design-tool-v1.ts`). | No typed payload record exists. Design's text document, text frames and 3D scene travel as JSON or a query string inside text fields (`community/design/tool.json`), and a nested chart arrives as an image (R14). | 2, 9, 13, 14 |
| 9. Separate concerns | Editor selection and panel state travel as `_ui=`, apart from the document's inputs (`shells/web/src/lib/editor-state.ts`), and `CompiledDocument` stays a transient result (`engine/src/document-api.ts`). | `diffDocuments` takes row identity from hydrated output (F10), and whether a recorded local waiver belongs to the revision waits on Q2. | 1, 2, 18 |
| 10. Least authority for strict execution | Sideloaded and remote tools run in a strict worker that refuses the mount rather than fall back, with undeclared host namespaces left out (`shells/web/src/bridge/hook-worker.ts`), and `isolate` is set only from evidence (`scripts/tool-isolation.ts`). | No authored effect envelope exists, and a hook that overran its budget keeps running in the realm with nothing binding its grant to the attempt (`engine/src/runtime.ts`, C6). | 12, 20 |
| 11. Accessible intent | `ChartSpecV1` carries accessibility metadata as a structured member (`packages/core/src/chart-v1.ts`). | No accessibility finding type and no per-output check, because plan 195 is not built, and a chart nested through `composes` loses its metadata. | 2, 4, 13, 24 |
| 12. Media and operation independence | The transform path `file()` takes a file in and hands bytes out with no canvas (`packages/core/src/host-v1/export.ts`), and inspection and validation return typed answers (`engine/src/document-api.ts`). | Unpack, Prepare, Batch, Verify and the Rebrand review have no operation adapters (R13), and the manifest has no field that declares a tool's operations. | 3, 4, 5, 16, 25 |
| 13. Existing tools remain usable | `lolly smoke` renders every tool in the active content profile at its defaults, with each skip stated (`shells/cli/src/smoke.ts`), and `migrateSessionRecord` reads a record written at any earlier format (`engine/src/session-record.ts`). | No adapter states which guarantees it preserves, a migration returns no findings, and the tool and project reader gates pass a manifest whose `minReader` was deleted (`shells/web/src/lib/lolly-pack.ts`). | 5, 12, 14, 21 |

Cases 24 and 25 were added because of this table. Before them no case tested invariant 11 on its own, since cases 2, 4 and 13 check accessibility metadata or reading order only beside other facts, and invariant 5 rested on case 7 alone, which covers the live-input half of Q4. Case 24 tests accessible intent in four outputs and case 25 the utility half of Q4.

## Measured conformance and contextual acceptance

These are two answers and must never be merged into one (R8, D12).

**Measured conformance** lists the suite and its released version, the reference environment, the comparator version and the per-feature results, split into core and extended. A core feature must never be skipped, and a skipped test must never read as a pass (R8). A report is data: trust is the separate decision the authority below records, never a consequence of publishing one (R8, D12).

**Contextual acceptance** records the authority, the suite reference it judged and each exception it granted (R8, D12). The authority is the Lolly Work administrator for a governed brand and the local person otherwise (D12).

An authority may select a suite, require stricter checks, approve references or accept an exception. None of those decisions may change the meaning of a suite version already published under another identity (R8). A local waiver of a creative rule must leave measured conformance to the unmodified rule reading failed or excepted, never passed (R6, C4).

What exists today is the material for suites, not a suite. `lolly smoke` renders every tool in the active content profile at its defaults, each to its first Node-native format (`shells/cli/src/smoke.ts`). It skips a tool gated on a live capture and a transform tool with no committed fixture. Each skip carries a stated reason and is never counted as a pass. The host conformance kit checks that a live host carries the members the contract declares (`packages/core/src/host-conformance.ts`). The comparison modules return `undetermined` when a budget cuts the comparison short (`engine/src/compare.ts`). An incomplete comparison must never be recorded as a pass (C8).

## What this model does not promise

Two of these limits bound promises made above. The first item bounds local evaluation, which the thesis lists among its outcomes. The second bounds invariant 4, which states what a fidelity claim is tested against in place of identical pixels. The other three say what this draft is not.

- **No cloud service.** The core requires no hosted service, and a governed client that loses its connection must keep enforcing the policy it holds (R7). The web shell does not do that yet. Once its cached copy is 24 hours old and the server cannot be reached, it drops that copy and fails closed on the surfaces the policy governs (`shells/web/src/org/index.ts`). The [policy chapter](policy.html) records this as a change the model requires. What a link needs in order to render again is the list in `docs/reproducibility.md`, and no item on it is an account.
- **No identical pixels across hardware.** The model makes no unconditional promise of identical pixels on all hardware. This draft adds no conformance or reproducibility claim beyond those `docs/determinism.md` already makes. It states no threshold (C8) and no latency target, because the plan's section 18 puts baseline device measurements before any responsiveness claim.
- **No fabrication or spatial commitment.** Spatial experiences, fabrication and unfamiliar media are extensibility checks, not roadmap items or gates for the first native suite (D5). They exist to keep the extension boundary coherent.
- **No frozen types.** Nothing here freezes a document type, an outcome vocabulary, the patch envelope or the migration contract. Run the counterexamples in the [proof cases chapter](proof-cases.html) before freezing anything, and read the draft shapes in this specification as review material only (R11).
- **No claim that this is built.** The chapters describe a model and name the contracts it would rest on. The [status chapter](status.html) lists what is decided, what is open and what is not built.

## Open points

- **Q4, what "non-recordable" means for the first two cases.** Default: for a utility, capture off, retention none and replay semantic from pinned inputs; for a live-input tool, capture unsupported unless the person turns it on, retention for the session and no replay. Touches invariant 5.
- **Q5, who may publish a suite under the `lolly` namespace.** Default: `lolly/*` suites are published from this repository's CI only; Work instances and local people publish under their own namespace; an acceptance record always says which suite it judged. Touches the conformance and acceptance section.
- **Q6, whether the specification is public from the first draft.** Default: yes, because the constraints, determinism and reproducibility pages already make public promises this model keeps. Touches this chapter as a whole.

The other open questions (Q1 on first interchange routes, Q2 on the granularity of local rejection, Q3 on freshness for a governed claim) are carried, with their defaults, by the [conformance](conformance.html) and [policy](policy.html) chapters.

## Precedents

- `docs/glossary.md`
- `docs/constraints.md`
- `docs/determinism.md`
- `docs/reproducibility.md`
- `docs/design-tool-contract.md`
- `schemas/tool.schema.json`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/canvas-op-v1.ts`
- `packages/core/src/rights-v1.ts`
- `packages/core/src/file-operation-v1.ts`
- `packages/core/src/host-conformance.ts`
- `packages/core/src/host-v1/apis.ts`
- `packages/core/src/host-v1/export.ts`
- `packages/core/src/chart-v1.ts`
- `schemas/canvas-op.schema.json`
- `schemas/blocks-wire-order.json`
- `community/design/tool.json`
- `engine/src/session-record.ts`
- `engine/src/url-mode.ts`
- `engine/src/document-api.ts`
- `engine/src/inputs.ts`
- `engine/src/hook-worker-core.ts`
- `engine/src/compare.ts`
- `engine/src/emoji-treatment.ts`
- `engine/src/validate.ts`
- `engine/src/loader.ts`
- `engine/src/runtime.ts`
- `engine/src/c2pa.ts`
- `scripts/tool-requires.ts`
- `scripts/tool-isolation.ts`
- `services/mcp/src/tools.ts`
- `shells/web/src/lib/lolly-pack.ts`
- `shells/web/src/lib/editor-state.ts`
- `shells/web/src/lib/installed-tools.ts`
- `shells/web/src/bridge/hook-worker.ts`
- `shells/web/src/org/index.ts`
- `shells/cli/src/smoke.ts`
- `shells/cli/src/output.ts`
- `shells/cli/src/exit-codes.ts`
- `packages/node-shell/src/lolly-file.ts`
- `packages/node-shell/src/rebrand-run-manifest.ts`

# Records and identity

> This chapter is a draft for review, dated 2026-09-24.  

The model has four records: definition, instance, evaluation and artifact. All four already exist in this repository, spread over at least seven contracts with no shared naming (R4). This chapter gives each contract a record, states what identity each record keeps and says which record every override mechanism changes. It adds no file format and no new type.

The thesis and the invariants are in the [constitution](constitution.html). The rules for the rows inside an instance are in [source rows, payloads and patches](source-and-patches.html).

## The four records

A Lolly document represents a complete tool (D1). Completeness is a property of the four records together. The model must never require that a definition, a person's authored state and an execution history share one version number (R4).

- A definition record must hold the typed interface, the operations, the dependencies, the tokens and the authored rules of one tool (D1).
- An instance record must hold authored state against one definition (R4). Evaluating an instance under a new organisation policy must never silently rewrite it (R6). A rewrite that does happen is a versioned transform that reports findings ([packaging](packaging.html)).
- An evaluation record must describe one run of one operation, naming the definition and instance revisions it ran against (R15).
- An artifact record is the produced bytes, content addressed where they are carried (R4).
- Every record must carry its own `schemaVersion` (R11). Several records may live in one package: a `.lolly` file already carries a session, a tool bundle, a project or a renovation in one zip (`shells/web/src/lib/lolly-pack.ts`).

A definition may be reused by more than one instance. A Chart definition used by two Design instances with different datasets keeps one identity, and changing one dataset must never mutate the definition (R4). The two instances keep independent identity and state (plan section 1, and the "Chart inside Design and another tool" case in [proof cases](proof-cases.html)).

## The record map

![The record map: each existing contract in the repository on the left, with one edge to the record it holds on the right.](/info/diagrams/document-model/record-map.svg)

| Record | Existing contract | Where | Identity and change today |
|---|---|---|---|
| Tool definition | `tool.json` (closed schema, `additionalProperties: false`), `template.html`, `hooks.js`, `styles.css`; `DesignToolDefinitionV1` for tools compiled from Design; the `.lolly` kind `tool` bundle with `minReader: 2`. | `schemas/tool.schema.json`, `packages/core/src/design-tool-v1.ts`, `shells/web/src/lib/lolly-pack.ts`, `packages/node-shell/src/lolly-file.ts` | Permanent `id`, SemVer `version`. An installed Design tool keeps immutable revisions served from `/tools/<id>/.revisions/<digest>/`, and the same version with different bytes is refused (`shells/web/src/lib/installed-tools.ts`). |
| Authored instance | The session record at format 4: slot, tool id and version, data, design-system stamp, emoji stamp and `rightsDecisions`; the `.lolly` kind `session`; the URL, readable or packed. | `engine/src/session-record.ts`, `engine/src/url-mode.ts`, `schemas/blocks-wire-order.json` | A slot id on device. The field order of every blocks input is a frozen, append-only wire contract; the packed `z` token is raw DEFLATE, so a one-field edit changes the whole token and the JSON rows are the only diffable form. |
| Evaluation result | `CompiledDocument` (transient); the C2PA manifest with actions and source ingredients; `AttributionReceiptV1` with `ReceiptStateV1`; `FileOperationReportV1`; the rebrand `RunFileV1` with its `RunOutcomeV1` and report; the `.lolly` manifest's `engineVersion`, `app`, `exportedAt` and fonts by sha256. | `engine/src/document-api.ts`, `engine/src/c2pa.ts`, `packages/core/src/rights-v1.ts`, `packages/core/src/file-operation-v1.ts`, `packages/node-shell/src/rebrand-run-manifest.ts` | Bound to one run. Nothing joins these six forms today (F1, plan section 4.1). |
| Produced artifacts | Exported bytes; the renders Projects keeps; `.lolly` carried assets, split into `asset` rows that carry bytes and `asset-ref` rows the recipient resolves locally, under an integrity map. | `shells/web/src/lib/lolly-pack.ts` | Content addressed where carried. |

R4 fixes the mapping. A definition is the tool bundle, carried as a signed catalog tool or as a `.lolly` kind `tool` with immutable revision digests. An instance is the session record, a `.lolly` kind `session` or a URL. An evaluation is a receipt: the C2PA manifest, `AttributionReceiptV1` and the proposed `EvaluationReceiptV1` that the [evaluation chapter](evaluation.html) describes. An artifact is the exported bytes.

Each record keeps the identity rules it has today. A tool `id` and an asset `id` are permanent contracts and must never be renamed or reused to express a version (invariant 7, plan section 13, `docs/glossary.md`). Resource identity is a stable logical id plus an immutable revision or digest for one evaluation (invariant 7, plan section 13).

## CompiledDocument stays transient

`compileDocument` mounts a runtime, reads the input model and returns a `CompiledDocument` holding the manifest, the model, the values, the resolved token references where the model holds any, the hydrated markup and the scoped styles, which are null when the tool ships none (`engine/src/document-api.ts`).

- A `CompiledDocument` must never be persisted as a document record (R4).
- A reader must never treat a compile result as a source of authored content (R4).
- A compile result may be held as a cache (plan section 6.3, review gate 5). A revision covers the definition binding and the authoritative instance content. It excludes caches. A cached result is therefore keyed by the revisions it was produced from and never stands in for them. [Evaluation](evaluation.html) states when such a cache is invalidated.

Today `diffDocuments` fills its `boxes` field by matching every `id=` attribute in the hydrated markup with a regular expression (`engine/src/document-api.ts`). That makes a lowered result into a second source of authored content, which the source rule forbids (R1). Semantic diff must move to the authored rows (R1). The [packaging chapter](packaging.html) states the source-aware path and what stays as it is for template tools that have no rows.

Component expansion, chart lowering, text shaping and timeline sampling produce derived evaluation graphs in the same way. Such a graph is never edited and is never persisted as a document (R1).

## Definition contents

The contents below are the draft grouping, not a file layout. Nothing here is added to `packages/core` or `schemas/` in this wave.

draft shape

```text
  Tool definition
    identity + schemaVersion + engineVersion + authorship/provenance
    typed inputs + defaults + validation (the input model, unchanged)
    operations + typed outcomes + declared effects
    resources + immutable dependency resolution rules
    tokens + authored rules
    reusable components and public interfaces
    optional compositions, interactions and timelines
    output targets + execution requirements (one `requires` vocabulary)
    extensions (used / required) + separate authoring metadata
```

The input model stays exactly as it is: inputs are declared in the manifest and never inferred from the template (`schemas/tool.schema.json`). The [operations chapter](operations.html) owns the operation and outcome entries. [Values and time](values-and-time.html) owns the typed values and the execution requirements, and [capabilities, trust and extensions](extensions.html) owns the extension lists.

A composition has a coordinate domain and roots (plan section 6.2). A row in an appropriate domain has an identity, a kind, one owner, an order and the traits that apply to it (plan section 6.2, R1). Transforms and visibility must never be modelled as universal row properties, because an extraction operation has no rotation (plan section 6.2, D11). Geometry, text, media, charts, layout and interaction use composable traits rather than an inheritance hierarchy (plan section 6.2). A component exposes a typed interface, and an instance may provide overrides only at declared override points (R1, `packages/core/src/design-tool-v1.ts`).

## The component v0

`DesignToolDraftV1` is already a component contract (`packages/core/src/design-tool-v1.ts`). Its parts map onto the component vocabulary one for one.

| Component concept | In `DesignToolDraftV1` today |
|---|---|
| Public interface | `inputs`, each a `DesignInputV1` wrapping one `InputSpec`. |
| Declared override points | `targets`, each a variant id, a layer id and one of nine properties. |
| Constraints | `approved` lists per input, `fixedInputs` per choice option, `DesignTextRuleV1` and `DesignImageRuleV1`. |
| Atomic multi-property change | `DesignChoiceV1.options[].writes`, a list of writes applied together. |
| Declarative text composition | `DesignTextRecipeV1`, a target plus input and literal parts. |
| One writer per property | `validateDesignTool` keys every write by variant, layer and property and refuses a second writer. |
| Compiled form | `DesignToolDefinitionV1` adds `compilerVersion`, `rendererDigest`, scoped `css` and `dependencies` with digests. |

Slide masters are components with slots. A master is design-system data a brand pack ships, holding archetypes with named placeholder roles and the furniture each one shows; `seedFrame` lays a new slide down and `applyArchetype` re-lays one into another archetype, reading the `master`, `role` and `furniture` fields on each seeded layer (`shells/web/src/views/free-canvas/slide-masters.ts`).

`ComponentDefinitionV1` must be specified as the generalisation of `DesignToolDraftV1` and never as a new object (R1, `packages/core/src/design-tool-v1.ts`). The generalisation adds a semantic version, a dependency set and a lock section. It keeps the target and ownership rules that `validateDesignTool` enforces today (`packages/core/src/design-tool-v1.ts`). An instance pins a version or a range, maps parent values explicitly and may select only declared variants (R11). Rive splits a typed View Model from its instances and its bindings, which is the same three-way separation between interface, values and wiring (plan section 6.4; D13 is the direction that makes the comparison worth stating).

Two identity rules follow the component into the source rows. An expanded component row keeps the source row id and the instance id together (R1). A component upgrade whose override target disappeared is reported rather than dropped (R1). [Source rows, payloads and patches](source-and-patches.html) states both.

## Three overlay mechanisms

Three different override mechanisms already ship. The model must never unify their implementations, and it says which record each one changes so a fourth is not added without that answer (R4).

| Mechanism | Where | Record it changes | What it does |
|---|---|---|---|
| `extends: "community"` | `schemas/tool.schema.json`, `packages/node-shell/src/content-roots.ts` | Definition | A brand-pack tool declares a per-file union with the community tool of the same id. The overlay file wins on a filename collision and the marker is stripped from the manifest consumers see. A declared overlay whose base is missing is an error, never a partial tool. |
| Slide masters | `shells/web/src/views/free-canvas/slide-masters.ts` | Definition | Archetypes with placeholder roles and furniture are seeded into a frame. Furniture is committed locked, and a layer a person drew by hand carries none of the three fields and is never re-laid. |
| `composes` and `compose.renderUrl` | `schemas/tool.schema.json`, `packages/core/src/host-v1/compose.ts` | Evaluation | A nested render is produced through the same load and render path and handed to the template as an embeddable asset. Recursion is depth and cycle guarded by the host. |

A `composes` result is an image, so it changes the evaluation and never the definition or the instance (`packages/core/src/host-v1/compose.ts`). Appearance can pass a still comparison while the editable round trip fails, which is the flattened-chart case in [proof cases](proof-cases.html). A nested chart must instead be a typed payload record referenced from the row (R14), and [source rows, payloads and patches](source-and-patches.html) carries that rule.

An author who adds a fourth override mechanism must name the record it changes before it ships (R4).

## Chapters re-homed from plans 190 to 196

Seven September plans define contracts this model needs, in six chapters. None of them is built, except that plan 193 is partly built. This specification settles the records and identity rules those plans share: each becomes a chapter of the model rather than a separate contract (R4).

| Chapter | Source plan | Status | What this specification settles | Where |
|---|---|---|---|---|
| Dependency graph and components | 190 | Not built | The record and identity rules above. The graph is emitted at evaluation and pinned in the receipt, never kept as a second source (R1). | This chapter and [evaluation](evaluation.html) |
| Locales, layouts, variants | 191 | Not built | Layout selection uses the `ExpressionV1` rules, and a selected layout id is part of the instance snapshot (R2). | [Values and time](values-and-time.html) |
| Declarative charts | 192, 193 | 193 partly built | A `chart` layer kind whose row references a `ChartSpecV1` payload by id (R14). Renderer adapters pass the same conformance suite. | [Source rows and patches](source-and-patches.html), [conformance](conformance.html) |
| Rules and expressions | 194 | Not built | The JSON AST with a closed operator set, a static cost estimate at authoring time and a runtime budget (R2). | [Values and time](values-and-time.html) |
| Accessibility findings | 195 | Not built | An accessibility finding uses the one finding entry every other finding table in the tree uses: a code, a severity, a message and an optional path and evidence (R5, `packages/core/src/file-v1.ts`). | [Evaluation](evaluation.html), [conformance](conformance.html) |
| Timelines and motion capabilities | 196 | Not built | The `motion-2d/1` suite is staged after `TimelineV1` exists (R12); the clocks are specified with the other time rules. | [Values and time](values-and-time.html), [conformance](conformance.html) |

Each of those plans keeps its own build sequence. This chapter settles only the records they attach to and the identity rules they must keep.

## Open points

- **Q2, granularity and persistence of local rejection.** Default: per rule, instance scoped, recorded on the session record with a fingerprint (R6). A derived tool revision is a Design tool export and is not part of 0.1. This touches the instance record, which already carries `rightsDecisions` keyed by work and kind with a fingerprint (`engine/src/session-record.ts`). The rule side is in [constraints, authority and local choice](policy.html). Evidence that would change it: a need to share a rejected state as a reusable tool.
- **Q6, whether the specification is public from the first draft.** Default: yes, published under `/info/` with the draft notice, since the constraints, determinism and reproducibility pages already make public promises this model has to keep. Evidence that would change it: a reason to keep the drafts private until the pilots pass. The [status chapter](status.html) records the answer.

The other open questions are settled in other chapters. Q1 and Q5 are answered in [Conformance and fidelity](conformance.html). Q3 is answered in [Constraints, authority and local choice](policy.html). Q4 is answered in [Operations and outcomes](operations.html).

## Precedents

- `schemas/tool.schema.json`
- `schemas/blocks-wire-order.json`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/rights-v1.ts`
- `packages/core/src/file-v1.ts`
- `packages/core/src/file-operation-v1.ts`
- `packages/core/src/host-v1/compose.ts`
- `packages/node-shell/src/lolly-file.ts`
- `packages/node-shell/src/content-roots.ts`
- `packages/node-shell/src/rebrand-run-manifest.ts`
- `engine/src/c2pa.ts`
- `engine/src/document-api.ts`
- `engine/src/session-record.ts`
- `engine/src/url-mode.ts`
- `shells/web/src/lib/lolly-pack.ts`
- `shells/web/src/lib/installed-tools.ts`
- `shells/web/src/views/free-canvas/slide-masters.ts`
- `docs/glossary.md`

# Source rows, payloads and patches

> This chapter is a draft for review, dated 2026-09-24.  

This chapter specifies the authoritative content of an instance and the ways a program changes it. [Records and identity](records.html) lists the four records and says which existing contract holds each one. Here the subject is narrower: the rows a person authored and the typed payload records those rows point at. It also covers the patches that edit both.

The rules restate plan resolutions R1 and R14 with the review corrections C1, C2 and C7. The working reference is the collaboration contract in `packages/core/src/canvas-op-v1.ts` and the Design document it already carries.

## One authoritative source

The authoritative source of an instance must be a flat list of rows plus a small set of typed payload records (R1). Component expansion, chart lowering, text shaping and timeline sampling produce derived evaluation graphs. A derived graph must never be edited and must never be persisted as a document (R1). `CompiledDocument` in `engine/src/document-api.ts` is one such derived result and stays a transient compile result.

Today one path breaks that rule and the specification records it. `diffDocuments` in the same file compares two documents' rows by scraping `id=` attributes out of the hydrated HTML (`ids(a.hydrated)`), so a lowered graph is acting as a second source (F10). Semantic diff moves to the source rows, and [packaging](packaging.html) carries the migration. A compiled document keeps a comparison path of its own for template tools that have no source rows.

![Rows keyed by permanent id in the boxes lane, each with one owner pointer and one order key, beside the params lane of input values, with the text document and chart spec payload records referenced by id.](/info/diagrams/document-model/source-rows-and-payloads.svg)

Design is the worked example. Its document is the `boxes` input in `community/design/tool.json`: a flat array of rows, each with a permanent `id`, a `kind`, geometry fields and a `frame` field naming the artboard that owns it.

| Rule | Statement | Evidence |
|---|---|---|
| One owner pointer | A row must carry exactly one ownership pointer, and children must be found by query (R1). | The `frame` field on a Design row; `childLayerIds` in `packages/core/src/design-v1.ts` is derived by the read model. |
| No redundant children | A source must never keep a second list of children beside the pointer (R1). | Penpot keeps a parent, a children list and a frame pointer and needs a validator and a repair module to hold them together. |
| One order rule | Paint order must be a per-row order key with the row id as the tie-break (R1). | `ReferenceCanvasDoc` in `packages/core/src/canvas-op-v1.ts` sorts alive rows by order key then by id. Design does not yet: see below. |

Array position in a serialised document must be derived from the order rule and must never be a second source of order (R1, C2). An adapter that keeps its own list, as the Lolly Work adapter does with a Yjs array, must state the relationship between that list and the order keys (C2).

Design is the first adapter that owes this statement. Its rows paint in `boxes` array position inside an artboard, and its `order` field orders artboards only, tie-broken on ascending x rather than on the row id. The blocks array position is also a frozen positional wire form, so the relationship runs both ways. The order key is authoritative, and the serialised array is written in that order. The migration adds the per-row key without moving an existing row's position. [Packaging](packaging.html) owns it.

Rows must be flat maps of scalar fields (R1). Nesting a tree inside a row breaks the collaboration contract, which is a per-field last-writer-wins register over exactly this shape. Each field merges independently, keyed by a Lamport clock with the client id as the tie-break.

The contract splits field writes into two lanes, and the specification keeps that split (R1). `laneForField` in `packages/core/src/canvas-op-v1.ts` puts the five geometry roles on the geometry lane, which never invalidates a raster. The role names default to `x`, `y`, `w`, `h` and `rot` and are resolved from the tool's own field configuration before they cross the seam, so a tool that renames them keeps the split. Everything else rides the content lane, including the structural `frame`, `group`, `order` and `kind` fields. So an ownership change is an ordinary field write, and a concurrent move and restyle of one row compose without a lost update.

## Typed payload records

A row field may hold a scalar and nothing else (R1, C1). `Scalar` in `packages/core/src/canvas-op-v1.ts` is string, number, boolean or null, and `schemas/canvas-op.schema.json` enforces the same union (C1). A structured value therefore has no place in a row field. Encoding one as a string is refused by the [constitution](constitution.html), whose rule 8 states it directly: a structured value travels as a typed payload record referenced by id, never as an ad hoc string. The consequence this chapter adds is the practical one. A domain hidden in a string cannot be validated, diffed or merged by anything but the tool that wrote it.

A structured value must live in a typed, versioned payload record referenced from the row by id (R1, C1). The payload must be replaced as one atomic value, and it must never be spread across scalar fields (R1).

Design comes closest for text, and the part it gets right is the division. The `textDocument` input holds one document of stories. A row points at its story in `textStory` and its frame settings in `textFrame`. `composeDesignStories` in `community/design/hooks.js` refuses a frame settings object that carries `id`, `storyId`, `width` or `height`, because identity and size belong to the row. It also refuses a story whose named frame is missing, or whose row points at a different story. What it does not yet supply is the record. The document travels as JSON inside a `longtext` input and the frame settings as JSON inside a `text` row field (`JSON.parse(inp.textDocument)`, `JSON.parse(box.textFrame)`), with no `kind` and no `schemaVersion`. The division is the precedent; the typed record is owed here too, and the [proof cases](proof-cases.html) carry it beside the `3d` scene.

draft shape

```text
    TypedPayloadRecordV1
      id             permanent id, referenced by one or more rows
      kind           the payload type, such as text-story, chart-spec or scene
      schemaVersion  the payload's own version, upgraded on read
      value          the typed content, replaced as one atomic value
```

This is a draft shape for review, not a type. Nothing is added to `packages/core` or `schemas/` in this wave (R11).

Two consequences follow:

- Concurrent edits to one payload conflict or replace the whole payload, until finer operations are specified for that payload kind (R1, C1). An implementation may not claim field-level collaborative editing of a payload before those operations exist.
- The scalar wire contract stays unchanged for existing clients (C1). A row gains a pointer field and never a structured value. A blocks input's field order is frozen and append-only in `schemas/blocks-wire-order.json`, so that pointer is appended at the end. [Packaging](packaging.html) owns that migration.

The `3d` layer kind is the loudest case, not the only one. Its `scene` field in `community/design/tool.json` carries the 3D Studio's settings as a link query inside a 2D row, which is a coordinate domain smuggled in as text, and it must become a typed payload record referenced from the row (R1). In the same input, `textFrame`, `textWrap` and `vectorSource` hold JSON in a `text` field, and `path`, `kf`, `fx` and `grad` hold their own grammars. The rule reaches all of them. It does not follow that all of them move the same way. `path`, `kf`, `fx` and `grad` are compact because they travel in a link, so the specification must say whether a published grammar with a decoder satisfies the rule or whether the row pointer replaces it. [Packaging](packaging.html) owns that decision. The migration appends a field beside the string and leaves the existing links working; the [proof cases](proof-cases.html) carry it.

## The chart payload

A chart inside a Design document must be a `chart` layer kind whose row references a `ChartSpecV1` payload record by id (R14). `packages/core/src/chart-v1.ts` already holds that type, with datasets, series, scales, theme, motion, presentation and accessibility as structured members. None of them fits in a scalar field, which is why the chart follows the `textDocument` pattern rather than the row (C1, R14).

The payload must be lowered by pure engine code at evaluation time, and the lowered result must never be persisted as the source (R1, R14). A payload's datasets, encodings and accessibility metadata must survive nesting, lowering, serialising, reopening, copying, patching, synchronising and export (R14, C1). The [proof cases](proof-cases.html) run that sequence, and no step in it may turn the chart into a string or a flattened image. Synchronisation replaces the whole payload as one atomic value until finer chart operations are specified (R1).

Today's behaviour is different, and the specification records the difference. `composes` and `compose.renderUrl` nest a child render as an image, so the chart arrives flattened with no datasets and no accessibility metadata. That is the failing proof case, not the pilot (R14). It appears in the [proof cases](proof-cases.html) as the flattened chart whose pixels match while the editable round trip fails.

## What a revision covers

A revision covers the definition binding and the authoritative instance content, including order and payloads (C2). A revision excludes selection, caches and receipts (C2).

| In the revision | Out of the revision |
|---|---|
| The definition the instance is bound to, with its version | Editor selection and panel state, carried as `_ui=` today (`docs/url-app-links.md`) |
| Every source row, its fields, its owner pointer and its order key | Rendered caches, thumbnails and derived graphs |
| Every typed payload record the rows reference | Receipts and acceptance records, which name the revision they describe |

A receipt, a signature or an acceptance describes exactly the revision it was made against, and a later revision must never inherit it (C7). [Evaluation and receipts](evaluation.html) and [packaging](packaging.html) carry that rule for receipts and packages.

## Where compare-and-swap is atomic

Compare-and-swap on a base revision is atomic only where a mutable head exists (C2). Two such heads exist today: the shell's session store, whose record layout is `engine/src/session-record.ts`, and a collaboration room held by the Lolly Work adapter. Neither stores a revision yet. The session record versions its own layout and the engine that wrote it, and the collaboration contract orders field writes by Lamport clock with the client id as the tie-break. The revision is a field the envelope adds at both heads rather than one it reads. Against either head, a patch envelope whose base revision no longer matches must be refused rather than merged (C2).

The MCP path is a stateless transformation of supplied inputs, and it must declare that mode (C2). `resolveInputs` in `services/mcp/src/tools.ts` takes the caller's inputs, applies `layerOperations` then `layerPatches` and returns a new value. It never reads a persistent head, so a hash of the supplied inputs could not detect another writer's change elsewhere. A stateless path must never claim to protect an external head (C2).

A stale revision alone does not identify the rows that changed. Changed ids may be returned only when a retained base or a change history supports the claim (C2).

Today neither channel carries a precondition at all. `layerOperations` and `layerPatches` in `services/mcp/src/tools.ts` apply to whatever inputs the caller supplied. An agent therefore edits without a base, which is the gap the envelope closes. The stateless mode is kept and named rather than removed (C2).

## The patch envelope

![A patch envelope carrying a base revision and an ordered list of operations, applied all or nothing against a mutable head, with merge validation after convergence blocking an ownership cycle.](/info/diagrams/document-model/patch-envelope.svg)

A patch envelope carries a base revision and an ordered list of operations (R1).

draft shape

```text
    PatchEnvelopeV1
      baseRevision   the revision the operations were written against
      ops[]          add | duplicate | remove | reparent | reorder | set, applied in order
```

This is a draft shape for review, not a type. Nothing is added to `packages/core` or `schemas/` in this wave (R11).

The operation names are the ones the MCP surface already uses, split there across two arguments: `layerOperations` carries the five structural operations and `layerPatches` carries `set` (`.claude/skills/lolly/reference/design.md`, `services/mcp/src/tools.ts`).

Two differences from today are part of the contract. The envelope interleaves structural operations and field sets in one ordered list, while the MCP surface applies every `layerOperations` entry before every `layerPatches` entry. So an existing caller's two arrays must keep their phased meaning, and the envelope arrives as an additive, explicitly versioned path beside them, never as a reinterpretation of those two fields. Both channels are also Design-only today. The envelope is defined for any instance whose source is rows, so a tool that has no row source refuses it by name rather than silently accepting it.

| Operation | What it does | Addressing |
|---|---|---|
| `add` | Insert a row, optionally before or after a named sibling. | The new row's permanent id, refused if it already exists. |
| `duplicate` | Clone a row. An artboard's children are cloned too, each under a new id the caller supplies; a row that is not an artboard may not supply child ids. | Source id, the new id and, for an artboard with children, a complete old-id to new-id map. |
| `remove` | Delete a row. Removing an artboard that still has children requires an explicit cascade, so nothing is orphaned or deleted by default. | Row id, plus the cascade flag for a non-empty artboard. |
| `reparent` | Change a row's owner pointer. | Row id plus the new owner, or none. |
| `reorder` | Move a row before or after a sibling. | Row id plus an anchor id. |
| `set` | Merge declared fields into one row. | Row id; the id itself can never be changed. |

An envelope must apply all or nothing, including when the invalid operation is the last one in the list (C2). A target that was removed must fail by id and never by position (R1). The MCP path already fails this way: every operation with a target resolves it through a row lookup that reports the missing id in its error, and `add` refuses an id that already exists.

Where no mutable head exists the envelope still carries its base revision, and the receiving path records that it applied the operations as a transformation rather than as a compare-and-swap (C2).

## Merge validation after convergence

Two individually valid edits can converge on an invalid document, so validation must run after convergence and not only on each patch (R1, C2).

The counterexample runs against the reference model in `packages/core/src/canvas-op-v1.ts`, and [proof cases](proof-cases.html) carries it as a merge to be proved. Alice sets A's owner to B while Bob sets B's owner to A. Each edit is valid against the same base. The reference CRDT converges per field, so both writes survive in either delivery order and the merged document owns A inside B inside A. A base revision precondition cannot prevent it, because neither patch was stale.

After convergence, an ownership cycle, a deleted owner, a dangling reference or a torn atomic choice must block evaluation deterministically, and both edits must be kept for repair (R1, C2). A torn atomic choice is a set of fields one option writes as a unit (`choices` in `packages/core/src/design-tool-v1.ts`) whose members merged from two different options. An implementation must never silently drop an edge to make a merge converge (C2). The finding vocabulary already exists in part: `packages/core/src/design-v1.ts` raises `design.layer.artboard-missing` for a dangling owner and `design.layer.id-duplicate` for a repeated id, and [Operations and outcomes](operations.html) gives every finding one shape.

Cycles in ownership are always invalid (R1). Value dependencies, resource dependencies and interaction graphs have their own cycle rules, which [values and time](values-and-time.html) states.

The merge cases must be proved in both delivery orders and across a reconnect and a checkpoint (C2). The [proof cases](proof-cases.html) list them: the concurrent reparent, delete against reparent, two concurrent atomic choices, an invalid final operation in an envelope and two patches against one mutable head.

## Component expansion and upgrade

A component expands into rows at evaluation time. Expansion must be bounded by depth and by fan-out, and a recursive reference must be refused at validation with the path that reached it (R1). This chapter sets no numbers. The bounds must be declared by the implementation and refused at validation with a reason (R1). Two bounded paths exist already. `createRuntime` in `engine/src/runtime.ts` carries a compose stack into nested renders so a tool that embeds itself trips a cycle and depth guard instead of recursing. `validateDesignTool` in `packages/core/src/design-tool-v1.ts` refuses a draft over its artboard, layer, input and choice limits.

An expanded row must keep the source-row id and the instance id together, so repeated components stay distinct and re-evaluation preserves correspondence (R1).

Component upgrade must be an explicit migration (R1). An override whose target disappeared must be reported and must never be silently dropped (R1). `validateDesignTool` already raises `missing-layer` for a target input whose linked object was removed, and the finding gives the input and the layer. [Packaging](packaging.html) states the general migration rules, and [records and identity](records.html) covers the component contract that `DesignToolDraftV1` holds today.

## Open points

- **Q2, granularity and persistence of local rejection.** Default: per rule, instance-scoped, recorded on the session record with a fingerprint of the facts it was made about. This chapter does not settle whether a recorded waiver is part of the authoritative instance content a revision covers. [Constraints, authority and local choice](policy.html) owns that decision, and this chapter follows it.
- **Q6, whether the specification is public from the first draft.** Default: yes, published to `/info/build/` beside the constraints and determinism pages, with the draft notice at the head of every chapter. Those pages already make public promises this model has to keep.

No other open question touches this chapter.

## Precedents

- `packages/core/src/canvas-op-v1.ts`
- `schemas/canvas-op.schema.json`
- `packages/core/src/design-v1.ts`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/chart-v1.ts`
- `services/mcp/src/tools.ts`
- `community/design/tool.json`
- `community/design/hooks.js`
- `engine/src/document-api.ts`
- `engine/src/runtime.ts`
- `engine/src/session-record.ts`
- `schemas/blocks-wire-order.json`
- `.claude/skills/lolly/reference/design.md`
- `docs/url-app-links.md`

# Operations and outcomes

> This chapter is a draft for review, dated 2026-09-24.  

An operation is what a tool can do. A Lolly document represents a complete tool (D1). It declares operations beside inputs, dependencies and rules. Rendering is one operation among several. A tool that draws nothing a person keeps is still a valid tool (D11).

This chapter specifies six operation shapes and the outcome one run returns. It then specifies the lifecycle a multi-stage run moves through and the rules that hold when a run stops early. It restates plan sections 4.2 and 7 in normative form, with the correction the review recorded as C3. The records these operations read and write are specified in [Records and identity](records.html); the pipeline that runs one is specified in [Evaluation and receipts](evaluation.html).

## The six operation shapes

| Operation shape | Example in the tree today | Success criterion |
|---|---|---|
| Render | Design produces PNG or SVG. | The artifact satisfies its output target and passes the conformance suite the run claims. |
| Transform | Rebrand applies a reviewed plan to a deck. | The result satisfies the transformation contract, and unresolved work is reported. |
| Extract | Unpack returns selected content from a source file. | The requested resources and the relevant source relationships are returned. |
| Inspect and validate | `lolly_inspect` and `lolly_validate` report a document's contents or findings. | A typed result is returned. No artifact is required. |
| Present and interact | Agenda runs a searchable programme from an interactive HTML export built with `render.portable`. | State, actions, accessibility and presentation follow the declared contract. |
| Authorised host action | Commit an operation's files to a chosen destination. | The declared action completes inside its grant. Partial completion or failure is reported. |

These are useful shapes, not a closed enum (plan section 7). A namespace may declare another shape, and a host must never treat the six as exhaustive. Two adjacent buttons can drive two operations: previewing a Rebrand result and committing its files are separate operations with separate outcomes. Plan section 4.2 lists a seventh row, multi-stage, whose single instance is Rebrand. It is not a seventh shape: a multi-stage run is a sequence of operations under one run lifecycle, specified below.

The evidence for each shape already exists under other names. `engine/src/document-api.ts` carries the inspect and validate verbs as `validateDocument`, `inspectDocument`, `diffDocuments` and `measureDocument`. `compileDocument` in the same file is not one of them. It calls `createRuntime`, so it runs document code and returns a transient compile result (R4). The `inspectDocument(bytes)` overload is the extract shape over a file, beside `lolly validate --metadata` in `shells/cli/src/validate.ts` and the `#/unpack` route. `packages/core/src/host-v1/export.ts` carries render as `render()` and the authorised host action as `download()` and `share()`. The same file carries the transform path as `file()`, which never watermarks and never embeds provenance. `FileOperationRequestV1` in `packages/core/src/file-operation-v1.ts` is that path's typed request. Thirteen tool directories declare `privacy: on-device` with a `file` input and an `exportFile` hook, which is a raw directory count rather than a resolved one. `schemas/tool.schema.json` carries the present shape as `render.portable`, and `community/agenda/tool.json` is the one tool to declare it.

![The six operation shapes, the three parts of one outcome and the four run lifecycle values, drawn as three separate groups.](/info/diagrams/document-model/operation-shapes.svg)

## Operations without an artifact

An operation must never require a canvas, a scene graph or a visual export to be valid (D11). Inspection, validation and measurement return a typed answer and write nothing.

Unpack, Prepare, Batch, Verify and the Rebrand review are shell routes today, not manifest tools. Each must gain an operation adapter with typed inputs and a typed outcome (R13). None of the five needs a canvas. Until those adapters exist, this chapter describes them as operations and the tree does not yet expose them as such.

## OutcomeV1

One terminated run returns one outcome. Three things the tree conflates into a single enum today are kept apart in it (R5, C3). The first is the typed result of an inspection. The second is the list of artifacts produced and committed. The third is the termination, meaning how the run ended.

```text
draft shape

  OutcomeV1
    operation       the operation id
    termination     succeeded | partial | failed | cancelled | blocked
    code?           a stable error code; this draft does not specify the code table
    result?         the typed answer of an inspection, validation or measurement
    artifacts[]     what was produced and committed: { id, facts, destination, state },
                    kept whatever the termination
    effects[]       host actions attempted: { kind, target, state }, using the receipt states
    findings[]      { code, severity, message, path?, evidence? }, one shape everywhere
    attempt         { id, resumable }, so a resumed run reconciles against what it committed
    recording       { capture, retention, replay } as declared for this run
    receipt?        EvaluationReceiptV1 when the recording policy retains one
```

This is a draft shape, not a frozen type. Nothing is added to `packages/core` or `schemas/` in this wave, and the plan's section 18 orders the counterexample fixtures before any of it is frozen.

No tree-wide code table exists. `REBRAND_ERROR_CODES`, with the `RUN_PERMANENT_ERROR_CODES` and `RUN_RETRYABLE_ERROR_CODES` split in `packages/node-shell/src/rebrand-run-manifest.ts`, is the only worked example. Transience there is a property of the code, not a grant to retry.

A terminated run must carry exactly one `termination`, and must keep every artifact it committed whatever the termination (R5, C3). A `findings` entry must use one shape across every operation (R5). The file operation findings in `packages/core/src/file-v1.ts` and the rules findings in [Constraints, authority and local choice](policy.html) must then read the same way.

| Termination | Meaning |
|---|---|
| `succeeded` | The operation completed. There may be no artifact. |
| `partial` | The operation did not complete and at least one artifact or effect was committed. |
| `failed` | The operation did not complete and committed nothing. The cause was an error. |
| `cancelled` | The operation stopped on request and committed nothing. |
| `blocked` | The operation could not start or continue because a declared requirement was unmet. |

`partial` is the state a run reaches when it committed something and did not finish. The reason is in `code` and in `findings`, not in the termination. `fileBatchReportV1` in `packages/core/src/file-operation-v1.ts` already resolves a cancelled batch with one success to `partially_succeeded` rather than to `cancelled`, so the model follows the adapter rather than correcting it.

This draft reads `blocked` as an unmet requirement rather than an error: a missing extension declared in `extensionsRequired`, an unmet host API in `requires`, or two active required rules that conflict. [Constraints, authority and local choice](policy.html) specifies the conflict case, and [Capabilities, trust and extensions](extensions.html) specifies refusal by name. A block is scoped to the operation that needed the thing. Declarations are read before any operation is selected, and refusal happens at the affected operation and output (plan section 9, steps 1 and 2). An unsupported motion extension therefore never prevents safe inspection of the same document.

## The run lifecycle

A run lifecycle sits beside the outcome and never inside it (R5). `pending`, `running`, `needs-review` and `ready` say where a multi-stage run is. Only a terminated run carries a termination.

The Rebrand run manifest keeps its four values on disk unchanged. `RUN_OUTCOMES` in `packages/node-shell/src/rebrand-run-manifest.ts` is `pending`, `ready`, `needs-review` and `failed`, and that file's own comment records that `needs-review` is not a success. The caller records an outcome and a stage together, so `ready` at the plan stage means a plan is ready and nothing has been exported. The model must never rewrite those persisted values to reach its own vocabulary.

| Persisted value | Where it lives | How it maps |
|---|---|---|
| `pending` | `RunOutcomeV1` | Lifecycle `pending`. No termination. |
| `ready` | `RunOutcomeV1` | Lifecycle `ready`. No termination. The recorded stage says what is ready. |
| `needs-review` | `RunOutcomeV1` | Lifecycle `needs-review`. No termination. |
| `failed` | `RunOutcomeV1` | Termination `failed`, or `partial` when an output was written, with the error code. |
| `ready`, `needs-review`, `failed` | `FILE_OUTCOMES` and `FileOutcomeV1` in `packages/core/src/rebrand-v1.ts`, persisted per stage as `StageRunV1.outcome` in `shells/cli/src/rebrand.ts` | The same mapping as `RunOutcomeV1`, read against the stage that recorded it. |
| `succeeded` | `FileOperationReportV1.state` | Termination `succeeded`. |
| `partially_succeeded` | `FileOperationReportV1.state` | Termination `partial`. |
| `failed` | `FileOperationReportV1.state` | Termination `failed`. |
| `cancelled` | `FileOperationReportV1.state` | Termination `cancelled`, with no committed artifact. |
| `prepared`, `written`, `readback-confirmed`, `delivered`, `destination-confirmed`, `user-recorded-external-action` | `ReceiptStateV1` in `packages/core/src/rights-v1.ts` | The `state` of one entry in `effects[]`. `user-recorded-external-action` is a person's statement about a destination, not the host's observation of one, and an outcome must never read it as a confirmed commit. |

An outcome is produced only when a run terminates. A run at `pending`, `running` or `needs-review` has not terminated and returns no outcome. The artifacts and effects it already committed are held on the run record, and they appear in `artifacts[]` and `effects[]` of the outcome the terminating run returns. A run that stops while review is still owed terminates `partial`, with the review requirement in `findings` (R5, C3).

The stage a run reached is a third fact, separate from the termination and from the lifecycle. `PROJECT_STAGES` in `packages/core/src/rebrand-v1.ts` runs `ingest`, `census`, `plan`, `review`, `compile` and `done`. The manifest records a stage per input file beside its outcome. A stage must never be read as a termination.

## Rules that hold when a run stops early

### A success may have no artifact

An inspection may succeed with a result and no artifact (R5, C3). A host must never require an artifact to record a success.

`assertFileOperationReport` in `packages/core/src/file-operation-v1.ts` refuses a succeeded single-file report with no output. It also refuses a failed or cancelled one that claims an output. That invariant must stay inside that adapter. It describes one file in and one file out. It must never be generalised to inspection, presentation or a run that writes several files.

### Committed artifacts are kept

A run that stops before completing must keep every artifact it committed, and must report each one under `artifacts[]` with its destination and state (R5, C3). That run terminates as `partial`, which is what `partial` is for. The nearest precedent does not yet do this. `resume()` in `packages/node-shell/src/rebrand-run-manifest.ts` clears `entry.outputs` when an input's bytes changed, so the files remain on disk while the record of them leaves the manifest. An operation adapter must retain those entries against the attempt that wrote them.

The write rules come from `packages/node-shell/src/rebrand-run-manifest.ts` and must never be relaxed. An output is written to a temporary name in the same directory and then renamed, so a half-written file never appears under its real name. A name is claimed with an exclusive create, so two writes of one name produce two files. A collision takes a numeric suffix and the entry records both the requested and the written name. `allocateFileName` in `packages/core/src/file-v1.ts` reserves the returned name immediately, including a generated suffix. `overwrite: true` is the caller stating the opposite intent and must be explicit.

There is no promise of universal rollback and no promise of exactly-once execution (R5).

### Retry reads committed effects, never the code alone

A transient error code must never by itself authorise a retry after an uncertain write (R5, C3). Retry eligibility must read the code together with the committed artifacts, the attempt identity in `attempt` and the operation's declared idempotency.

`isRetryable` in `packages/node-shell/src/rebrand-run-manifest.ts` reads the error code alone, against `RUN_RETRYABLE_ERROR_CODES`, which is `REBRAND_ERROR_CODES` less `RUN_PERMANENT_ERROR_CODES`. A failed entry that carries no code at all is retryable by default. That answer says which inputs to offer again. It is sound there because a retried input is re-hashed and resumed from its recorded stage. It is not a grant to repeat a committed effect. The model must add the committed-effect facts before a retry crosses a destination.

### An unknown completion is not a failure

A run whose acknowledgement was lost must be reported as unknown, never as failed (R5). This draft records the uncertainty on the affected entry in `effects[]`, and does not resolve whether an unknown commit terminates as `partial` or needs a value of its own. The table's `partial` means a commit that is known to have happened. The proof case in [Proof cases](proof-cases.html) settles both the field and the termination.

### Passive work commits nothing

Passive inspection, thumbnails, layout recalculation and reference testing must never commit a file-writing effect (plan section 7). The authored effect envelope enforces the rule, with grants bound to the operation, phase and attempt (R3, C6). The execution class the run uses is where a grant is enforced, never a generated inventory, which is lint. A hook that resolves after its attempt was cancelled must never commit a new effect. A `trusted-realm` run carries its stated limits and no strict claim, and [Evaluation and receipts](evaluation.html) specifies the classes. A replay must never silently repeat an external action. A replay uses captured effect results or a controlled simulation, unless a new execution authorises the actions again.

## Results with more than one artifact

A still export of a multi-artboard document fans out to one file per board. A PDF or a PPTX makes one page per board. An event kit is a ZIP of many files. An operation result is therefore a list of artifacts with a stable per-artifact identity, never one blob (R5, plan section 7.2).

The identity is the board id, the page index or the path inside the kit, and each of those is already a permanent identifier (R1). A caller can therefore address one artifact of a fan-out without re-reading the whole result. Whether the same instance at the same revision produces the same artifact ids on two runs is a draft decision. Plan section 7.2 requires the identity to be stable and says nothing about reproducing it across runs, so the fan-out export case in [Proof cases](proof-cases.html) settles it.

The `s` parameter is the selector and the model keeps it. `s` is reserved in `engine/src/url-mode.ts`. `s=2` is a one-based position in presentation order, any other value is a frame id, and an `.N` suffix picks a build step. `engine/src/frame-address.ts` resolves the value against the rendered pages for the web fan-out and for the CLI alike, so `?s=2&format=png` is a link to one artifact. The selector does not reach the formats that carry every frame by construction. `FRAME_FILTER_SKIP_FORMATS` in the same module excludes `pdf`, `zip`, `html`, `pptx`, `scorm` and the motion formats. A filter there would produce a one-page document where the format's own answer is the whole deck. A page index inside a multi-page container is therefore an artifact id in the result, not an address `s` resolves. `pdf-cmyk` is not in that set, because the web fan-out already renders one press-ready PDF per page. Build steps are presenter-only and a still export always shows every build.

## What each operation must specify

An operation declaration must state each of these (plan section 7.1):

- the input and result types and their cardinality;
- the resources, execution capabilities and permission scopes it requires;
- its declared effects, their trigger conditions and who owns each destination;
- its completion, review, blocked, failed, cancelled and partial semantics;
- its cancellation boundaries, its retry rules and its idempotency;
- whether a result can be returned without writing any output file.

A suite tests the features the suite declares. An operation declares what it supports. An operation that claims a suite and does not declare one of its core features fails that suite, because core features cannot be skipped (R8). An item that no claimed suite covers is reported as untested, never as covered. [Conformance and fidelity](conformance.html) specifies suites and features.

## Recording, receipts and the outcome

`recording` states what this run captured, what it retained and whether it can be replayed. The three are independent axes, and an operation may succeed with none of them (plan section 9.1). `receipt` would carry an `EvaluationReceiptV1` when the recording policy retains one. No such type exists in the tree. R4 lists it as new, and nothing is added to `packages/core` or `schemas/` in this wave. R15 lists what it would record, and [Evaluation and receipts](evaluation.html) specifies it.

An operation that is not recordable is still a valid operation (D11). A live-input tool and a local utility are the two cases the plan works through, and Q4 below carries their defaults.

## Open points

- **Q4. What "non-recordable" means for the first two cases, a local utility and a live-input tool.** Default from plan section 16: for a utility, capture off, retention none and replay semantic from pinned inputs. For live input, capture unsupported unless the person turns it on, retention session, replay none. Evidence that would change it: a regulated workflow that needs durable receipts for utilities.

The other open questions are carried in other chapters, each with its default. Q1 and Q5 are carried in [Conformance and fidelity](conformance.html). Q2 and Q3 are carried in [Constraints, authority and local choice](policy.html). Q6 is carried in [Status and open points](status.html).

## Precedents

- `packages/core/src/file-operation-v1.ts`
- `packages/core/src/file-v1.ts`
- `packages/node-shell/src/rebrand-run-manifest.ts`
- `packages/core/src/rebrand-v1.ts`
- `packages/core/src/rights-v1.ts`
- `packages/core/src/host-v1/export.ts`
- `engine/src/document-api.ts`
- `engine/src/url-mode.ts`
- `engine/src/frame-address.ts`
- `shells/cli/src/rebrand.ts`
- `shells/cli/src/validate.ts`
- `schemas/tool.schema.json`
- `community/agenda/tool.json`
- `services/mcp/src/tools.ts`

# Values, expressions, code and time

> This chapter is a draft for review, dated 2026-09-24.  

A property holds a value. This chapter specifies what kinds of value a property may hold, which of them a host may evaluate, how imperative code reaches the same properties, what powers that code runs with and which clocks a run may read. It states resolutions R2 and R3 and the correction the review recorded as C6 in normative form.

The records these values live in are specified in [Records and identity](records.html), and the rows and payloads that hold them in [Source rows, payloads and patches](source-and-patches.html). The pipeline that runs an evaluation is specified in [Evaluation and receipts](evaluation.html), and the rules that decide whether a value is permitted at all in [Constraints, authority and local choice](policy.html).

## Typed values and property permissions

A property must declare its type, and permissions must be stated per property rather than per document (R2, plan invariant 8). A property of type `T` may permit literals, token references, data bindings, conditionals, expressions and timed values, in any combination its declaration names.

Permission is never implied by another permission. Allowing a font token on a text property must never imply allowing a structural change during a layout pass (invariant 8). A property that permits a literal must never therefore permit an expression.

One writer owns one property, and explicit composition must be declared where several writers are supported (invariant 8). `validateDesignTool` already refuses two controls on one property, and a choice owns several properties at once as one atomic value (`packages/core/src/design-tool-v1.ts`).

A structured value must travel as a typed payload record referenced from its row by id, and must never be spread across scalar fields or hidden in an ad hoc string (R1, R14, C1). The collaboration row permits nothing but strings, numbers, booleans and null (`packages/core/src/canvas-op-v1.ts`, `schemas/canvas-op.schema.json`). [Source rows, payloads and patches](source-and-patches.html) specifies the payload rule; this chapter states only that a value's type decides where it lives.

```text
draft shape

  PropertyDeclarationV1
    id            the property id, permanent
    type          the value type
    permits       which of literal, token, binding, conditional, expression, timed
    facts         the facts an expression on this property may read
    clocks        the clocks a timed value on this property may read
    writer        the one authoritative writer, or a declared composition
```

This is a draft shape for review, not a type. No shared type is frozen until the counterexamples in [Proof cases](proof-cases.html) pass, so nothing is added to `packages/core` or `schemas/` in this wave (plan status, review recommendation).

## ExpressionV1

`ExpressionV1` is a proposed contract from plan 194 and nothing in this repository implements it yet. The only part of it that ships is the `showIf` subset in the next section. The rules below state what an implementation would have to hold to.

Declarative logic is a JSON abstract syntax tree and never source text (R2, plan 194). A host must never evaluate a document expression with `eval` or with a function constructor. The tree is data, so a host can read it, cost it and refuse it before anything runs, which is what invariant 2 requires.

The operator set is closed (R2). It covers boolean logic, equality and order, presence, string length, numeric arithmetic, finite membership and bounded aggregate counts. A namespace may not widen it, because a wider operator set is a different language with a different cost model. This is proposed, not resolved: R2 closes the set and D10 opens namespace publication, and no resolution yet says which wins.

Leaf reads are limited to declared facts (R2). The facts are the declared input ids, the canvas size and aspect, the explicit locale and direction, dataset counts and component parameters. An expression must never read the network, the clock, the locale of the machine, a random source, ambient JavaScript or geometry it has already influenced.

Every expression must carry a static cost estimate produced at validation, and an estimate over the declared limit must be refused at authoring time with its reason (R2). Every evaluation must run under a runtime budget, and an evaluation that exceeds it must stop and record a finding rather than continue.

A static estimate is meaningful only over bounded inputs, so the bounds are part of the contract (R2, review gate 4). The declaration must bound string lengths, list and map sizes, expanded component counts and the total evaluation work one operation may spend. Cancellation and invalidation behaviour must be defined for a budget that runs out, and the finding must say which bound was reached.

Value dependencies must be acyclic by default (R2). There is no general fixed-point solver in this model, and a later suite that wants one must specify its convergence and its failure behaviour first.

An expression must serialise canonically, sorted and byte-stable, so it can be hashed into an evaluation receipt (R2, R15). Two documents that differ only in key order must produce one digest.

Evaluation returns the value and a trace of the facts it read, the rule it matched and the outcome it set (R2). The trace is what makes a semantic diff able to explain a changed decision rather than only report a changed pixel.

## showIf is the shipped subset

`showIf` is the v0 of this language and must keep its current meaning when it is read as an expression (R2). It ships today in `engine/src/inputs.ts` as `matchesShowIf`, and `schemas/tool.schema.json` declares it.

One object is an AND: every `{ inputId: value }` pair must match. A value that is a list is membership, so `{ chartType: ["bar", "line"] }` matches either. An array of objects is an OR. It states a condition an ANDed map cannot.

`showIf` is a visibility overlay and nothing more. URL parameters, hooks and validation must see the input either way (`schemas/tool.schema.json`). A select option carries the same shape, and the option currently selected must stay offered whatever the condition says, so a saved session or a shared link never changes meaning.

Two rules follow. A host must never extend `showIf` with source text, arithmetic or a fact outside the declared inputs, because that would make the v0 subset a second language (R2). An expression that replaces a `showIf` must produce the same visibility for the same model, or the migration is not a migration.

## Hooks are eight named protocols

Imperative logic stays in `hooks.js` and is specified as protocol contracts over the existing hook names, never as a new module system (R3, D4). There are eight names, and they are already the lifecycle the engine drives (`engine/src/runtime.ts`).

Each protocol must state six things: the typed context it receives, the patch it may return, the optional host APIs it calls, its authored effect envelope, its time budget and the execution classes it may run in (R3). The table below states the context, the patch and the budget for each name, and whether the protocol is bound to the shell realm. The host APIs a protocol may call, its authored effect envelope and its class list are owed, because no manifest declares an envelope today.

Patch semantics are unchanged and are the same for every patch-returning protocol (`engine/src/runtime.ts`). A returned key that matches a declared input id updates that input. A key with no matching input goes into `extras`, a parallel store the template may read without the value being a user-facing input.

| Protocol | Context it receives | What it returns | Budgeted | Runs off the shell realm |
|---|---|---|---|---|
| `onInit` | input model, language, host, an optional intermediate report | input ids and extras | yes | yes |
| `onInput` | the above plus the changed input id and value | input ids and extras | yes | yes |
| `onFrame` | the `onInit` context plus one camera frame | input ids and extras | no | yes |
| `onLevel` | the `onInit` context plus one audio level | input ids and extras | no | yes |
| `beforeExport` | model, language, the live node, format, export options, host | mutates the node | yes | no |
| `afterExport` | the same context, run in export's `finally` | restores the node | yes | no |
| `exportFile` | model, language, host, caller options | bytes out | yes | yes |
| `exportStill` | the `beforeExport` context | bytes out, or null | yes | no |

The budgets live in `HOOK_BUDGET_MS` in `engine/src/runtime.ts`, one entry for each of the six budgeted protocols. The specification points at that constant rather than restating its values, so a tuned budget never contradicts a published number.

Three protocols are bound to the shell realm because they carry a live DOM node. `IN_REALM_ONLY_HOOK_NAMES` in `engine/src/hook-worker-core.ts` lists `beforeExport`, `afterExport` and `exportStill`, and a worker executor must refuse them rather than pretend.

A budget bounds the wait and never the code (`engine/src/runtime.ts`). A raced-out asynchronous hook keeps running, and a synchronous overrun cannot be preempted at all. A raced-out `onInit` or `onInput` may apply its patch late, and only while no newer run of either has started. An export protocol must never apply late, because a late patch after a written file describes a file that was never produced.

`onFrame` and `onLevel` run once per frame or per sample and are not budgeted. The runtime must drop an overlapping frame rather than queue it, so a slow per-frame render throttles itself (`engine/src/runtime.ts`).

A hook must never be disguised as an expression language, and a hook must never be migrated to declarative rules unless its behaviour is exactly representable (R3).

## The authored effect envelope

The effect envelope is a proposed contract and no manifest declares one today. It is authored and enforced, never generated (R3, C6). An effect is a host action a protocol may commit: a write, a download, a network call, a delivery to a destination.

A generated inventory is lint and must never be authority (C6). `analyseRequires` in `scripts/tool-requires.ts` is a regular expression over `host.<api>` followed by a member access. It reports `text` for `host.text.toPath` and nothing for `host["text"]` or for a local alias assigned first. A pattern that a rename evades cannot decide what code may do.

The public `requires` field keeps its present meaning and must never be repurposed (R11, review gate 1). It lists the optional host APIs the hooks call unguarded. The runtime refuses a mount when the host lacks one. `HOST_V1_OPTIONAL_APIS` in `packages/core/src/host-v1/apis.ts` is the enumerable list. One internal evaluator may normalise the four gates of engine range, host APIs, extensions and reader version, without changing what the manifest field says.

```text
draft shape

  EffectEnvelopeV1
    protocol      one of the eight names
    apis[]        the optional host APIs this protocol may call
    effects[]     { kind, destination, trigger } the author declares
    classes[]     the execution classes this protocol may run in
    delegates?    the powers a composed child may receive, named one by one
```

Grants are bound to the operation, the phase and the attempt (R3, C6). Four rules follow. The first three are proof case 20 in [Proof cases](proof-cases.html). The delegation rule has no fixture yet and is stated here as a rule to argue with:

- An undeclared effect must fail in a strict execution class.
- Passive inspection must never write through a hook, whatever the envelope says.
- A strict attempt that was cancelled or that ran past its budget must keep no usable write grant, even while its code is still running in the realm.
- A composed child must receive delegated powers explicitly and must never inherit a power because its parent holds one.

Observed effects must be recorded when the recording policy permits (R3). Attempted host actions are listed in `OutcomeV1.effects[]`, specified in [Operations and outcomes](operations.html) (R5). A retained receipt records the execution class the run used (R15).

## The five execution classes

An execution class is the trust and enforcement context code runs in. The class a run used must be recorded in its receipt (R15), because a portable API contract is not proof of isolation (plan section 12.2, `docs/constraints.md`).

![The five execution classes of the document model and what each one enforces, from trusted in-realm injection to the reserved interpreter class.](/info/diagrams/document-model/execution-classes.svg)

| Class | What it is today | What it enforces |
|---|---|---|
| `trusted-realm` | First-party catalog hooks loaded with `new Function('host', body)` in the shell's realm (`engine/src/runtime.ts`). | Closure-scope injection of the host bridge. Not a boundary: in a browser the code can reach page globals (`docs/constraints.md`). |
| `isolated-worker` | A tool with `isolate: true`, run through the Worker executor with in-realm fallback (`shells/web/src/bridge/hook-worker.ts`). | A separate realm, a proxied host and the seeded feature detects of `engine/src/hook-worker-core.ts`. Fallback keeps compatibility, so the claim is weaker. |
| `strict-worker` | Sideloaded and remote tools, with fallback disabled (`docs/constraints.md`). | The same worker policy plus global lockdown, with refusal of the mount when the worker cannot start. `STRICT_AMBIENT_GLOBALS` in `engine/src/hook-worker-core.ts` lists what is removed before the hooks compile. |
| `node-worker` | The CLI executor under `worker_threads` (`packages/node-shell/src/hook-worker.ts`). | The same protocol and host proxy on Node, with no DOM at all. |
| `vm` | Reserved. An interpreter in WebAssembly or a hardened compartment. | Nothing yet. It is named so a future class does not arrive without a contract. |

Two rules hold across all five (R3, plan section 12.2). Trusted compatibility execution must be classified as trusted and must never inherit a strict claim, whatever the tool's manifest says. A strict class must enforce the powers it claims and must refuse an unsafe fallback rather than downgrade quietly.

A tool does not assert its own class. `isolate: true` is written from evidence by `scripts/tool-isolation.ts`, which requires hooks free of realm-bound globals and a byte-identical render in-realm against the worker before it sets the flag (R3). A manifest claim with no such evidence is a declaration, not a classification.

## Invalidation and layout

State can affect layout and layout can affect text flow. This model defines a dependency model and invalidation rules, and it must never promise that every stage runs exactly once (R2, invariant 6).

Undeclared feedback is prohibited for the first version (R2, invariant 6). A declaration must state which measurement or state change restarts which stage. An explicit measurement output must feed a named later stage, rather than an expression reading geometry and rewriting it.

A stale asynchronous result must be discarded rather than applied over a newer one. The runtime already holds that rule for `onInit` and `onInput` through its run sequence (`engine/src/runtime.ts`), and the model states it as the general case.

An unresolved required value or a failed fit must block output at the affected operation rather than produce a silent substitute. The precedent is the `NEEDS_BROWSER` error code, which fails an export that needs a text layout check the host cannot perform (`engine/src/runtime.ts`, `shells/cli/src/exit-codes.ts`). A cycle must never be hidden behind implementation-dependent iteration.

An edit must invalidate its actual dependants (R2). Baseline measurements must precede any claim about responsive editing (review gate 5). They cover load, a one-property edit, a chart-data edit, patch validation and export, taken at declared document limits. They run on the web shell and on a native or CLI shell, and they record memory as well as elapsed time. This draft states no latency target, because the current evidence supports none.

## Units, coordinate domains and colour

The type system needs dimensions, physical units, angles, durations, frames, colours with colour-space metadata, vectors, matrices and typed resource references (R2, `engine/src/units.ts`).

Unit conversion must happen at one site. That site is `engine/src/units.ts`, which parses a dimension, holds inches as the canonical intermediate and converts to pixels, points or a CSS length. A shell's export bridge applies it per format and must never reimplement it. `CSS_DPI` in that module is 96, the pixels-per-inch the design canvas's own geometry is measured in. The three conventions below are separate resolutions, and the table gives the constant that holds each.

Three DPI conventions exist today, and the model keeps all three rather than collapsing them (R15, `engine/src/units.ts`):

| Convention | Value | Where it lives |
|---|---|---|
| Export default for physical units | 300 | The `dpi` URL parameter in `engine/src/url-mode.ts`, applied by each shell's export bridge |
| Rebrand reference space | 96 | `REBRAND_REFERENCE_DPI` in `packages/core/src/rebrand-v1.ts`, the space every source adapter normalises to |
| Per-document resolution | authored | The `documentDpi` input in `community/design/tool.json`, used when the document exports in a physical unit |

Every conversion a run applied must be recorded in its receipt (R15, gotcha 2). A fidelity claim about a physical output is unreadable without the DPI that produced it.

The output target owns colour handling, and the model must never treat colour as a document-wide setting (the **output target** definition in [the constitution](constitution.html), `engine/src/preflight.ts`). An output target states its colour space: sRGB, an HDR target, `pdf-cmyk` or `cmyk-tiff` with an ICC press condition, EXR or Radiance. `engine/src/preflight.ts` already separates the formats that build a process separation from the one format that emits a real spot plate.

A comparator must compare in the output's own colour space and must never convert both sides to sRGB first (R9). [Conformance and fidelity](conformance.html) specifies the comparator.

A coordinate domain must be declared and must never be inferred (R1). The `3d` layer kind carries a scene as a URL-encoded string inside a two-dimensional row, which is a second domain smuggled in as text (R14, `schemas/blocks-wire-order.json`). It must gain a typed payload record referenced from the row, appended beside the existing string, so the frozen wire order stays frozen and an existing link keeps working. A versioned migration with findings must then name the typed record authoritative once both are present ([Packaging, identity and migration](packaging.html), invariant 13).

## The five clocks

Clocks must be explicit and separate (R15, `docs/agenda.md`). A run may read more than one, and a receipt must name which clocks fed it and with what values.

| Clock | What it answers | Precedent |
|---|---|---|
| Event time | What is happening. | Agenda's event clock decides which sessions are current, in the event's own time zone, or in floating event time on the display device's clock when no zone is set (`docs/agenda.md`). |
| Presentation time | Which scene and which title position to show. | Agenda's presentation clock (`docs/agenda.md`). |
| Reference time | A frozen moment for review or export. | Agenda's reference time and its snapshot, evergreen and rehearsal video policies (`docs/agenda.md`). |
| Media time | Position inside one clip. | The Sequence clock that drives the light Lottie player (`engine/lottie.md`). |
| Wall time | When the export ran. | The ICS `DTSTAMP` and the PDF creation date, which `docs/determinism.md` records as the reason those formats are not byte-identical. |

Agenda is the precedent for the first three because it already separates them and already exports under three named time policies (D2, `docs/agenda.md`). Snapshot holds the reference moment or the moment export began. Evergreen shows a programme without claiming it is live. Rehearsal advances from an explicit reference time.

Deterministic motion frames come from a declared frame clock rather than from the machine clock. `shells/web/src/bridge/frame-clock.ts` drives a registered canvas at a normalised loop time. It freezes the tool's own animation loop for the capture. It passes the clip length and the target size, so a tool never guesses either. It is keyed per canvas and never a global, so one tool's clock cannot reach another.

Plan 196's `TimelineV1` is not built, and motion sampling is staged behind it (R12). Until then a timed value must name the clock it reads, and a static preview must choose a declared state and a declared time sample (R12, R15).

Interaction distinguishes four things that are often conflated: user intent, device input, state transition and effect (invariant 11). An activation may declare pointer, keyboard or another input alternative, and a declared alternative is what makes the intent reachable rather than the device.

## Open points

- **Q4. What "non-recordable" means for the first two cases, a local utility and a live-input tool.** Default from plan section 16: for a utility, capture off, retention none and replay semantic from pinned inputs. For a live-input tool, capture unsupported unless the person turns it on, retention for the session and no replay. It touches the `onFrame` and `onLevel` protocols above, which are the live-input path. Evidence that would change it: a regulated workflow that needs durable receipts for utilities.

The other open questions are carried elsewhere, each on its recommended default and each still open. Q1 and Q5 are in [Conformance and fidelity](conformance.html), Q2 and Q3 in [Constraints, authority and local choice](policy.html), Q6 in [Status and open points](status.html).

## Precedents

- `engine/src/inputs.ts`
- `engine/src/runtime.ts`
- `engine/src/hook-worker-core.ts`
- `engine/src/units.ts`
- `engine/src/url-mode.ts`
- `engine/src/preflight.ts`
- `engine/lottie.md`
- `packages/core/src/host-v1/apis.ts`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/canvas-op-v1.ts`
- `packages/core/src/rebrand-v1.ts`
- `packages/node-shell/src/hook-worker.ts`
- `scripts/tool-isolation.ts`
- `scripts/tool-requires.ts`
- `schemas/tool.schema.json`
- `schemas/canvas-op.schema.json`
- `schemas/blocks-wire-order.json`
- `shells/web/src/bridge/frame-clock.ts`
- `shells/web/src/bridge/hook-worker.ts`
- `shells/cli/src/exit-codes.ts`
- `community/design/tool.json`
- `docs/agenda.md`
- `docs/constraints.md`
- `docs/determinism.md`

# Evaluation and receipts

> This chapter is a draft for review, dated 2026-09-24.  

This chapter specifies how one operation is evaluated and what a run may record about itself. It restates the evaluation and recording contract in normative form. It adds R15 for the receipt, and it carries the review's correction that an unmet declaration is refused at the operation boundary. The operation shapes and the outcome a terminated run returns are specified in [Operations and outcomes](operations.html). The typed values, the budgets, the execution classes and the clocks this pipeline reads are specified in [Values, expressions, code and time](values-and-time.html). The rule set it evaluates against is specified in [Constraints, authority and local choice](policy.html).

## The pipeline is nine steps

The pipeline puts capability and operation selection ahead of dependent work (plan section 9). It states which step depends on which. It does not state that one run passes through every step. An operation that produces no artifact still reaches step 8, because step 8 is also where a typed result is returned. It simply terminates with a result and an empty `artifacts[]`, which R5 keeps apart and C3 requires (R5, C3, D11).

![The nine evaluation steps in order, with inspection before any document code runs and refusal raised at the operation boundary rather than at parse time](/info/diagrams/document-model/evaluation-pipeline.svg)

1. **Inspect and parse.** Read the package without executing document code. Validate the schema version, the integrity map, the structure, the declared limits and the extension declarations. Read `extensionsRequired` here and refuse later, at step 2.
2. **Select.** Choose the operation, the output target and the execution class. Determine the effective policy and the permitted capability envelope. Refuse here when a declaration read at step 1 is unmet for this operation.
3. **Negotiate.** Settle support and select the declared fallbacks before any dependent computation or effect. Resolve only permitted resources, against immutable revisions and digests.
4. **Snapshot.** Validate and snapshot the typed inputs and the authored state. Resolve token scopes, locale, clocks, seed and environment.
5. **Instantiate.** Expand components into an identity-preserving evaluation graph. Evaluate expressions and domain rules under their budgets. Reject invalid cycles and unresolved required values.
6. **Resolve.** Settle layout or other domain constraints and apply the declared invalidation when state, time or a measurement changes.
7. **Validate.** Check the plan or the output against the effective policy, including proposed repairs and findings.
8. **Act.** Render, present, return a typed result or commit the authorised effects.
9. **Report.** Return an outcome and the receipt the recording policy permits.

A host must run steps 1 and 2 before step 3, because a resource resolved for an operation that was never selected is work done outside the grant (R3). A host may merge later steps, and a host may repeat steps 5 and 6 under the invalidation rules (plan sections 9 and 8.3), so the numbering is a dependency order and never a pass count.

### Inspection runs no document code

Step 1 must never execute a tool's hooks, template or expressions. `validateDocument` in `engine/src/document-api.ts` is the shape: with a manifest target it answers through `validateManifest` in `engine/src/validate.ts` and runs nothing of the tool. A host must answer an inspection from declarations alone.

`compileDocument` in the same file calls `createRuntime`, which runs the `onInit` hook before it hydrates the template, so a compiled document is the product of execution and never the answer to an inspection. A host must never reach for the compile verb to satisfy step 1.

The declared dependency envelope and the dependencies discovered during execution must be reported separately (R15). The declared side is already answerable from `engine/src/document-api.ts` without running the tool; the discovered side is what the receipt's `inputs[]` records. A host that merges them cannot later report which resource the document declared and which one the run discovered while executing.

### Refusal happens at the operation boundary

An unmet requirement must be refused for the operation and the output target that need it, never at parse time for the whole package (R10, R11), which is the boundary the four gates in `packages/core/src/host-v1/apis.ts` and `schemas/tool.schema.json` already refuse at.

An unsupported motion extension must never prevent safe inspection of the same package. A host must therefore record the unmet declaration at step 1 and raise it at step 2, against the selected operation. The outcome for the refused operation terminates as `blocked`, which [Operations and outcomes](operations.html) specifies, and the refusal must name the missing item so a caller can act on it. [Capabilities, trust and extensions](extensions.html) specifies refusal by name.

One gate already refuses by name before any document code runs, though it is scoped per mount rather than per operation. `missingRequires` in `packages/core/src/host-v1/apis.ts` compares a manifest's `requires` against the optional host APIs a shell actually provides, and `engine/src/runtime.ts` throws before any hook runs. The model keeps the public `requires` field with the meaning it has today and scopes the refusal to the operation (R11).

## Dynamic dependencies stay inside the grant

Dynamic dependencies remain possible. A late resource or capability request must fit inside the grant the run already holds, or the run must stop and ask for an explicit additional grant (R3).

- A host must never widen a grant because a document asked it to. The document is the party being constrained.
- A grant is bound to the operation, the phase and the attempt (R3). A strict attempt that was cancelled, or that ran past its budget, must retain no usable write grant, even while its realm code is still running. Trusted compatibility execution keeps its stated limits and cannot make that promise, which is why the class a run used travels in the receipt (R3, C6).
- Nested execution must never acquire a power because its parent holds one (R3). A composed child runs in its own execution class and receives delegated powers explicitly, inside the depth and cycle bound `engine/src/bake.ts` applies before it starts.
- Passive inspection must never write through a hook (R3).

One precedent in the tree supports the first rule. `schemas/tool.schema.json` makes the network capability fail closed: without a `network.allowlist` block, or for a URL no entry matches, every `host.net.fetch` rejects, and `createNetAPI` in `packages/node-shell/src/net.ts` is where that rejection is raised for every shell. A late fetch to an unlisted host is refused rather than negotiated. The third rule has its own: the `ComposeAPI` contract in `packages/core/src/host-v1/compose.ts` requires the host to reject a nested render whose stack already holds the child's `toolId`, or that exceeds the maximum compose depth, so a child render is bounded before it starts.

The second rule has a measured cause. `HOOK_BUDGET_MS` in `engine/src/runtime.ts` races an async hook against its budget, and the comment records what the race does not do: the hook's own code keeps executing after the wait is abandoned, because there is no preemption inside the JavaScript realm it runs in. A grant that outlives the attempt would therefore let abandoned code commit an effect for a run that already terminated.

## Recording is three axes

Rendering, reconstruction, capture and retention are independent properties (D11). A run declares three axes together, and replay is the one that answers for reconstruction.

```text
draft shape

  recording
    capture     unsupported | off | on     can external observations be captured, and were they
    retention   none | session | durable   may the capture be kept, and for how long
    replay      none | semantic | byte     what a replay can promise under the same conformance suite
```

This is a draft shape for review, not a type. No shared type is frozen until the counterexamples in plan section 17 pass, so nothing is added to `packages/core` or to `schemas/` in this wave (plan status line; Codex recommendation).

The three must be read independently (D11). A reproducible transform may have no rendered frame. A visual interaction may retain no input recording. A recording may exist while replay is unavailable, because the executor that would replay it is missing. A host must never infer one axis from another, and a host must never treat `capture: unsupported` as a failure.

## An outcome is immediate, a receipt is conditional

Every operation must return an immediate outcome (R5, D11). A durable receipt and any retention of raw input are conditional on the recording policy.

Adopting this model must never turn a local utility into a permanently logged one (plan section 9.1, D11). A tool that declares `privacy: 'on-device'` in its manifest processes the person's own content, and `engine/src/runtime.ts` already stamps nothing into its output: no provenance metadata and no watermark. Retention for such a run follows the recording policy and defaults to none under Q4 below.

A receipt is therefore optional on the outcome. A caller must treat an absent receipt as "no receipt was retained" and never as "the run was not evaluated".

## EvaluationReceiptV1

Where the recording policy retains one, a receipt records the facts R15 names.

```text
draft shape

  EvaluationReceiptV1
    schemaVersion
    definition        { id, version, revision digest }
    instance          { id, revision }
    inputs[]          input and resource digests, with the redaction state of each
    fonts[]           { family, weight, style, source, file?, sha256? } per resolved face
    shaping           { engine, version }
    textFallbacks[]   runs that stayed live text: { text, reason }
    emoji             { set, version, treatment }
    models[]          { id, version, licence } for any on-device model the run used
    conversions[]     unit and DPI conversions applied, with the site that applied each
    clocks            the clocks the run read and the value each supplied
    environment       { shell, engine, suite versions, execution class }
    policy            { version, issuer, attestedAt }, plus the rules a waiver deactivated
    fallbacks[]       the declared fallbacks this run selected
    findings[]        the same finding shape the outcome carries
    results[]         result identities, including one entry per artifact of a fan-out
    effects[]         the effects this run committed, each with its declared envelope entry and its grant, recorded where the recording policy permits (R3, C6)
    repeatability     the declared repeatability class for this run
    redactions[]      { field, reason }, one entry per field withheld or reduced
    omissions[]       { field, reason }, one entry per field this host could not observe
```

This is a draft shape for review, not a type. No shared type is frozen until the counterexamples in plan section 17 pass (plan status line; Codex recommendation).

Three rules hold over every field.

- A receipt must record what was measured, never what was assumed. `AttributionReceiptV1` in `packages/core/src/rights-v1.ts` is the precedent: it carries `expected` and `observed` as separate lists and a `checks` array, and its own contract states that it describes one output measured after writing.
- A receipt must be scoped to the revisions it names (C7). A receipt describing an earlier revision must be retained as a historical record when that revision changes, and must never describe the new revision as accepted.
- A receipt must never be required for an operation to succeed (D11).

An effect recorded here is an observation, never a permission: a receipt that lists an effect does not authorise it on a later run.

### Fonts, shaping and live text

A receipt must record the font files a run resolved, by digest, together with the shaping engine and its version (R15).

`LollyFontEntry` in `shells/web/src/lib/lolly-pack.ts` is the precedent and it already draws the distinction the receipt needs. It records a face as identity rather than bytes. It digests the whole source file rather than a subset, because a subset is a function of the text. And it allows `source: 'platform'` with no file and no digest, as the record of a run that drew in whatever the machine had installed. A receipt must keep that third case as a named state, because a run with no resolvable digest is a different claim from a run with one.

A receipt must record every text run that stayed a live `<text>` element, and a host must never claim font-independent fidelity for such a run (R15). The fact already exists and goes nowhere durable. `shells/web/src/bridge/export-svg-text-runs.ts` logs a warning and keeps the element when an outline cannot be produced. `shells/cli/src/run.ts` collects the same runs through an `onTextFallback` callback, reports each with its reason and refuses the export under `--strict`. The comparator probe in the plan's evidence hit exactly this: one caption kept live text because `dominant-baseline` shifted its baseline. The CLI emitted the warning to standard error and the product kept nothing, so the probe had to capture it by hand into `environment.cliWarnings` of `pixel-comparator-results.json` (F14).

`docs/determinism.md` already states the public limit this serves: vector export converts text to outlines, so the bytes depend on which font file was resolved, and a machine with a different font set is a different render. The receipt records that dependency so a reader can check it afterwards.

### Clocks

Clocks are explicit and separate, and a receipt must name which ones fed the run and with what values (R15, plan section 8.5).

| Clock | What it decides | Precedent |
|---|---|---|
| Event | What is happening. | `docs/agenda.md`: the event clock decides which sessions are happening. |
| Presentation | Which scene and which title position to show. | `docs/agenda.md`: the presentation clock decides the scene and the title position. |
| Reference | A frozen moment for review. | `docs/agenda.md`: reference time fixes a moment for review. |
| Media | Position inside one clip. | `shells/web/src/bridge/frame-clock.ts`: a driven frame receives a normalised loop time and the clip's real length in seconds. |
| Wall | When the export ran. | `shells/web/src/lib/lolly-pack.ts`: the package manifest records an export time. |

A run that read no clock must record none (R15). A host must never substitute wall time for a clock the document did not read (R15).

### Units and DPI conversions

Each unit and DPI conversion a run applied must be recorded with the site that applied it (R15, plan section 8.4).

`engine/src/units.ts` holds the conversion maths and must stay the only place that holds it (plan section 8.4). The conversion is applied at several boundaries, one per export format in each shell's export bridge, which is why a receipt records the site as well as the number. Three DPI conventions are live at once: the rebrand reference space at 96 (`REBRAND_REFERENCE_DPI` in `packages/core/src/rebrand-v1.ts`), the export default at 300 (the `dpi` parameter in `engine/src/url-mode.ts`) and a Design document's own resolution (the `documentDpi` input in `community/design/tool.json`). A receipt that records the converted number without the convention cannot be read back, so both must travel.

### Emoji, models and policy

- The emoji set and the brand treatment must be recorded (R15). The session record already carries the pair as a stamp at format 3 and keeps it as the two reserved parameters verbatim rather than as resolved bytes (`engine/src/session-record.ts`), and the receipt must record the same identity.
- Any on-device model must be recorded by identity and version (R15). `packages/node-shell/src/ml/matte-models.ts` carries a version string per model and copies it verbatim into the provenance edit step, which is the identity a receipt reuses.
- The effective policy version, its issuer and the execution class the run used must be recorded (R15, R3). Where a local waiver deactivated a rule, the receipt must list the bypassed rules, and measured conformance to the unmodified rule reads failed, never passed (R6). The waiver is recorded as an exception on the acceptance, never on the report (R8).

## Redaction is an explicit field

A receipt must never imply that it holds every private input (R15).

- A field withheld for privacy must appear in `redactions[]` with a reason.
- A field this host could not observe must appear in `omissions[]` with a reason.
- An absent field with no entry in either list means the run did not read that fact at all.

A reader must therefore be able to tell three states apart: recorded, withheld and never observed. A host that drops a field silently collapses those three into one answer.

A decision a person made must be recorded against the facts it was made about, never as a blanket permission. `RightsDecisionV1` in `packages/core/src/rights-v1.ts` is the precedent already shipping: a decision carries a fingerprint of the situation as it stood, and the evaluator applies the choice only while the facts still hash to it.

## Replaying an interaction

A claimed replayable interaction must reproduce the specified semantic state under the named conformance suite, from a pinned initial state and a normalised timestamped input trace (R15, R8).

- Live sensor or network observations must be captured as inputs or excluded from the claim.
- A replay must never silently repeat an external action. It uses captured effect results or a controlled simulation, unless a new execution authorises the actions again (R3).
- A `byte` replay claim must be made only for a run that recorded every fact the bytes depend on, because byte identity is a separate and stronger claim than semantic identity (invariant 4). A run that fell back to live text, or that resolved a platform face with no digest, may claim `semantic` at most, since the receipt cannot name the file the recipient will shape with (R15, F14).
- A comparison that could not finish must never count as a replay (C8). `compareSources` in `engine/src/compare.ts` answers `undetermined` when a source was incomplete or a budget ran out, reports completeness separately and lists its own limitations. A caller must read that answer as not proven, never as equal.

One normalisation is needed before any comparison. Signing timestamps and provenance containers need not be byte-identical for a visual or semantic result, and `stripSvgC2pa` in `packages/node-shell/src/lolly-file.ts` is the existing normalisation both sides of a comparison must apply. `docs/determinism.md` states the reason: a default export is signed afresh with a new key and timestamp on every run, so two correct runs differ in exactly that block.

A live run is a weaker case again. `engine/src/runtime.ts` runs the `onFrame` and `onLevel` hooks once per frame or sample, leaves them outside the time box and drops overlapping samples so a slow per-frame render throttles itself. The frame count of such a run is an observation of the machine that ran it. A host must therefore never claim a byte replay of a live-input run, and Q4 below carries the default for what it records instead.

## Open points

- **Q2. Granularity and persistence of local rejection.** Default from plan section 16: per rule, instance-scoped, recorded on the session record with a fingerprint (R6). It touches this chapter because the receipt lists the bypassed rules, and the granularity of the waiver decides what there is to list. Evidence that would change it: a need to share a rejected state as a reusable tool.
- **Q3. What freshness a governed-claim export needs, and who states it.** Default from plan section 16: freshness is a policy rule the instance issues, with a validity interval and a stated behaviour when it cannot be established. With no such rule, the held policy is enforced and the receipt says which policy version was evaluated and when it was last attested. Evidence that would change it: an organisation that needs a hard deadline, or a fully offline site.
- **Q4. What "non-recordable" means for the first two cases, a local utility and a live-input tool.** Default from plan section 16: for a utility, capture off, retention none, replay semantic from pinned inputs. For live input, capture unsupported unless the person turns it on, retention session, replay none. Evidence that would change it: a regulated workflow that needs durable receipts for utilities.

Q1 and Q5 are answered in [Conformance and fidelity](conformance.html). Q6 is answered in [Status and open points](status.html).

## Precedents

- `engine/src/runtime.ts`
- `engine/src/document-api.ts`
- `engine/src/validate.ts`
- `engine/src/session-record.ts`
- `engine/src/units.ts`
- `engine/src/url-mode.ts`
- `engine/src/bake.ts`
- `engine/src/compare.ts`
- `packages/core/src/host-v1/apis.ts`
- `packages/core/src/host-v1/compose.ts`
- `packages/core/src/rights-v1.ts`
- `packages/core/src/rebrand-v1.ts`
- `packages/node-shell/src/lolly-file.ts`
- `packages/node-shell/src/net.ts`
- `packages/node-shell/src/ml/matte-models.ts`
- `shells/web/src/lib/lolly-pack.ts`
- `shells/web/src/bridge/export-svg-text-runs.ts`
- `shells/web/src/bridge/frame-clock.ts`
- `shells/cli/src/run.ts`
- `schemas/tool.schema.json`
- `community/design/tool.json`
- `docs/agenda.md`
- `docs/determinism.md`

# Constraints, authority and local choice

> This chapter is a draft for review, dated 2026-09-24.  

A constraint is a rule the software enforces, not guidance a style guide offers (`docs/constraints.md`). Constraints can travel with a tool and can also be supplied by Lolly Work (D6). This chapter specifies how those sources compose into the one rule set an evaluation ran under, which this model calls the effective policy.

Three separate decisions are kept apart throughout: a creative rule, structural validity and a device grant. Rejecting an approved-font rule must never make an invalid graph valid, and it must never grant code a device power (R6). A single control labelled "accept constraints" must never combine brand acceptance, code trust and device permission (R6).

This chapter restates plan section 10 in normative form, with the correction the review recorded as C4 and the correction it recorded as C5. The pipeline step that determines effective policy is specified in [Evaluation and receipts](evaluation.html). The suites and reports that measure a result are specified in [Conformance and fidelity](conformance.html). Lolly Work is a separate repository, and a `lolly-work/` path in this chapter is a file in that checkout rather than in this tree.

## What a rule is

A rule is a typed predicate with an identity, so that a finding can name it and a waiver can be recorded against it (R6).

```text
draft shape

  RuleV1
    id              permanent within its issuer's namespace
    revision        changes when the predicate or its parameters change
    issuer          who placed the rule: an instance overlay, the tool author, a person
    scope           the inputs, rows, collections or output targets it applies to
    class           advisory | waivable-creative | structural
    severity        how a violation is reported when the rule is active
    phase           the validation phase it runs in, per the evaluation pipeline
    predicate       an ExpressionV1 (R2, plan 194's JSON AST, defined there and not built),
                    evaluated under its budget
    repair?         guidance for the patch a repair would propose
```

This is a draft shape, not a frozen type. Nothing is added to `packages/core` or `schemas/` in this wave, and the plan's section 18 orders the counterexample fixtures before any of it is frozen.

Every enforced rule and every exception must name its issuer (R6). Lolly Work already carries that attribution where an operator supplied it. `resolveInputAccess` in `lolly-work/server/src/policy/overlay.ts` copies the overlay's display name onto `by` and the rule author's text onto `reason` on every matched rule. Both stay absent when the overlay is unnamed or the rule carries no reason. The question "why is this locked" is answered by the same resolution that locked it, so the answer and the lock can never disagree. A Kubernetes reader will recognise the arrangement: an issuer here does what a field manager does there (D13). This model closes the remaining case. An issuer is required, not optional, so an unnamed overlay must never lock an input while naming nobody.

## Three rule classes

A rule must declare one of three classes (R6, C4). The class decides who, if anyone, may set the rule aside.

| Class | Meaning | Who may set it aside |
|---|---|---|
| Advisory | A preference. A violation is reported and never stops the operation. | Nobody needs to: it does not block. |
| Waivable creative | An authored creative rule, such as an approved font or a palette. | The authority for this execution, recorded as an exception. |
| Structural | Structural validity, a required input or a device grant. | Nobody. A waiver against a structural rule must be refused. |

The review found the plan alternating between "the stronger rule wins" and "both rules conflict" without saying which case applied (C4). The class is what decides. A waivable creative rule may be deactivated by a recorded waiver. A structural rule may not, whatever the context (R6).

## The three layers

Rules reach an evaluation from three layers. Each layer must name the issuer of every rule it contributes (R6).

![Authority decided first, then the applicable exceptions, then the active rule set composed from the instance overlay, the authored rules and the local decisions, then evaluation, with the issuer carried on every edge.](/info/diagrams/document-model/policy-layers.svg)

**1. Instance overlay, supplied by Lolly Work.** `lolly-work/server/src/policy/overlay.ts` defines `AccessLevel` as `editable`, `choice`, `locked` or `hidden` per input and group, with an optional fixed `value`, an allowed set and a written `reason`. The same overlay carries an `enforce` block with `formats`, `c2pa`, `watermark` and an `escalation` string that points at an approval chain. `lolly-work/server/src/policy/org-config.ts` resolves that overlay per caller into `OrgConfigPayload.tools`, where a hidden tool is absent rather than marked, and folds the result into one `policyVersion` string that behaves as an ETag. `checkParams` in `lolly-work/server/src/policy/overlay.ts` refuses a locked, hidden or out-of-set parameter at the render route (`lolly-work/server/src/render/pipeline.ts`). The refusal carries the same `by` and `reason` the shell shows, so the overlay is enforcement and not presentation.

**2. Authored rules, travelling with the tool.** `packages/core/src/design-tool-v1.ts` defines `DesignInputV1.approved` as an approved value list, `fixedInputs` on each `DesignChoiceV1` option as the inputs that option owns, and `DesignTextRuleV1` and `DesignImageRuleV1` as per-property rules. The same module reports an `ownership` finding when an option and a reader input both write one property, which is the one-writer-per-property rule. Catalog and material locks belong to this layer too: `brandLock` on a tokens asset in `schemas/asset.schema.json` makes a brand authoritative and disables brand customisation. A hosted design system carries the material form of the same lock. `DesignSystemRecord.locked` in `shells/web/src/lib/design-system/registry.ts` is set from the catalog's `brandLock`, and it is what makes hosted colours, type and logos read-only. That lock is why "Make an editable copy" is a separate act rather than an edit.

**3. Local decisions.** A person's recorded waivers and exceptions. `engine/src/session-record.ts` already carries `rightsDecisions` on the instance at format 4, and `packages/core/src/rights-v1.ts` defines the decision it holds.

A layer must never be read as a precedence order on its own (C4). The order in which rules are composed is specified next, and authority comes first.

## The resolution order

Effective policy resolves in four steps, in this order, and a host must never reorder them (R6, C4):

1. **Authority.** Decide who holds acceptance authority for this execution. In governed execution it is the Lolly Work administrator for the brand. Otherwise it is the local person using the client (D12).
2. **Exceptions.** Collect the exceptions that authority permits, including any recorded local waiver whose facts still match.
3. **Active set.** Compose the active rule set from the rule-bearing layers, the instance overlay and the authored rules, then deactivate each rule a permitted exception names. The local-decision layer contributes no rules of its own: it acts here, through the exceptions collected in step 2.
4. **Evaluate.** Evaluate the active rules and report the findings.

Composing first and consulting the person afterwards was the fault the review named (C4). A recorded rejection that never reaches the active set is a receipt entry and nothing more, which contradicts D7.

## Local waivers in unmanaged execution

In unmanaged execution an authorised local waiver must deactivate the named waivable creative rule, for the named instance and for the fingerprinted facts (R6, C4). The original rule must be preserved in the definition, and the waiver must be recorded on the instance.

The recording pattern already exists. `packages/core/src/rights-v1.ts` defines `RightsDecisionV1` with a `fingerprint` field, and `RightsEvaluationV1` carries a `situation` hash taken over the same facts with the decisions left out. A decision applies only while the facts still hash to that value. A changed source or format retires the choice rather than letting it answer for something it was never about. `engine/src/runtime.ts` exposes `setRightsDecision` and `setRightsDecisions`, and `engine/src/session-record.ts` persists the list.

A local waiver must follow the same three rules (R6):

- it states one rule id and one instance, and never a class of rules or a blanket setting;
- it carries the fingerprint of the facts it was made about, and a changed fact requires a fresh decision;
- it is recorded where the instance is recorded, so it survives a reload the way `rightsDecisions` does.

Measured conformance to the unmodified rule must read failed or excepted, never passed (R6, C4). The receipt must list the rules a run bypassed (R6), among the fields R15 requires of it. A waiver changes what this evaluation enforced, and it never changes what a suite measured.

A local waiver must never deactivate a structural rule or grant a device capability (R6). Device capabilities stay in the `capabilities` list of `schemas/tool.schema.json` and are granted by the host, not by a rule decision.

## Governed exceptions

In governed execution only overlay-permitted exceptions apply, and the document must never grant itself one (R6). A rule that arrives with the tool must never authorise its own bypass. A person editing under an overlay that forbids an exception must never be granted one.

Where the overlay carries an approval chain, the exception is a request rather than a decision. `lolly-work/server/src/approvals/engine.ts` defines a chain as an ordered list of steps. Each step has an eligible group and a step rule of `any`, a quorum or `all`. Each act is evaluated with the actor's identity, so the submitter can never clear their own request. An approval state moves through `submitted`, `in_review` and then one of `approved`, `rejected` or `withdrawn`. An exception granted this way records the approval it came from.

## Conflicts stop the operation

When two active required predicates conflict, the affected operation must stop, and the report must carry both rule ids and the affected values (R6). A host must never choose one of the two silently, and it must never invent a repair to reconcile them.

This is the case the class table does not cover, because neither rule is waivable and neither is advisory. Stopping is the outcome, and the outcome carries a `blocked` termination as specified in [Operations and outcomes](operations.html).

## The context table

| Context | Behaviour |
|---|---|
| Local person accepts the constraints | Evaluate against them and expose the findings. |
| Local person rejects or relaxes a constraint | Deactivate the named waivable creative rule for this instance and these facts. Keep the original definition. Record the waiver against the rule id and the fingerprint on the instance. A changed fact needs a fresh decision. The receipt lists the bypassed rules. |
| Governed execution | Apply the authenticated overlay and the exceptions it permits. The document must never grant itself an exemption. |
| Required rules conflict | Report both rule ids, the affected values and the conflict. Stop the affected operation. |
| Governed client offline | Keep enforcing the policy the client holds. Report the four facts below separately. Apply any explicit freshness rule the policy carries. Keep cached work usable. |

## A governed client that loses its connection

A governed client that loses its connection must keep enforcing the policy it holds, and nothing relaxes (R7). The review found the plan describing offline behaviour the design system code does not have, so the four facts below are stated as four separate answers rather than one status (C5).

| Fact | What it answers | Where the tree answers it today |
|---|---|---|
| Refresh due | Has the check interval lapsed | `ORG_CONFIG_TTL_MS` in `shells/web/src/org/index.ts` for the organisation policy, against the `policyVersion` ETag (`lolly-work/server/src/policy/org-config.ts`); `HOSTED_CHECK_INTERVAL_MS` in `shells/web/src/lib/design-system/hosted.ts` for a hosted design system |
| Update known | Was a change detected that could not be brought down | The `stale` outcome of `refreshHostedDesignSystem` |
| Validity | Is the held policy still valid on its own terms | Represented today only as a cache horizon, and the wrong way round: `ORG_CONFIG_TTL_MS` (24 hours) in `shells/web/src/org/index.ts` makes `readCachedOrgConfig` evict a held copy past that age, after which the boot path fails closed. This chapter's rule replaces that horizon with an explicit validity rule. |
| Attestation | When was the policy last attested by its issuer | Not represented today; `policyVersion` identifies the policy, not its attestation |

`refreshHostedDesignSystem` in `shells/web/src/lib/design-system/hosted.ts` returns `unreachable` when the host cannot be reached, and it returns `stale` only after a detected change failed to import. `checkHostedDesignSystems` skips the sweep when the browser reports itself offline. A refresh interval that has lapsed must never be read as an expired policy credential (C5).

Today the shell does not meet this rule. `shells/web/src/org/index.ts` serves a cached organisation policy only inside `ORG_CONFIG_TTL_MS`. Past 24 hours it drops the copy, sets no policy at all and clamps the export and input seams closed. Enforcing the held policy instead, and reporting the four facts, is a change this model requires. Plan section 17's 25-hour offline case is where it is measured.

A freshness deadline for a governed-claim export must be an explicit policy rule, with an issuer, a validity interval and a stated behaviour when freshness cannot be established (R7). With no such rule, the held policy must be enforced, and the receipt must say which policy version was evaluated and when it was last attested (R15 for the policy version, Q3's default for the attestation).

Cached material must stay usable (R7). A hosted record keeps its core material on the device and records `lastSyncedAt` (`shells/web/src/lib/design-system/hosted.ts`). The device keeps working when the host has gone away, and the row reports how long ago it last synced. A receipt must never describe a held policy as current online approval (C5).

## Leave and Make an editable copy

Leave and "Make an editable copy" are two actions with different results, and this model must never merge them (R7, C5).

`leaveInstance` in `shells/web/src/lib/instance-leave.ts` removes what the organisation supplied: its cached configuration, its pack and the tools that pack installed, the install identity this device spoke while enrolled and the native shell session. For a hosted design system the same act is scoped to the record: plan 186 section 3.6 defines Leave there as "Remove this design system", running this sweep plus the namespace delete. What the person made stays: their sessions, images, preferences and any tool they sideloaded themselves. `countSessionsUsingInstanceTools` in the same module reports how many saved sessions belong to a tool the pack installed, so the exit dialog can say those sessions will not open until the device reconnects. Leave must never be turned into a copy operation (C5).

"Make an editable copy" is a separate, explicit action. `createDesignSystem` in `shells/web/src/lib/design-system/manage.ts` writes a `local` record, and `shells/web/src/lib/design-system/registry.ts` defines that source union. Where the copy was made from a hosted or local system, the record points at it in `forkedFrom`, with a version where the source carried one. A copy of the shipped system records no source today, and this model requires one. A copy made this way must state its dependency set, and it must carry no inherited governance claim (R7). The copy is the person's material from that moment, and the organisation's rules no longer travel with it.

## Repair

Repair is validate first, then propose (R6). A host must validate, then propose an explicit patch, and never mutate authored content to satisfy a rule without saying so.

- Applying a repair must require the authority specified for that operation (R6, D12).
- Automatic repair must have a declared rule behind it, and the applied repair must appear in the effective result (R6).
- A repair proposal must be a patch against a stated base revision, as specified in [Source rows, payloads and patches](source-and-patches.html).

A conflict between two required rules must never produce an invented repair (R6). The operation stops instead, and a person decides.

## Measured conformance and contextual acceptance

Two answers must stay separate, always (R8):

1. **Measured conformance.** The suite, its released version, the reference environment, the comparator version and the per-feature results.
2. **Contextual acceptance.** Whether the relevant authority accepts that result, with each exception named.

An authority may select a suite, require stricter checks, approve references or accept an exception (R8). None of those decisions may change the meaning of a suite version already published under another identity. A local waiver is an acceptance decision and never a measurement, which is why measured conformance to the waived rule still reads failed or excepted (R6, C4).

Lolly Work's planned effective-schema projection (Work plan 42, not built) and the engine's `documentSchema` (`engine/src/document-api.ts`) are the same projection of policy onto a schema. The model must specify one function for both (plan section 10.2), so that a locked input reads the same way in the shell, the command line and the MCP surface.

## Open points

- **Q2. The granularity and persistence of local rejection.** Default from plan section 16: per rule, instance-scoped, recorded on the session record with a fingerprint, following the `rightsDecisions` pattern (R6). A derived tool revision is a Design tool export and is not part of 0.1. Evidence that would change it: a need to share a rejected state as a reusable tool.
- **Q3. What freshness a governed-claim export needs, and who states it.** Default from plan section 16: freshness is a policy rule the instance issues, with a validity interval and a stated behaviour when it cannot be established. With no such rule the held policy is enforced, and the receipt says which policy version was evaluated and when it was last attested. Leave and "Make an editable copy" keep their current meanings (R7). Evidence that would change it: an organisation that needs a hard deadline, or a fully offline site.

The other open questions are carried, with their plan section 16 defaults, in other chapters: Q1 and Q5 in [Conformance and fidelity](conformance.html), Q4 in [Operations and outcomes](operations.html) and Q6 in [Status and open points](status.html). All six remain Andy's to answer. A default is what drafting proceeds on, not an answer.

## Precedents

In this repository:

- `docs/constraints.md`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/rights-v1.ts`
- `engine/src/session-record.ts`
- `engine/src/runtime.ts`
- `engine/src/document-api.ts`
- `schemas/asset.schema.json`
- `schemas/tool.schema.json`
- `shells/web/src/lib/design-system/hosted.ts`
- `shells/web/src/lib/design-system/manage.ts`
- `shells/web/src/lib/design-system/registry.ts`
- `shells/web/src/lib/instance-leave.ts`
- `shells/web/src/org/index.ts`

In Lolly Work, which is a separate repository and not part of this tree:

- `lolly-work/server/src/policy/overlay.ts`
- `lolly-work/server/src/policy/org-config.ts`
- `lolly-work/server/src/approvals/engine.ts`
- `lolly-work/server/src/render/pipeline.ts`

# Conformance and fidelity

> This chapter is a draft for review, dated 2026-09-24.  

Conformance is what a suite measured. Acceptance is what an authority decided about that measurement. The two answers must never be merged into one verdict (R8, D12).

This chapter specifies conformance suites, their features and the report a run emits. It then specifies the acceptance record an authority writes, the three checks that test a still-image claim, the motion checks, the editable round trips and the conformance of operations that draw nothing. It restates plan section 11 in normative form, with the corrections the review recorded as C8. The operations being measured are specified in [Operations and outcomes](operations.html). The authority that accepts a result is specified in [Constraints, authority and local choice](policy.html).

## Two separate answers

A measured result and an accepted result answer different questions, so this specification keeps them in separate records (R8).

**Measured conformance** states the suite name and its released version, the implementation identity, the reference environment, the fixture, reference and comparator digests and the per-feature results (R8, C8). It is a fact about one run on one machine.

**Contextual acceptance** states the authority, the suite reference it judged and each exception it granted (R8, D12). It is a decision, and a different authority may decide otherwise about the same report.

An authority may select a suite, require stricter checks, approve references or accept an exception (R8). None of those decisions may change the meaning of a suite version already published under another identity (R8). A local waiver of a creative rule must leave measured conformance to the unmodified rule reading failed, never passed (R6, C4). The waiver is recorded as an exception on the acceptance, never on the report (R8).

![A conformance suite producing a report on one side and an authority producing an acceptance on the other, with no arrow that merges the two into a single verdict.](/info/diagrams/document-model/conformance-vs-acceptance.svg)

## Suites and features

A conformance suite is an immutable versioned set of fixtures, references, a comparator and a reference environment (plan section 3). A suite must be released under a version, and a report must never name a branch or a commit in its place (R8).

Each suite declares named features, and each feature is marked core or extended (R8). A core feature must never be skipped (R8). An extended feature may be unsupported, and the report must list it as unsupported rather than silently omit it (R8).

Requiredness is scoped to the affected operation or output (R11). Missing motion support must never block inspection or a still export that has no motion dependency (R11), which is the rule the four gates in `packages/core/src/host-v1/apis.ts` and `schemas/tool.schema.json` already follow.

Independent authors, Work instances and local people may publish suites and feature namespaces under their own namespace and version (D10, plan section 10.2). A suite namespace carries publisher identity. What names they take, and who may publish under `lolly/*`, is open as Q5 below, with its default recorded there.

### Fixtures

A fixture must identify its inputs, its immutable dependencies, the reference environment, the features it claims, the expected artifact or typed result and the comparison rules (D3, `tests/docs-shots-vector.test.ts`).

Expected trees and traces may supplement output checks (D3). They must never replace them, because a valid JSON tree is not evidence of a rendered result (D3).

A fixture must declare its comparison regions itself (R9, C8). A region discovered from the difference between a candidate and its reference is exploratory evidence, never a region a fixture may declare.

## ConformanceReportV1

The report shape follows Gateway API's conformance report, which the plan consulted as outside practice (R8, plan section 15).

```text
draft shape

  ConformanceReportV1
    suite           { name, version }  a released version, never a branch or a commit
    implementation  { project, version, shell, mode, contact }
    date
    features
      core          { result: passed | failed, statistics, failedTests[] }
      extended      { result, statistics, supportedFeatures[], unsupportedFeatures[], skippedTests[] }
    environment     { fonts, shapingEngine, rasteriser, codecs, gpu, os }
    digests         { fixture, reference, comparator }
```

This is a draft shape, not a frozen type. Nothing is added to `packages/core` or `schemas/` in this wave, and the plan's section 18 orders the counterexample fixtures in [Proof cases](proof-cases.html) before any of it is frozen.

A skipped test must never be recorded as a pass (R8). A report whose suite version does not match the fixtures it ran is invalid (R8). A report must bind to the fixture, reference and comparator digests as well as the suite name and version (C8). Reports are data, and publishing one grants no trust (R8).

## AcceptanceV1

```text
draft shape

  AcceptanceV1
    authority   { kind: work-administrator | local-person, instance, id, name }
    subject     { definition, instance, revision, operation }
    report      { suite, version, reportDigest }
    decision    accepted | accepted-with-exceptions | rejected
    exceptions[]  { target: ruleId | featureName, reason, by, date, scope }
    date
```

An acceptance must name the report it judged by digest (R8, C8). An acceptance describes exactly the revision named in its subject. When that revision changes the acceptance stays as history and must never describe the new revision as previously accepted (C7). [Packaging, identity and migration](packaging.html) states the same rule for receipts and signatures. An exception must name its target, its reason and the authority that granted it (R6, R8). An acceptance must never restate a measured result as passed when the report said otherwise (R8).

The governed authority is the Work administrator for the brand, recorded with the instance they administer (D12). Otherwise it is the local person using the client (D12).

## The native 0.1 suites

| Suite | Scope | State in this draft |
|---|---|---|
| `still-2d/1` | PNG, SVG and PDF, including print marks, bleed and CMYK output targets. | The one native suite for 0.1 (R12). |
| `motion-2d/1` | Timed appearance and duration for motion output targets. | Staged after plan 196's `TimelineV1` exists (R12). |
| `present/1` | Interactive presentation and its state transitions. | Stays an adapter until Agenda's portable runtime contract is typed (R12). |

The evidence folder records 25 raw manifest rows declaring video and one declaring `render.portable`, raw directory counts taken before content profile resolution (C9), while the frame clock exists and `TimelineV1` does not (R12).

## Still-image fidelity

D8 allows a very low pixel difference, difficult to notice without close inspection. The plan reads that intent as no visible loss on close inspection (plan section 2) and keeps it, tested by three checks rather than by one number (R9).

Each fixture declares the parameters of all three checks, its fuzz band, its regions and the facts to read back from the artifact, and a claim must pass all three (R9). A fixture that cannot state one of them must say why in the fixture, and a report that rests on it must name the check that did not run (C8).

1. **Whole-image fuzz band.** A per-channel maximum difference and a maximum count of differing pixels, each with a lower and an upper bound, in the form web-platform-tests uses (R9). A tolerance that must accept identical output has a lower bound of zero. This catches gross drift and admits anti-aliasing.
2. **Region-scoped structural checks.** Regions the fixture declares, such as a legal line, a logo or chart labels, compared by ink ratio and structural similarity inside the region (R9). The fixture declares the region (R9). The comparator must never derive it from the difference it is measuring (C8).
3. **Semantic checks on the produced artifact.** Text content, page count, chart data and layer inventory, read from the artifact or from an independently extracted representation, compared through `compareSources` in `engine/src/compare.ts`, which dispatches by mode to `compareText` in `engine/src/compare-text.ts` and `compareStructure` in `engine/src/compare-structure.ts` (R9). Comparing authored text with the same authored text would miss a broken exporter, so the comparison must read the output (C8).

Pixel counts alone must never decide a still-image claim (R9).

![Three boxes, a whole-image fuzz band, region checks and semantic checks, each with its own arrow into a single result box, labelled whole image, ink and SSIM, and text and pages.](/info/diagrams/document-model/comparator-three-checks.svg)

### The evidence and what it does not settle

A comparator probe rendered a QR code with a caption through the CLI shell as render A, then derived three variants from it. B removed the caption glyphs and left every box unchanged. C shifted the image by half a pixel. D rasterised at 1023 px and resampled to 1024 px. The caption region is 6.09% of the canvas. The probe is exploratory evidence and is not published with this specification.

| Pair | Changed pixels, whole image | SSIM, whole image | Changed pixels in the caption region | Ink ratio in the region | SSIM in the region |
|---|---|---|---|---|---|
| A vs B, caption gone | 1.46% | 0.969 | 24% | 0.00 | 0.43 |
| A vs C, half-pixel shift | 1.25% | 0.968 | 8.8% | 1.00 | 0.89 |
| A vs D, resampled | 1.29% | 0.968 | 6.9% | 1.00 | 0.95 |

The three pairs sit within a quarter of a point of each other on changed pixels and within 0.002 on SSIM. A real content loss and two harmless changes are therefore indistinguishable by a whole-image number. The region-scoped measures separate them at once. A text-presence check on the artifact would separate them with no pixels at all, by construction rather than by measurement: B was made by removing the caption glyphs from the source, and the probe ran no such check.

The probe establishes the need for complementary checks. It sets no threshold (R9, C8).

It also does not establish a validated combined comparator or cross-shell conformance (C8). Its region was discovered from the A and B difference, which a fixture must never do (C8). Its fonts were whatever the rasteriser found on one machine, and resvg-js does not report which files it resolved. The raster was composited onto white. The structural similarity is one small implementation. Nobody decoded the QR afterwards, so "nothing lost" in C and D describes the source edit only.

Thresholds must be calibrated on accepted and rejected examples before any number enters this specification (R9). Masks must be explicit and justified, and human review sets and reviews the bands and must never replace the comparator (R9, plan section 11.2).

### The output-validation boundary

An output-validation boundary sits after rendering and before delivery of any artifact that claims acceptance (C8). An artifact that has not passed that boundary must never be delivered with an acceptance claim attached.

Comparison for a claim must run at native raster scale with pinned fonts and rasteriser, with declared colour handling and alpha treatment (R9, C8). The preview-grade comparison in `engine/src/compare-visual.ts` samples at most 768 px per edge with nearest-neighbour sampling on white, and it states that limit in its own result. It stays a preview tool and must never decide a conformance claim.

An `undetermined` result must never be a pass (R9, C8). `compareSources` in `engine/src/compare.ts` returns `undetermined` when a budget cut the comparison short, and `engine/src/compare-budget.ts` is where that budget is spent. Harmless truncation of displayed detail must be distinguished from incomplete comparison coverage (C8).

PNG alpha, PDF page boxes and colour intent must each have their own checks (C8). A white RGB fixture says nothing about CMYK print conformance, and a suite must never generalise from one to the other (C8).

A run whose text stayed a live `<text>` element rather than an outline must record that in its receipt, and it must never claim font-independent fidelity (R15). [Evaluation and receipts](evaluation.html) specifies the receipt.

## Motion fidelity

Appearance is compared at normalised progress points, and total duration and the time mapping are compared separately (D9, plan section 11.3).

Sampling must include the start, the end, scene and transition boundaries, authored events and samples immediately around discontinuities (D9). Each suite must bound its sampling interval, with denser checks for fast motion (D9).

Loop behaviour and audio timing must be tested where they are claimed (D9).

Sampling is evidence at the tested points. It does not prove the absence of a defect between them, and the suite must state that limit (D9).

## Editable round trips

Visual similarity is necessary and insufficient for an interchange claim (R14). A raster fallback may claim appearance and must never claim editable-vector preservation (R14).

Each route must declare preservation separately for six dimensions: appearance, editable objects, semantic data, interactions, timing and constraints (R14, `engine/src/penpot-file.ts`).

A route is tested by a meaningful edit after import, followed by an export, followed by a comparison across all six dimensions (R14). The required edits are moving a layer, changing a text run and swapping a token.

The first routes are proposed, not settled. They are Q1 below, and the table states them as the recommended default.

| Pilot | Proposed route | What exists today |
|---|---|---|
| Design | To and from Penpot. | The `.penpot` writer in `engine/src/penpot-file.ts`; the editable import is `parsePenpotBinfile` in `shells/web/src/views/design-import.ts` over the pure mapper `engine/src/design-map.ts` (the Unpack reader in `shells/web/src/views/penpot-import.ts` extracts, it does not round trip). |
| Design | To native PPTX and back. | The deck reader `readPptx` in `engine/src/pptx-read.ts`, reached by the Rebrand source path in `packages/node-shell/src/pptx.ts`. |
| Chart | To and from CSV and JSON data, plus the SVG semantic overlay. | The portable chart contract in `packages/core/src/chart-v1.ts`. |
| Agenda | To ICS, portable HTML and PPTX. | The outputs listed in `docs/agenda.md`. |

Today's `composes` and `compose.renderUrl` paths produce a flattened image (R14). That image may pass an appearance check and must fail an editable round trip (R14). [Proof cases](proof-cases.html) carries it as a failing case, not as a pilot.

## Conformance without a picture

An operation that draws nothing is still measurable (D11, R13). The three paragraphs below state what its suite tests.

For Rebrand, compare the approved changes against the rebrand plan, the protected content against the source and the output against a reference for that rebrand plan (R13). Partial completion is read through `OutcomeV1` as [Operations and outcomes](operations.html) specifies.

For Unpack and the other route operations, compare typed results, resource inventories, declared effects and failure behaviour (plan section 11.5, R13). A fabricated preview must never stand in for the real outcome (R13).

For interactive Agenda, test search, navigation, calendar output, state transitions and accessibility (plan section 11.5).

## Material that exists today

None of this is a suite yet. These are the parts a suite would be assembled from, and each states its own limit.

| Piece | What it establishes | Where |
|---|---|---|
| `lolly smoke` | Every tool in the active content profile renders at its defaults to its first Node-native format. A skip is never counted as a pass. | `shells/cli/src/smoke.ts` |
| Export characterisation | A fixed matrix of nine tools, chosen so every export branch is exercised, is rendered twice through the real web export path and classified as hashed or size-banded. Same machine only. | `scripts/characterize-export.ts` |
| Comparison modules | Bounded text and structure comparison with stable-id moves, an explicit `undetermined` result and a preview-grade visual mode that states its limits. | `engine/src/compare.ts`, `engine/src/compare-structure.ts`, `engine/src/compare-visual.ts`, `engine/src/compare-budget.ts` |
| Host conformance kit | A live host carries the members the contract declares, checked at run time rather than at compile time. | `packages/core/src/host-conformance.ts` |
| Docs shots | A vector baseline is compared exactly, and every raster baseline carries a written reason in one allowlist that fails in both directions. | `tests/docs-shots-vector.test.ts` |

When a suite exists, its first reports come from this repository's continuous integration, where the render gate, `lolly smoke` and the export characterisation script already run. A page listing each shell's results is the cheapest trust asset the suite can carry (plan sections 11.1 and 18).

This draft adds no conformance or reproducibility claim beyond the ones `docs/determinism.md` already makes, and it states no threshold and no latency target (C8).

## Open points

- **Q1. The first editable interchange routes per pilot.** Default from plan section 16: the four routes and three edits in the editable round trips section above. Evidence that would change it: a customer route that outranks them.
- **Q5. Who may publish a suite under the `lolly` namespace, and how a local or Work suite is named.** Default from plan section 16: `lolly/*` suites are published from this repository's continuous integration only, Work instances publish under `work:<instance>/*`, local people publish under `local/*` and an acceptance record always states the suite it judged. Evidence that would change it: a partner programme that needs a shared namespace.

The other open questions are carried in other chapters, each with its recommended default and none of them yet answered (plan section 16). Q2 and Q3 are in [Constraints, authority and local choice](policy.html). Q4 is in [Operations and outcomes](operations.html). Q6 is in [Status and open points](status.html).

## Precedents

- `engine/src/compare.ts`
- `engine/src/compare-structure.ts`
- `engine/src/compare-text.ts`
- `engine/src/compare-visual.ts`
- `engine/src/compare-budget.ts`
- `engine/src/penpot-file.ts`
- `engine/src/design-map.ts`
- `engine/src/pptx-read.ts`
- `packages/core/src/host-conformance.ts`
- `packages/core/src/host-v1/apis.ts`
- `packages/node-shell/src/pptx.ts`
- `packages/core/src/chart-v1.ts`
- `schemas/tool.schema.json`
- `shells/cli/src/smoke.ts`
- `shells/web/src/views/design-import.ts`
- `shells/web/src/views/penpot-import.ts`
- `scripts/characterize-export.ts`
- `tests/docs-shots-vector.test.ts`
- `docs/determinism.md`
- `docs/agenda.md`

# Capabilities, trust and extensions

> This chapter is a draft for review, dated 2026-09-24.  

A document declares what it needs. A host declares what it can do. This chapter specifies how those two declarations meet, what an independent publisher may add to the model and what a host must do with a part it cannot read. It restates plan section 12 with resolutions R10, R11 and the review correction C7. It also carries the parts of R3 that decide where code runs. Lolly Work is a separate repository, and a `lolly-work/` path in this chapter is a file in that checkout rather than in this tree.

Independent authors may publish feature namespaces and conformance fixtures (D10). Spatial experiences, fabrication and unfamiliar media are extension checks rather than roadmap items, and they exist here to keep the boundary coherent (D5). The suites those fixtures belong to are specified in [conformance and fidelity](conformance.html). The operation that a missing extension refuses is specified in [operations and outcomes](operations.html). The execution classes named below are defined in [values, expressions, code and time](values-and-time.html).

## Capabilities are data with limits

A host must report its capabilities as data with limits rather than as booleans (plan section 14, gotcha 4, against `lolly-work/server/src/render/capabilities.ts`). A boolean answers whether a thing is possible in general. A caller needs to know whether this output target, at this size, for this duration, is possible here.

Both current precedents are too coarse. The model takes them as the starting point, not as the form to copy.

- `RenderCapabilities` in Lolly Work is `formats: string[]` plus `hookedTools: boolean` (`lolly-work/server/src/render/capabilities.ts`). It is deployment scoped and the same for every caller. One boolean covers every hooked tool, whatever that tool renders. The plan states where this grows to: `MotionCapabilitiesV1` and its still-image sibling (plan section 12). Neither is built; `MotionCapabilitiesV1` is one of the types plan 196 defines on paper (F2), so the shape below is drawn to meet it rather than to replace it.
- The manifest `capabilities` list and its host side are membership tests (`schemas/tool.schema.json`, `packages/core/src/host-v1/host.ts`). A shell declares `camera` or it does not. Neither side states a ceiling.

The tree already holds the better pattern on one path. `pickWebCodecsVideo` in `shells/web/src/bridge/video-shared.ts` probes the real encoder with width, height, bitrate and framerate. It walks a ladder of codecs and returns the one that answered, or nothing when none did. The caller in `shells/web/src/lib/video-jobs.ts` then fails the job rather than guessing, though its message gives no limit. That is a capability answered against the work being asked for.

Rules for 0.1:

- A host must report supported conformance suites and their released versions, not a single support flag (R8, plan section 12). The names and versions a host reports here are the same ones a `ConformanceReportV1` names, which [conformance and fidelity](conformance.html) specifies (R8).
- A host must report quantitative limits per output target: format, colour handling including HDR, alpha, audio, maximum geometry and maximum duration (plan section 14, gotcha 4, in the manner of `shells/web/src/bridge/video-shared.ts`).
- A capability report must never be the authority for what a run may do. Grants are bound to the operation, the phase and the attempt (R3), and [evaluation and receipts](evaluation.html) owns that boundary.
- A document must declare a minimum core `schemaVersion` and the named versioned features it uses (R11, plan section 12).

This chapter states no number. Where a limit is a number, that number belongs to a host and to a release, never to the model.

```text
draft shape

  CapabilityReportV1
    host          shell, engine version, the execution classes it can provide
    suites[]      { suite, releasedVersion, features: { core[], extended[] } }
    outputs[]     one entry per output target it can produce:
                    { format, colour, alpha, audio, maxGeometry, maxDuration, paging }
    extensions[]  { namespace, schemaVersion range, stages }
    stated        which fields are measured and which are declared by the host
```

This is a draft shape for review. Nothing is added to `packages/core` or to `schemas/` until the counterexamples in plan section 17 pass as contract fixtures (plan status line and section 18, first pull requests). The shape is drawn to meet `MotionCapabilitiesV1` and its still-image sibling, not to replace them (plan section 12, F2).

## Requiredness is scoped to the operation

A requirement is always a requirement for something. A host must scope requiredness to the affected operation and output target, never to the document as a whole (plan section 12, third compatibility gate of the review).

The consequence is the rule the review asked for. A host must inspect declarations first and refuse at the operation boundary. Missing motion support must never prevent a safe inspection, and it must never block a still export that has no motion dependency (third compatibility gate of the review, plan section 12). [Evaluation and receipts](evaluation.html) places that refusal at step 2 of the pipeline, after the operation and the output target are chosen.

When the refusal comes, it must name what is missing. A blocked termination in [operations and outcomes](operations.html) carries the unmet requirement, so a caller can tell an unmet extension from an error.

## Where a document declares its extensions

A document declares its extensions in one place. An `extensions` map keyed by namespace holds each extension's own record with its `schemaVersion` (R10). Two lists name what the document uses and what it cannot do without: `extensionsUsed` and `extensionsRequired` (R10, and glTF's precedent below). The root manifest stays closed, because `tool.json` refuses unknown keys and that is right for a definition (plan section 14, gotcha 14). An extension adds nothing to the root.

## What an extension declares

An extension must identify itself completely before a host decides anything about it (R10, plan section 12).

```text
draft shape

  ExtensionDeclarationV1
    namespace       permanent and publisher owned; never renamed, never reused
    publisher       who published it, for attribution and never for trust
    version         immutable; a released version is never re-cut
    schemas[]       the records and fields it adds, each with its own schemaVersion
    dependencies    the complete scope it reads; see the next section
    stages[]        which evaluation steps it takes part in
    execution       the execution classes its code may run in, when it has code
    powers[]        the effects its code asks to commit, authored not generated
    outputs[]       what it adds to an output target, and what that addition means
    compatibility   the reader and engine versions that can read it
```

The `outputs[]` entry is where the fabrication thought experiment is answered. A manufacturability check is an output-target validator contributed by an extension, not a new operation (plan section 14, gotchas 9 and 11). That is the test of whether the boundary holds for a medium nobody here has built (D5).

Publication, a signature and a passing fixture grant no code trust and no host authority (D10, plan section 12). A signature says who published the bytes. It never says what those bytes may do. Validators and comparators that execute code are themselves subject to an execution class, because a comparator is code like any other (R3, plan section 12).

The chrome slot contract is the nearest in-tree model for the identity half of this. `packages/core/src/extension-v1.ts` gives a slot a permanent id and requires a namespaced extension id. It carries a channel that is recorded for governance and for a provenance chip, and never as a security boundary. Its own header makes control-plane extensions org-trusted and community ones opt-in at the deployer's risk, and it states that a hydrated component runs in the shell realm with no sandbox either way. Ordering inside a channel is a tiebreak under the channel governance rank, so an author cannot place a community component ahead of a control-plane one. Its namespace and channel model is reused here. Its runtime is not (plan section 13).

A namespace must never reach into a reserved name. `RESERVED` in `engine/src/url-mode.ts` is the closed set of URL parameters that are not inputs, and two prefixes are reserved: any name starting with `_`, which is where future reserved params are minted from, and any name starting with `pkg.`, an in-use namespace for Linux-package export metadata. This chapter adds one rule that the plan does not state, for the [status chapter](status.html) to confirm: an extension that needs a URL parameter takes it from its own namespace, never from either reserved prefix and never from the reserved set itself.

## Dependency scopes, and what a host without the extension must do

Preserving bytes does not preserve meaning (R10). A host that carries an unknown part through a save has kept the bytes. It has not kept the part correct, because an edit elsewhere in the document can invalidate what those bytes describe.

An extension must therefore declare a complete dependency scope (R10, C7). Three kinds of scope exist, and a declaration that lists only the first is incomplete.

| Scope kind | What it covers | Which edits break it |
|---|---|---|
| Rows and values | The rows the extension reads and the fields it reads on them | Changing a value, deleting a row or replacing a typed payload record |
| Collection membership and order | The collections whose membership or order the extension depends on | Adding a row, removing a row or reordering a collection |
| Global settings | Document-wide settings such as units, DPI and colour handling | One edit at the document level, touching no row the extension named |

A list of currently read row ids alone cannot protect the second and third kinds (C7). That is why the declaration is scoped rather than enumerated.

A host that lacks an extension must do all of the following (R10, C7):

- It must preserve the extension's bytes through import, local persistence, restart, edit and save. [Packaging, identity and migration](packaging.html) specifies the retained package state that carries them.
- It must protect the declared scope. It must refuse an edit that touches the scope, or it must offer an explicit lossy copy before any data changes.
- It must never label a loss after the loss happened. Labelling after the fact is not protection (C7).
- Where a scope is incomplete or absent, it must protect the whole relevant document rather than guess.
- It may still inspect safely where inspection is feasible, because inspection changes nothing.
- It must never claim to have rendered a feature it does not support (R10).
- It must refuse the affected operation by name when a required extension is missing (R10).

The counterexample is stated in the plan: glTF declares `extensionsUsed` and `extensionsRequired`, and some loaders only warn on an unmet required extension rather than refusing (R10). One import path in this tree does part of what this model asks for. `shells/web/src/lib/studio3d/source.ts` reads `extensionsRequired` out of a GLB manifest and refuses the import when the model requires Draco, Meshopt or KTX2 compression. That refusal comes at the import boundary rather than later in the decode. It reports the three compression formats rather than the extension that triggered it, so it is a precedent for refusing early and only a partial one for naming the unmet item. It also tests a fixed list, so a required extension outside that list is not refused. The rule here is the general case that list is a start on.

An extension's content travels as package parts under the integrity map, never as manifest keys (R10). The reason is in `shells/web/src/lib/lolly-pack.ts`: the writer rebuilds the manifest from typed fields on every save, so an unknown manifest key is dropped. A part is the safer carrier because the integrity map already covers and verifies it. The carry itself does not exist yet: `LollyBuildInput` takes only typed fields, so a part a reader did not interpret is not written back today. [Packaging, identity and migration](packaging.html) specifies the collision, ownership and collection rules for those parts, and the retained package state that has to join the read to the next write (C7).

## The four gates

Four independent gates decide whether a document can be read and run here. Each already exists or is proposed, and each refuses at its own boundary.

| Gate | What it compares | Where it runs today | What it does when unmet |
|---|---|---|---|
| Engine range | The manifest `engineVersion` range against the running engine | `engine/src/loader.ts` with `engine/src/semver-range.ts` | Refuses to load the tool. A range it cannot parse is treated as unsatisfiable, so an unrecognised range fails closed |
| Host APIs | The manifest `requires` list against the optional APIs the host provides | `engine/src/runtime.ts` with `packages/core/src/host-v1/apis.ts` | Refuses the mount before any hook runs. An unknown name counts as missing, so a typo fails loudly |
| Extensions | `extensionsRequired` against what the host implements | Proposed. The nearest precedent is the GLB import in `shells/web/src/lib/studio3d/source.ts` | Refuses the affected operation by name, at the operation boundary |
| Reader version | The container `minReader` against the reader's own version | `packages/node-shell/src/lolly-file.ts` | Refuses to open the package and says a newer reader is needed |

The public `requires` field keeps exactly the meaning it has now: the optional `HostV1` APIs a tool's hooks call without feature-detecting them (`schemas/tool.schema.json`). It must never be repurposed to carry engine, reader or extension requirements (R11, first compatibility gate of the review). One internal evaluator may normalise the four gates so their diagnostics read the same way. The four fields stay separate on the wire.

`missingRequires` in `packages/core/src/host-v1/apis.ts` is the current implementation of the second gate, and `HOST_V1_OPTIONAL_APIS` is the enumerable list it compares against. A tool that guards an API (`host.text?.`) must not list it, and `scripts/tool-requires.ts` computes the list from the hooks, so a manifest that disagrees with what the analyser can see fails `validate:catalog`. The analyser is a regular expression and not a program analysis, so agreement with it is not proof: the limits are stated under the legacy execution boundary below (C6).

Each record carries its own `schemaVersion` (R11). A reader upgrades on read and may downgrade on write. A container carries `minReader`, and a definition carries `engineVersion`. Preservation-capable readers and writers must ship before any new part is emitted, behind an effective reader gate for earlier clients. An additive field is not safe merely because an old reader ignores it (C7).

These versions move independently and their relationships are written down rather than assumed (R11). The practice already exists: `lolly-work/engine-pin.json` records the engine and SDK versions that checkout vendors, with a content hash per pinned package and per schema file. The document model version and the suite versions join that same pin file rather than gaining a mechanism of their own (plan section 13, adoption sequence).

## The legacy execution boundary

`HostV1` is the portable API contract (`packages/core/src/host-v1/host.ts`). It is not a proof of isolation (plan section 12.2).

This is already written down where the code lives. `docs/constraints.md` states that trusted compatibility hooks run through `new Function('host', ...)` in the shell's realm, and `engine/src/runtime.ts` says the same at the site (plan E8). That is closure-scope injection and not a security sandbox, so a hook in a browser shell can still reach `window`, `document` and `fetch`. `packages/core/src/extension-v1.ts` says the same about a hydrated chrome component. The model does not correct either statement. It records which context actually enforced a run.

Rules:

- The evaluation receipt must state the execution class the run used (R15, plan section 12.2). The class in the receipt states what actually enforced that run.
- An adapter may keep trusted compatibility execution, and it must be classified as such (R3, plan section 12.2). Adoption of this model must never retroactively make an existing hook strict.
- A strict class must enforce the powers it claims and must refuse an unsafe fallback (R3, plan section 12.2). The sideloaded and remote path in `shells/web/src/bridge/hook-worker.ts` already rejects the mount when isolation cannot start, rather than falling back into the realm.
- Effects are declared by the author and enforced by the execution class (R3, C6). A generated inventory assists declaration and linting. It is never the authority.

The last rule has evidence behind it. `analyseRequires` in `scripts/tool-requires.ts` is a regular expression over `host.<api>` member access. The review probed it: `host.text.toPath()` is detected, `host["text"].toPath()` is not, and a local alias assigned from `host.text` is not (C6). A generated list is therefore lint, and an authored effect envelope is the contract.

The enforcement pattern a strict class needs already exists. `strictHostShape` in `shells/web/src/bridge/hook-worker.ts` reduces an untrusted mount's host proxy to the capabilities the manifest declared, and it omits a namespace entirely rather than exposing a stub that rejects after sensitive arguments have crossed the channel. Its own comment states the rule as absence being the policy. `engine/src/hook-worker-core.ts` carries the wire protocol both worker executors share, and it lists `beforeExport`, `afterExport` and `exportStill` as the hooks that stay in the realm. `shells/web/src/bridge/hook-worker.ts` states the reason: they receive a live DOM element, which cannot cross the worker boundary.

Which tools run isolated is decided from evidence rather than from an author's assertion. `scripts/tool-isolation.ts` sets the manifest `isolate` flag only when a static read finds no realm-bound global and a render at defaults is byte-identical in-realm and in a worker. The model keeps that arrangement (R3). A manifest hint is a request. The receiving shell assigns trust independently, and `schemas/tool.schema.json` already says so.

## Open points

- **Q5, who may publish a suite under the `lolly` namespace, and how a local or Work suite is named.** Default from plan section 16: `lolly/*` suites are published from this repository's CI only. A Work instance publishes under `work:<instance>/*` and a local person publishes under `local/*`. An acceptance record always says which suite it judged. The same namespace discipline governs an extension namespace, which is why the question touches this chapter as well as [conformance and fidelity](conformance.html). Evidence that would change it: a partner programme that needs a shared namespace.
- **Q6, whether the specification is public from the first draft.** Default from plan section 16: yes, published under `/info/` with the draft notice at the head of every chapter. An open extension model that independent publishers are invited into is hard to review in private. Evidence that would change it: a reason to keep the drafts private until the pilots pass. The [status chapter](status.html) records the answer.

Q1 is carried in [conformance and fidelity](conformance.html). Q2 and Q3 are carried in [constraints, authority and local choice](policy.html). Q4 is carried in [operations and outcomes](operations.html).

## Precedents

- `docs/constraints.md`
- `packages/core/src/extension-v1.ts`
- `packages/core/src/host-v1/apis.ts`
- `packages/core/src/host-v1/host.ts`
- `schemas/tool.schema.json`
- `engine/src/url-mode.ts`
- `engine/src/loader.ts`
- `engine/src/semver-range.ts`
- `engine/src/runtime.ts`
- `engine/src/hook-worker-core.ts`
- `scripts/tool-requires.ts`
- `scripts/tool-isolation.ts`
- `shells/web/src/bridge/hook-worker.ts`
- `shells/web/src/bridge/video-shared.ts`
- `shells/web/src/lib/video-jobs.ts`
- `shells/web/src/lib/studio3d/source.ts`
- `shells/web/src/lib/lolly-pack.ts`
- `packages/node-shell/src/lolly-file.ts`
- `lolly-work/server/src/render/capabilities.ts`
- `lolly-work/engine-pin.json`

# Packaging, identity and migration

> This chapter is a draft for review, dated 2026-09-24.  

This chapter specifies how a document is serialised, how a package carries parts its reader does not understand, how identity holds across a version change and how a migration is written. It restates plan section 13 with the review correction C7, plus the two packaging findings F10 and F11.

The records themselves are named in [records and identity](records.html). The rows inside an instance are in [source rows, payloads and patches](source-and-patches.html). The four gates a reader applies are in [capabilities, trust and extensions](extensions.html), and this chapter does not repeat them.

## Two serialised forms

The model keeps two serialised forms and neither replaces the other.

| | `.lolly.json` | `.lolly` |
|---|---|---|
| Status | Proposed. No such file is written today | Built (`shells/web/src/lib/lolly-pack.ts`, `packages/node-shell/src/lolly-file.ts`) |
| Purpose | The plain source form, preferred for diff and review | The container for a document and its packaged dependencies |
| Carries | The records, each with its own `schemaVersion` | The same records as parts, plus the assets, fonts and tools they need |
| Gates | The record versions | `minReader` over the whole container, plus the record versions |
| Integrity | Whatever the transport provides | The SRI map over every payload part |

Both forms must carry a `schemaVersion` per record (R11). The container must keep `minReader` and the integrity map it has today (R11). A plain source form for review is a proposal in this wave, and nothing is added to `packages/core` or `schemas/` here.

## Parts under the integrity map

Extension content and opaque imports must travel as parts under the integrity map, and never as manifest keys (R10, F11).

The reason is in the writer. `buildLollyFile` in `shells/web/src/lib/lolly-pack.ts` constructs the manifest from typed input on every save, so a manifest member the writer does not know is dropped at the next save. A part named in the integrity map travels inside the file and is verified on read. Carrying it across the next save is what the retained package state below has to add, because the writer builds its zip entries from typed input too and `buildIntegrity` runs over exactly that set (`shells/web/src/lib/lolly-pack.ts`). Four payload parts already travel this way: `design-system.json`, `templates.json`, `CREDITS.txt` and `renovation/*.json`.

- A part must occupy one declared path (`shells/web/src/lib/lolly-pack.ts`).
- A reader must compare the manifest's declared path against the single path the writer would have used (`shells/web/src/lib/lolly-pack.ts`, `renovationPartPath`).
- Every retained part must be listed in the integrity map (C7). `verifyIntegrity` in `shells/web/src/lib/bundle.ts` iterates the map rather than the archive, so a part present in the zip but absent from the map is never checked.
- A reader must refuse a part that the map does not cover. The tool bundle already does this: a `tool/` path with no integrity entry, or a `tool/` path the inventory does not name, fails the read (`shells/web/src/lib/lolly-pack.ts`).
- The map is built over the payload parts before the manifest and the README are added, so it covers the payload and never itself (`shells/web/src/lib/lolly-pack.ts`).
- Integrity must be a gate rather than a report. A file whose parts do not match its map must be refused, not passed on with a warning (`packages/node-shell/src/lolly-file.ts`). The gate is as wide as the map and no wider. Both readers return early when a manifest carries no `integrity` at all, so that a file written before the map existed still opens (`verifyParts` in `packages/node-shell/src/lolly-file.ts`, `verifyIntegrity` in `shells/web/src/lib/bundle.ts`). Only the tool, project and renovation payloads refuse a missing map. The model has to choose between two answers and say which. It can refuse a map-less container once preservation-capable writers ship, or it can keep reading one and record the read as unverified.

## The retained package state

Preserving unknown fields at parse time is not a round trip (C7). `LollyFileContents.files` hands the caller every parsed part, while the builder takes typed input and rebuilds the manifest from it (`shells/web/src/lib/lolly-pack.ts`). Nothing joins the read to the next write.

A retained package state must join them (C7). It is a proposal in this wave. The [proof cases](proof-cases.html) carry its evidence as the preservation round trip: import a future optional extension, save, restart, make an unrelated permitted edit, export and compare the opaque bytes.

- The retained state must survive import, local persistence, restart, edit and save (C7).
- It must record which parts are known and which are opaque (C7).
- It must record who owns each part, so two readers cannot both claim one path (C7).
- Two owners must never hold one path (C7). A second claim on a path must be refused when the package is read, and both claimants must be recorded in the retained state rather than one silently winning. The tool inventory already refuses a repeated declared path, and the renovation reader refuses a part at a path the writer would not have used (`shells/web/src/lib/lolly-pack.ts`, `readLollyFile` and `renovationShapeProblem`).
- It must state what the integrity map covers for this package (C7).
- It must state when an unreferenced opaque part is collected (C7).

draft shape

```text
    RetainedPackageStateV1
      parts[]         { path, owner: reader | extension | opaque, integrity, seenAtRevision }
      manifestExtras  the manifest members this reader did not interpret, kept verbatim
      collisions[]    { path, claimant, resolution } for a path claimed twice
      released[]      { path, revision } for each opaque part collected, and when
```

An opaque part may be collected only when no retained record references it and the release is recorded (C7). A host must never treat its own inability to read a part as grounds for collecting it (R10). [Capabilities, trust and extensions](extensions.html) states what else a host owes an extension it lacks.

## Envelope fields: recomputed and carried verbatim

The writer recomputes part of the manifest on every save. A reader must never treat a recomputed field as carried content (C7).

| Envelope field | On every save | Why |
|---|---|---|
| `counts` | Recomputed | Derived from the assets this save actually wrote |
| `exportedAt` | Recomputed | The wall clock of this save, one of the four clocks named in [values and time](values-and-time.html) |
| `integrity` | Recomputed | Built over the payload parts of this save |
| `app` | Recomputed | The application that wrote this save |
| `engineVersion`, `fonts` | Supplied fresh by the writing app | They describe this save's engine and the faces this render resolved, the way `app` does |
| `format`, `formatVersion`, `minReader` | Recomputed | The writer sets the gate from the kind and the parts present |
| Everything else | Carried verbatim | The writer did not compute it, so the sender's value is the only copy (C7) |

Everything the table does not name must be carried verbatim through the retained package state, and not only through the parsed object (C7). That includes the members a reader did not interpret.

`assets` is the mixed case and needs its own rule. Each row keeps the sender's identity, and a row that carries bytes takes its `checksum` from the integrity map of this save. A by-reference row has no path and no checksum to take (`shells/web/src/lib/lolly-pack.ts`). A row must keep its `kind`, its id and its source across a save, and where it carries bytes the checksum must be recomputed with the map (C7).

## Revision-scoped receipts

A receipt, a signature or an acceptance must describe exactly the revision it was made against (C7). When that revision changes, the record must stay as history, and it must never describe the new revision as previously accepted (C7). What a revision covers is in [source rows, payloads and patches](source-and-patches.html), and the receipt itself is in [evaluation and receipts](evaluation.html).

The container already carries evaluation facts beside the payload: `engineVersion`, `app`, `exportedAt` and the fonts by sha256 (`shells/web/src/lib/lolly-pack.ts`). All four describe the save that wrote them, because the writer takes each one from this save rather than from the file it read. A retained receipt must never be re-dated the same way. A save that carries a receipt forward must carry it as a record of an earlier revision (C7).

A font digest is taken over the whole source font file and never over the subset an export embedded (`packages/node-shell/src/lolly-file.ts`). A receipt that identifies a font by digest therefore points at the file a reader could fetch again.

## Migration as a versioned transform

Every migration must be a versioned transform that returns findings, and no import may rewrite content invisibly (R11). The findings half has no working precedent in this tree, which the paragraph below states. The version rules are OpenTimelineIO's, adopted as written (R11).

- Each record must carry its own `schemaVersion` (R11). The shipped stamps already play that role under another name: the session record's `formatVersion` (`engine/src/session-record.ts`) and the container's `formatVersion` (`shells/web/src/lib/lolly-pack.ts`). `schemaVersion` is the model's name for that role, not a second field beside them, and no record gains a version stamp in this wave.
- A reader must upgrade on read (R11).
- A writer may downgrade on write, and the downgrade path must have no gaps (R11).
- A field added with a correct default needs no migration function (R11).
- A container must carry `minReader`, and a definition must carry `engineVersion` (R11).

`migrateSessionRecord` in `engine/src/session-record.ts` is the working precedent for the read side. The record layout is at format 4. A record with no `formatVersion` is treated as version 0. The four steps so far are additive, so each is a no-op on the data, and the branch exists for the first step that is not. A record written by a newer application is read as it stands and reported, rather than discarded, because losing a saved session is worse than reading it optimistically.

Two limits of that precedent are stated rather than hidden. It is a read-side migration only, so the optional downgrade path R11 allows has no implementation yet. It returns no findings, so an import cannot yet report what a migration changed (plan section 13).

## Old-client gates before new parts

A preservation-capable reader and writer must ship before any new part is emitted (R11, C7). The emission must sit behind an effective reader gate for earlier clients (C7). An additive field is never safe merely because an old reader ignores it (C7).

The test is what an older reader would do with the file, not what it would miss.

| Part | Gate today | Why |
|---|---|---|
| `design-system.json`, `templates.json`, `CREDITS.txt` | Additive at reader 1 | A reader that predates the part never looks for it, and the file still opens as the session in `session.json` |
| `tool/*` bundle | `minReader: 2` | A file of the tool kind carries no `session.json`, so an earlier reader has no document to open |
| `sessions/<key>.json` project tree | `minReader: 3` | An earlier reader would open the first session and drop the rest |
| `renovation/*.json` | `minReader: 4` | A renovation-only file carries no `session.json`, so an earlier reader opens an empty document under the renovation's tool id and silently drops the project |

Each gate is in `shells/web/src/lib/lolly-pack.ts`. A gate must fail closed on a missing field, because an absent number is not a pass (C7, R11). One of the four does this today: the renovation check rejects a manifest whose `minReader` is absent as well as one whose value is too low. The project and tool checks compare with `<` against a possibly absent field, where `undefined < 3` is false, so a manifest with the field deleted passes them. That is the work the rule names, not a description of the file.

## Stable ids, revisions and digests

A stable id must never be renamed to express a version (invariant 7). Resource identity must be a stable logical id plus an immutable revision or digest for one evaluation (invariant 7). A tool id and an asset id are permanent contracts (`docs/glossary.md`).

`installDesignRevision` in `shells/web/src/lib/installed-tools.ts` shows the rule at work. The artifact digest is a SHA-256 over the sorted list of path and file-hash pairs, so it covers the whole file set, paths included, rather than the version string a designer typed. Files are staged under `/tools/<id>/.revisions/<digest>/` before the discovery pointer is published. The same version with different bytes is refused, and the message asks the designer for a new revision.

Inside a container an asset row is either an `asset` that carries bytes at a path or an `asset-ref` the recipient resolves locally (`shells/web/src/lib/lolly-pack.ts`). Both name the sender's base asset id, and import matches that id to a receiver-local one. A reader must never treat the path as the identity (invariant 7, `shells/web/src/lib/lolly-pack.ts`).

## The source-aware diff

A semantic diff must run over the authored rows with stable-id alignment, and it must define its input and result semantics (R1). It must never take identity from a lowered render (R1, `engine/src/document-api.ts`).

`diffDocuments` in `engine/src/document-api.ts` compares boxes today by matching `id=` attributes with a regular expression over the hydrated markup. That markup is produced after the runtime has run, so a lowered graph is acting as a second source of authored content (F10). The source rule forbids it (R1), and [records and identity](records.html) points the replacement at this chapter.

The alignment already exists. `compareStructure` with `arrayAlignment: 'id'` in `engine/src/compare-structure.ts` aligns two arrays of rows by `id` and keeps the longest common order, so inserting one row reports one insertion rather than moving every row after it. `compareSources` reports that alignment as `stable-id` (`engine/src/compare.ts`).

Three rules follow.

- A comparison that ran past its budget or was cut short must never read as equal. `compareSources` returns `undetermined` when the result is partial (`engine/src/compare.ts`), and the diff path must keep that answer distinct from equality (C8).
- The compiled path must stay available for template tools and for compiled documents with no source rows, and it must be named as compiled (R1, `engine/src/document-api.ts`). `compileDocument` builds its values after the runtime has run, so repointing a diff at those values does not by itself produce an authored snapshot (`engine/src/document-api.ts`).
- URL state must be decoded through the manifest before comparison, including the compact `z` token, and must never be compared byte for byte (R1, `engine/src/url-mode.ts`). Today the string path of `diffDocuments` compares raw query parameters. `z` is raw DEFLATE over the whole readable query, expanded by `expandQuery` at the load boundary (`engine/src/url-pack.ts`, `engine/src/url-mode.ts`), so one field edit changes the entire token and a byte comparison reports the whole document as changed.

The input and the result are stated here. The input is the instance's authored rows. Each blocks input's row list is taken after URL state has been expanded through the manifest, with every typed payload record resolved by its id. A compiled document is never the input. The result is given per blocks input: the added, removed and changed row ids under `arrayAlignment: 'id'`, a separate reorder set for a row whose order key alone moved, a per-field change list for a changed row and one atomic change for a payload record. The whole comparison also carries one answer, drawn from `identical-bytes`, `equivalent-content`, `different` and `undetermined` (`engine/src/compare.ts`).

## The frozen wire order

The field order of every blocks input is a permanent wire contract and must stay append-only (`schemas/blocks-wire-order.json`). The compact blocks URL form is positional, so reordering, renaming or removing a field scrambles every link already shared. `validate:catalog` fails any change that is not a pure append, and it fails an append too until the pin file ratchets forward in the same change (`scripts/validate-catalog.ts`).

A typed payload record referenced from the row is the migration that respects that order (plan section 13, R1). The pointer field is appended at the end of the input's field list, beside the opaque string it will replace, and never in place of it.

- An appended pointer field must never displace an existing field (`schemas/blocks-wire-order.json`).
- The opaque string must keep resolving for as long as the wire order carries it, so a link shared before the migration still opens (`schemas/blocks-wire-order.json`).
- A reader that finds both must state which one it read, and a writer must state which one it wrote, as a migration finding (R11, plus the findings rule this chapter's migration section states).

The `3d` layer is the open case. The `scene` field on `design:boxes` carries the 3D Studio's own settings as a link query inside a 2D row (`community/design/tool.json`). That is a coordinate domain carried as text, and [source rows, payloads and patches](source-and-patches.html) requires a typed payload record instead. The same input shows the append pattern already: `textStory` and `textFrame` were added after `scene` and `animationEdits` rather than replacing anything (`schemas/blocks-wire-order.json`). The [proof cases](proof-cases.html) carry the migration as a case to be built.

## Independent versions, one pin

`HostV1`, the document model, the conformance suites and the package reader must be versioned independently, with their relationships documented (F15, plan section 13, adoption sequence 6).

The practice exists. Lolly Work vendors engine 1.199.0 against this tree and verifies the vendored content by hash in `engine-pin.json`, a file in that separate repository. The pin carries a version and a content hash per package, plus a hash per schema file.

The document model version and the suite versions must be recorded in the same pin file (`engine-pin.json`, in the Lolly Work repository). That addition is a proposal in this wave, and [conformance and fidelity](conformance.html) owns what a suite version means.

## Current contracts

Plan section 13 lists the contracts a migration has to account for. Each row is a concern to answer, not a change already made.

| Existing contract | Migration concern |
|---|---|
| `tool.json`, templates and hook lifecycles | Map typed inputs and operations; protocol contracts over the existing hooks; no promise of arbitrary row editing for template tools |
| Design-generated tools | Reuse the compiler, the rules, strict execution and immutable revisions; `DesignToolDraftV1` becomes the component v0 |
| Saved Design `boxes` and its SDK read model | The read model stays a read model; the source adapter is the row list itself |
| `ChartSpecV1` | Preserved through nesting and lowering, as the `chart` layer kind |
| `compileDocument` and `CompiledDocument` | Kept as the transient compile result; the diff repointed at the source rows |
| `.lolly` session, tool, project and renovation payloads | Identity, inventory, integrity and reader gates preserved; the parts pattern for extensions |
| Rebrand stages and run records | Source, plan, compiled output, review and partial execution stay distinct records; their outcome vocabulary merges into one |
| Shell routes that act as tools | Unpack, Prepare, Batch, Verify and the Rebrand review get operation adapters (R13) |
| `packages/core/src/extension-v1.ts` | The chrome slot contract. Its namespace and channel model are reused for document extensions; its runtime is not |

The outcome vocabulary those run records merge into is in [operations and outcomes](operations.html). The five route adapters are in the same chapter.

## Open points

- **Q2, granularity and persistence of local rejection.** Default: per rule, instance-scoped, recorded on the session record with a fingerprint of the facts the decision was made about (R6). A derived tool revision is a Design tool export and is not part of 0.1. It touches this chapter because the session record travels inside a container verbatim, so a recorded waiver is carried with the instance and must be scoped to the revision it names (C7). [Constraints, authority and local choice](policy.html) owns the decision. Evidence that would change it: a need to share a rejected state as a reusable tool.
- **Q6, whether the specification is public from the first draft.** Default: yes. The chapters publish to `/info/build/`, since the constraints, determinism and reproducibility pages already make public promises this model has to keep. The draft notice stays at the head of every chapter until the status chapter says otherwise. The [status chapter](status.html) records the answer.

The other open questions do not touch this chapter.

## Precedents

In this repository:

- `shells/web/src/lib/lolly-pack.ts`
- `shells/web/src/lib/bundle.ts`
- `shells/web/src/lib/installed-tools.ts`
- `packages/node-shell/src/lolly-file.ts`
- `packages/node-shell/src/rebrand-run-manifest.ts`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/chart-v1.ts`
- `packages/core/src/extension-v1.ts`
- `engine/src/session-record.ts`
- `engine/src/document-api.ts`
- `engine/src/compare.ts`
- `engine/src/compare-structure.ts`
- `engine/src/url-mode.ts`
- `engine/src/url-pack.ts`
- `schemas/blocks-wire-order.json`
- `schemas/tool.schema.json`
- `community/design/tool.json`
- `docs/glossary.md`

In Lolly Work, which is a separate repository and not part of this tree:

- `engine-pin.json`

# Proof cases

> This chapter is a draft for review, dated 2026-09-24.  

This chapter lists the cases the document model must be proved against. Each case is a paper example now and an executable fixture later (plan section 17). None of them is built.

The order of work matters. The plan's section 18 places these cases before any shared type is frozen, so a case that fails is a reason to change the model rather than the fixture. No type is added to `packages/core` and no schema to `schemas/` in this wave.

What a fixture must state is specified in [Conformance and fidelity](conformance.html): its inputs, its immutable dependencies, the reference environment, the features it claims, the expected artifact or typed result and the comparison rules. This chapter states the cases and what each one must show.

## How to read a case

Each case carries two parts. The setup lists what is involved: the records, the operation and, where it decides the answer, the execution class. The expected outcome is what must hold for the case to pass.

None of the 25 has a fixture yet, so the status of every case below is the same until one is built.

A case states the resolution or confirmed decision it tests. Plan section 17 lists the cases and their required evidence; the ids in this chapter are this chapter's own mapping of each case onto the decisions in plan section 2 and the corrections in section 0.1.

A case must never be reworded to match an implementation that failed it. That is why the plan's section 18 places these fixtures before any shared type is frozen: a failure is evidence about the model, not about the fixture.

Twelve cases come from the three pilots and the utilities. Eleven more were added by review: four after the first round and seven from the final review's findings C1 to C7. Two more, 24 and 25, were added on 2026-09-26 for invariants that had no case of their own. The thirteen added cases are grouped below under the resolution or decision each one tests.

## Pilot and utility cases

**1. Constrained Design, two people.** One authored Design tool is opened twice, once in governed execution and once unmanaged (D12).
Expected outcome: governed execution must enforce the mandatory rules, and the unmanaged person may reject a waivable creative rule without mutating the original definition and without keeping a false approval claim (R6, `packages/core/src/design-tool-v1.ts`).

**2. Chart inside Design and inside another tool.** One chart definition is placed in a Design document and in a second tool, each with its own dataset.
Expected outcome: typed data bindings, public overrides, brand rules and accessibility metadata must survive composition as a `chart` layer, and the two instances must keep independent identity and state (R14, `packages/core/src/chart-v1.ts`).

**3. Design across print, raster and motion.** One authored Design document is rendered to a print target, a raster target and a motion target.
Expected outcome: one authored contract must serve the three output targets with explicit units, resources, timing and stated differences between the outputs (D2, `engine/src/units.ts`).

**4. Agenda in interactive web, slides and motion.** One schedule is published as an interactive page, as slides and as a video.
Expected outcome: the three outputs must preserve the event, presentation and reference clocks, the presentation state and its transitions, the reading order and the declared differences between them (D2, R15, `docs/agenda.md`).

**5. Unpack without a canvas.** The Unpack route takes a design file apart through an operation adapter, and that route has no manifest (R13, `shells/web/src/views/pdf-extract.ts`).
Expected outcome: inspection must stay passive and extraction must return a typed result with its effects declared (R5). A cancellation and a failed item must each have a stated outcome (R13).

**6. Rebrand with partial completion.** A reviewed plan is compiled over a deck and the run stops partway.
Expected outcome: intentional changes and protected content must stay distinguishable, and review, retries and output ownership must stay explicit under `OutcomeV1` (R5, `packages/node-shell/src/rebrand-run-manifest.ts`).

**7. Non-recorded live operation.** A tool that reads live input runs with capture off.
Expected outcome: the operation must succeed without retaining raw input, replay must not be offered and the immediate outcome must be useful on its own (D11, R15).

**8. Unknown extension.** A document carrying an extension this host does not implement is opened.
Expected outcome: safe inspection and byte preservation must work, and an edit that touches the extension's declared dependency scope must be refused or offered as an explicit lossy copy before any data changes (R10, [Capabilities, trust and extensions](extensions.html)).

**9. Flattened chart with matching pixels.** A chart nested through `composes` is compared with the same chart as a native layer.
Expected outcome: the appearance check must pass while the editable round trip must fail, because the nested render returns an image asset and not a chart (R14, `packages/core/src/host-v1/compose.ts`).

**10. Motion with a brief defect.** A short animation carries a defect between two sampled progress points.
Expected outcome: the timing, transition and coverage checks must expose the defect that percentage samples alone miss (D9, R12).

**11. Missing or conflicting requirements.** One run is given a missing font, an unsupported capability and two conflicting required rules.
Expected outcome: each must fail at the boundary that owns it, with no silent substitution and no committed effect. The missing font must fail at the resource boundary (R15). The unsupported capability must fail at the requirement gate, scoped to the affected operation (R11). The two conflicting required rules must stop the affected operation at the policy boundary, reported with both ids (R6, `engine/src/runtime.ts`).

**12. Legacy compatibility.** Representative existing tools run unchanged through an adapter.
Expected outcome: their behaviour must be preserved, and the adapter must report its trust, editing and repeatability limits rather than inherit a strict claim (R3, `engine/src/hook-worker-core.ts`).

## Cases added by the review

### Typed payloads and one authoritative source (R1, R14, C1)

**13. Typed payload round trip.** A nested chart is serialised, reopened, copied, patched, synchronised and exported.
Expected outcome: datasets, encodings and accessibility metadata must survive every step, two concurrent edits to one payload must produce the documented outcome and no step may turn the chart into a string or a flattened image (R14, C1). The collaboration row value is a string, a number, a boolean or null, so a chart must travel as its own payload record (`packages/core/src/canvas-op-v1.ts`, `schemas/canvas-op.schema.json`). The mechanism is specified in [Source rows, payloads and patches](source-and-patches.html).

**14. Second coordinate domain.** Design's `3d` layer carries its scene as a URL-encoded string inside a scalar field today (`community/design/tool.json`).
Expected outcome: that scene must become a typed payload record referenced by the row, without breaking the frozen blocks wire order and without breaking an already shared link (R1, `schemas/blocks-wire-order.json`).

### Concurrent merges (R1, C2)

**15. Concurrent reparent.** Five merges: Alice reparents A into B while Bob reparents B into A, a delete against a reparent, two concurrent atomic choices, an envelope whose final operation is invalid and two patches against one mutable head (C2).
Expected outcome: each merge must be run in both delivery orders and across a reconnect and a checkpoint, and each invalid result must block evaluation with both edits kept for repair (R1). The reference model converges per field and produces the cycle in either order, so validation must run after convergence rather than on each patch alone (`packages/core/src/canvas-op-v1.ts`). The MCP path transforms supplied inputs and holds no head, so its outcome must record that no base revision was checked (`services/mcp/src/tools.ts`).

### Outcomes that are not one file (R5, C3)

**16. Outcomes beyond one file.** Five runs are compared (C3): a zero-artifact inspection, a batch cancelled after two of three writes, a failure after a committed write, an unknown completion after a lost acknowledgement and a Rebrand plan that is `ready` before any export.
Expected outcome: the inspection must succeed with a typed result and no artifact (R5). The cancelled batch must keep both committed files (R5). The failure after a committed write must keep that artifact under `artifacts[]` (R5). The unknown completion must be reported as unknown and never as failed (R5). The Rebrand plan that is `ready` must read as a lifecycle state and never as a succeeded export (R5). Each must map onto the persisted legacy vocabularies without changing them: the single-file invariant stays inside its adapter (`packages/core/src/file-operation-v1.ts`), and `pending` and `ready` stay run lifecycle states beside the outcome (`packages/node-shell/src/rebrand-run-manifest.ts`). [Operations and outcomes](operations.html) specifies the split.

**17. Fan-out export.** A three-artboard document is exported as stills.
Expected outcome: one outcome must carry three artifacts with stable per-artifact ids (R5, plan section 7.2). Each artifact must be addressed by the reserved `s` state selector and never by an index into the artifact list. The selector carries a frame id where the document stamps one and a one-based position in presentation order where it does not (`engine/src/url-mode.ts`, `engine/src/frame-address.ts`).

### Local choice and the offline governed client (R6, R7, C4, C5)

**18. Local waiver.** An authored approved-font rule is rejected locally, the same rejection is then attempted under a Lolly Work overlay that forbids it and then a fingerprinted fact changes (C4).
Expected outcome: the unmanaged operation must complete with an explicit exception, the governed attempt must be blocked and the changed fact must require a fresh decision (R6). Measured conformance to the unmodified rule must read failed or excepted and never passed (R6, C4). The recorded-decision pattern is the session record's rights decisions, keyed by work and kind with a fingerprint (`engine/src/session-record.ts`). The web view, the CLI and the MCP validation projection must agree on all three results (C4). [Constraints, authority and local choice](policy.html) specifies the resolution order.

**19. Offline governed client.** A governed client loses its connection and the same authored document is opened 25 hours later (C5).
Expected outcome: the document must open and stay usable, a governed claim must follow the explicit freshness rule and the receipt must name the policy version it evaluated rather than claim current online approval (R7). A host that cannot be reached answers `unreachable`, and a stale mark must stay reserved for an update that could not be brought down (`shells/web/src/lib/design-system/hosted.ts`). Leave must keep the person's sessions and perform its documented cleanup, and an editable copy must be a separate action with known dependencies and no inherited governance claim (R7, `shells/web/src/lib/instance-leave.ts`).

### Effect authority (R3, C6)

**20. Effect authority.** Four attempts: a bracketed member access and a local alias against a strict grant, a write through a hook during passive inspection, a hook that resolves after cancellation and a trusted compatibility run (C6).
Expected outcome: an alias and a dynamic access must not bypass a strict grant (R3). Passive inspection must not write through a hook (R3). A hook that resolves after cancellation must not commit a new strict effect (R3). A trusted run must never receive a strict claim (R3). The generated inventory is a pattern over `host.<api>` and misses both alias forms, so it is lint and never authority (`scripts/tool-requires.ts`). A hook that overran its budget keeps running in the realm, which is why a grant binds to the operation, the phase and the attempt (`engine/src/runtime.ts`). [Values, expressions, code and time](values-and-time.html) specifies the execution classes.

### Preservation across save and restart (R10, R11, C7)

**21. Preservation round trip.** A future optional extension is imported, saved locally, restarted, edited in an unrelated permitted way and exported (C7).
Expected outcome: the opaque bytes must compare equal after that round trip (R10). A touch on a protected dependency must be refused or routed through an explicit lossy copy (R10). An older client must refuse by its reader gate (R11). A stale receipt must never describe the new revision as accepted (R11). Additive parts already travel under the integrity map while the manifest is rebuilt from typed fields on every save, so a retained package state must connect those two boundaries (`shells/web/src/lib/lolly-pack.ts`). [Packaging, identity and migration](packaging.html) specifies that state.

### Fidelity evidence (R9, R15, C8)

**22. Vanished caption.** A render loses its caption, and no whole-image measurement separates it from a harmless half-pixel shift or a resample: the three pairs sit within 0.2 points of each other on changed pixels and on SSIM (C8, `plans/276-document-model-evidence/README.md`).
Expected outcome: the three checks must read the produced artifact or an independently extracted representation of it, never the authored source (R9, C8). They must fail that render. An unchanged result must pass. An accepted shift must pass only inside a calibrated band. Text with similar ink but the wrong content must fail (R9). An incomplete extraction or an undetermined comparison must never count as a pass (`engine/src/compare.ts`). PNG alpha, PDF page boxes and colour intent must each have their own checks, and a white RGB fixture must never be generalised to CMYK print conformance (C8). The measurements that motivated this case are in [Conformance and fidelity](conformance.html), and this fixture must replace them before any number enters a suite (C8).

**23. Live-text fallback.** A run whose text stayed a live `<text>` element instead of an outline is exported.
Expected outcome: the receipt must record that fallback per run, with the resolved font files by digest, the shaping engine version and the emoji set and treatment (R15). The run must not claim font-independent fidelity (R15). The command line already warns on an unresolvable font or a baseline shift it cannot place exactly, and a strict run promotes that warning to a refusal (`shells/cli/src/svg-outline.ts`). Today that fact reaches standard error, the `--json` envelope's `warnings` list and, under `--strict`, the exit code, but never a receipt (`shells/cli/src/output.ts`, `shells/cli/src/envelope.ts`). The receipt is the gap this case closes.

### Invariants with no case of their own (D1, D11, R5, R15)

Two cases were added when the table in the [constitution](constitution.html) mapped each invariant to its cases. No case tested invariant 11 on its own, and only case 7, the live-input half of Q4, stood behind invariant 5.

**24. Accessible intent across outputs.** One chart whose accessibility metadata carries a title, a description, a reading order and a data table is exported to SVG, PDF, PNG and MP4 (`packages/core/src/chart-v1.ts`).
Expected outcome: each output must carry the parts its format has a place for, and each must be checked by a check of its own that reads the exported file, never the screen render or the spec (D1, C8). An output with no place for the reading order or the table must report that loss as a finding in the one finding shape, never drop it silently (R5, `packages/core/src/file-v1.ts`). A declared motion description must reach the motion output or be reported missing. No output may be recorded as accessible because another output was. Today the chart template writes the title and description into the SVG as `<title>`, `<desc>` and an `aria-label` (`community/chart/template.html`). A test pins that template source (`tests/chart-spec.test.ts`), and nothing reads the metadata back from an exported file.

**25. Local utility with no record.** The on-device `strip-data` tool removes metadata from a person's file under Q4's default for a utility: capture off, retention none and replay semantic from pinned inputs (`community/strip-data/tool.json`).
Expected outcome: the operation must succeed with a typed outcome and no receipt, and an absent receipt must read as "no receipt was retained", never as "not evaluated" (D11, R15). The output must carry no provenance and no watermark. The host must keep neither the input file nor a record of the run once it ends, and the run must declare that it kept none rather than leave it to be inferred. A second run on the same input file, held by the person and not by the host, must compare as `equivalent-content` or `identical-bytes` with the first (`engine/src/compare.ts`). Today a picked file never reaches a link (`engine/src/url-mode.ts`) and the output carries no stamp (`engine/src/runtime.ts`), but no declaration says what a run captured or kept, so neither fact can be checked from outside the run.

## What must never pass

For every admitted visual suite, schema validity alone must never pass the gate (D3, plan section 17). For an action-only operation, a fabricated preview must never stand in for the real outcome (D11, plan section 17).

Two further limits follow from the same rule. A skipped test must never be recorded as a pass (R8). A comparison that was cut short by its budget must never be recorded as a pass, and it must stay distinguishable from a harmless truncation of displayed detail (R9, `engine/src/compare-budget.ts`).

This chapter makes no conformance or reproducibility claim beyond the ones `docs/determinism.md` already makes, and it states no threshold.

Spatial and fabrication examples stay paper probes in this wave (D5). They test whether an extension boundary holds, and they are not a commitment to build those suites.

## The five that come first

The plan's section 18 names five of these cases as the ones that run before any shared type is frozen, because each one carries contract semantics that a type would otherwise settle by accident.

| Order | Case | What a failure would change |
|---|---|---|
| 1 | 13, typed payload round trip | Where a structured value lives and how it synchronises (R1, R14) |
| 2 | 15, concurrent reparent | What a revision covers and where a merge is validated (R1) |
| 3 | 16, outcomes beyond one file | Whether a typed result, the committed artifacts and a termination stay separate (R5) |
| 4 | 18, local waiver | Whether a recorded local decision has any effect (R6, D7) |
| 5 | 21, preservation round trip | What is preserved across save, restart and an older client (R10, R11) |

The patch envelope and the source-aware diff land when their semantics are fixed in [Source rows, payloads and patches](source-and-patches.html) and [Packaging, identity and migration](packaging.html), not before (plan section 18). Cases 13, 15 and 16 are the evidence those semantics are settled.

## Open points

- **Q1. The first editable interchange routes per pilot.** Default from plan section 16: the four routes and three edits named in [Conformance and fidelity](conformance.html). No case in this chapter yet covers a route end to end: cases 2 and 9 test only the chart half of an editable round trip, so a route fixture is owed when Q1 is answered. Evidence that would change it: a customer route that outranks them.
- **Q2. The granularity and persistence of a local rejection.** Default from plan section 16: per rule, instance-scoped, recorded on the session record with a fingerprint, which is what case 18 asserts. A derived tool revision is a Design tool export and is not part of 0.1. Evidence that would change it: a need to share a rejected state as a reusable tool.
- **Q3. What freshness a governed-claim export needs, and who states it.** Default from plan section 16: freshness is a policy rule the instance issues with a validity interval and a stated behaviour when it cannot be established, which is what case 19 asserts. With no such rule the held policy is enforced, and the receipt records the policy version it evaluated and when it was last attested. Evidence that would change it: an organisation that needs a hard deadline, or a fully offline site.
- **Q4. What non-recordable means for a local utility and a live-input tool.** Default from plan section 16: for a utility, capture off, retention none and replay semantic from pinned inputs. For live input, capture unsupported unless the person turns it on, retention session and replay none. Case 25 tests the first and case 7 the second. Evidence that would change it: a regulated workflow that needs durable receipts for utilities.

The other open questions are settled in other chapters. Q5 is answered in [Conformance and fidelity](conformance.html). Q6 is answered in [Status and open points](status.html).

## Precedents

- `packages/core/src/canvas-op-v1.ts`
- `packages/core/src/chart-v1.ts`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/file-operation-v1.ts`
- `packages/core/src/host-v1/compose.ts`
- `packages/node-shell/src/rebrand-run-manifest.ts`
- `schemas/canvas-op.schema.json`
- `schemas/blocks-wire-order.json`
- `engine/src/runtime.ts`
- `engine/src/compare.ts`
- `engine/src/compare-budget.ts`
- `engine/src/frame-address.ts`
- `engine/src/session-record.ts`
- `engine/src/units.ts`
- `engine/src/hook-worker-core.ts`
- `engine/src/url-mode.ts`
- `services/mcp/src/tools.ts`
- `scripts/tool-requires.ts`
- `shells/cli/src/envelope.ts`
- `shells/cli/src/output.ts`
- `shells/cli/src/svg-outline.ts`
- `shells/web/src/lib/lolly-pack.ts`
- `shells/web/src/lib/design-system/hosted.ts`
- `shells/web/src/lib/instance-leave.ts`
- `shells/web/src/views/pdf-extract.ts`
- `community/design/tool.json`
- `docs/agenda.md`
- `docs/determinism.md`
- `packages/core/src/file-v1.ts`
- `community/chart/template.html`
- `community/strip-data/tool.json`
- `tests/chart-spec.test.ts`
- `plans/276-document-model-evidence/README.md`

# Status and open points

> This chapter is a draft for review, dated 2026-09-24.  

This is that status chapter. It records what is decided, what the final review corrected, what is still Andy's to answer, what is not built and the order the work starts in. The other eleven chapters state the model: [constitution](constitution.html), [records](records.html), [source rows and patches](source-and-patches.html), [operations](operations.html), [values and time](values-and-time.html), [evaluation](evaluation.html), [policy](policy.html), [conformance](conformance.html), [extensions](extensions.html), [packaging](packaging.html) and [proof cases](proof-cases.html).

## What this draft is

The specification is an integration document. It names four records, gives every contract already in the tree a role in one of them and adds only what is missing. A chapter must name an existing contract rather than fork it (R4). A chapter that describes something not yet built must state that in the same sentence (`plans/276-lolly-document-model.md` status line, `plans/276-execution.md` section 3).

No shared type is frozen before the counterexample fixtures pass (plan section 18). Nothing is added to `packages/core` or `schemas/` in this wave, and every proposed record appears as an indented draft shape instead (`plans/276-execution.md` section 3). A draft shape must never be read as a type, and no reader may treat a field name in one as stable.

This draft makes no conformance claim and states no threshold (C8), and it sets no latency target because the current evidence justifies none (plan section 18, review gate 5). It adds no reproducibility promise beyond the ones `docs/determinism.md` already makes. The probe numbers appear in one chapter, [conformance](conformance.html), as evidence that a single whole-image number cannot separate a lost caption from a half-pixel shift (C8).

The notice at the head of every chapter must stay until this chapter records that people have read the draft (`plans/276-execution.md` section 5). That review has not happened, so this chapter records no reading. The chapters that most need it are the constitution, the source rows and patches, the policy and the conformance chapters, because each fixes semantics the later contracts are built on (`plans/276-execution.md` section 5).

## Confirmed decisions

These come from Andy's recorded direction. They are settled and must not be reopened by a chapter (D1 to D13).

| ID | Decision | Chapter that carries it |
|---|---|---|
| D1 | The document represents a complete tool: its interface, behaviour, dependencies, permitted changes and operations. | [constitution](constitution.html) |
| D2 | The pilots are Design, Chart and Agenda: Design spans print, raster and motion, Chart tests nesting and typed data, Agenda spans interactive web, slides and motion. | [proof cases](proof-cases.html) |
| D3 | A claimed renderer suite must pass reference-render fixtures, because a valid JSON tree is not evidence of a render. | [conformance](conformance.html) |
| D4 | Typed declarative behaviour is preferred, with an imperative escape hatch that carries its own execution and trust contract. | [values and time](values-and-time.html) |
| D5 | Spatial and fabricated outputs are extensibility examples, never priorities or gates for 0.1. | [extensions](extensions.html) |
| D6 | Constraints may travel with a tool and may also be supplied by Lolly Work, and the two are modelled separately. | [policy](policy.html) |
| D7 | Outside a governed instance the local person may accept or reject a constraint, and the rejection has an explicit representation. | [policy](policy.html) |
| D8 | Still-image fidelity allows a very low pixel difference, difficult to notice without close inspection. | [conformance](conformance.html) |
| D9 | Motion must match time and look similar at corresponding percentage points, with extra coverage around transitions. | [conformance](conformance.html) |
| D10 | Independent authors may publish feature namespaces and conformance fixtures. | [extensions](extensions.html) |
| D11 | Some operations are non-recordable or non-replayable, and a utility may act on files without rendering anything. | [operations](operations.html) |
| D12 | Reference and acceptance authority is the Lolly Work administrator for a governed brand and the local person otherwise. | [policy](policy.html) |
| D13 | The model reads well to the Kubernetes, Penpot and Rive communities in their own idioms, as direction and not as instruction. | [records](records.html) |

D8 is restated by the comparator probe, not reopened. Andy's intent is no visible loss under close inspection, and the conformance chapter tests that intent with three declared checks instead of one number (D8, R9).

## Resolutions adopted from evidence

Each resolution is adopted unless Andy objects (R1 to R15). A chapter must cite the resolution id, or a repository path, beside the rule it states (`plans/276-execution.md` section 3).

| ID | Resolution | Chapter |
|---|---|---|
| R1 | The canonical source is flat rows keyed by permanent id, one owner pointer and one order key, with structured values in typed payload records and patches carrying a base revision. | [source rows and patches](source-and-patches.html) |
| R2 | Declarative logic is a closed JSON expression tree with a static cost estimate refused at authoring time and a runtime budget per evaluation, and `showIf` stays as its v0 subset. | [values and time](values-and-time.html) |
| R3 | Imperative logic stays in hooks, specified as eight protocol contracts with an authored effect envelope, per-attempt grants and an execution class that enforces them. | [values and time](values-and-time.html) |
| R4 | The four records map onto existing persisted forms, and the compile result is transient and never persisted as a document. | [records](records.html) |
| R5 | One outcome vocabulary with three separate parts: a typed result, a record of committed artifacts and a termination, with review states on a run lifecycle beside it. | [operations](operations.html) |
| R6 | Effective policy resolves authority first, then composes attributed layers, and a local waiver deactivates a named waivable creative rule only in unmanaged execution. | [policy](policy.html) |
| R7 | A governed client that loses its connection keeps enforcing the policy it holds, and refresh due, update known, policy validity and attestation stay four separate facts. | [policy](policy.html) |
| R8 | Measured conformance is a suite report with core and extended features, and contextual acceptance is a separate record naming the authority and each exception. | [conformance](conformance.html) |
| R9 | The still-image comparator is three checks declared per fixture: a whole-image fuzz band, region-scoped measures and semantic checks on the artifact. | [conformance](conformance.html) |
| R10 | Extensions live in a namespaced map with used and required lists, a version per extension and a complete dependency scope a host without it must protect. | [extensions](extensions.html) |
| R11 | Every record carries its own version, readers upgrade on read, a container carries a minimum reader and one internal evaluator normalises the four gates. | [packaging](packaging.html) |
| R12 | Native 0.1 is one still-image suite, `still-2d/1`, over PNG, SVG and PDF, with motion staged after the timeline contract and interactive output kept as an adapter. | [conformance](conformance.html) |
| R13 | Unpack, Prepare, Batch, Verify and the Rebrand review get operation adapters with typed inputs and outcomes, and none of them needs a canvas. | [operations](operations.html) |
| R14 | A chart inside Design is a layer kind whose row references a chart payload record by id, lowered by pure engine code and replaced as one atomic value. | [source rows and patches](source-and-patches.html) |
| R15 | An evaluation receipt records the definition and instance revisions, the resolved fonts by digest, the shaping engine, the emoji set, the clocks, the effective policy version and the execution class. | [evaluation](evaluation.html) |

## Corrections from the final review

Codex reviewed the finalised plan on 2026-09-24. Every finding was checked against the code before it changed the plan, and all of them held. A chapter must not restate a claim these corrections removed (C1 to C9); the Change column of plan section 0.1 records what each one removed.

| ID | Correction | Verified against |
|---|---|---|
| C1 | Typed values cannot travel in the scalar collaboration rows, so typed payload storage is specified apart from flat ownership. | `packages/core/src/canvas-op-v1.ts`, `schemas/canvas-op.schema.json` |
| C2 | Two valid reparents merge into an ownership cycle in either delivery order, and a revision precondition alone cannot prevent it. | `plans/276-document-model-evidence/crdt-cycle-probe.mjs` over `packages/core/src/canvas-op-v1.ts`, and `services/mcp/src/tools.ts` for the stateless edit path |
| C3 | The single-file rule does not generalise, because an inspection succeeds with no artifact and a cancelled batch keeps the files it wrote. | `packages/core/src/file-operation-v1.ts`, `packages/node-shell/src/rebrand-run-manifest.ts` |
| C4 | Placing a local decision below the authored required rules makes a local rejection ineffective, so authority resolves first. | Read of the authority rule in plan section 10.1 |
| C5 | Offline answers `unreachable` rather than `stale`, cached material keeps working, Leave removes rather than forks and "Make an editable copy" is a separate action. | `shells/web/src/lib/design-system/hosted.ts`, `shells/web/src/lib/instance-leave.ts` |
| C6 | A generated effect inventory is a regular-expression scan that an alias evades, so it is lint and never authority. | `scripts/tool-requires.ts`, `engine/src/runtime.ts` |
| C7 | Preserving unknown fields at parse time is not a round trip, because the writer rebuilds its manifest from typed input on every save. | `shells/web/src/lib/lolly-pack.ts` |
| C8 | The probe supports the direction and settles no method, and an incomplete comparison must never pass. | `engine/src/compare.ts` |
| C9 | The tool count is two numbers, not one: 81 raw directories with 79 distinct ids, and the tools a content profile resolves, which is 67 for `lolly-start` and 77 for `suse`. The evidence scripts were also made rerunnable from the repository, and the retained comparator fixture was regenerated from the measured transformation. | `packages/node-shell/src/content-roots.ts` |

Five compatibility gates came from the same review and are stated in the chapters they affect. The public `requires` field keeps its meaning. Diff gains a source-aware path. An unmet extension is refused at the operation boundary. Aggregate work is bounded, and not only expression size. Incremental edits are measured on a device before any responsiveness claim.

## Open questions

Six questions are Andy's. Each has a default so that drafting proceeds without an answer (plan section 16), and a chapter that depends on one states the question and its default in its Open points (`plans/276-execution.md` section 1A).

| ID | Question | Default | Evidence that would change it |
|---|---|---|---|
| Q1 | The first editable interchange routes per pilot. | The four routes and three edits in the conformance chapter. | A customer route that outranks them. |
| Q2 | The granularity and persistence of a local rejection. | Per rule, instance-scoped, recorded on the session record against a fingerprint of the facts. A derived tool revision is a Design tool export and is not part of 0.1 (R6). | A need to share a rejected state as a reusable tool. |
| Q3 | What freshness a governed-claim export needs, and who states it. | Freshness is a policy rule the instance issues, with a validity interval and a stated behaviour when it cannot be established. With no such rule the held policy is enforced, and the receipt says which policy version was evaluated and when it was last attested. Leave and "Make an editable copy" keep their current meanings (R7). | An organisation that needs a hard deadline, or a fully offline site. |
| Q4 | What non-recordable means for a local utility and for a live-input tool. | Utility: capture off, retention none, replay semantic from pinned inputs. Live input: capture unsupported unless the person turns it on, retention session, replay none. | A regulated workflow that needs durable receipts for utilities. |
| Q5 | Who may publish a suite under the `lolly` namespace, and how a local or Work suite is named. | Suites under `lolly/*` are published from this repository's continuous integration only, instances publish under `work:<instance>/*`, people publish under `local/*` and an acceptance record always states the suite it judged (R8). | A partner programme that needs a shared namespace. |
| Q6 | Whether the specification is public from the first draft. | Yes, because `docs/constraints.md`, `docs/determinism.md` and `docs/reproducibility.md` already make public promises this model keeps. | A reason to keep the drafts private until the pilots pass. |

## What is not built

Nothing in this specification is implemented. The list below is what a reader must not assume exists.

- No shared document type and no schema. `packages/core/src/document-v1.ts` and `schemas/document-v1.schema.json` do not exist.
- No shared outcome, receipt, report or acceptance type. `packages/core/src` holds no `OutcomeV1`, `EvaluationReceiptV1`, `ConformanceReportV1` or `AcceptanceV1` (R5, R8, R15).
- No expression, rule, timeline, dependency graph or accessibility finding type, and no `ComponentDefinitionV1`. Plans 190, 191, 192, 194, 195 and 196 define them and none is built. Plan 193 is partly built, and its `ChartSpecV1` is in `packages/core/src/chart-v1.ts` (plan section 0, F2). The component v0 does exist: `packages/core/src/design-tool-v1.ts` holds `DesignToolDraftV1`, and plan 190's type is specified as its generalisation (plan section 6.4).
- No conformance suite and no fixtures. `tests/conformance/` does not exist, and no suite version has been released (R8, R12).
- No three-check comparator. `engine/src/compare.ts` re-exports `compareStructure` and `comparisonBudget`, and its visual mode answers a preview-grade whole-image comparison. No band is calibrated (R9, C8).
- No chart layer kind. Composition still produces a flattened image, which the proof cases record as a failing case rather than the pilot (R14).
- No operation adapters for Unpack, Prepare, Batch, Verify and the Rebrand review (R13).
- No patch envelope and no source-aware diff. `engine/src/document-api.ts` still diffs hydrated HTML (R1).
- No device measurements. No latency target is justified by the current evidence, and none is stated (plan section 18).

## The order the work starts in

The work starts in this order, each step small.

1. The vocabulary and the record map as public prose, with authority, authored identity, typed payload storage, operation results and committed-effect semantics worked as concrete examples (R4). In progress: this specification and the summary page `docs/document-model.md` are that step, pending the read described above.
2. The counterexamples of the [proof cases](proof-cases.html) chapter as contract fixtures, covering the scalar row boundary, the concurrent cycle, partial cancellation, an effective local waiver and a save-and-restart preservation case (C1, C2, C3, C4, C7).
3. Only the smallest additive types and adapters those fixtures justify, starting with an outcome and a finding type mapped onto the three existing vocabularies (R5).
4. Package preservation and old-client gates, before any new native document is emitted (R10, R11).
5. The three-check comparator calibrated on positive and negative cases, before any suite pass is advertised (R9).
6. The chart layer kind and the five route adapters incrementally, with existing tools and local offline operation retained throughout (R13, R14).

Two rules bind that order. No shared type may be frozen before the fixtures in step 2 pass (C1, C2, C3, C4, C7). The patch envelope and the source-aware diff land only after the semantics in [source rows and patches](source-and-patches.html) and in [packaging](packaging.html) are settled, never before (plan section 18, R1). This draft states them as draft shapes and builds neither.

## Completion criteria for the planning phase

| Criterion | State |
|---|---|
| Inventory and vocabulary checked against the working tree | Met, first pass; the per-format capability matrix is outstanding |
| Confirmed decisions retained without reopening them | Met |
| Remaining product choices recorded with defaults, including staged scope | Met |
| Pilot and utility examples expressed with expected outcomes | Met, as the [proof cases](proof-cases.html) chapter |
| Effective governance, local rejection and acceptance claims distinguished | Met |
| Fidelity, timing and editable preservation each have a testable contract | Met, one of them supported by a probe |
| Package and identity compatibility and legacy execution classifications agreed | Met |
| The counterexamples pass as contract fixtures before any shared type is frozen | Outstanding |
| Andy's answers to Q1 to Q6, or acceptance of the defaults | Outstanding |
| Specification drafting started | In progress: this draft |

## Change log

| Date | Change |
|---|---|
| 2026-09-24 | Draft for review. The twelve chapters were written from the plan 276 consolidation after its final review, with D1 to D13 confirmed, R1 to R15 adopted from evidence, C1 to C9 corrected and Q1 to Q6 recorded with their defaults. No type is frozen and no conformance claim is made. |
| 2026-09-26 | Constitution. The thirteen invariants gain four reading groups with their numbers unchanged, and a table of where each one holds today, what is not met yet and which proof cases test it; the table finds no case that tests invariant 11 on its own and only case 7 behind invariant 5. The no-cloud limit now states R7 as a requirement and records that the web shell fails closed after 24 hours offline instead. The limits section says which two promises its first two items bound. Affects R7. |
| 2026-09-26 | Proof cases. Cases 24 and 25 are added for the two gaps the constitution's table found: one chart's accessibility metadata checked in each of four outputs, and a local utility that keeps no record of its run. The table now maps both, and the count of cases is 25. Q4 stays open; case 25 tests its default for a utility. Affects R5 and R15. |

A change to any chapter must add a row here with its date and what changed, so that the chapter sources in `docs/spec/document-model/` carry one history (`plans/276-execution.md` section 1A). A row lists the resolution ids the change affects, which is this specification's own convention.

## Open points

- **Q6. Whether the specification is public from the first draft.** Default from plan section 16: yes, published from `docs/` to the `/info` site, because `docs/constraints.md`, `docs/determinism.md` and `docs/reproducibility.md` already make public promises this model keeps. Evidence that would change it: a reason to keep the drafts private until the pilots pass.

The other five questions are recorded in the open questions table above with their defaults, and each is carried, unanswered, by the chapter it touches: Q1 and Q5 in [conformance](conformance.html), Q2 and Q3 in [policy](policy.html), Q4 in [operations](operations.html).

## Precedents

- `docs/constraints.md`
- `docs/determinism.md`
- `docs/reproducibility.md`
- `docs/glossary.md`
- `packages/core/src/canvas-op-v1.ts`
- `packages/core/src/chart-v1.ts`
- `packages/core/src/design-tool-v1.ts`
- `packages/core/src/file-operation-v1.ts`
- `packages/node-shell/src/rebrand-run-manifest.ts`
- `packages/node-shell/src/content-roots.ts`
- `schemas/canvas-op.schema.json`
- `services/mcp/src/tools.ts`
- `scripts/tool-requires.ts`
- `engine/src/compare.ts`
- `engine/src/runtime.ts`
- `engine/src/document-api.ts`
- `shells/web/src/lib/lolly-pack.ts`
- `shells/web/src/lib/design-system/hosted.ts`
- `shells/web/src/lib/instance-leave.ts`
- `plans/276-document-model-evidence/crdt-cycle-probe.mjs`
