diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 09d850f..98f6708 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,11 +13,17 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
jobs:
build-and-test:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ persist-credentials: false
# pnpm version comes from the package.json "packageManager" field
# (pnpm 10+), which is required to read `overrides` from
# pnpm-workspace.yaml consistently with the committed lockfile.
@@ -40,7 +46,7 @@ jobs:
office:
name: Office / Python ${{ matrix.python-version }}
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@@ -51,7 +57,10 @@ jobs:
env:
PYTHONPATH: src
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..c5ae120
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,169 @@
+# Inkspan Architecture
+
+Inkspan is a standalone authoring product and an embeddable module for CWL
+applications. The architecture deliberately separates deterministic editor and
+conversion behavior from host-owned transport, identity, tenancy, persistence,
+and model policy so the same package can run independently or inside a modular
+MSA composition.
+
+## Standalone product boundary
+
+Inkspan owns editor and deterministic conversion surfaces.
+
+The standalone package provides:
+
+- Markdown and HTML authoring through TipTap and ProseMirror;
+- strict link and inline-image validation;
+- SSR-safe React hydration;
+- provider-neutral Yjs collaboration bindings;
+- canonical versioned document envelopes and strict UTF-8 bytes;
+- SHA-256 revision evidence and local revision-guarded restore;
+- bounded single-flight autosave coordination and durable strong-validator
+ session helpers;
+- email serialization and framework-independent base64 conversion; and
+- a network-free Office renderer for deterministic DOCX, XLSX, and PPTX output.
+
+Hosts own transport, authorization, tenant isolation, persistence, credentials, migration, retention, and model-use policy.
+
+Inkspan therefore never opens a production collaboration connection, chooses a
+tenant, stores a provider secret, creates a durable database transaction, decides
+a retention schedule, or authorizes an AI operation. A standalone adopter can
+provide those capabilities directly; a CWL host can provide them through shared
+platform services.
+
+## Modular MSA composition
+
+The modular boundary is intentionally additive. Importing Inkspan does not
+require naruon or contextual-orchestrator, while a CWL host can compose all
+three without replacing Inkspan's deterministic local contracts.
+
+```mermaid
+flowchart LR
+ Browser[Browser or desktop shell]
+ Panel[naruon compose / ui.panel host]
+ Inkspan[Inkspan editor module]
+ Evidence[Revision evidence and autosave]
+ Collab[Host-owned Yjs provider]
+ Store[Host persistence service]
+ Models[contextual-orchestrator]
+ Office[Office renderer]
+ Control[ContextualWisdomLab/.github control plane]
+
+ Browser --> Panel
+ Panel --> Inkspan
+ Inkspan --> Evidence
+ Inkspan <--> Collab
+ Evidence --> Store
+ Panel --> Models
+ Inkspan --> Office
+ Control -. reusable CI, security, release policy .-> Inkspan
+ Control -. reusable CI, security, release policy .-> Panel
+ Control -. reusable CI, security, release policy .-> Models
+```
+
+### Component responsibilities
+
+| Component | Owns | Must not assume |
+| --- | --- | --- |
+| Inkspan | Editing, deterministic import/export, canonical envelopes, local revision evidence, local autosave ordering, accessible editor controls | User identity, tenant authority, durable commit success, provider credentials, retention, or model policy |
+| ContextualWisdomLab/naruon | Product composition, route and panel lifecycle, authenticated host API calls, accessible conflict and recovery UX | That local Inkspan revision evidence is a server commit or authorization grant |
+| ContextualWisdomLab/contextual-orchestrator | Provider-neutral model routing and host-approved model execution policy | Direct ownership of editor state, tenant persistence, or collaboration transport |
+| ContextualWisdomLab/.github | Reusable CI, security, review, provenance, and release policy | Runtime authorization or tenant data access |
+| Host persistence service | Atomic writes, server-selected strong validators, tenant isolation, migration, encryption, retention, audit storage | That browser-side checks replace server-side validation |
+| Host collaboration service | Connection, room authorization, awareness policy, update persistence, provider lifecycle | That Inkspan may create or destroy the host provider |
+
+## Data ownership matrix
+
+| Data or evidence | Local Inkspan responsibility | Host responsibility | Shareability |
+| --- | --- | --- | --- |
+| Editor document | Validate and transform deterministically | Authorize access, persist, encrypt, migrate, retain | Private unless host policy explicitly permits sharing |
+| Canonical envelope | Produce and validate exact schema/version bytes | Store, sign, classify, migrate, and apply retention | Usually private; contains the complete document |
+| Local SHA-256 revision | Detect local equality and guard local restore | Never treat as authorization or durable commit evidence | Metadata only under tenant policy |
+| Server-selected strong `ETag` | Validate syntax before use in a session | Select atomically and enforce `If-Match` in the write transaction | Tenant-confidential concurrency metadata |
+| Yjs updates and awareness | Bind the supplied `Y.Doc` to the editor | Authorize rooms, transport, persist, redact, expire, and destroy providers | Host policy decides |
+| Model prompt and output | Insert or restore validated results | Approve model use, credentials, redaction, routing, logging, and retention | Host policy decides |
+| Release evidence | Expose deterministic tests and package contracts | Verify CI, provenance, approvals, and publication policy | Shareable only after secrets and tenant data are excluded |
+
+## Optimistic-concurrency sequence
+
+Inkspan coordinates local ordering, but the host service remains the only source
+of durable success. The host returns a new server-selected strong `ETag` after
+each accepted write. A local revision digest is never substituted for that
+validator.
+
+```mermaid
+sequenceDiagram
+ participant U as Author
+ participant I as Inkspan
+ participant H as naruon or standalone host
+ participant S as Persistence service
+
+ U->>I: Edit document
+ I->>I: Capture immutable envelope and local revision
+ I->>H: Enqueue save evidence
+ H->>S: PUT document with If-Match: strong ETag
+ alt Current durable validator matches
+ S-->>H: 200/204 plus replacement strong ETag
+ H-->>I: saved with validated replacement validator
+ else Durable version changed
+ S-->>H: 412 Precondition Failed
+ H-->>I: conflict
+ I-->>U: Host renders accessible conflict, compare, merge, or fork flow
+ else Transport or ambiguous failure
+ S--xH: Failure without durable proof
+ H-->>I: blocked failure
+ I-->>U: Host requires explicit authenticated recovery
+ end
+```
+
+## SSR and panel lifecycle
+
+A server-rendered host may render Inkspan's deterministic shell, but the
+interactive editor, browser-only provider, and `Y.Doc` must be created in a
+client boundary. In a Next.js App Router integration, naruon should keep the
+`'use client'` boundary as small as practical and pass only serializable,
+non-secret configuration into the panel. Provider secrets remain server-side.
+
+The host owns provider creation and destruction. Inkspan may subscribe to the
+supplied document and awareness state, but it must not create or destroy the
+host provider. This allows one panel to mount and unmount without terminating a
+provider shared by other product surfaces.
+
+## Security and privacy boundaries
+
+- Fail closed on malformed envelopes, unsafe links, external or active image
+ sources, invalid validators, unsupported versions, and ambiguous save results.
+- Do not place full envelopes, conflict bodies, prompts, model output, provider
+ keys, access tokens, or tenant identifiers in ordinary logs or metrics.
+- Treat local equality evidence separately from authenticated, shareable release
+ or audit evidence.
+- Keep collaboration authorization, persistence authorization, and model-use
+ authorization independent even when the same user initiates all three.
+- Pin reusable workflow sources immutably and require exact-head CI, security,
+ packaging, provenance, and independent review before release publication.
+- Do not claim WCAG, OWASP ASVS, NIST, ISO, or protocol conformance from this
+ architecture document alone; verification belongs to the complete host and
+ deployed product.
+
+## Acquisition evidence boundary
+
+An acquisition reviewer should be able to verify the product without receiving
+private tenant content or production credentials. Shareable evidence includes:
+
+- source, licenses, dependency locks, SBOMs, immutable workflow pins, and release
+ provenance;
+- exact-head unit, integration, security, accessibility, packaging, and release
+ results;
+- public API declarations, architecture and operator records, migration
+ contracts, and rollback procedures; and
+- deterministic fixtures that contain no customer data.
+
+Local-only or restricted evidence includes production documents, conflict
+bodies, Yjs updates, awareness metadata, provider credentials, authorization
+claims, private model prompts and outputs, tenant-scoped validators, and
+security findings that expose exploitable deployment detail.
+
+The authoritative naruon composition guide is
+[`docs/naruon-compose-ui-panel.md`](docs/naruon-compose-ui-panel.md). Standards,
+claim boundaries, and decision history are recorded in
+[`docs/doctoring/naruon-modular-architecture.md`](docs/doctoring/naruon-modular-architecture.md).
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b94ef8..f7db67e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim
## [Unreleased]
+### Documentation
+- Added an authoritative standalone and modular MSA architecture contract with reviewable deployment, optimistic-concurrency, data-ownership, security, and acquisition-evidence diagrams and tables
+- Added a beginner-readable naruon compose and ui.panel integration guide covering narrow client hydration, server-selected strong validators, accessible conflict handling, host-owned Yjs lifecycle, contextual-orchestrator boundaries, and local-versus-shareable evidence
+- Added an opaque editing-context remount for the complete editor and autosave example, latest-generation asynchronous capture ordering, encoded document path segments, redacted recovery status, and lazy state-owned session identity to prevent cross-document state reuse
+- Bounded the host save example with a fresh abort deadline, exposed authenticated conflict recovery through `session.resume(...)`, generated an instance-unique accessible heading relationship, and strengthened fenced-TSX ordering contracts
+- Added stale-generation conflict recovery and operational save failure recovery through one reason-aware single-flight host workflow, so newer local edits cannot hide or duplicate recovery while retained work remains blocked
+- Added exact-head read-only CI with fixed Ubuntu 24.04 runners, immutable action pins, explicit contributor-head checkout, disabled persisted Git credentials, and a documented merge-result compatibility boundary
+- Added deterministic documentation contract tests and APA 7th doctoring grounded in RFC 9110, WCAG 2.2, NIST SP 800-204, NIST SP 800-204D, OWASP ASVS 5.0.0, React, current Next.js App Router guidance, and GitHub Actions primary documentation
+
## [0.5.29] — 2026-08-05
### Added
diff --git a/docs/doctoring/exact-head-ci-evidence.md b/docs/doctoring/exact-head-ci-evidence.md
new file mode 100644
index 0000000..64f1912
--- /dev/null
+++ b/docs/doctoring/exact-head-ci-evidence.md
@@ -0,0 +1,144 @@
+# Doctoring record: exact-head CI evidence
+
+- **Status:** Accepted
+- **Decision date:** 2026-08-06
+- **Scope:** Repository-owned `CI` workflow only
+- **Runtime change:** None; this changes verification source and runner controls
+
+## Problem
+
+GitHub's `pull_request` event exposes a synthetic pull-request merge ref as the
+default checkout target. The prior Inkspan workflow used checkout defaults, so a
+reported CI success described GitHub's temporary merge commit rather than the
+immutable contributor head named in the pull request. The checkout action also
+persisted its GitHub token into local Git configuration by default.
+
+A synthetic merge test can be useful compatibility evidence, but it is not exact
+source identity evidence. Treating it as the pull-request head can make reviews,
+coverage reports, packages, and release claims appear bound to a commit that the
+contributor branch never contained. Persisting credentials is also unnecessary
+for a read-only build and enlarges the impact of untrusted build or test code.
+
+## Decision
+
+The repository-owned `CI` workflow checks out:
+
+```yaml
+ref: ${{ github.event.pull_request.head.sha || github.sha }}
+persist-credentials: false
+```
+
+For a pull request, this selects the immutable contributor head. For a protected
+branch push, it selects the event commit through `github.sha`. Both jobs use the
+fixed `ubuntu-24.04` runner label and the immutable `actions/checkout` v7.0.1
+commit. Workflow permissions remain `contents: read`, and no write permission,
+secret-bearing model call, release publication, approval, or branch update is
+introduced.
+
+`FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` is declared at workflow scope so the pinned
+actions run under the repository's reviewed current JavaScript action runtime
+rather than a deprecated implicit runtime.
+
+## Evidence boundary
+
+A successful run under this contract is exact-head CI evidence for the selected
+source commit. It is not merge-result compatibility evidence. Branch protection,
+a merge queue, or a separately reviewed integration test may still require a
+fresh synthetic or protected-base integration result before merge.
+
+The workflow result also does not replace Security Scan, Semgrep, CodeQL,
+independent review, release provenance, package publication, or deployment
+acceptance. Each required surface must bind its own result to the intended source
+identity. In particular, an organization-required workflow that still checks a
+synthetic merge ref must not be described as exact-head evidence merely because
+its check appears on the pull request.
+
+Local-versus-shareable evidence remains explicit. Checkout logs and exact source
+SHAs are shareable release evidence. Repository tokens, runner credentials,
+customer content, tenant identifiers, private callback output, and dependency
+registry credentials are not.
+
+## Security consequences
+
+The contributor head is untrusted input. Disabling credential persistence keeps
+the workflow token out of the checked-out repository's ordinary Git
+configuration after checkout. The action still receives the job token long
+enough to fetch the exact source, and the job retains only read permissions.
+
+This is defense in depth rather than a sandbox claim. Test processes can still
+read files and use the network permitted by the hosted runner. Secrets must not
+be attached to this untrusted pull-request job, and a future write-capable step
+must use a separate trusted workflow boundary rather than broadening this one.
+
+Fixed runner labels reduce silent environment movement but do not make the hosted
+image immutable. The exact image version remains visible in every job log.
+Third-party actions are pinned to full commit identities, and the contract test
+rejects mutable tags.
+
+## Test-first evidence
+
+Commit `14096f37f1aa47fb7f6661fea3b505193680aaf8` added the first workflow
+contract before production changes. Commit
+`e68781a8a2c96d5b103900e2622bf7de59925706` added this doctoring and changelog
+contract before either record existed.
+
+Pull-request CI run `31066658465` produced the intended red result: all 550 other
+JavaScript tests and both Office Python matrix jobs passed, while the three new
+contracts failed for `ubuntu-latest`, the missing Node 24 action-runtime policy,
+and the absent doctoring record. Its checkout log also showed
+`persist-credentials: true` and the synthetic `refs/pull/64/merge` source. That
+run is historical TDD evidence, not success evidence.
+
+Commit `bf348d6d2ad00589a990bc447f90548709533b4c` applied the workflow repair.
+The final head must prove from its own checkout log that the exact contributor
+head was selected and `persist-credentials: false` was effective before this
+record can support merge readiness.
+
+## Rejected alternatives
+
+### Keep the default checkout ref and rename the claim
+
+Rejected because the product and acquisition contract requires exact-head
+coverage, packaging, and review evidence. A merge-only result cannot substitute
+for the reviewed source identity.
+
+### Use `pull_request_target`
+
+Rejected because that event's privileged base-repository context is unsafe for
+executing untrusted pull-request code. This read-only workflow needs no privileged
+context.
+
+### Keep persisted credentials for convenience
+
+Rejected because no workflow step commits, pushes, tags, opens a pull request,
+or publishes a release. Retaining the credential has no product benefit.
+
+### Use mutable action tags or `ubuntu-latest`
+
+Rejected because both silently change the verification implementation. Full
+action commits and a fixed runner family make changes reviewable and reproducible
+enough for this hosted-runner boundary.
+
+## Rollback
+
+Rollback restores the prior workflow and removes this record and its contract.
+That rollback must also remove every exact-head claim because the default
+pull-request checkout returns to a synthetic merge source and persisted Git
+credentials. It does not provide a safe emergency path for publication or
+approval.
+
+No package version, database object, migration, runtime dependency, provider,
+credential, scheduler, model call, or release is introduced.
+
+## APA 7 references
+
+GitHub, Inc. (n.d.). *Events that trigger workflows*. GitHub Docs. Retrieved
+August 6, 2026, from
+https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows
+
+GitHub, Inc. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs.
+Retrieved August 6, 2026, from
+https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
+
+GitHub, Inc. (n.d.). *actions/checkout*. GitHub. Retrieved August 6, 2026, from
+https://github.com/actions/checkout
diff --git a/docs/doctoring/naruon-modular-architecture.md b/docs/doctoring/naruon-modular-architecture.md
new file mode 100644
index 0000000..4e07af6
--- /dev/null
+++ b/docs/doctoring/naruon-modular-architecture.md
@@ -0,0 +1,340 @@
+# Doctoring record: naruon modular architecture
+
+- **Status:** Accepted
+- **Decision date:** 2026-08-05
+- **Scope:** Repository architecture, naruon composition, acquisition evidence,
+ and operator-facing integration guidance
+- **Runtime change:** None
+
+## Problem
+
+Inkspan already had strong implementation-level boundaries for editing,
+collaboration, deterministic conversion, revision evidence, and durable autosave,
+but those boundaries were distributed across feature-specific documents. A
+buyer, naruon integrator, or new operator could not review one authoritative
+system view that explained:
+
+- how standalone use differs from modular CWL composition;
+- which responsibilities remain with Inkspan and which belong to the host;
+- how `ContextualWisdomLab/.github`, `ContextualWisdomLab/naruon`, and
+ `ContextualWisdomLab/contextual-orchestrator` fit together;
+- where SSR, Yjs provider lifecycle, strong HTTP validators, accessibility, and
+ release evidence cross trust boundaries;
+- how to prevent cross-document state reuse when a host changes the authorized
+ document, workspace, or tenant context in an existing client tree; and
+- which evidence may be shared during support, procurement, or acquisition
+ diligence without disclosing tenant data.
+
+That gap increased integration error risk even though the underlying package
+contracts were already provider-neutral.
+
+## Decision
+
+Add one authoritative root `ARCHITECTURE.md` and one beginner-readable naruon
+`compose` / `ui.panel` integration guide.
+
+The architecture explicitly states:
+
+> Inkspan owns editor and deterministic conversion surfaces.
+> Hosts own transport, authorization, tenant isolation, persistence,
+> credentials, migration, retention, and model-use policy.
+
+The documentation uses reviewable Mermaid diagrams for the modular component
+map and RFC 9110 optimistic-concurrency sequence. It defines local versus
+shareable evidence, host-owned Yjs provider lifecycle, narrow client hydration,
+server-selected strong validators, bounded host save callbacks, accessible
+conflict recovery through `session.resume(...)`, instance-unique panel labels,
+exact-head release evidence, and one opaque editing-context lifecycle boundary
+that remounts the editor, autosave session, pending digest state, and status state
+together. Document identifiers are encoded before transport and remain subject
+to host-side authorization and route validation.
+
+## Architectural consequences
+
+### Positive
+
+- Standalone adopters receive a complete boundary without installing naruon or
+ contextual-orchestrator.
+- CWL hosts can compose common infrastructure without moving editor-owned
+ validation or deterministic conversion into the host.
+- Server-side secrets, tenant authorization, persistence, and model-use policy
+ remain outside the browser editor package.
+- Integrators receive a concrete fail-closed autosave and conflict sequence with
+ a bounded request and explicit authenticated recovery handoff.
+- A host-selected opaque editing-context key prevents uncontrolled editor state,
+ durable validators, and in-flight digest completion from crossing authorized
+ document boundaries.
+- Multiple panels on one page retain separate accessible heading relationships.
+- Acquisition reviewers can distinguish reproducible product evidence from
+ restricted customer or deployment evidence.
+- The diagrams are version-controlled, text-reviewable, and render directly in
+ GitHub without an external design artifact.
+
+### Trade-offs
+
+- The guide is an architecture contract rather than a ready-made naruon
+ persistence adapter. Host implementations still need their own authenticated
+ APIs and database transactions.
+- The editing-context key deliberately discards client-local editor and autosave
+ state on an authorized context transition. A host that permits drafts across
+ transitions must persist and reauthorize those drafts outside Inkspan before
+ issuing the new context.
+- The example's ten-second deadline is illustrative. Each host must derive a
+ bounded timeout, retry, and idempotency policy from its operating environment.
+- Mermaid rendering is useful for review but is not a substitute for accessible
+ prose; every diagram is accompanied by equivalent text and tables.
+- Documentation tests can prove fenced example structure, required ordering, and
+ required statements remain present, but they do not prove a deployed host is
+ secure, accessible, reliable, or conformant.
+
+## Security boundary
+
+The decision follows the least-authority split recommended for modular systems:
+
+- the editor receives document state and non-secret presentation configuration;
+- the host resolves identity, tenant membership, provider credentials, and
+ durable storage authority;
+- the host issues a fresh opaque editing-context lifecycle value after each
+ authorized load or context transition;
+- the host bounds every durable save callback with a fresh timeout or abort
+ signal and treats timeout or abort as ambiguous rather than successful;
+- conflict recovery supplies a server-selected strong validator only after an
+ authenticated reload, merge, fork, discard, or equivalent confirmed decision;
+- the persistence service selects and atomically enforces strong entity tags;
+- the collaboration host owns room authorization, update persistence, and
+ provider lifecycle;
+- contextual-orchestrator owns only host-approved provider-neutral model
+ execution; and
+- the central `.github` repository owns reusable CI, security, review,
+ provenance, and release policy rather than runtime data authority.
+
+The editing-context value is not an authorization grant, tenant identifier,
+durable validator, audit identifier, or credential. It must not be derived from
+a document body or treated as server-side access evidence. Its sole purpose is to
+provide a non-secret client lifecycle identity so React destroys the entire
+stateful subtree when the authorized editing context changes. The host still
+validates every document identifier and every write independently.
+
+The architecture does not claim compliance with OWASP ASVS 5.0.0, NIST SP
+800-204, NIST SP 800-204D, WCAG 2.2, or RFC 9110 by documentation alone. Those
+sources inform the boundary and verification plan. Conformance and assurance
+must be assessed on the complete deployed host and its operating controls.
+
+## Accessibility boundary
+
+The naruon `ui.panel` guide requires keyboard-operable conflict actions,
+labelled status and conflict regions, controlled focus movement, restrained
+live-region announcements, and a unique React-generated heading relationship for
+each panel instance. This supports WCAG 2.2-oriented integration, but the host
+remains responsible for testing the complete page, all responsive states, and
+third-party content before making a conformance claim.
+
+An editing-context remount also replaces focusable editor DOM. The host therefore
+owns deterministic focus placement after the newly authorized panel is mounted
+and must not leave focus on a removed node or announce private document content.
+
+## SSR and hydration boundary
+
+The React hydrateRoot API attaches an interactive tree to server-generated markup.
+Next.js App Router separates Server and Client Components and uses `'use client'`
+to declare the client module boundary. The integration therefore keeps Inkspan,
+browser-only providers, event handlers, and `Y.Doc` creation in the narrow
+client panel while authorization, credential access, and initial durable loading
+remain server-side.
+
+React associates state with a component's position in the render tree and
+supports an explicit `key` to reset a stateful subtree. The host-facing wrapper
+therefore keys an inner client-session component with the opaque authorized
+editing-context value. The autosave coordinator is retained in lazy React state
+inside that keyed component. It is not created with `useMemo`, because React
+documents `useMemo` as a performance optimization rather than a semantic
+identity guarantee and may discard its cached value. The initializer has no
+transport, timer, credential, persistence, or storage side effects; cleanup
+closes the retained session when the keyed subtree is removed.
+
+The server-facing boundary receives only serializable data. A host-owned client
+composition creates the conflict-recovery callback inside the client boundary
+rather than attempting to serialize a function from a Server Component.
+
+This is an architectural recommendation, not a dependency on Next.js. A
+traditional React SSR host may apply the same separation with its own server and
+client entry points.
+
+## Optimistic-concurrency boundary
+
+RFC 9110 defines validators and conditional request semantics. Inkspan validates
+strong entity-tag syntax and coordinates local single-flight ordering, but the
+host persistence transaction remains authoritative. The host must compare the
+`If-Match` value atomically with the durable representation and return a new
+server-selected strong `ETag` after an accepted write.
+
+A timeout, disconnect, abort, or malformed response is ambiguous. The host does
+not advance the durable validator or automatically retry unless separate
+idempotency evidence establishes the prior outcome. After an authenticated
+conflict decision, the client recovery boundary calls `session.resume(...)` so
+the validated replacement tag is installed immediately before retained work
+continues.
+
+A local Inkspan SHA-256 revision remains equality evidence for local deterministic
+operations. It is not substituted for a durable HTTP validator, authorization
+decision, signature, tenant identifier, or audit record.
+
+## Acquisition evidence boundary
+
+Shareable evidence is deliberately reproducible from source and non-customer
+fixtures: exact-head CI, security results, package hashes, SBOMs, provenance,
+licenses, public declarations, deterministic conversion fixtures, operator
+records, migration contracts, and rollback procedures.
+
+Restricted evidence includes document envelopes, conflict bodies, Yjs updates,
+awareness state, prompts, model outputs, provider credentials, tenant-scoped
+validators, authorization claims, and deployment-specific exploitable findings.
+Hashing, canonicalization, encryption, or successful CI does not automatically
+make restricted evidence shareable.
+
+The editing-context key is intentionally non-secret, but it remains local product
+state and should not be promoted into logs, analytics dimensions, support
+artifacts, or acquisition evidence. Reproducible tests prove the lifecycle
+contract without recording a tenant-derived value.
+
+## Verification
+
+`src/architectureDocumentation.test.ts` extracts the fenced TSX example and fails
+unless:
+
+- the root architecture contains the standalone and modular ownership boundary;
+- the CWL repositories and host responsibilities are named;
+- both deployment and optimistic-concurrency Mermaid diagrams exist;
+- the naruon guide includes client-boundary, strong-validator, accessible
+ conflict, provider-lifecycle, credential, and evidence contracts;
+- the complete editor/autosave subtree is keyed by an opaque editing-context
+ value, uses lazy component state rather than `useMemo` for session identity,
+ and encodes the document identifier before transport;
+- each panel creates an instance-unique heading ID and binds its own
+ `aria-labelledby` relationship;
+- the host save callback supplies a fresh bounded abort signal;
+- the latest-generation capture, digest, first guard, enqueue, and second guard
+ remain in the required order;
+- the conflict path exposes a host recovery callback that invokes
+ `session.resume(...)` with the recovered strong validator;
+- this doctoring record retains the authoritative standards references; and
+- `CHANGELOG.md` records the unreleased buyer-visible documentation slice.
+
+The repository-wide TypeScript, 100% production statement/branch/function/line
+coverage, package, Office, security, SAST, review, and exact-head branch
+protection gates remain authoritative.
+
+## Rejected alternatives
+
+### Put naruon-specific behavior inside Inkspan
+
+Rejected because it would break standalone operation, make the editor own host
+transport and identity concerns, and couple releases to one product shell.
+
+### Publish a complete persistence adapter in this slice
+
+Rejected because transport, authorization, tenant isolation, database schema,
+migration, retention, and credential policy are host responsibilities. A sample
+adapter could accidentally become an insecure de facto production contract.
+
+### Reuse one client panel across authorized document contexts
+
+Rejected because an uncontrolled editor, autosave validator, pending digest, and
+status state can remain associated with the same React tree position after props
+change. That creates a credible cross-document state reuse path in which content
+from the previous document may be sent to the next document route. Keying the
+whole inner session prevents partial reset and binds cleanup to one authorized
+editing context.
+
+### Use `useMemo` as the autosave session identity boundary
+
+Rejected because React defines memoization as a performance optimization rather
+than a semantic guarantee. A mutable coordinator with explicit cleanup belongs
+in state owned by the keyed session subtree. The host changes the key, not a
+memoization dependency, to replace the complete authorized editing context.
+
+### Leave durable save callbacks unbounded
+
+Rejected because an unresolved host callback retains the single-flight request
+and prevents later enqueue, flush, and close operations from completing. The
+host must provide a finite timeout or abort boundary and preserve ambiguous-write
+semantics.
+
+### Reuse one static accessible heading ID
+
+Rejected because multiple panels could produce duplicate IDs and cause
+`aria-labelledby` to reference the wrong heading. Each mounted panel creates its
+own React-generated ID.
+
+### Display a conflict without exposing a recovery handoff
+
+Rejected because status text alone cannot install the authenticated replacement
+validator or resume retained work. The host client boundary must receive a
+bounded recovery callback tied to the exact autosave session.
+
+### Store provider credentials in panel props or environment-reading editor code
+
+Rejected because browser props are observable and package-level environment
+resolution would violate the host-owned secret boundary.
+
+### Treat local SHA-256 evidence as the server validator
+
+Rejected because the server is responsible for representation selection and
+atomic conditional writes. Local equality evidence cannot prove durable commit
+success.
+
+### Use an external drawing-only artifact
+
+Rejected for the authoritative record because a binary or hosted-only diagram is
+harder to diff, review, version, and validate in repository CI. Figma remains
+appropriate for interaction design when visual fidelity materially improves a
+future UI slice; the current architecture is better represented as text and
+Mermaid.
+
+## Rollback
+
+Rollback removes `ARCHITECTURE.md`, `docs/naruon-compose-ui-panel.md`, this
+record, and the associated documentation contract test, then removes the
+unreleased changelog entry. No runtime, package export, dependency, database,
+credential, workflow, or published version rollback is required.
+
+A host that has implemented the opaque editing-context boundary must not remove
+that boundary merely because this documentation slice is rolled back. It should
+retain or replace the protection with an independently verified equivalent that
+prevents state and validator reuse across authorized document contexts.
+
+## APA 7 references
+
+Chandramouli, R. (2019). *Security strategies for microservices-based
+application systems* (NIST Special Publication 800-204). National Institute of
+Standards and Technology. https://doi.org/10.6028/NIST.SP.800-204
+
+Chandramouli, R., Kautz, F., & Torres-Arias, S. (2024). *Strategies for the
+integration of software supply chain security in DevSecOps CI/CD pipelines*
+(NIST Special Publication 800-204D). National Institute of Standards and
+Technology. https://doi.org/10.6028/NIST.SP.800-204D
+
+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110;
+STD 97). RFC Editor. https://doi.org/10.17487/RFC9110
+
+Meta Platforms, Inc. (n.d.). *Client React DOM APIs: hydrateRoot*. React.
+Retrieved August 5, 2026, from https://react.dev/reference/react-dom/client
+
+Meta Platforms, Inc. (n.d.). *Preserving and resetting state*. React. Retrieved
+August 5, 2026, from https://react.dev/learn/preserving-and-resetting-state
+
+Meta Platforms, Inc. (n.d.). *useMemo*. React. Retrieved August 5, 2026, from
+https://react.dev/reference/react/useMemo
+
+Meta Platforms, Inc. (n.d.). *useState*. React. Retrieved August 5, 2026, from
+https://react.dev/reference/react/useState
+
+Open Worldwide Application Security Project Foundation. (2025). *OWASP
+Application Security Verification Standard 5.0.0*.
+https://owasp.org/www-project-application-security-verification-standard/
+
+Vercel. (2026, March 16). *Server and Client Components*. Next.js.
+https://nextjs.org/docs/app/getting-started/server-and-client-components
+
+World Wide Web Consortium. (2024, December 12). *Web Content Accessibility
+Guidelines (WCAG) 2.2*. https://www.w3.org/TR/WCAG22/
diff --git a/docs/doctoring/stale-generation-conflict-recovery.md b/docs/doctoring/stale-generation-conflict-recovery.md
new file mode 100644
index 0000000..773455e
--- /dev/null
+++ b/docs/doctoring/stale-generation-conflict-recovery.md
@@ -0,0 +1,208 @@
+# Doctoring record: stale-generation conflict recovery
+
+- **Status:** Accepted
+- **Decision date:** 2026-08-06
+- **Scope:** Naruon `compose` / `ui.panel` durable autosave example
+- **Runtime change:** None; this record corrects the host integration contract
+
+## Problem
+
+The documented panel protected asynchronous document capture with a monotonically
+increasing edit generation. That guard correctly prevented an older, slower
+revision digest from being enqueued after a newer edit, but the same guard was
+also applied immediately after the autosave queue settled.
+
+That ordering created a blocked-but-unreported conflict path:
+
+1. edit generation A is captured, enqueued, and becomes the active host save;
+2. generation B arrives while A is active, increments the generation, and is
+ retained as pending work;
+3. A receives `412 Precondition Failed`, so the single-flight queue enters its
+ blocked conflict state while B remains retained;
+4. the old example observes that A is stale and returns before requesting host
+ recovery; and
+5. B cannot start until recovery, while no accessible recovery workflow is
+ opened and the UI may continue to report an ordinary saving state.
+
+A related path existed for operational failures. A callback exception, abort,
+timeout, or malformed result rejects the active `enqueue()` promise and blocks
+the same queue with reason `failure`. The example displayed a generic action
+message but did not actually invoke a host recovery workflow, so pending newer
+work could remain blocked indefinitely.
+
+A generation guard is appropriate for superseded local presentation work. It is
+not authority to discard a queue-wide blocking outcome whose recovery is needed
+by newer retained work. A status message alone is not a recovery mechanism.
+
+## Decision
+
+Use two generation guards with separate responsibilities and one reason-aware
+recovery boundary.
+
+The first guard remains between digest completion and enqueue. It rejects a stale
+capture before that document can enter the durable queue.
+
+The invariant is: blocking outcomes before stale-generation status suppression.
+A conflict therefore opens one host-owned recovery workflow even when the request
+that first observed it belongs to an older generation. Only non-blocking saved,
+unchanged, superseded, or closed presentation updates are suppressed when their
+generation is stale.
+
+If `enqueue()` rejects, the example reads the document-free queue snapshot. When
+that snapshot is blocked and supplies a stable `blockedReason`, the
+host callback failure and a durable conflict both request recovery through the
+same bounded interface. The host receives only `conflict` or `failure`; it does
+not receive the document, validator, callback value, or private exception.
+
+The panel keeps one local `durableRecoveryPending` ref and issues one
+single-flight recovery request for one blocked session. Multiple callers may
+share the active queue outcome, so this ref prevents duplicate dialogs or
+competing authenticated reloads without becoming persistence, authorization, or
+a durable lock.
+
+For `conflict`, the host may compare, merge, fork, discard, or perform an
+authenticated reload. For `failure`, the host must first determine whether an
+ambiguous write committed and obtain the authoritative current representation
+and server-selected strong `ETag`; it must not blindly retry the failed evidence.
+
+The recovery callback calls `session.resume(recoveredStrongEntityTag)`. Inkspan
+validates the strong entity tag and installs it before retained work starts. A
+malformed validator fails closed, preserves the previous durable base, and
+returns a generic recovery status without exposing the supplied value or private
+exception.
+
+Operational callback failures are handled from the document-free session
+snapshot. If the queue is blocked, the host recovery action remains active even
+when an older edit generation observed the failure. The ordering invariant is
+therefore queue-wide rather than generation-local.
+
+## Ownership boundary
+
+Inkspan continues to own only deterministic document evidence, the local
+single-flight queue, stable blocked-reason metadata, strong-tag syntax
+validation, and durable-validator handoff. The host continues to own:
+
+- authentication, authorization, and tenant isolation;
+- atomic `If-Match` enforcement and persistence;
+- request deadlines, cancellation, idempotency, and retry policy;
+- accessible compare, merge, fork, discard, verify, and reload workflows;
+- determination of whether an ambiguous write committed;
+- selection of the recovered server validator; and
+- private transport and incident telemetry.
+
+The local pending flag and blocked reason are not tenant identifiers, audit
+records, distributed locks, or proof that durable recovery succeeded. The host
+callback returning `true` means only that the current local session accepted the
+validated recovery transition.
+
+## Security and privacy consequences
+
+The repair prevents a newer keystroke from hiding a durable conflict that still
+blocks newer work. It also prevents an operational save failure from leaving
+retained work blocked behind a message that offers no callable recovery path.
+Several shared callers cannot open parallel recovery workflows for the same
+blocked session.
+
+Generic status text contains no document body, server validator, callback value,
+tenant metadata, credential, or private exception. The panel never retries an
+ambiguous write automatically. A recovered validator must come from an
+authenticated durable reload or equivalent confirmed host decision.
+
+## Accessibility consequences
+
+A blocked queue remains represented by one accessible recovery surface until the
+supplied recovery callback succeeds or the host deliberately abandons the
+editing context. New local edits must not dismiss, duplicate, or obscure that
+surface. The host remains responsible for focus movement, keyboard operation,
+labelling, reason-appropriate actions, and restoration to the editor after
+resolution.
+
+## Test-first evidence
+
+Commit `43a211b0818636016e2e80d9ceaaad5ab7af1fd7` added the original ordering
+and single-flight conflict contract before the guide implemented it.
+Pull-request workflow run `31065769175` produced the intended red result: the
+new documentation test failed while 549 other JavaScript tests and both Office
+Python package jobs passed. That workflow checked GitHub's synthetic pull-request
+merge ref, so it is historical TDD evidence rather than exact-head acceptance
+evidence.
+
+Commit `984a35b3dfb140b8f1099e0413f80fc4d1103e9b` extended the red contract to
+require this doctoring record and `CHANGELOG.md` evidence. Commit
+`bdfb75179f42cb10217803248685bc4e79578d05` then changed the fenced integration
+example so a conflict is handled before the second generation guard and the host
+recovery request is single-flight.
+
+Commit `f6cdcf4c5879c4c1661731590d9295fb61485205` added the reason-aware
+operational-recovery contract before the guide implemented it. The contract
+requires the public `DocumentAutosaveBlockedReason`, one shared recovery guard,
+conflict recovery before stale status suppression, and a catch path that invokes
+host recovery from a blocked snapshot rather than merely changing text.
+
+Commit `dd8edbe8e4b8953ed5ef91fe864c052879b79b07` then implemented the
+reason-aware host boundary. The final integrated head must still pass
+repository-wide TypeScript, 100% production statement/branch/function/line
+coverage, package consumers, Office, security, SAST, review, and
+branch-protection gates. Red runs and commits are historical TDD evidence and are
+not merge evidence.
+
+## Rejected alternatives
+
+### Keep the second generation guard before conflict handling
+
+Rejected because a stale active request can be the request that transitions the
+whole queue into a blocked state while newer work remains pending.
+
+### Display an operational error without invoking recovery
+
+Rejected because the queue remains blocked and newer retained work cannot start.
+A visible message that has no associated host recovery workflow is not actionable
+reliability behavior.
+
+### Resume automatically with the previous validator
+
+Rejected because the previous validator may have been rejected or may no longer
+describe durable state. Automatic retry would violate authenticated conflict and
+ambiguous-write boundaries.
+
+### Open one recovery workflow per enqueue caller
+
+Rejected because active or pending revisions can share outcomes. Parallel
+recovery workflows can race to install different durable bases and present
+inconsistent user decisions.
+
+### Bind recovery to the generation that observed the block
+
+Rejected because both conflict and failure block the queue, not only one visual
+generation. Recovery must remain available to unblock retained newer work.
+
+### Expose the original callback error to select recovery behavior
+
+Rejected because transport exceptions can contain URLs, headers, tenant data,
+provider details, or other private material. The stable document-free blocked
+reason is sufficient for control flow.
+
+## Rollback
+
+Rollback restores the prior example and removes the expanded documentation
+contract. Such a rollback also restores the known risks that a newer edit can
+hide a queue-wide conflict and an operational failure can block retained work
+without invoking recovery. A production host should not adopt that rollback
+unless it already provides an independently verified equivalent recovery
+coordinator.
+
+No package version, runtime dependency, database object, migration, credential,
+network client, provider, scheduler, or release publication is introduced by
+this documentation repair.
+
+## APA 7 references
+
+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110;
+STD 97). RFC Editor. https://doi.org/10.17487/RFC9110
+
+Herlihy, M. P., & Wing, J. M. (1990). Linearizability: A correctness condition
+for concurrent objects. *ACM Transactions on Programming Languages and Systems,
+12*(3), 463–492. https://doi.org/10.1145/78969.78972
+
+Meta Platforms, Inc. (n.d.). *useRef*. React. Retrieved August 6, 2026, from
+https://react.dev/reference/react/useRef
diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md
new file mode 100644
index 0000000..6cb069e
--- /dev/null
+++ b/docs/naruon-compose-ui-panel.md
@@ -0,0 +1,447 @@
+# Naruon compose and ui.panel integration
+
+This guide shows how to embed Inkspan in a naruon composition without making
+Inkspan depend on naruon. The same editor package remains usable in a standalone
+React application, while naruon owns product routing, authenticated service
+calls, tenant context, persistence, credentials, conflict UX, and model policy.
+
+## Integration goals
+
+A correct integration should:
+
+1. keep the interactive editor in a narrow browser boundary;
+2. keep provider credentials and authorization decisions outside Inkspan;
+3. use server-selected strong `ETag` values for durable optimistic concurrency;
+4. let Inkspan coordinate only deterministic local editing and save ordering;
+5. keep the host-created `Y.Doc` and collaboration provider lifecycle outside the
+ editor module;
+6. expose an accessible conflict, operational recovery, and unsaved-state
+ experience; and
+7. separate local evidence from shareable evidence used for operations or due
+ diligence.
+
+## Recommended composition
+
+Use a server component or equivalent host loader to authorize the document and
+load its durable representation. Pass only serializable document data, a
+server-selected strong `ETag`, an opaque non-secret editing-context lifecycle
+identifier, and non-secret presentation options into one small host client
+boundary. That host client boundary creates the durable-recovery callback; a
+server component must not attempt to serialize a function prop.
+
+```tsx
+// app/documents/[documentId]/inkspan-panel.tsx
+'use client';
+
+import { useEffect, useId, useRef, useState } from 'react';
+import {
+ CwlEditor,
+ type CwlEditorHandle,
+} from '@contextualwisdomlab/cwl-editor';
+import {
+ createDocumentAutosaveSession,
+ isStrongHttpEntityTag,
+ type DocumentAutosaveBlockedReason,
+ type DocumentAutosaveSession,
+} from '@contextualwisdomlab/cwl-editor/autosave';
+import '@contextualwisdomlab/cwl-editor/styles.css';
+
+interface InkspanPanelProps {
+ readonly editingContextId: string;
+ readonly documentId: string;
+ readonly initialMarkdown: string;
+ readonly initialStrongEntityTag: string;
+ readonly requestDurableRecovery: (
+ blockedReason: DocumentAutosaveBlockedReason,
+ resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean,
+ ) => void;
+}
+
+export function InkspanPanel(props: InkspanPanelProps) {
+ return ;
+}
+
+function InkspanPanelSession({
+ documentId,
+ initialMarkdown,
+ initialStrongEntityTag,
+ requestDurableRecovery,
+}: InkspanPanelProps) {
+ const titleId = useId();
+ const editorRef = useRef(null);
+ const editGeneration = useRef(0);
+ const durableRecoveryPending = useRef(false);
+ const [saveMessage, setSaveMessage] = useState('Document loaded.');
+ const [session] = useState(() =>
+ createDocumentAutosaveSession({
+ initialStrongEntityTag,
+ async save(request) {
+ const encodedDocumentId = encodeURIComponent(documentId);
+ const response = await fetch(`/api/documents/${encodedDocumentId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'If-Match': request.ifMatchStrongEntityTag,
+ },
+ body: JSON.stringify(request.evidence.envelope),
+ signal: AbortSignal.timeout(10_000),
+ });
+
+ if (response.status === 412) {
+ return { status: 'conflict' };
+ }
+ if (!response.ok) {
+ throw new Error('Document save failed without durable proof');
+ }
+
+ const nextStrongEntityTag = response.headers.get('ETag');
+ if (!isStrongHttpEntityTag(nextStrongEntityTag)) {
+ throw new Error('Document save omitted a valid strong ETag');
+ }
+ return {
+ status: 'saved',
+ nextStrongEntityTag,
+ };
+ },
+ }),
+ );
+
+ useEffect(
+ () => () => {
+ editGeneration.current += 1;
+ void session.close();
+ },
+ [session],
+ );
+
+ function requestBlockedSessionRecovery(
+ blockedReason: DocumentAutosaveBlockedReason,
+ ): void {
+ if (durableRecoveryPending.current) return;
+ durableRecoveryPending.current = true;
+ setSaveMessage(
+ blockedReason === 'conflict'
+ ? 'Saving paused. Resolve the durable document conflict.'
+ : 'Saving paused. Verify the durable document state before retrying.',
+ );
+
+ try {
+ requestDurableRecovery(blockedReason, (recoveredStrongEntityTag) => {
+ try {
+ const resumed = session.resume(recoveredStrongEntityTag);
+ if (resumed) {
+ setSaveMessage(
+ 'Recovered changes resumed with a durable validator.',
+ );
+ }
+ return resumed;
+ } catch {
+ if (session.getSnapshot().state === 'blocked') {
+ setSaveMessage(
+ 'Recovery requires a valid server-selected strong ETag.',
+ );
+ }
+ return false;
+ } finally {
+ durableRecoveryPending.current = false;
+ }
+ });
+ } catch {
+ durableRecoveryPending.current = false;
+ if (session.getSnapshot().state === 'blocked') {
+ setSaveMessage('Saving paused. Use the host recovery action.');
+ }
+ }
+ }
+
+ async function captureAndQueueLatestDocument(): Promise {
+ const capturedGeneration = ++editGeneration.current;
+ setSaveMessage('Saving changes.');
+
+ try {
+ const evidence =
+ await editorRef.current?.getDocumentEnvelopeRevisionEvidence();
+ if (
+ evidence === undefined ||
+ evidence === null ||
+ capturedGeneration !== editGeneration.current
+ ) {
+ return;
+ }
+
+ const outcome = await session.enqueue(evidence);
+ if (outcome.status === 'conflict') {
+ requestBlockedSessionRecovery('conflict');
+ return;
+ }
+ if (capturedGeneration !== editGeneration.current) {
+ return;
+ }
+
+ if (outcome.status === 'closed') {
+ setSaveMessage('Saving is unavailable because this session closed.');
+ } else {
+ setSaveMessage('All current changes are saved or queued.');
+ }
+ } catch {
+ const snapshot = session.getSnapshot();
+ if (
+ snapshot.state === 'blocked' && snapshot.blockedReason !== null
+ ) {
+ requestBlockedSessionRecovery(snapshot.blockedReason);
+ } else if (capturedGeneration === editGeneration.current) {
+ setSaveMessage('Saving paused. Use the host recovery action.');
+ }
+ }
+ }
+
+ return (
+
+ Document editor
+ {
+ void captureAndQueueLatestDocument();
+ }}
+ />
+
+ {saveMessage}
+
+
+ );
+}
+```
+
+The host must issue a new opaque `editingContextId` for every authorized document load
+and whenever the authorized workspace, tenant, or document context changes. The
+value is a UI lifecycle key only: it is not an authorization grant, tenant
+identifier, durable validator, or audit identifier, and it should not be logged.
+Keying the complete client session prevents React from reusing an uncontrolled
+editor, autosave validator, pending digest, or status state for a different
+document. Keying only `CwlEditor` is insufficient because the autosave session
+and asynchronous capture state must be replaced in the same lifecycle boundary.
+
+The session uses lazy component state rather than `useMemo`. The keyed child owns
+that state for one authorized editing context, and changing the key discards the
+whole session subtree. Memoization is an optimization and must not be treated as
+the semantic identity or disposal boundary for a mutable autosave coordinator.
+The session constructor performs no transport, timer, credential, or storage
+side effect; the retained session is closed by the component cleanup.
+
+Each panel instance creates its own React `useId()` value, so multiple editors on
+one page retain distinct heading relationships. The opaque editing-context key
+controls lifecycle replacement; the generated heading ID controls only the
+local accessible-name relationship and is not an authorization or audit value.
+
+The first generation guard prevents an older, slower asynchronous envelope
+digest from being enqueued after a newer edit. The second guard suppresses only
+stale non-blocking status updates after queue settlement. A blocking conflict is
+handled before that second guard: if an older active save conflicts after a newer
+edit has already retained pending work, the host must still receive the recovery
+request that can unblock the latest work. A production host may debounce before
+capture to reduce hashing frequency, but it must preserve both ordering rules.
+
+A host callback exception, abort, timeout, or malformed success response rejects
+the active enqueue promise and blocks the same queue with reason `failure`.
+The catch path therefore reads only document-free lifecycle metadata and invokes
+the same host-owned recovery boundary with the exact stable blocked reason. It
+does this even when the request that observed the failure belongs to an older
+edit generation, preventing retained newer work from remaining blocked behind a
+misleading saving state.
+
+Multiple callers can share one active queue outcome. The
+`durableRecoveryPending` ref therefore permits only one in-flight host recovery
+workflow for the blocked session, whether the reason is `conflict` or `failure`.
+It does not authorize recovery and is not persisted or logged. The host should
+keep one accessible recovery surface active until the supplied callback returns
+`true`; a `false` result means the session was no longer blocked or the supplied
+validator was not accepted. A malformed recovery validator fails closed without
+replacing the durable base.
+
+The example applies a fresh ten-second `AbortSignal` to each host-owned save
+request so one unresolved callback cannot retain the single-flight queue forever.
+Ten seconds is illustrative rather than a universal service-level objective. The
+host must select a bounded deadline from its own latency and reliability policy,
+and an aborted or timed-out write remains ambiguous: do not claim success,
+advance the validator, or retry automatically without idempotency evidence.
+
+The `requestDurableRecovery` function is created by the host client composition,
+not passed across the server-component serialization boundary. For `conflict`,
+it should open an accessible compare, merge, fork, discard, or authenticated
+reload workflow. For `failure`, it must first determine whether the ambiguous
+write committed, then obtain the authoritative current representation and
+server-selected strong `ETag`; it must not blindly retry the rejected evidence.
+Only after either workflow reaches a confirmed durable decision may it invoke
+the supplied callback with that recovered validator.
+
+The callback delegates to `session.resume(...)`, which validates and installs the
+new durable base immediately before retained work continues. The host may invoke
+the same supplied callback again after a `false` result, but it must not open a
+second competing recovery workflow while the first remains active. The stable
+blocked reason is control metadata, not authorization or proof of persistence.
+
+Private callback exceptions, document bodies, response values, and validators
+remain outside the generic status message. The example is intentionally
+transport-neutral beyond ordinary host `fetch()`. A production naruon
+composition should place authentication, tenant resolution, request deadlines,
+retry budgets, idempotency, telemetry, and error translation inside the host API
+layer rather than the editor module.
+
+## compose contract
+
+A naruon `compose` layer should treat Inkspan as one bounded capability module.
+It may combine editor output with templates, workflows, contextual-orchestrator,
+or other CWL services, but it must preserve these ownership rules:
+
+- Inkspan receives only the document state and non-secret behavior options needed
+ for editing.
+- The composition root resolves authorization and tenant context before the
+ panel receives a document.
+- The composition root issues a fresh opaque editing-context lifecycle value for
+ every authorized load and context transition.
+- The host client composition creates durable recovery callbacks; server
+ components pass only serializable, non-secret data into that boundary.
+- The composition root decides whether model use is allowed and which reviewed
+ contextual-orchestrator policy applies.
+- Model output returns as untrusted content and enters Inkspan through validated
+ insertion or revision-guarded restore paths.
+- Durable save success is established only by the host persistence transaction
+ and its replacement strong validator.
+- The composition root owns shutdown and cancellation when a route, workspace,
+ or application session ends.
+
+Inkspan must not read provider credentials, model credentials, database
+credentials, or host authorization tokens. It also must not infer tenant
+identity from a document body, revision digest, collaboration room name, editing
+context value, blocked reason, or server validator.
+
+## ui.panel contract
+
+A naruon `ui.panel` host should provide the surrounding product experience:
+
+- document title, owner, workspace, and classification labels;
+- save, offline, reconnecting, conflict, operational recovery, and read-only
+ status;
+- accessible conflict actions such as compare, merge, fork, discard, and retry;
+- accessible operational-failure actions such as verify, reload, resume, or
+ abandon after determining the durable state;
+- confirmation before destructive replacement;
+- model-use disclosure and user controls required by host policy;
+- navigation and focus restoration when the panel opens or closes; and
+- a support-safe error reference that excludes the document body and credentials.
+
+Use `role="status"` or another appropriate live-region pattern for asynchronous
+save state. Do not announce every keystroke. When the queue blocks, move focus to
+a labelled recovery region or dialog and provide a deterministic path back to
+the editor. Every panel heading and `aria-labelledby` target must be unique in the
+rendered page. A newer keystroke must not dismiss, duplicate, or hide a durable
+recovery workflow that still blocks retained work.
+
+## Durable autosave and conflict handling
+
+The initial validator and every successful replacement must be a server-selected
+strong `ETag`. The host persistence service must atomically compare `If-Match`
+inside the same transaction that writes the new document representation.
+
+Treat outcomes as follows:
+
+| Outcome | Host behavior |
+| --- | --- |
+| Saved with replacement strong validator | Install the returned validator before the next save begins |
+| `412 Precondition Failed` | Pause automatic progression and show exactly one accessible conflict workflow, even when a newer local edit already exists |
+| Timeout, disconnect, abort, callback exception, or malformed response | Treat as ambiguous; do not claim saved, advance the validator, or retry automatically; show exactly one operational recovery workflow |
+| Authenticated recovery load | Supply the newly confirmed server validator and original blocked reason through the recovery callback so `session.resume(...)` installs the validator before retained work continues |
+| Route or panel shutdown | Stop new work, let any active transport settle according to host policy, then discard private in-memory evidence |
+
+A local Inkspan SHA-256 revision is equality evidence for deterministic local
+operations. It is not a durable server validator and must never replace the
+host's `ETag`.
+
+## Collaboration lifecycle
+
+For real-time editing, create the `Y.Doc`, provider, room authorization, and
+awareness policy in the host composition. Inkspan may bind the supplied document
+to the editor, but it must not create or destroy the host provider.
+
+This matters when one provider is shared by multiple panels, presence surfaces,
+or background synchronization tasks. Unmounting an editor panel must not
+silently terminate collaboration used elsewhere. The host should explicitly
+destroy the provider only when the owning workspace or application lifecycle
+ends.
+
+## contextual-orchestrator integration
+
+The host may call `ContextualWisdomLab/contextual-orchestrator` for insertion,
+rewrite, review, or structured document generation. Keep the integration
+provider-neutral:
+
+1. capture one immutable Inkspan envelope and local revision;
+2. let the host authorize and dispatch the model operation;
+3. validate the returned content through Inkspan's ordinary safe-content path;
+4. apply a delayed result only with revision-guarded restore or an explicit
+ compare/merge/fork decision; and
+5. store only host-approved audit metadata, never private intermediate reasoning.
+
+The editor package does not select reasoning effort, models, credentials, prompt
+retention, or provider regions. Those decisions remain with the host and
+contextual-orchestrator policy.
+
+## Local evidence and shareable evidence
+
+**Local evidence** may contain full envelopes, conflict bodies, Yjs updates,
+awareness state, prompts, model output, tenant identifiers, server validators,
+or deployment-specific security findings. Keep it within the authorized product
+boundary and retention policy.
+
+**Shareable evidence** for support, release acceptance, procurement, or
+acquisition review should be deliberately produced from non-customer fixtures.
+Examples include exact-head CI results, package hashes, SBOMs, provenance,
+license inventories, deterministic conversion fixtures, accessibility test
+results, public API declarations, and redacted operator runbooks.
+
+Never promote local evidence to shareable evidence merely because it is hashed,
+canonicalized, encrypted, or attached to a successful CI run.
+
+## Failure checklist
+
+Before enabling the panel in production, verify that:
+
+- the server rejects unauthorized document IDs before returning content;
+- the host issues a fresh opaque editing-context lifecycle value for every
+ authorized document load and context transition;
+- the complete editor and autosave session remount together when that lifecycle
+ value changes;
+- document path segments are encoded before transport and revalidated by the
+ authorized server route;
+- every durable write uses an authenticated atomic `If-Match` transaction;
+- missing, weak, malformed, or stale validators fail closed;
+- request timeouts and cancellation are host-owned and bounded;
+- aborts and timeouts remain ambiguous rather than being reported as saved;
+- blocking conflict outcomes are processed before stale-generation status
+ suppression so a newer edit cannot hide the required recovery workflow;
+- a blocked operational save failure also invokes the host recovery workflow,
+ including when an older request first observes the failure;
+- exactly one durable recovery workflow is requested for one blocked session;
+- the stable `conflict` or `failure` reason is passed without private error data;
+- authenticated recovery installs its confirmed strong validator through
+ `session.resume(...)` before retained work continues;
+- operational recovery verifies the durable state instead of automatically
+ retrying an ambiguous write;
+- every panel instance has a unique accessible heading relationship;
+- recovery UI is keyboard-operable and announced without exposing document text
+ in generic telemetry;
+- provider and model credentials never enter client props, document envelopes,
+ error messages, or collaboration awareness state;
+- the host provider survives panel remounts when it is shared;
+- release evidence excludes tenant data and is bound to the exact package and
+ source head; and
+- rollback restores a previously verified package rather than bypassing
+ validation, security, or review gates.
+
+See [`../ARCHITECTURE.md`](../ARCHITECTURE.md) for the system diagrams,
+[`doctoring/naruon-modular-architecture.md`](doctoring/naruon-modular-architecture.md)
+for the architecture decision, and
+[`doctoring/stale-generation-conflict-recovery.md`](doctoring/stale-generation-conflict-recovery.md)
+for the concurrency repair evidence.
diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts
new file mode 100644
index 0000000..6bd0b2a
--- /dev/null
+++ b/src/architectureDocumentation.test.ts
@@ -0,0 +1,246 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+import { describe, expect, it } from 'vitest';
+
+const repositoryFile = (path: string): string =>
+ readFileSync(resolve(process.cwd(), path), 'utf8');
+
+const fencedCodeBlock = (markdown: string, language: string): string => {
+ const opening = `\`\`\`${language}\n`;
+ const start = markdown.indexOf(opening);
+ expect(start).toBeGreaterThanOrEqual(0);
+ const bodyStart = start + opening.length;
+ const end = markdown.indexOf('\n```', bodyStart);
+ expect(end).toBeGreaterThan(bodyStart);
+ return markdown.slice(bodyStart, end);
+};
+
+const markerPosition = (
+ text: string,
+ marker: string,
+ startAt = 0,
+): number => {
+ const position = text.indexOf(marker, startAt);
+ expect(position).toBeGreaterThanOrEqual(startAt);
+ return position;
+};
+
+describe('acquisition-ready modular architecture documentation', () => {
+ it('defines one authoritative standalone and modular ownership boundary', () => {
+ const architecture = repositoryFile('ARCHITECTURE.md');
+
+ expect(architecture).toContain('# Inkspan Architecture');
+ expect(architecture).toContain('## Standalone product boundary');
+ expect(architecture).toContain('## Modular MSA composition');
+ expect(architecture).toContain('## Data ownership matrix');
+ expect(architecture).toContain('## Acquisition evidence boundary');
+ expect(architecture).toContain('ContextualWisdomLab/.github');
+ expect(architecture).toContain('ContextualWisdomLab/naruon');
+ expect(architecture).toContain(
+ 'ContextualWisdomLab/contextual-orchestrator',
+ );
+ expect(architecture).toContain(
+ 'Inkspan owns editor and deterministic conversion surfaces.',
+ );
+ expect(architecture).toContain(
+ 'Hosts own transport, authorization, tenant isolation, persistence, credentials, migration, retention, and model-use policy.',
+ );
+ });
+
+ it('renders reviewable deployment and optimistic-concurrency diagrams', () => {
+ const architecture = repositoryFile('ARCHITECTURE.md');
+
+ expect(architecture).toContain('```mermaid\nflowchart LR');
+ expect(architecture).toContain('```mermaid\nsequenceDiagram');
+ expect(architecture).toContain('If-Match');
+ expect(architecture).toContain('412 Precondition Failed');
+ expect(architecture).toContain('strong ETag');
+ expect(architecture).toContain('Y.Doc');
+ expect(architecture).toContain('Office renderer');
+ });
+
+ it('provides a naruon compose and ui.panel integration contract', () => {
+ const integration = repositoryFile('docs/naruon-compose-ui-panel.md');
+
+ expect(integration).toContain('# Naruon compose and ui.panel integration');
+ expect(integration).toContain("'use client'");
+ expect(integration).toContain('compose');
+ expect(integration).toContain('ui.panel');
+ expect(integration).toContain('server-selected strong `ETag`');
+ expect(integration).toContain('accessible conflict');
+ expect(integration).toContain('local evidence');
+ expect(integration).toContain('shareable evidence');
+ expect(integration).toContain('must not read provider credentials');
+ expect(integration).toContain('must not create or destroy the host provider');
+ });
+
+ it('validates fenced autosave structure, generation ordering, and durable recovery', () => {
+ const integration = repositoryFile('docs/naruon-compose-ui-panel.md');
+ const example = fencedCodeBlock(integration, 'tsx');
+
+ expect(example).toContain('initialStrongEntityTag,');
+ expect(example).toContain("'If-Match': request.ifMatchStrongEntityTag");
+ expect(example).toContain('signal: AbortSignal.timeout(10_000),');
+ expect(example).toContain('isStrongHttpEntityTag(nextStrongEntityTag)');
+ expect(example).toContain('nextStrongEntityTag,');
+ expect(example).not.toContain('loadedStrongEntityTag:');
+ expect(example).not.toContain('request.ifMatch,');
+ expect(example).not.toContain('strongEntityTag: nextStrongEntityTag');
+ expect(example).toContain('const editGeneration = useRef(0);');
+ expect(example).toContain('type DocumentAutosaveBlockedReason,');
+ expect(example).toContain('const durableRecoveryPending = useRef(false);');
+ expect(example).toContain(
+ 'readonly requestDurableRecovery: (\n blockedReason: DocumentAutosaveBlockedReason,\n resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean,\n ) => void;',
+ );
+ expect(example).toContain(
+ 'function requestBlockedSessionRecovery(\n blockedReason: DocumentAutosaveBlockedReason,\n ): void {',
+ );
+ expect(example).toContain('if (durableRecoveryPending.current) return;');
+ expect(example).toContain('durableRecoveryPending.current = true;');
+ expect(example).toContain(
+ 'requestDurableRecovery(blockedReason, (recoveredStrongEntityTag) => {',
+ );
+ expect(example).toContain(
+ 'const resumed = session.resume(recoveredStrongEntityTag);',
+ );
+ expect(example).not.toContain('requestConflictRecovery');
+ expect(example).not.toContain('conflictRecoveryPending');
+
+ const resumeDeclaration = markerPosition(
+ example,
+ 'const resumed = session.resume(recoveredStrongEntityTag);',
+ );
+ const successfulResumeBranch = markerPosition(
+ example,
+ 'if (resumed) {',
+ resumeDeclaration,
+ );
+ const pendingRecoveryRelease = markerPosition(
+ example,
+ 'durableRecoveryPending.current = false;',
+ successfulResumeBranch,
+ );
+ const resumeReturn = markerPosition(
+ example,
+ 'return resumed;',
+ successfulResumeBranch,
+ );
+ expect(pendingRecoveryRelease).toBeLessThan(resumeReturn);
+
+ const capture = markerPosition(
+ example,
+ 'const capturedGeneration = ++editGeneration.current;',
+ );
+ const digest = markerPosition(
+ example,
+ 'await editorRef.current?.getDocumentEnvelopeRevisionEvidence();',
+ capture,
+ );
+ const firstGenerationGuard = markerPosition(
+ example,
+ 'capturedGeneration !== editGeneration.current',
+ digest,
+ );
+ const enqueue = markerPosition(
+ example,
+ 'const outcome = await session.enqueue(evidence);',
+ firstGenerationGuard,
+ );
+ const conflictBranch = markerPosition(
+ example,
+ "if (outcome.status === 'conflict') {",
+ enqueue,
+ );
+ markerPosition(
+ example,
+ "requestBlockedSessionRecovery('conflict');",
+ conflictBranch,
+ );
+ const secondGenerationGuard = markerPosition(
+ example,
+ 'capturedGeneration !== editGeneration.current',
+ conflictBranch,
+ );
+ const catchBlock = markerPosition(example, '} catch {', secondGenerationGuard);
+ const blockedSnapshot = markerPosition(
+ example,
+ "snapshot.state === 'blocked' && snapshot.blockedReason !== null",
+ catchBlock,
+ );
+ markerPosition(
+ example,
+ 'requestBlockedSessionRecovery(snapshot.blockedReason);',
+ blockedSnapshot,
+ );
+
+ expect(example).toContain('void captureAndQueueLatestDocument();');
+ expect(example).toContain('void session.close();');
+ });
+
+ it('uses instance-unique labels and remounts the complete authorized client session', () => {
+ const integration = repositoryFile('docs/naruon-compose-ui-panel.md');
+ const example = fencedCodeBlock(integration, 'tsx');
+ const doctoring = repositoryFile(
+ 'docs/doctoring/naruon-modular-architecture.md',
+ );
+ const changelog = repositoryFile('CHANGELOG.md');
+
+ expect(example).toContain('readonly editingContextId: string;');
+ expect(example).toContain('function InkspanPanelSession(');
+ expect(example).toContain('');
+ expect(example).toContain('Document editor
');
+ expect(example).not.toContain('id="document-editor-title"');
+ expect(example).toContain(
+ 'const [session] = useState',
+ );
+ expect(example).not.toContain('useMemo');
+ expect(integration).toContain(
+ 'must issue a new opaque `editingContextId` for every authorized document load',
+ );
+ expect(doctoring).toContain('cross-document state reuse');
+ expect(changelog).toContain('opaque editing-context remount');
+ });
+
+ it('records authoritative standards and the unreleased product change', () => {
+ const doctoring = repositoryFile(
+ 'docs/doctoring/naruon-modular-architecture.md',
+ );
+ const conflictDoctoring = repositoryFile(
+ 'docs/doctoring/stale-generation-conflict-recovery.md',
+ );
+ const changelog = repositoryFile('CHANGELOG.md');
+
+ expect(doctoring).toContain('APA 7 references');
+ expect(doctoring).toContain('RFC 9110');
+ expect(doctoring).toContain('WCAG 2.2');
+ expect(doctoring).toContain('NIST SP 800-204');
+ expect(doctoring).toContain('NIST SP 800-204D');
+ expect(doctoring).toContain('OWASP ASVS 5.0.0');
+ expect(doctoring).toContain('React hydrateRoot');
+ expect(doctoring).toContain('Next.js App Router');
+ expect(conflictDoctoring).toContain(
+ '# Doctoring record: stale-generation conflict recovery',
+ );
+ expect(conflictDoctoring).toContain('single-flight recovery request');
+ expect(conflictDoctoring).toContain(
+ 'blocking outcomes before stale-generation status suppression',
+ );
+ expect(conflictDoctoring).toContain(
+ 'host callback failure and a durable conflict both request recovery',
+ );
+ expect(changelog).toContain(
+ 'authoritative standalone and modular MSA architecture contract',
+ );
+ expect(changelog).toContain('naruon compose and ui.panel integration');
+ expect(changelog).toContain('stale-generation conflict recovery');
+ expect(changelog).toContain('operational save failure');
+ });
+});
diff --git a/src/workflowExactHead.test.ts b/src/workflowExactHead.test.ts
new file mode 100644
index 0000000..8a44b57
--- /dev/null
+++ b/src/workflowExactHead.test.ts
@@ -0,0 +1,61 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+import { describe, expect, it } from 'vitest';
+
+/** Read one authoritative repository file as UTF-8 text. */
+function repositoryFile(path: string): string {
+ return readFileSync(resolve(process.cwd(), path), 'utf8');
+}
+
+const workflow = repositoryFile('.github/workflows/ci.yml');
+
+const CHECKOUT_PIN =
+ 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1';
+
+describe('exact-head CI workflow contract', () => {
+ it('uses a fixed runner and checks out the immutable current PR head', () => {
+ expect(workflow).not.toContain('ubuntu-latest');
+ expect(workflow.match(/runs-on: ubuntu-24\.04/g)).toHaveLength(2);
+ expect(workflow.match(new RegExp(CHECKOUT_PIN, 'g'))).toHaveLength(2);
+ expect(
+ workflow.match(
+ /ref: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/g,
+ ),
+ ).toHaveLength(2);
+ expect(workflow.match(/persist-credentials: false/g)).toHaveLength(2);
+ });
+
+ it('keeps the workflow read-only and hash-pins every third-party action', () => {
+ expect(workflow).toContain('permissions:\n contents: read');
+ expect(workflow).not.toContain('contents: write');
+ expect(workflow).toContain(
+ 'env:\n FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true',
+ );
+
+ const usesLines = workflow
+ .split('\n')
+ .map((line) => line.trim())
+ .filter((line) => line.startsWith('- uses:'));
+ expect(usesLines.length).toBeGreaterThan(0);
+ for (const line of usesLines) {
+ expect(line).toMatch(/@[0-9a-f]{40}(?:\s+#\s+v[^\s]+)?$/u);
+ }
+ });
+
+ it('records the evidence boundary and unreleased hardening', () => {
+ const doctoring = repositoryFile(
+ 'docs/doctoring/exact-head-ci-evidence.md',
+ );
+ const changelog = repositoryFile('CHANGELOG.md');
+
+ expect(doctoring).toContain(
+ '# Doctoring record: exact-head CI evidence',
+ );
+ expect(doctoring).toContain('synthetic pull-request merge ref');
+ expect(doctoring).toContain('immutable contributor head');
+ expect(doctoring).toContain('persist-credentials: false');
+ expect(doctoring).toContain('not merge-result compatibility evidence');
+ expect(changelog).toContain('exact-head read-only CI');
+ });
+});