From a166919c08e95196b65f585a259ec927bc3afa27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:10:15 +0900 Subject: [PATCH 01/37] test(docs): define modular architecture contract --- src/architectureDocumentation.test.ts | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/architectureDocumentation.test.ts diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts new file mode 100644 index 0000000..be19067 --- /dev/null +++ b/src/architectureDocumentation.test.ts @@ -0,0 +1,77 @@ +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'); + +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('records authoritative standards and the unreleased product change', () => { + const doctoring = repositoryFile( + 'docs/doctoring/naruon-modular-architecture.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(changelog).toContain( + 'authoritative standalone and modular MSA architecture contract', + ); + expect(changelog).toContain('naruon compose and ui.panel integration'); + }); +}); From b7e5a91e8dcce6176cfd39b94f41fb93436fa8c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:11:24 +0900 Subject: [PATCH 02/37] docs(architecture): define standalone and modular MSA boundary --- ARCHITECTURE.md | 169 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 ARCHITECTURE.md 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). From 62714aadb49bbc409d383354f6d42501d8744e97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:12:43 +0900 Subject: [PATCH 03/37] docs(naruon): add compose and ui.panel integration guide --- docs/naruon-compose-ui-panel.md | 242 ++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 docs/naruon-compose-ui-panel.md diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md new file mode 100644 index 0000000..cf396c0 --- /dev/null +++ b/docs/naruon-compose-ui-panel.md @@ -0,0 +1,242 @@ +# 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, 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`, and non-secret presentation options into one +small client component. + +```tsx +// app/documents/[documentId]/inkspan-panel.tsx +'use client'; + +import { useMemo, useRef } from 'react'; +import { + CwlEditor, + type CwlEditorHandle, +} from '@contextualwisdomlab/cwl-editor'; +import { + createDocumentAutosaveSession, + type DocumentAutosaveSession, +} from '@contextualwisdomlab/cwl-editor/autosave'; +import '@contextualwisdomlab/cwl-editor/styles.css'; + +interface InkspanPanelProps { + readonly documentId: string; + readonly initialMarkdown: string; + readonly initialStrongEntityTag: string; +} + +export function InkspanPanel({ + documentId, + initialMarkdown, + initialStrongEntityTag, +}: InkspanPanelProps) { + const editorRef = useRef(null); + + const session = useMemo( + () => + createDocumentAutosaveSession({ + loadedStrongEntityTag: initialStrongEntityTag, + async save(request) { + const response = await fetch(`/api/documents/${documentId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'If-Match': request.ifMatch, + }, + body: JSON.stringify(request.evidence.envelope), + }); + + 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'); + return { + status: 'saved', + strongEntityTag: nextStrongEntityTag, + }; + }, + }), + [documentId, initialStrongEntityTag], + ); + + return ( +
+

Document editor

+ { + const evidence = + await editorRef.current?.getDocumentEnvelopeRevisionEvidence(); + if (evidence) { + await session.enqueue(evidence); + } + }} + /> +

+ Save and conflict status belongs to the host panel. +

+
+ ); +} +``` + +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 component. + +## 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 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, 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, recovery, and read-only status; +- accessible conflict actions such as compare, merge, fork, discard, and retry; +- 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 a conflict occurs, move focus +to a labelled conflict region or dialog and provide a deterministic path back to +the editor. + +## 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 the accessible conflict workflow | +| Timeout, disconnect, or malformed response | Treat as ambiguous; do not claim saved or advance the validator | +| Authenticated recovery load | Supply the newly loaded server validator to the session before resuming retained work | +| 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; +- 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; +- conflict 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 and +[`doctoring/naruon-modular-architecture.md`](doctoring/naruon-modular-architecture.md) +for standards and decision evidence. From 6267a76c91bd2b3e4f6551d48cd5b30afae729ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:14:01 +0900 Subject: [PATCH 04/37] docs(doctoring): record naruon modular architecture decision --- docs/doctoring/naruon-modular-architecture.md | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 docs/doctoring/naruon-modular-architecture.md diff --git a/docs/doctoring/naruon-modular-architecture.md b/docs/doctoring/naruon-modular-architecture.md new file mode 100644 index 0000000..9217de0 --- /dev/null +++ b/docs/doctoring/naruon-modular-architecture.md @@ -0,0 +1,219 @@ +# 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; 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, accessible conflict handling, and exact-head +release evidence. + +## 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. +- 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. +- 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 required statements remain present, but they do + not prove a deployed host is secure, accessible, 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 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 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, and restrained +live-region announcements. 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. + +## SSR and hydration boundary + +React hydrateRoot 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. + +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 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. + +## Verification + +`src/architectureDocumentation.test.ts` 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; +- 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. + +### 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. + +## 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 + +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/ From 7fa0315308fc293080b4465e47ef9afb7d0eb84e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:14:47 +0900 Subject: [PATCH 05/37] docs(changelog): record modular architecture slice --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b94ef8..bab0f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ 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 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, and current Next.js App Router guidance + ## [0.5.29] — 2026-08-05 ### Added From 93b28eb579907cf4fe6dd271c4224aaf12f20931 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:17:08 +0900 Subject: [PATCH 06/37] fix(docs): align naruon sample with autosave API --- docs/naruon-compose-ui-panel.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index cf396c0..c2bcb26 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -30,13 +30,14 @@ small client component. // app/documents/[documentId]/inkspan-panel.tsx 'use client'; -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { CwlEditor, type CwlEditorHandle, } from '@contextualwisdomlab/cwl-editor'; import { createDocumentAutosaveSession, + isStrongHttpEntityTag, type DocumentAutosaveSession, } from '@contextualwisdomlab/cwl-editor/autosave'; import '@contextualwisdomlab/cwl-editor/styles.css'; @@ -57,13 +58,13 @@ export function InkspanPanel({ const session = useMemo( () => createDocumentAutosaveSession({ - loadedStrongEntityTag: initialStrongEntityTag, + initialStrongEntityTag, async save(request) { const response = await fetch(`/api/documents/${documentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', - 'If-Match': request.ifMatch, + 'If-Match': request.ifMatchStrongEntityTag, }, body: JSON.stringify(request.evidence.envelope), }); @@ -76,15 +77,25 @@ export function InkspanPanel({ } const nextStrongEntityTag = response.headers.get('ETag'); + if (!isStrongHttpEntityTag(nextStrongEntityTag)) { + throw new Error('Document save omitted a valid strong ETag'); + } return { status: 'saved', - strongEntityTag: nextStrongEntityTag, + nextStrongEntityTag, }; }, }), [documentId, initialStrongEntityTag], ); + useEffect( + () => () => { + void session.close(); + }, + [session], + ); + return (

Document editor

@@ -111,7 +122,9 @@ export function InkspanPanel({ 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 component. +inside the host API layer rather than the editor component. It should also +translate rejected `enqueue()` promises into a redacted panel state instead of +leaving an unhandled asynchronous error. ## compose contract From 2a4179b5ff803dc0bec5cfd59177d6391b0ad659 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:19:24 +0900 Subject: [PATCH 07/37] test(docs): guard autosave capture ordering --- src/architectureDocumentation.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index be19067..be2f622 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -55,6 +55,25 @@ describe('acquisition-ready modular architecture documentation', () => { expect(integration).toContain('must not create or destroy the host provider'); }); + it('keeps the autosave example exact and rejects out-of-order capture completion', () => { + const integration = repositoryFile('docs/naruon-compose-ui-panel.md'); + + expect(integration).toContain('initialStrongEntityTag,'); + expect(integration).toContain("'If-Match': request.ifMatchStrongEntityTag"); + expect(integration).toContain('isStrongHttpEntityTag(nextStrongEntityTag)'); + expect(integration).toContain('nextStrongEntityTag,'); + expect(integration).not.toContain('loadedStrongEntityTag:'); + expect(integration).not.toContain('request.ifMatch,'); + expect(integration).not.toContain('strongEntityTag: nextStrongEntityTag'); + expect(integration).toContain('const editGeneration = useRef(0);'); + expect(integration).toContain('const capturedGeneration = ++editGeneration.current;'); + expect(integration).toContain( + 'capturedGeneration !== editGeneration.current', + ); + expect(integration).toContain('void captureAndQueueLatestDocument();'); + expect(integration).toContain('void session.close();'); + }); + it('records authoritative standards and the unreleased product change', () => { const doctoring = repositoryFile( 'docs/doctoring/naruon-modular-architecture.md', From 941374c3947488938aba83b47a800169d726d483 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:20:37 +0900 Subject: [PATCH 08/37] fix(docs): prevent stale autosave capture ordering --- docs/naruon-compose-ui-panel.md | 60 +++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index c2bcb26..3b72543 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -30,7 +30,7 @@ small client component. // app/documents/[documentId]/inkspan-panel.tsx 'use client'; -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { CwlEditor, type CwlEditorHandle, @@ -54,6 +54,8 @@ export function InkspanPanel({ initialStrongEntityTag, }: InkspanPanelProps) { const editorRef = useRef(null); + const editGeneration = useRef(0); + const [saveMessage, setSaveMessage] = useState('Document loaded.'); const session = useMemo( () => @@ -91,11 +93,46 @@ export function InkspanPanel({ useEffect( () => () => { + editGeneration.current += 1; void session.close(); }, [session], ); + 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 (capturedGeneration !== editGeneration.current) { + return; + } + + if (outcome.status === 'conflict') { + setSaveMessage('Saving paused. Resolve the durable document conflict.'); + } else if (outcome.status === 'closed') { + setSaveMessage('Saving is unavailable because this session closed.'); + } else { + setSaveMessage('All current changes are saved or queued.'); + } + } catch { + if (capturedGeneration === editGeneration.current) { + setSaveMessage('Saving paused. Use the host recovery action.'); + } + } + } + return (

Document editor

@@ -103,28 +140,29 @@ export function InkspanPanel({ ref={editorRef} mode="markdown" defaultValue={initialMarkdown} - onChange={async () => { - const evidence = - await editorRef.current?.getDocumentEnvelopeRevisionEvidence(); - if (evidence) { - await session.enqueue(evidence); - } + onChange={() => { + void captureAndQueueLatestDocument(); }} />

- Save and conflict status belongs to the host panel. + {saveMessage}

); } ``` +The generation guard prevents an older, slower asynchronous envelope digest from +being enqueued after a newer edit. A production host may debounce before capture +to reduce hashing frequency, but it must preserve the same latest-generation +ordering rule. + 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 component. It should also -translate rejected `enqueue()` promises into a redacted panel state instead of -leaving an unhandled asynchronous error. +inside the host API layer rather than the editor component. It should translate +redacted failure states into explicit recovery actions rather than expose private +callback errors or document bodies. ## compose contract From ff8ea7d26bbbc7b58eae13f75c3cce44a2ea0c3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:24:25 +0900 Subject: [PATCH 09/37] test(docs): require isolated authorized editing contexts --- src/architectureDocumentation.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index be2f622..7a58465 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -74,6 +74,25 @@ describe('acquisition-ready modular architecture documentation', () => { expect(integration).toContain('void session.close();'); }); + it('remounts the complete client session when the authorized editing context changes', () => { + const integration = repositoryFile('docs/naruon-compose-ui-panel.md'); + const doctoring = repositoryFile( + 'docs/doctoring/naruon-modular-architecture.md', + ); + const changelog = repositoryFile('CHANGELOG.md'); + + expect(integration).toContain('readonly editingContextId: string;'); + expect(integration).toContain('function InkspanPanelSession('); + expect(integration).toContain(' { const doctoring = repositoryFile( 'docs/doctoring/naruon-modular-architecture.md', From 84b610eaae98557c089b56fe2ce06b8ed6153a0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:25:17 +0900 Subject: [PATCH 10/37] fix(docs): isolate authorized editor session lifecycles --- docs/naruon-compose-ui-panel.md | 35 +++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index 3b72543..0e348be 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -23,8 +23,8 @@ A correct integration should: 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`, and non-secret presentation options into one -small client component. +server-selected strong `ETag`, an opaque non-secret editing-context lifecycle +identifier, and non-secret presentation options into one small client component. ```tsx // app/documents/[documentId]/inkspan-panel.tsx @@ -43,12 +43,17 @@ import { import '@contextualwisdomlab/cwl-editor/styles.css'; interface InkspanPanelProps { + readonly editingContextId: string; readonly documentId: string; readonly initialMarkdown: string; readonly initialStrongEntityTag: string; } -export function InkspanPanel({ +export function InkspanPanel(props: InkspanPanelProps) { + return ; +} + +function InkspanPanelSession({ documentId, initialMarkdown, initialStrongEntityTag, @@ -62,7 +67,8 @@ export function InkspanPanel({ createDocumentAutosaveSession({ initialStrongEntityTag, async save(request) { - const response = await fetch(`/api/documents/${documentId}`, { + const encodedDocumentId = encodeURIComponent(documentId); + const response = await fetch(`/api/documents/${encodedDocumentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -152,6 +158,15 @@ export function InkspanPanel({ } ``` +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 generation guard prevents an older, slower asynchronous envelope digest from being enqueued after a newer edit. A production host may debounce before capture to reduce hashing frequency, but it must preserve the same latest-generation @@ -174,6 +189,8 @@ or other CWL services, but it must preserve these ownership rules: 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 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 @@ -185,8 +202,8 @@ or other CWL services, but it must preserve these ownership rules: 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, or -server validator. +identity from a document body, revision digest, collaboration room name, editing +context value, or server validator. ## ui.panel contract @@ -275,6 +292,12 @@ canonicalized, encrypted, or attached to a successful CI run. 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; From 5ca3ac2647a7bc43ef8bbe7ebf28fb7c384c2d13 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:26:23 +0900 Subject: [PATCH 11/37] test(docs): reject memoized autosave session identity --- src/architectureDocumentation.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index 7a58465..fe73963 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -86,6 +86,13 @@ describe('acquisition-ready modular architecture documentation', () => { expect(integration).toContain('', + ); + expect(integration).not.toContain('useMemo'); expect(integration).toContain( 'must issue a new opaque `editingContextId` for every authorized document load', ); From 8d472d70f7ea00f5a1a056c47cd285c3f066db42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:28:26 +0900 Subject: [PATCH 12/37] fix(docs): bind autosave identity to keyed client state --- docs/naruon-compose-ui-panel.md | 74 +++++++++++++++++---------------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index 0e348be..93365e8 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -30,7 +30,7 @@ identifier, and non-secret presentation options into one small client component. // app/documents/[documentId]/inkspan-panel.tsx 'use client'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { CwlEditor, type CwlEditorHandle, @@ -61,40 +61,37 @@ function InkspanPanelSession({ const editorRef = useRef(null); const editGeneration = useRef(0); const [saveMessage, setSaveMessage] = useState('Document loaded.'); - - const session = useMemo( - () => - 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), - }); - - 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, - }; - }, - }), - [documentId, initialStrongEntityTag], + 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), + }); + + 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( @@ -167,6 +164,13 @@ 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. + The generation guard prevents an older, slower asynchronous envelope digest from being enqueued after a newer edit. A production host may debounce before capture to reduce hashing frequency, but it must preserve the same latest-generation From 3c6dbff3286f884927c8b65c55dc25bc4f5ac4ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:29:50 +0900 Subject: [PATCH 13/37] docs(doctoring): record authorized editing-context isolation --- docs/doctoring/naruon-modular-architecture.md | 83 ++++++++++++++++++- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/naruon-modular-architecture.md b/docs/doctoring/naruon-modular-architecture.md index 9217de0..4657cf3 100644 --- a/docs/doctoring/naruon-modular-architecture.md +++ b/docs/doctoring/naruon-modular-architecture.md @@ -19,7 +19,9 @@ system view that explained: - 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; 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. @@ -41,8 +43,11 @@ The architecture explicitly states: 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, accessible conflict handling, and exact-head -release evidence. +server-selected strong validators, accessible conflict handling, 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 @@ -55,6 +60,9 @@ release evidence. - 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. +- A host-selected opaque editing-context key prevents uncontrolled editor state, + durable validators, and in-flight digest completion from crossing authorized + document boundaries. - Acquisition reviewers can distinguish reproducible product evidence from restricted customer or deployment evidence. - The diagrams are version-controlled, text-reviewable, and render directly in @@ -65,6 +73,10 @@ release evidence. - 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. - 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 required statements remain present, but they do @@ -77,6 +89,8 @@ 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 persistence service selects and atomically enforces strong entity tags; - the collaboration host owns room authorization, update persistence, and provider lifecycle; @@ -85,6 +99,13 @@ The decision follows the least-authority split recommended for modular systems: - 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 @@ -98,15 +119,29 @@ live-region announcements. 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 -React hydrateRoot attaches an interactive tree to server-generated markup. +React `hydrateRoot` 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. + 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. @@ -136,6 +171,11 @@ 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` fails unless: @@ -145,6 +185,11 @@ make restricted evidence shareable. - 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; +- the latest-generation guard prevents an older asynchronous digest from being + enqueued after a newer edit; - this doctoring record retains the authoritative standards references; and - `CHANGELOG.md` records the unreleased buyer-visible documentation slice. @@ -165,6 +210,22 @@ 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. + ### Store provider credentials in panel props or environment-reading editor code Rejected because browser props are observable and package-level environment @@ -191,6 +252,11 @@ 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 @@ -208,6 +274,15 @@ 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/ From 8be2b5f1d5cd837b0668079598dd641645ce255d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:30:32 +0900 Subject: [PATCH 14/37] docs(changelog): record isolated naruon editing contexts --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bab0f4f..a286a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim ### 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 - 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, and current Next.js App Router guidance ## [0.5.29] — 2026-08-05 From edf5f0f070e0055f2b280c3f2396c01fac8c5588 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:36:00 +0900 Subject: [PATCH 15/37] fix(docs): preserve editing-context contract phrase --- docs/naruon-compose-ui-panel.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index 93365e8..ce184db 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -155,9 +155,9 @@ function InkspanPanelSession({ } ``` -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 +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 From ae0debd4f3fd74f8e253df46b40d307154be76e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:36:53 +0900 Subject: [PATCH 16/37] fix(docs): preserve hydration contract phrase --- docs/doctoring/naruon-modular-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/naruon-modular-architecture.md b/docs/doctoring/naruon-modular-architecture.md index 4657cf3..1e13c5d 100644 --- a/docs/doctoring/naruon-modular-architecture.md +++ b/docs/doctoring/naruon-modular-architecture.md @@ -125,7 +125,7 @@ and must not leave focus on a removed node or announce private document content. ## SSR and hydration boundary -React `hydrateRoot` attaches an interactive tree to server-generated markup. +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 From eb784d0559f4dad2741cd5bba8f24223d3a6f255 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 02:05:23 +0900 Subject: [PATCH 17/37] test(docs): require bounded recoverable panel example --- src/architectureDocumentation.test.ts | 104 ++++++++++++++++++++------ 1 file changed, 81 insertions(+), 23 deletions(-) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index fe73963..8baf6f4 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -6,6 +6,26 @@ 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'); @@ -55,44 +75,82 @@ describe('acquisition-ready modular architecture documentation', () => { expect(integration).toContain('must not create or destroy the host provider'); }); - it('keeps the autosave example exact and rejects out-of-order capture completion', () => { + it('validates fenced autosave structure and rejects out-of-order capture completion', () => { const integration = repositoryFile('docs/naruon-compose-ui-panel.md'); + const example = fencedCodeBlock(integration, 'tsx'); - expect(integration).toContain('initialStrongEntityTag,'); - expect(integration).toContain("'If-Match': request.ifMatchStrongEntityTag"); - expect(integration).toContain('isStrongHttpEntityTag(nextStrongEntityTag)'); - expect(integration).toContain('nextStrongEntityTag,'); - expect(integration).not.toContain('loadedStrongEntityTag:'); - expect(integration).not.toContain('request.ifMatch,'); - expect(integration).not.toContain('strongEntityTag: nextStrongEntityTag'); - expect(integration).toContain('const editGeneration = useRef(0);'); - expect(integration).toContain('const capturedGeneration = ++editGeneration.current;'); - expect(integration).toContain( + 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( + 'readonly requestConflictRecovery: (\n resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean,\n ) => void;', + ); + expect(example).toContain( + 'requestConflictRecovery((recoveredStrongEntityTag) => {', + ); + expect(example).toContain( + 'const resumed = session.resume(recoveredStrongEntityTag);', + ); + + 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, + ); + markerPosition( + example, 'capturedGeneration !== editGeneration.current', + enqueue, ); - expect(integration).toContain('void captureAndQueueLatestDocument();'); - expect(integration).toContain('void session.close();'); + + expect(example).toContain('void captureAndQueueLatestDocument();'); + expect(example).toContain('void session.close();'); }); - it('remounts the complete client session when the authorized editing context changes', () => { + 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(integration).toContain('readonly editingContextId: string;'); - expect(integration).toContain('function InkspanPanelSession('); - expect(integration).toContain(''); + expect(example).toContain('

Document editor

'); + expect(example).not.toContain('id="document-editor-title"'); + expect(example).toContain( 'const [session] = useState', ); - expect(integration).not.toContain('useMemo'); + expect(example).not.toContain('useMemo'); expect(integration).toContain( 'must issue a new opaque `editingContextId` for every authorized document load', ); From 3dd8434e9b50fcca7ba98a5b593066f0305189d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 02:06:55 +0900 Subject: [PATCH 18/37] docs: bound autosave recovery and panel labels --- docs/naruon-compose-ui-panel.md | 61 ++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index ce184db..3fd5bd7 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -24,13 +24,15 @@ A correct integration should: 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 client component. +identifier, and non-secret presentation options into one small host client +boundary. That host client boundary creates the conflict-recovery callback; a +server component must not attempt to serialize a function prop. ```tsx // app/documents/[documentId]/inkspan-panel.tsx 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useId, useRef, useState } from 'react'; import { CwlEditor, type CwlEditorHandle, @@ -47,6 +49,9 @@ interface InkspanPanelProps { readonly documentId: string; readonly initialMarkdown: string; readonly initialStrongEntityTag: string; + readonly requestConflictRecovery: ( + resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean, + ) => void; } export function InkspanPanel(props: InkspanPanelProps) { @@ -57,7 +62,9 @@ function InkspanPanelSession({ documentId, initialMarkdown, initialStrongEntityTag, + requestConflictRecovery, }: InkspanPanelProps) { + const titleId = useId(); const editorRef = useRef(null); const editGeneration = useRef(0); const [saveMessage, setSaveMessage] = useState('Document loaded.'); @@ -73,6 +80,7 @@ function InkspanPanelSession({ 'If-Match': request.ifMatchStrongEntityTag, }, body: JSON.stringify(request.evidence.envelope), + signal: AbortSignal.timeout(10_000), }); if (response.status === 412) { @@ -124,6 +132,16 @@ function InkspanPanelSession({ if (outcome.status === 'conflict') { setSaveMessage('Saving paused. Resolve the durable document conflict.'); + requestConflictRecovery((recoveredStrongEntityTag) => { + const resumed = session.resume(recoveredStrongEntityTag); + if ( + resumed && + capturedGeneration === editGeneration.current + ) { + setSaveMessage('Recovered changes resumed with a durable validator.'); + } + return resumed; + }); } else if (outcome.status === 'closed') { setSaveMessage('Saving is unavailable because this session closed.'); } else { @@ -137,8 +155,8 @@ function InkspanPanelSession({ } return ( -
-

Document editor

+
+

Document editor

Date: Thu, 6 Aug 2026 02:08:01 +0900 Subject: [PATCH 19/37] docs: record bounded recovery verification --- docs/doctoring/naruon-modular-architecture.md | 78 +++++++++++++++---- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/docs/doctoring/naruon-modular-architecture.md b/docs/doctoring/naruon-modular-architecture.md index 1e13c5d..4e07af6 100644 --- a/docs/doctoring/naruon-modular-architecture.md +++ b/docs/doctoring/naruon-modular-architecture.md @@ -36,18 +36,18 @@ Add one authoritative root `ARCHITECTURE.md` and one beginner-readable naruon 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, accessible conflict handling, 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. +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 @@ -59,10 +59,12 @@ authorization and route validation. 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. +- 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 @@ -77,10 +79,13 @@ authorization and route validation. 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 required statements remain present, but they do - not prove a deployed host is secure, accessible, or conformant. +- 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 @@ -91,6 +96,10 @@ The decision follows the least-authority split recommended for modular systems: 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; @@ -114,10 +123,11 @@ 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, and restrained -live-region announcements. 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. +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 @@ -142,6 +152,10 @@ 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. @@ -154,6 +168,13 @@ 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. @@ -178,7 +199,8 @@ contract without recording a tenant-derived value. ## Verification -`src/architectureDocumentation.test.ts` fails unless: +`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; @@ -188,8 +210,13 @@ contract without recording a tenant-derived value. - 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; -- the latest-generation guard prevents an older asynchronous digest from being - enqueued after a newer edit; +- 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. @@ -226,6 +253,25 @@ 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 From 2f8035079e87567e1788e8296101ae85ffbfbdd7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 02:08:48 +0900 Subject: [PATCH 20/37] docs: record bounded panel recovery contract --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a286a5f..37fe60f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - 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 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, and current Next.js App Router guidance ## [0.5.29] — 2026-08-05 From 43a211b0818636016e2e80d9ceaaad5ab7af1fd7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:29:42 +0900 Subject: [PATCH 21/37] test: require stale-generation conflict recovery --- src/architectureDocumentation.test.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index 8baf6f4..ee33091 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -75,7 +75,7 @@ describe('acquisition-ready modular architecture documentation', () => { expect(integration).toContain('must not create or destroy the host provider'); }); - it('validates fenced autosave structure and rejects out-of-order capture completion', () => { + it('validates fenced autosave structure, generation ordering, and conflict recovery', () => { const integration = repositoryFile('docs/naruon-compose-ui-panel.md'); const example = fencedCodeBlock(integration, 'tsx'); @@ -88,9 +88,13 @@ describe('acquisition-ready modular architecture documentation', () => { expect(example).not.toContain('request.ifMatch,'); expect(example).not.toContain('strongEntityTag: nextStrongEntityTag'); expect(example).toContain('const editGeneration = useRef(0);'); + expect(example).toContain('const conflictRecoveryPending = useRef(false);'); expect(example).toContain( 'readonly requestConflictRecovery: (\n resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean,\n ) => void;', ); + expect(example).toContain('function requestDurableConflictRecovery(): void {'); + expect(example).toContain('if (conflictRecoveryPending.current) return;'); + expect(example).toContain('conflictRecoveryPending.current = true;'); expect(example).toContain( 'requestConflictRecovery((recoveredStrongEntityTag) => {', ); @@ -117,10 +121,20 @@ describe('acquisition-ready modular architecture documentation', () => { 'const outcome = await session.enqueue(evidence);', firstGenerationGuard, ); + const conflictBranch = markerPosition( + example, + "if (outcome.status === 'conflict') {", + enqueue, + ); + markerPosition( + example, + 'requestDurableConflictRecovery();', + conflictBranch, + ); markerPosition( example, 'capturedGeneration !== editGeneration.current', - enqueue, + conflictBranch, ); expect(example).toContain('void captureAndQueueLatestDocument();'); From 984a35b3dfb140b8f1099e0413f80fc4d1103e9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:34:43 +0900 Subject: [PATCH 22/37] test: bind conflict recovery doctoring --- src/architectureDocumentation.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index ee33091..7049a6f 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -176,6 +176,9 @@ describe('acquisition-ready modular architecture documentation', () => { 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'); @@ -186,9 +189,17 @@ describe('acquisition-ready modular architecture documentation', () => { 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(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'); }); }); From bdfb75179f42cb10217803248685bc4e79578d05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:36:46 +0900 Subject: [PATCH 23/37] docs: preserve recovery across newer edits --- docs/naruon-compose-ui-panel.md | 105 ++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 24 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index 3fd5bd7..0099429 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -67,6 +67,7 @@ function InkspanPanelSession({ const titleId = useId(); const editorRef = useRef(null); const editGeneration = useRef(0); + const conflictRecoveryPending = useRef(false); const [saveMessage, setSaveMessage] = useState('Document loaded.'); const [session] = useState(() => createDocumentAutosaveSession({ @@ -110,6 +111,40 @@ function InkspanPanelSession({ [session], ); + function requestDurableConflictRecovery(): void { + if (conflictRecoveryPending.current) return; + conflictRecoveryPending.current = true; + setSaveMessage('Saving paused. Resolve the durable document conflict.'); + + try { + requestConflictRecovery((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 { + conflictRecoveryPending.current = false; + } + }); + } catch { + conflictRecoveryPending.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.'); @@ -126,29 +161,24 @@ function InkspanPanelSession({ } const outcome = await session.enqueue(evidence); + if (outcome.status === 'conflict') { + requestDurableConflictRecovery(); + return; + } if (capturedGeneration !== editGeneration.current) { return; } - if (outcome.status === 'conflict') { - setSaveMessage('Saving paused. Resolve the durable document conflict.'); - requestConflictRecovery((recoveredStrongEntityTag) => { - const resumed = session.resume(recoveredStrongEntityTag); - if ( - resumed && - capturedGeneration === editGeneration.current - ) { - setSaveMessage('Recovered changes resumed with a durable validator.'); - } - return resumed; - }); - } else if (outcome.status === 'closed') { + if (outcome.status === 'closed') { setSaveMessage('Saving is unavailable because this session closed.'); } else { setSaveMessage('All current changes are saved or queued.'); } } catch { - if (capturedGeneration === editGeneration.current) { + const snapshot = session.getSnapshot(); + if (snapshot.state === 'blocked') { + setSaveMessage('Saving paused. Use the host recovery action.'); + } else if (capturedGeneration === editGeneration.current) { setSaveMessage('Saving paused. Use the host recovery action.'); } } @@ -194,10 +224,27 @@ 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 generation guard prevents an older, slower asynchronous envelope digest from -being enqueued after a newer edit. A production host may debounce before capture -to reduce hashing frequency, but it must preserve the same latest-generation -ordering rule. +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. + +Multiple callers can share one active queue outcome. The +`conflictRecoveryPending` ref therefore permits only one in-flight host recovery +workflow for the blocked session. 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 catch path reads only document-free session lifecycle metadata. It surfaces a +blocked queue even when the request that observed the failure belongs to an older +edit generation, preventing a newer retained save from leaving the UI stuck on a +misleading saving state. Private callback exceptions, document bodies, and +validators remain outside the generic status message. 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. @@ -212,7 +259,9 @@ accessible compare, merge, fork, or discard workflow. Only after that workflow performs an authenticated durable reload or equivalent confirmed decision may it invoke the supplied callback with the recovered server-selected strong `ETag`. The callback delegates to `session.resume(...)`, which validates and installs the -new durable base immediately before retained work continues. +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 example is intentionally transport-neutral beyond ordinary host `fetch()`. A production naruon composition should place authentication, tenant resolution, @@ -265,7 +314,8 @@ Use `role="status"` or another appropriate live-region pattern for asynchronous save state. Do not announce every keystroke. When a conflict occurs, move focus to a labelled conflict 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. +rendered page. A newer keystroke must not dismiss, duplicate, or hide a durable +conflict workflow that still blocks retained work. ## Durable autosave and conflict handling @@ -278,8 +328,8 @@ 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 the accessible conflict workflow | -| Timeout, disconnect, abort, or malformed response | Treat as ambiguous; do not claim saved or advance the validator | +| `412 Precondition Failed` | Pause automatic progression and show exactly one accessible conflict workflow, even when a newer local edit already exists | +| Timeout, disconnect, abort, or malformed response | Treat as ambiguous; do not claim saved or advance the validator; expose the blocked recovery action without private callback detail | | Authenticated recovery load | Supply the newly loaded server validator through the recovery callback so `session.resume(...)` installs it 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 | @@ -347,8 +397,13 @@ Before enabling the panel in production, verify that: - 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; +- only one conflict-recovery workflow is requested for one blocked session; - authenticated conflict recovery installs its strong validator through `session.resume(...)` before retained work continues; +- blocked operational failures remain visible even when an older request first + observes the failure; - every panel instance has a unique accessible heading relationship; - conflict UI is keyboard-operable and announced without exposing document text in generic telemetry; @@ -360,6 +415,8 @@ Before enabling the panel in production, verify that: - rollback restores a previously verified package rather than bypassing validation, security, or review gates. -See [`../ARCHITECTURE.md`](../ARCHITECTURE.md) for the system diagrams and +See [`../ARCHITECTURE.md`](../ARCHITECTURE.md) for the system diagrams, [`doctoring/naruon-modular-architecture.md`](doctoring/naruon-modular-architecture.md) -for standards and decision evidence. +for the architecture decision, and +[`doctoring/stale-generation-conflict-recovery.md`](doctoring/stale-generation-conflict-recovery.md) +for the concurrency repair evidence. From 01ee6a6d2f6c2420172f8cfd2eb7e81d61e1578d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:39:01 +0900 Subject: [PATCH 24/37] docs: record stale conflict recovery decision --- .../stale-generation-conflict-recovery.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/doctoring/stale-generation-conflict-recovery.md diff --git a/docs/doctoring/stale-generation-conflict-recovery.md b/docs/doctoring/stale-generation-conflict-recovery.md new file mode 100644 index 0000000..3030b60 --- /dev/null +++ b/docs/doctoring/stale-generation-conflict-recovery.md @@ -0,0 +1,163 @@ +# 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 + conflict 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 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. + +## Decision + +Use two guards with separate responsibilities. + +The first guard remains between digest completion and enqueue. It rejects a stale +capture before that document can enter the durable queue. + +After enqueue settles, process 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. + +The panel keeps one local `conflictRecoveryPending` 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. + +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 visible even +when an older edit generation observed the failure. Blocking outcomes before +stale-generation status suppression is therefore an explicit integration +invariant. + +## Ownership boundary + +Inkspan continues to own only deterministic document evidence, the local +single-flight queue, 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, and reload workflows; +- selection of the recovered server validator; and +- private transport and incident telemetry. + +The local pending flag is not a tenant identifier, audit record, distributed +lock, 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 several shared callers from opening 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 durable conflict 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, and restoration to the editor after resolution. + +## Test-first evidence + +Commit `43a211b0818636016e2e80d9ceaaad5ab7af1fd7` added the ordering and +single-flight recovery contract before the guide implemented it. Exact 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. + +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. + +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. The red run is historical +TDD evidence and is 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. + +### Resume automatically with the previous validator + +Rejected because the previous validator is precisely the value rejected by the +durable service. Automatic retry would violate the 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 conflict + +Rejected because the conflict blocks the queue, not only one visual generation. +Recovery must remain available to unblock retained newer work. + +## Rollback + +Rollback restores the prior example and removes this record and its documentation +contract. Such a rollback also restores the known risk that a newer edit can hide +a queue-wide conflict. 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 From ba82ebbe3e43f6495d463b048ea8ae086fc26603 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:40:08 +0900 Subject: [PATCH 25/37] docs: record stale-generation conflict recovery --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37fe60f..30812bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - 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 ordering and one single-flight recovery request so a newer local edit cannot hide or duplicate the durable conflict workflow while retained work remains blocked - 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, and current Next.js App Router guidance ## [0.5.29] — 2026-08-05 From 10a79e71ca6c404ddc374e55332661046d0f0c30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:43:18 +0900 Subject: [PATCH 26/37] docs: make recovery invariant verifiable --- .../stale-generation-conflict-recovery.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/doctoring/stale-generation-conflict-recovery.md b/docs/doctoring/stale-generation-conflict-recovery.md index 3030b60..4df54c8 100644 --- a/docs/doctoring/stale-generation-conflict-recovery.md +++ b/docs/doctoring/stale-generation-conflict-recovery.md @@ -35,11 +35,11 @@ Use two guards with separate responsibilities. The first guard remains between digest completion and enqueue. It rejects a stale capture before that document can enter the durable queue. -After enqueue settles, process 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. +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. The panel keeps one local `conflictRecoveryPending` ref and issues one single-flight recovery request for one blocked session. Multiple callers may @@ -55,9 +55,8 @@ exception. Operational callback failures are handled from the document-free session snapshot. If the queue is blocked, the host recovery action remains visible even -when an older edit generation observed the failure. Blocking outcomes before -stale-generation status suppression is therefore an explicit integration -invariant. +when an older edit generation observed the failure. The ordering invariant is +therefore queue-wide rather than generation-local. ## Ownership boundary @@ -99,10 +98,11 @@ labelling, and restoration to the editor after resolution. ## Test-first evidence Commit `43a211b0818636016e2e80d9ceaaad5ab7af1fd7` added the ordering and -single-flight recovery contract before the guide implemented it. Exact 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. +single-flight recovery 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 From 14096f37f1aa47fb7f6661fea3b505193680aaf8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:44:25 +0900 Subject: [PATCH 27/37] test: require exact-head read-only CI --- src/workflowExactHead.test.ts | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/workflowExactHead.test.ts diff --git a/src/workflowExactHead.test.ts b/src/workflowExactHead.test.ts new file mode 100644 index 0000000..b0620b2 --- /dev/null +++ b/src/workflowExactHead.test.ts @@ -0,0 +1,43 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const workflow = readFileSync( + resolve(process.cwd(), '.github/workflows/ci.yml'), + 'utf8', +); + +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); + } + }); +}); From e68781a8a2c96d5b103900e2622bf7de59925706 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:48:27 +0900 Subject: [PATCH 28/37] test: bind exact-head CI doctoring --- src/workflowExactHead.test.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/workflowExactHead.test.ts b/src/workflowExactHead.test.ts index b0620b2..8a44b57 100644 --- a/src/workflowExactHead.test.ts +++ b/src/workflowExactHead.test.ts @@ -3,10 +3,12 @@ import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; -const workflow = readFileSync( - resolve(process.cwd(), '.github/workflows/ci.yml'), - 'utf8', -); +/** 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'; @@ -40,4 +42,20 @@ describe('exact-head CI workflow contract', () => { 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'); + }); }); From bf348d6d2ad00589a990bc447f90548709533b4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:51:23 +0900 Subject: [PATCH 29/37] ci: verify immutable pull request heads --- .github/workflows/ci.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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 }} From 6cc2c2a8808372d8c2333a566ecd215604ef95a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:52:13 +0900 Subject: [PATCH 30/37] docs: record exact-head CI boundary --- docs/doctoring/exact-head-ci-evidence.md | 144 +++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/doctoring/exact-head-ci-evidence.md 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 From ed404dfff4c26bcf69a3e67383c133ffaa9b1016 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:53:30 +0900 Subject: [PATCH 31/37] docs: record exact-head read-only CI --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30812bb..ada8687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - 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 ordering and one single-flight recovery request so a newer local edit cannot hide or duplicate the durable conflict workflow while retained work remains blocked -- 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, and current Next.js App Router guidance +- 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 From f6cdcf4c5879c4c1661731590d9295fb61485205 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 11:58:16 +0900 Subject: [PATCH 32/37] test: require operational autosave recovery --- src/architectureDocumentation.test.ts | 38 ++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index 7049a6f..0f6b59c 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -75,7 +75,7 @@ describe('acquisition-ready modular architecture documentation', () => { expect(integration).toContain('must not create or destroy the host provider'); }); - it('validates fenced autosave structure, generation ordering, and conflict recovery', () => { + it('validates fenced autosave structure, generation ordering, and durable recovery', () => { const integration = repositoryFile('docs/naruon-compose-ui-panel.md'); const example = fencedCodeBlock(integration, 'tsx'); @@ -88,19 +88,24 @@ describe('acquisition-ready modular architecture documentation', () => { expect(example).not.toContain('request.ifMatch,'); expect(example).not.toContain('strongEntityTag: nextStrongEntityTag'); expect(example).toContain('const editGeneration = useRef(0);'); - expect(example).toContain('const conflictRecoveryPending = useRef(false);'); + expect(example).toContain('type DocumentAutosaveBlockedReason,'); + expect(example).toContain('const durableRecoveryPending = useRef(false);'); expect(example).toContain( - 'readonly requestConflictRecovery: (\n resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean,\n ) => void;', + 'readonly requestDurableRecovery: (\n blockedReason: DocumentAutosaveBlockedReason,\n resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean,\n ) => void;', ); - expect(example).toContain('function requestDurableConflictRecovery(): void {'); - expect(example).toContain('if (conflictRecoveryPending.current) return;'); - expect(example).toContain('conflictRecoveryPending.current = true;'); expect(example).toContain( - 'requestConflictRecovery((recoveredStrongEntityTag) => {', + '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 capture = markerPosition( example, @@ -128,14 +133,25 @@ describe('acquisition-ready modular architecture documentation', () => { ); markerPosition( example, - 'requestDurableConflictRecovery();', + "requestBlockedSessionRecovery('conflict');", conflictBranch, ); - markerPosition( + 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();'); @@ -196,10 +212,14 @@ describe('acquisition-ready modular architecture documentation', () => { 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'); }); }); From dd8edbe8e4b8953ed5ef91fe864c052879b79b07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 12:03:47 +0900 Subject: [PATCH 33/37] docs: recover blocked operational saves --- docs/naruon-compose-ui-panel.md | 129 +++++++++++++++++++------------- 1 file changed, 77 insertions(+), 52 deletions(-) diff --git a/docs/naruon-compose-ui-panel.md b/docs/naruon-compose-ui-panel.md index 0099429..6cb069e 100644 --- a/docs/naruon-compose-ui-panel.md +++ b/docs/naruon-compose-ui-panel.md @@ -15,7 +15,8 @@ A correct integration should: 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, recovery, and unsaved-state experience; and +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. @@ -25,7 +26,7 @@ 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 conflict-recovery callback; a +boundary. That host client boundary creates the durable-recovery callback; a server component must not attempt to serialize a function prop. ```tsx @@ -40,6 +41,7 @@ import { import { createDocumentAutosaveSession, isStrongHttpEntityTag, + type DocumentAutosaveBlockedReason, type DocumentAutosaveSession, } from '@contextualwisdomlab/cwl-editor/autosave'; import '@contextualwisdomlab/cwl-editor/styles.css'; @@ -49,7 +51,8 @@ interface InkspanPanelProps { readonly documentId: string; readonly initialMarkdown: string; readonly initialStrongEntityTag: string; - readonly requestConflictRecovery: ( + readonly requestDurableRecovery: ( + blockedReason: DocumentAutosaveBlockedReason, resumeWithStrongEntityTag: (recoveredStrongEntityTag: string) => boolean, ) => void; } @@ -62,12 +65,12 @@ function InkspanPanelSession({ documentId, initialMarkdown, initialStrongEntityTag, - requestConflictRecovery, + requestDurableRecovery, }: InkspanPanelProps) { const titleId = useId(); const editorRef = useRef(null); const editGeneration = useRef(0); - const conflictRecoveryPending = useRef(false); + const durableRecoveryPending = useRef(false); const [saveMessage, setSaveMessage] = useState('Document loaded.'); const [session] = useState(() => createDocumentAutosaveSession({ @@ -111,13 +114,19 @@ function InkspanPanelSession({ [session], ); - function requestDurableConflictRecovery(): void { - if (conflictRecoveryPending.current) return; - conflictRecoveryPending.current = true; - setSaveMessage('Saving paused. Resolve the durable document conflict.'); + 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 { - requestConflictRecovery((recoveredStrongEntityTag) => { + requestDurableRecovery(blockedReason, (recoveredStrongEntityTag) => { try { const resumed = session.resume(recoveredStrongEntityTag); if (resumed) { @@ -134,11 +143,11 @@ function InkspanPanelSession({ } return false; } finally { - conflictRecoveryPending.current = false; + durableRecoveryPending.current = false; } }); } catch { - conflictRecoveryPending.current = false; + durableRecoveryPending.current = false; if (session.getSnapshot().state === 'blocked') { setSaveMessage('Saving paused. Use the host recovery action.'); } @@ -162,7 +171,7 @@ function InkspanPanelSession({ const outcome = await session.enqueue(evidence); if (outcome.status === 'conflict') { - requestDurableConflictRecovery(); + requestBlockedSessionRecovery('conflict'); return; } if (capturedGeneration !== editGeneration.current) { @@ -176,8 +185,10 @@ function InkspanPanelSession({ } } catch { const snapshot = session.getSnapshot(); - if (snapshot.state === 'blocked') { - setSaveMessage('Saving paused. Use the host recovery action.'); + if ( + snapshot.state === 'blocked' && snapshot.blockedReason !== null + ) { + requestBlockedSessionRecovery(snapshot.blockedReason); } else if (capturedGeneration === editGeneration.current) { setSaveMessage('Saving paused. Use the host recovery action.'); } @@ -232,19 +243,22 @@ 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 -`conflictRecoveryPending` ref therefore permits only one in-flight host recovery -workflow for the blocked session. 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 catch path reads only document-free session lifecycle metadata. It surfaces a -blocked queue even when the request that observed the failure belongs to an older -edit generation, preventing a newer retained save from leaving the UI stuck on a -misleading saving state. Private callback exceptions, document bodies, and -validators remain outside the generic status message. +`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. @@ -253,22 +267,27 @@ 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 `requestConflictRecovery` function is created by the host client composition, -not passed across the server-component serialization boundary. It should open an -accessible compare, merge, fork, or discard workflow. Only after that workflow -performs an authenticated durable reload or equivalent confirmed decision may it -invoke the supplied callback with the recovered server-selected strong `ETag`. +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. +second competing recovery workflow while the first remains active. The stable +blocked reason is control metadata, not authorization or proof of persistence. -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. It should translate -redacted failure states into explicit recovery actions rather than expose private -callback errors or document bodies. +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 @@ -282,7 +301,7 @@ or other CWL services, but it must preserve these ownership rules: 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 conflict and recovery callbacks; server +- 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. @@ -296,26 +315,29 @@ or other CWL services, but it must preserve these ownership rules: 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, or server validator. +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, recovery, and read-only status; +- 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 a conflict occurs, move focus -to a labelled conflict region or dialog and provide a deterministic path back to +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 -conflict workflow that still blocks retained work. +recovery workflow that still blocks retained work. ## Durable autosave and conflict handling @@ -329,8 +351,8 @@ Treat outcomes as follows: | --- | --- | | 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, or malformed response | Treat as ambiguous; do not claim saved or advance the validator; expose the blocked recovery action without private callback detail | -| Authenticated recovery load | Supply the newly loaded server validator through the recovery callback so `session.resume(...)` installs it before retained work continues | +| 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 @@ -399,13 +421,16 @@ Before enabling the panel in production, verify that: - 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; -- only one conflict-recovery workflow is requested for one blocked session; -- authenticated conflict recovery installs its strong validator through +- 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; -- blocked operational failures remain visible even when an older request first - observes the failure; +- operational recovery verifies the durable state instead of automatically + retrying an ambiguous write; - every panel instance has a unique accessible heading relationship; -- conflict UI is keyboard-operable and announced without exposing document text +- 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; From d47f8c4efd246fe7699dc443687eefd35b8deb33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 12:04:55 +0900 Subject: [PATCH 34/37] docs: record operational autosave recovery --- .../stale-generation-conflict-recovery.md | 123 ++++++++++++------ 1 file changed, 84 insertions(+), 39 deletions(-) diff --git a/docs/doctoring/stale-generation-conflict-recovery.md b/docs/doctoring/stale-generation-conflict-recovery.md index 4df54c8..5138dbc 100644 --- a/docs/doctoring/stale-generation-conflict-recovery.md +++ b/docs/doctoring/stale-generation-conflict-recovery.md @@ -20,17 +20,24 @@ That ordering created a blocked-but-unreported conflict path: 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 - conflict recovery; and + 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. +by newer retained work. A status message alone is not a recovery mechanism. ## Decision -Use two guards with separate responsibilities. +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. @@ -41,12 +48,23 @@ 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. -The panel keeps one local `conflictRecoveryPending` ref and issues one +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 @@ -54,33 +72,36 @@ 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 visible even +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, strong-tag syntax validation, and durable-validator handoff. -The host continues to own: +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, and reload workflows; +- 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 is not a tenant identifier, audit record, distributed -lock, or proof that durable recovery succeeded. The host callback returning -`true` means only that the current local session accepted the validated recovery -transition. +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 several shared callers from opening parallel -recovery workflows for the same blocked session. +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 @@ -89,31 +110,41 @@ authenticated durable reload or equivalent confirmed host decision. ## Accessibility consequences -A durable conflict remains represented by one accessible recovery surface until -the supplied recovery callback succeeds or the host deliberately abandons the +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, and restoration to the editor after resolution. +labelling, reason-appropriate actions, and restoration to the editor after +resolution. ## Test-first evidence -Commit `43a211b0818636016e2e80d9ceaaad5ab7af1fd7` added the ordering and -single-flight recovery 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 `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. - -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. The red run is historical -TDD evidence and is not merge evidence. +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 @@ -122,10 +153,16 @@ TDD evidence and is not merge evidence. 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 is precisely the value rejected by the -durable service. Automatic retry would violate the authenticated conflict and +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 @@ -134,17 +171,25 @@ 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 conflict +### 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 the conflict blocks the queue, not only one visual generation. -Recovery must remain available to unblock retained newer work. +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 this record and its documentation -contract. Such a rollback also restores the known risk that a newer edit can hide -a queue-wide conflict. A production host should not adopt that rollback unless it -already provides an independently verified equivalent recovery coordinator. +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 From 021481d641b37b84ffa06bf4ea389409c36d0e9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 12:06:26 +0900 Subject: [PATCH 35/37] docs: record operational save recovery --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ada8687..f7db67e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - 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 ordering and one single-flight recovery request so a newer local edit cannot hide or duplicate the durable conflict workflow while retained work remains blocked +- 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 From 88b9aaf2b658132ea7902494c63eb00f1e776d21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 12:10:12 +0900 Subject: [PATCH 36/37] docs: preserve recovery contract phrase --- docs/doctoring/stale-generation-conflict-recovery.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/stale-generation-conflict-recovery.md b/docs/doctoring/stale-generation-conflict-recovery.md index 5138dbc..773455e 100644 --- a/docs/doctoring/stale-generation-conflict-recovery.md +++ b/docs/doctoring/stale-generation-conflict-recovery.md @@ -49,10 +49,10 @@ 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. +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 From dc42dc29096eeeb554f72bd892252341720bedc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 13:03:46 +0900 Subject: [PATCH 37/37] test(docs): keep recovery single-flight until resume succeeds --- src/architectureDocumentation.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/architectureDocumentation.test.ts b/src/architectureDocumentation.test.ts index 0f6b59c..6bd0b2a 100644 --- a/src/architectureDocumentation.test.ts +++ b/src/architectureDocumentation.test.ts @@ -107,6 +107,27 @@ describe('acquisition-ready modular architecture documentation', () => { 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;',