From 14f65b3ea5c6c81bf8cbd43b1b6d8089add1dcee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:41:24 +0900 Subject: [PATCH 01/65] ci(browser): add cross-engine clipboard evidence job --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2444529a..01e77d2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,33 @@ jobs: - name: Build demo run: pnpm build:demo + browser-release-evidence: + name: Cross-engine Clipboard / Playwright 1.62.0 + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + PLAYWRIGHT_BROWSERS_PATH: /tmp/inkspan-playwright-browsers + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: pnpm + - name: Install root dependencies from the immutable lock + run: pnpm install --frozen-lockfile + - name: Install browser-test dependencies from the isolated immutable lock + run: pnpm --dir tests/browser install --frozen-lockfile + - name: Install Playwright browser revisions pinned by 1.62.0 + run: pnpm --dir tests/browser exec playwright install --with-deps chromium firefox webkit + - name: Verify real-engine rich clipboard release evidence + env: + INKSPAN_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: pnpm --dir tests/browser exec playwright test --config playwright.config.ts + office: name: Office / Python ${{ matrix.python-version }} runs-on: ubuntu-24.04 From ba95c146e598dd252d5a687e4c3ae1e5d82f9c92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:41:38 +0900 Subject: [PATCH 02/65] test(browser): ignore ephemeral browser evidence --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 9296a379..2f74949f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ coverage/ .eslintcache *.tsbuildinfo +# Browser release evidence is ephemeral exact-head CI output. +tests/browser/.browser-evidence/ +tests/browser/test-results/ + # Python build, test, and environment artifacts __pycache__/ *.py[cod] From 3f3bfa1408c88724c72757489b3d673f53cded79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:42:07 +0900 Subject: [PATCH 03/65] docs(browser): add cross-engine operability contract --- docs/OPERABILITY.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index b5699a91..61e01b9f 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -39,6 +39,16 @@ Markdown/HTML/editor conversion and Office rendering are deterministic local ope File publication must follow the documented atomic/non-overwrite behavior. A caller-requested overwrite remains explicit. A failed write or validation does not authorize cleanup of unrelated host files. +## Cross-engine clipboard assurance operations + +SafeClipboard is shipped on protected `main`; the browser-realistic release assurance is implemented on the active browser-assurance PR and remains non-authoritative until protected integration. That active gate uses dependency-locked **Playwright 1.62.0** Chromium, Firefox, and WebKit projects and binds every result to one **exact source head**, one browser-test lock digest, and one corpus version. + +Treat missing, skipped, cancelled, provisioning-failed, incomplete, or semantically divergent browser evidence as a **fail closed** release condition. Do not silently drop one engine or substitute predecessor-head results. The first response to divergence is to determine whether the difference is a sanitizer/integration defect, a standards-permitted serialization difference, or a test/environment defect. Unsafe behavior is repaired at the runtime boundary test-first. A safe difference is admitted only with focused regression evidence, current authoritative **standards** basis, threat analysis, exact affected engine/version evidence, canonical interpretation, compatibility impact, and explicit **rollback**. + +Browser evidence contains only committed synthetic fixtures and bounded version/hash/timing metadata; no tenant document, credential, model data, authorization context, or production clipboard payload belongs in the evidence bundle. The test scenario permits only loopback harness requests; browser installation happens before the scenario as a pinned build prerequisite. + +A Playwright/browser revision upgrade is an operational compatibility event. Rebuild the browser evidence from the new immutable lock on one exact source head and rerun the complete corpus. If browser provisioning is unavailable, only the rich-clipboard release lane is blocked; unrelated Inkspan work continues. Rolling back the browser gate leaves the 0.6.0 rich-clipboard publication claim unaccepted unless equivalent or stronger real-engine assurance replaces it. + ## Release operations Release publication occurs only from an exact integrated protected head. Release evidence includes package artifacts, deterministic checksums, CI/security/package/provenance results, required review, zero valid unresolved findings, and repository-policy acceptance. The normative inventory and digest rules are defined by the `docs/CONTRACTS.md` Release and rollback contract. @@ -72,7 +82,7 @@ Do not publish or reuse the artifact. Rebuild from exact source with determinist ### Browser/parser divergence -When a browser-specific clipboard/security difference is found, add it to the cross-engine corpus. Accept a difference only with explicit standards basis and threat analysis. Do not normalize a security-relevant difference away solely to regain parity. +When a browser-specific clipboard/security difference is found, reproduce it in the dependency-locked cross-engine corpus on the exact affected source head and browser versions. Classify the semantic/security result before changing expectations. Accept a difference only with explicit standards basis, threat analysis, compatibility consequence, and rollback. Do not normalize a security-relevant difference away solely to regain parity, and never convert an unavailable required browser into a successful result. ### Dependency or workflow incident @@ -90,7 +100,8 @@ Rollback is boundary-specific: - autosave/observer feature: fall back to explicit `getSnapshot()`/host coordination without rewriting durable state; - collaboration adapter: detach the adapter without destroying the host provider or Yjs document; - Office renderer change: revert the deterministic renderer behavior and rebuild artifacts; do not modify host files outside the explicit output target; +- browser assurance: revert the faulty gate only while keeping the affected rich-clipboard release claim unaccepted; never retain a release claim after removing its required engine evidence; - documentation: supersede inaccurate decisions with an ADR and synchronized canonical docs rather than deleting history; - release: issue a verified corrective release or supported withdrawal action; preserve provenance and incident evidence. -Every rollback requires fresh exact-head tests and must not weaken authorization, tenant isolation, release provenance, or host ownership boundaries. +Every rollback requires fresh exact-head tests and must not weaken authorization, tenant isolation, release provenance, browser-security evidence, or host ownership boundaries. From 5dcd75e29ad891a508de3f7d85160901fe00939e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:42:27 +0900 Subject: [PATCH 04/65] docs(browser): add cross-engine assurance doctoring --- .../cross-engine-rich-clipboard-assurance.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/doctoring/cross-engine-rich-clipboard-assurance.md diff --git a/docs/doctoring/cross-engine-rich-clipboard-assurance.md b/docs/doctoring/cross-engine-rich-clipboard-assurance.md new file mode 100644 index 00000000..91a22b64 --- /dev/null +++ b/docs/doctoring/cross-engine-rich-clipboard-assurance.md @@ -0,0 +1,49 @@ +# Cross-engine rich-clipboard release assurance + +Status: Implemented on active PR + +## Decision boundary + +Inkspan's SafeClipboard runtime is already integrated on protected `main`, but HTML fragment parsing, DOM reconstruction, CSS interpretation, serialization, and ProseMirror parsing are browser semantics. jsdom remains useful deterministic unit evidence; it is not real-engine conformance. The 0.6.0 rich-clipboard publication boundary therefore requires the same committed synthetic fixtures to execute through the supported TipTap/ProseMirror paste path in real Chromium, Firefox, and WebKit on one exact source head. + +This active implementation uses **Playwright 1.62.0** from the isolated `tests/browser/pnpm-lock.yaml`. That pinned Playwright release identifies Chromium 151.0.7922.34, Firefox 153, and WebKit 26.5 as its bundled browser versions. The CI evidence records the actual `browser.version()` value for each engine, the Playwright version, operating-system identity, corpus version, SHA-256 of the browser-test `pnpm-lock.yaml`, and the exact source head. Those observed values, rather than this prose, are the release evidence when a browser revision changes. + +## Test-first evidence + +RED commit `cd6945b642a5d48449e1a59581e728ce6b440ff6` added the permanent release-oracle contract before its implementation existed. Hosted CI failed at TypeScript resolution because `crossEngineClipboardEvidence` was deliberately absent. The contract also injects deliberate sanitized-HTML, ProseMirror-structure, rejection-behavior, missing-engine, duplicate-engine, and mixed-case divergences. The production oracle then implemented fail-closed three-engine consensus, and a later exact coverage run exposed and removed one unreachable fallback instead of excluding it from coverage. + +The browser corpus covers active content, external-resource and form subtrees, hidden CSS/ARIA/Office/popover content, safe and unsafe links, malformed formatting and paragraph reconstruction, table/list parser repair, SVG/MathML, interactive and native-widget fallback, semantic inline-style reconstruction, byte limits, node limits, and depth limits. A separate real-browser probe uses a revoked `Document` proxy to require stable redacted DOM-capability failure without reflecting the private source string. A representative Word-like fixture provides a generous release alarm rather than a universal performance benchmark. + +## Hermeticity and evidence minimization + +The browser scenario permits requests only to the loopback Vite harness and aborts any external request. SafeClipboard itself performs no network fetch. The workflow installs the exact browser revisions selected by pinned Playwright before the test scenario begins; browser provisioning is a build prerequisite, not application egress. + +The evidence files contain only public synthetic fixture identifiers, sanitized output, ProseMirror JSON produced from those synthetic fixtures, stable rejection codes, engine/runtime versions, lock digest, corpus version, source SHA, runner identity, and representative timing. They contain **no tenant document**, production clipboard payload, credential, model prompt/output, authorization context, user identity, or private local path. The committed corpus uses synthetic fixtures only. + +## Difference policy + +Security-relevant results must agree across Chromium, Firefox, and WebKit. The default comparator uses **no generic normalization** and no broad engine allowlist. A difference may be admitted only through a focused regression fixture and a reviewed rule that records the authoritative standards basis, exact affected engine/version, threat analysis, canonical interpretation, compatibility consequence, and rollback. An unexplained parser, sanitizer, error, or ProseMirror-structure difference must **fail closed**. + +The same rule applies when one project is missing, skipped, cancelled, unable to provision, or unable to emit exact-head evidence: the rich-clipboard release lane remains blocked. Other Inkspan work may continue; the browser gate itself does not become optional. + +## Compatibility and rollback + +Playwright/browser upgrades are compatibility events. Update the immutable browser-test lock, rerun every engine and the complete corpus, review any difference against current standards, and accept the new evidence only on the unchanged exact head. Do not transfer browser evidence from a predecessor commit. + +If the browser gate itself is faulty, rollback may revert the gate change while explicitly leaving the 0.6.0 rich-clipboard publication claim unaccepted. After protected integration, removing a required engine, weakening the corpus, broadening normalization, or replacing the exact-head evidence contract requires a superseding ADR and new threat analysis. A sanitizer defect discovered by the gate is fixed at the runtime boundary test-first rather than hidden in an engine-specific expectation. + +## Claim limits + +Passing these projects proves the committed SafeClipboard corpus and supported paste integration under the pinned Playwright engine builds on the recorded runner. It does not claim byte-identical behavior for every browser build, enterprise browser policy, extension environment, branded channel, downstream renderer, or arbitrary HTML. Hosts continue to own authorization, tenancy, persistence, CSP, application egress, deployment, model-use policy, and legal/privacy policy. + +## References + +Microsoft. (2026). *Playwright Test 1.62.0*. npm. https://www.npmjs.com/package/@playwright/test/v/1.62.0 + +Microsoft. (n.d.-a). *Browsers*. Playwright documentation. Retrieved August 10, 2026, from https://playwright.dev/docs/browsers + +Microsoft. (n.d.-b). *Projects*. Playwright documentation. Retrieved August 10, 2026, from https://playwright.dev/docs/test-projects + +Web Hypertext Application Technology Working Group. (2026). *HTML Standard: Parsing HTML documents* (Living Standard). Retrieved August 10, 2026, from https://html.spec.whatwg.org/multipage/parsing.html + +World Wide Web Consortium. (2026, June 24). *Clipboard API and events* (W3C Working Draft). https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/ From 2501ccc1202ddc4f7ce32f5405ccd7355cde7ea4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:43:02 +0900 Subject: [PATCH 05/65] feat(browser): add cross-engine clipboard evidence oracle --- src/crossEngineClipboardEvidence.ts | 247 ++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/crossEngineClipboardEvidence.ts diff --git a/src/crossEngineClipboardEvidence.ts b/src/crossEngineClipboardEvidence.ts new file mode 100644 index 00000000..2767e666 --- /dev/null +++ b/src/crossEngineClipboardEvidence.ts @@ -0,0 +1,247 @@ +import type { + ClipboardConfig, + ClipboardSanitizationErrorCode, +} from './extensions/SafeClipboard.js'; + +/** Version of the release corpus and its interpretation contract. */ +export const SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION = 1; + +/** Browser engines that must independently pass the rich-clipboard release gate. */ +export type CrossEngineClipboardEngine = 'chromium' | 'firefox' | 'webkit'; + +/** Security/semantic risk families exercised by the shared browser corpus. */ +export type CrossEngineClipboardRiskFamily = + | 'active-content' + | 'hidden-content' + | 'unsafe-link' + | 'malformed-markup' + | 'table-list' + | 'svg-mathml' + | 'parser-edge' + | 'resource-limit'; + +/** One immutable public fixture in the cross-engine rich-clipboard corpus. */ +export interface CrossEngineClipboardCase { + readonly id: string; + readonly riskFamily: CrossEngineClipboardRiskFamily; + readonly sourceHtml: string; + readonly expectedSanitizedHtml: string; + readonly expectedErrorCode: ClipboardSanitizationErrorCode | null; + readonly clipboardConfig?: ClipboardConfig; +} + +/** One browser observation used by the fail-closed release consensus oracle. */ +export interface CrossEngineClipboardObservation { + readonly caseId: string; + readonly engine: CrossEngineClipboardEngine; + readonly sanitizedHtml: string | null; + readonly documentJson: unknown | null; + readonly errorCode: ClipboardSanitizationErrorCode | null; +} + +const CORPUS: readonly CrossEngineClipboardCase[] = [ + { + id: 'active-script', + riskFamily: 'active-content', + sourceHtml: '

safe

', + expectedSanitizedHtml: '

safe

', + expectedErrorCode: null, + }, + { + id: 'active-resource-and-form', + riskFamily: 'active-content', + sourceHtml: + '
before
after
', + expectedSanitizedHtml: '
beforeafter
', + expectedErrorCode: null, + }, + { + id: 'hidden-display-and-aria', + riskFamily: 'hidden-content', + sourceHtml: + '

visibledisplayend

', + expectedSanitizedHtml: '

visibleend

', + expectedErrorCode: null, + }, + { + id: 'hidden-office-eof-comment', + riskFamily: 'hidden-content', + sourceHtml: + '

beforesecretafter

', + expectedSanitizedHtml: '

beforeafter

', + expectedErrorCode: null, + }, + { + id: 'hidden-content-visibility-popover', + riskFamily: 'hidden-content', + sourceHtml: + '
onetwothreefour
', + expectedSanitizedHtml: '
onefour
', + expectedErrorCode: null, + }, + { + id: 'unsafe-javascript-link', + riskFamily: 'unsafe-link', + sourceHtml: 'click', + expectedSanitizedHtml: 'click', + expectedErrorCode: null, + }, + { + id: 'safe-https-link', + riskFamily: 'unsafe-link', + sourceHtml: 'safe', + expectedSanitizedHtml: + 'safe', + expectedErrorCode: null, + }, + { + id: 'malformed-formatting', + riskFamily: 'malformed-markup', + sourceHtml: 'text', + expectedSanitizedHtml: 'text', + expectedErrorCode: null, + }, + { + id: 'malformed-paragraph', + riskFamily: 'malformed-markup', + sourceHtml: '

one

two', + expectedSanitizedHtml: '

one

two

', + expectedErrorCode: null, + }, + { + id: 'table-parser-repair', + riskFamily: 'table-list', + sourceHtml: '
cell
', + expectedSanitizedHtml: + '
cell
', + expectedErrorCode: null, + }, + { + id: 'ordered-list-repair', + riskFamily: 'table-list', + sourceHtml: '
  1. one
  2. two
', + expectedSanitizedHtml: '
  1. one
  2. two
', + expectedErrorCode: null, + }, + { + id: 'svg-and-mathml-subtrees', + riskFamily: 'svg-mathml', + sourceHtml: + '

axbyc

', + expectedSanitizedHtml: '

abc

', + expectedErrorCode: null, + }, + { + id: 'closed-details-summary', + riskFamily: 'parser-edge', + sourceHtml: '
label

secret

', + expectedSanitizedHtml: 'label', + expectedErrorCode: null, + }, + { + id: 'dialog-and-native-widget-fallback', + riskFamily: 'parser-edge', + sourceHtml: + 'closed

open

end

', + expectedSanitizedHtml: '

open

end

', + expectedErrorCode: null, + }, + { + id: 'semantic-style-reconstruction', + riskFamily: 'parser-edge', + sourceHtml: + 'styled', + expectedSanitizedHtml: 'styled', + expectedErrorCode: null, + }, + { + id: 'utf8-byte-ceiling', + riskFamily: 'resource-limit', + sourceHtml: '

private source

', + expectedSanitizedHtml: '', + expectedErrorCode: 'input_too_large', + clipboardConfig: { maxHtmlBytes: 1 }, + }, + { + id: 'node-ceiling', + riskFamily: 'resource-limit', + sourceHtml: '

onetwo

', + expectedSanitizedHtml: '', + expectedErrorCode: 'node_limit_exceeded', + clipboardConfig: { maxNodes: 2 }, + }, + { + id: 'depth-ceiling', + riskFamily: 'resource-limit', + sourceHtml: '

deep

', + expectedSanitizedHtml: '', + expectedErrorCode: 'depth_limit_exceeded', + clipboardConfig: { maxDepth: 1 }, + }, +] as const; + +/** Immutable adversarial corpus shared by every required browser project. */ +export const SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS: readonly CrossEngineClipboardCase[] = + Object.freeze(CORPUS.map((testCase) => Object.freeze({ ...testCase }))); + +const REQUIRED_ENGINES: readonly CrossEngineClipboardEngine[] = Object.freeze([ + 'chromium', + 'firefox', + 'webkit', +]); + +/** + * Require exact rich-clipboard parity across one observation from every engine. + * + * The default gate intentionally contains no broad normalization or difference + * allowlist. A future standards-permitted engine exception must first add a + * focused corpus case, threat rationale, explicit comparison rule, and rollback + * note rather than being silently normalized here. + */ +export function assertCrossEngineClipboardConsensus( + observations: readonly CrossEngineClipboardObservation[], +): void { + const engines = observations.map((item) => item.engine); + if ( + observations.length !== REQUIRED_ENGINES.length || + REQUIRED_ENGINES.some( + (engine) => engines.filter((candidate) => candidate === engine).length !== 1, + ) + ) { + throw new Error( + 'Cross-engine clipboard evidence requires exactly one observation from chromium, firefox, and webkit.', + ); + } + + const [reference, ...others] = observations as readonly [ + CrossEngineClipboardObservation, + CrossEngineClipboardObservation, + CrossEngineClipboardObservation, + ]; + if (others.some((item) => item.caseId !== reference.caseId)) { + throw new Error( + 'Cross-engine clipboard evidence must describe the same corpus case.', + ); + } + if (others.some((item) => item.errorCode !== reference.errorCode)) { + throw new Error( + 'Cross-engine clipboard rejection behavior differs across browser engines.', + ); + } + if (others.some((item) => item.sanitizedHtml !== reference.sanitizedHtml)) { + throw new Error( + 'Cross-engine clipboard sanitized HTML differs across browser engines.', + ); + } + + const referenceDocument = canonicalJson(reference.documentJson); + if (others.some((item) => canonicalJson(item.documentJson) !== referenceDocument)) { + throw new Error( + 'Cross-engine clipboard document structure differs across browser engines.', + ); + } +} + +function canonicalJson(value: unknown): string { + return JSON.stringify(value); +} From dcba38f4cdddd8a754b330dbe9dc62e0166d6abe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:43:20 +0900 Subject: [PATCH 06/65] test(browser): add cross-engine consensus contract --- src/crossEngineClipboardEvidence.test.ts | 118 +++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/crossEngineClipboardEvidence.test.ts diff --git a/src/crossEngineClipboardEvidence.test.ts b/src/crossEngineClipboardEvidence.test.ts new file mode 100644 index 00000000..a8430cf8 --- /dev/null +++ b/src/crossEngineClipboardEvidence.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from 'vitest'; +import { + SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS, + assertCrossEngineClipboardConsensus, + type CrossEngineClipboardObservation, +} from './crossEngineClipboardEvidence.js'; + +const observation = ( + engine: 'chromium' | 'firefox' | 'webkit', + overrides: Partial = {}, +): CrossEngineClipboardObservation => ({ + caseId: 'active-script', + engine, + sanitizedHtml: '

safe

', + documentJson: { type: 'doc', content: [{ type: 'paragraph' }] }, + errorCode: null, + ...overrides, +}); + +describe('cross-engine rich clipboard release oracle', () => { + it('keeps one bounded adversarial corpus spanning the required semantic risk families', () => { + const families = new Set( + SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.map((testCase) => testCase.riskFamily), + ); + + expect(families).toEqual( + new Set([ + 'active-content', + 'hidden-content', + 'unsafe-link', + 'malformed-markup', + 'table-list', + 'svg-mathml', + 'parser-edge', + 'resource-limit', + ]), + ); + expect(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length).toBeGreaterThanOrEqual(18); + expect( + SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.every( + (testCase) => + testCase.id.length > 0 && + testCase.sourceHtml.length > 0 && + testCase.expectedSanitizedHtml !== undefined, + ), + ).toBe(true); + }); + + it('accepts identical sanitized HTML, ProseMirror structure, and rejection behavior across all engines', () => { + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium'), + observation('firefox'), + observation('webkit'), + ]), + ).not.toThrow(); + }); + + it('fails closed when one engine reconstructs unsafe or divergent HTML', () => { + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium'), + observation('firefox'), + observation('webkit', { + sanitizedHtml: '

safe

', + }), + ]), + ).toThrow(/sanitized HTML differs across browser engines/u); + }); + + it('fails closed when ProseMirror structure or rejection behavior diverges', () => { + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium'), + observation('firefox'), + observation('webkit', { + documentJson: { + type: 'doc', + content: [{ type: 'heading', attrs: { level: 1 } }], + }, + }), + ]), + ).toThrow(/document structure differs across browser engines/u); + + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium'), + observation('firefox'), + observation('webkit', { errorCode: 'invalid_html' }), + ]), + ).toThrow(/rejection behavior differs across browser engines/u); + }); + + it('rejects incomplete, duplicate, or mixed-case observations instead of silently weakening the gate', () => { + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium'), + observation('firefox'), + ]), + ).toThrow(/exactly one observation from chromium, firefox, and webkit/u); + + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium'), + observation('chromium'), + observation('webkit'), + ]), + ).toThrow(/exactly one observation from chromium, firefox, and webkit/u); + + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium'), + observation('firefox', { caseId: 'different-case' }), + observation('webkit'), + ]), + ).toThrow(/same corpus case/u); + }); +}); From 4d1bc428e6a8346091f0db4d006eda70d25428c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:43:34 +0900 Subject: [PATCH 07/65] test(docs): bind cross-engine assurance documentation --- src/crossEngineClipboardDocumentation.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/crossEngineClipboardDocumentation.test.ts diff --git a/src/crossEngineClipboardDocumentation.test.ts b/src/crossEngineClipboardDocumentation.test.ts new file mode 100644 index 00000000..3b1c8622 --- /dev/null +++ b/src/crossEngineClipboardDocumentation.test.ts @@ -0,0 +1,59 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const documentText = (path: string): string => + readFileSync(resolve(process.cwd(), path), 'utf8').replace(/\s+/gu, ' ').trim(); + +describe('cross-engine clipboard documentation contract', () => { + it('documents the exact browser release gate and evidence identity', () => { + const guide = documentText('docs/clipboard-security.md'); + const doctoring = documentText( + 'docs/doctoring/cross-engine-rich-clipboard-assurance.md', + ); + const strategy = documentText('docs/TEST_STRATEGY.md'); + + for (const text of [guide, doctoring, strategy]) { + expect(text).toContain('Chromium'); + expect(text).toContain('Firefox'); + expect(text).toContain('WebKit'); + expect(text).toContain('Playwright 1.62.0'); + expect(text).toContain('exact source head'); + expect(text).toContain('corpus version'); + } + expect(doctoring).toContain('Chromium 151.0.7922.34'); + expect(doctoring).toContain('Firefox 153'); + expect(doctoring).toContain('WebKit 26.5'); + expect(doctoring).toContain('pnpm-lock.yaml'); + }); + + it('keeps browser differences fail-closed and narrowly reviewable', () => { + const doctoring = documentText( + 'docs/doctoring/cross-engine-rich-clipboard-assurance.md', + ); + const operability = documentText('docs/OPERABILITY.md'); + + for (const text of [doctoring, operability]) { + expect(text).toContain('fail closed'); + expect(text).toContain('standards'); + expect(text).toContain('rollback'); + } + expect(doctoring).toContain('no generic normalization'); + expect(doctoring).toContain('synthetic fixtures'); + expect(doctoring).toContain('no tenant document'); + }); + + it('records the current implementation maturity without calling the active PR shipped', () => { + const fitness = documentText('docs/DOCUMENTATION_FITNESS.md'); + const changelog = documentText('CHANGELOG.md'); + + expect(fitness).toContain( + 'Cross-engine browser-semantic release assurance', + ); + expect(fitness).toContain('`implemented_on_active_pr`'); + expect(fitness).toContain('SafeClipboard'); + expect(fitness).toContain('`implemented_on_protected_main`'); + expect(changelog).toContain('dependency-locked Chromium/Firefox/WebKit'); + expect(changelog).toContain('cross-engine rich-clipboard release gate'); + }); +}); From 2e6bdb2e39de8b0d1aa549ddb16c631b06fa23c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:43:51 +0900 Subject: [PATCH 08/65] test(ci): include browser evidence in exact-head contract --- src/workflowExactHead.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/workflowExactHead.test.ts b/src/workflowExactHead.test.ts index 8a44b57a..ccda889a 100644 --- a/src/workflowExactHead.test.ts +++ b/src/workflowExactHead.test.ts @@ -16,14 +16,14 @@ const CHECKOUT_PIN = 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(/runs-on: ubuntu-24\.04/g)).toHaveLength(3); + expect(workflow.match(new RegExp(CHECKOUT_PIN, 'g'))).toHaveLength(3); expect( workflow.match( /ref: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/g, ), - ).toHaveLength(2); - expect(workflow.match(/persist-credentials: false/g)).toHaveLength(2); + ).toHaveLength(3); + expect(workflow.match(/persist-credentials: false/g)).toHaveLength(3); }); it('keeps the workflow read-only and hash-pins every third-party action', () => { From 3d985734510f2843b6b01de0f3ee0878227c7fd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:44:01 +0900 Subject: [PATCH 09/65] test(browser): add real-engine harness --- tests/browser/harness.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/browser/harness.html diff --git a/tests/browser/harness.html b/tests/browser/harness.html new file mode 100644 index 00000000..1cc57ee4 --- /dev/null +++ b/tests/browser/harness.html @@ -0,0 +1,12 @@ + + + + + + Inkspan cross-engine clipboard harness + + +
+ + + From bd4194fcd60b13b4e4e94982db8e96ab2a4ed858 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:44:19 +0900 Subject: [PATCH 10/65] test(browser): add real-engine harness runtime --- tests/browser/harness.ts | 89 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/browser/harness.ts diff --git a/tests/browser/harness.ts b/tests/browser/harness.ts new file mode 100644 index 00000000..459917e6 --- /dev/null +++ b/tests/browser/harness.ts @@ -0,0 +1,89 @@ +import { Editor } from '@tiptap/core'; +import { + ClipboardSanitizationError, + sanitizeRichClipboardHtml, + type ClipboardConfig, + type ClipboardSanitizationErrorCode, +} from '../../src/extensions/SafeClipboard.js'; +import { buildExtensions } from '../../src/extensions/kit.js'; + +interface BrowserClipboardProbeRequest { + readonly sourceHtml: string; + readonly clipboardConfig?: ClipboardConfig; +} + +interface BrowserClipboardProbeResult { + readonly sanitizedHtml: string; + readonly documentJson: unknown | null; + readonly errorCode: ClipboardSanitizationErrorCode | null; +} + +interface BrowserHostileDocumentProbeResult { + readonly errorCode: ClipboardSanitizationErrorCode | null; + readonly message: string; +} + +declare global { + interface Window { + runInkspanClipboardProbe( + request: BrowserClipboardProbeRequest, + ): BrowserClipboardProbeResult; + runInkspanHostileDocumentProbe( + sourceHtml: string, + ): BrowserHostileDocumentProbeResult; + } +} + +window.runInkspanClipboardProbe = ( + request: BrowserClipboardProbeRequest, +): BrowserClipboardProbeResult => { + let errorCode: ClipboardSanitizationErrorCode | null = null; + const editor = new Editor({ + element: document.createElement('div'), + extensions: buildExtensions({ + clipboard: request.clipboardConfig, + onClipboardError: (error) => { + errorCode = error.code; + }, + }), + content: '', + }); + + try { + let sanitizedHtml = request.sourceHtml; + editor.view.someProp('transformPastedHTML', (transform) => { + sanitizedHtml = transform(sanitizedHtml, editor.view); + }); + + if (errorCode !== null) { + return Object.freeze({ sanitizedHtml, documentJson: null, errorCode }); + } + + editor.commands.setContent(sanitizedHtml, false); + return Object.freeze({ + sanitizedHtml, + documentJson: editor.getJSON(), + errorCode: null, + }); + } finally { + editor.destroy(); + } +}; + +window.runInkspanHostileDocumentProbe = ( + sourceHtml: string, +): BrowserHostileDocumentProbeResult => { + const revoked = Proxy.revocable(document, {}); + revoked.revoke(); + try { + sanitizeRichClipboardHtml(sourceHtml, undefined, revoked.proxy as Document); + return Object.freeze({ errorCode: null, message: '' }); + } catch (error) { + if (error instanceof ClipboardSanitizationError) { + return Object.freeze({ errorCode: error.code, message: error.message }); + } + return Object.freeze({ errorCode: 'invalid_html', message: 'unclassified' }); + } +}; + +export {}; From f53c283df9292bb67fe018698764a9200656d1e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:44:31 +0900 Subject: [PATCH 11/65] test(browser): pin isolated Playwright dependency --- tests/browser/package.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/browser/package.json diff --git a/tests/browser/package.json b/tests/browser/package.json new file mode 100644 index 00000000..fc99e7c1 --- /dev/null +++ b/tests/browser/package.json @@ -0,0 +1,9 @@ +{ + "name": "inkspan-cross-engine-browser-tests", + "private": true, + "type": "module", + "packageManager": "pnpm@11.5.3", + "devDependencies": { + "@playwright/test": "1.62.0" + } +} From d06c72691ffe347df74ceba2fb3c8562e9033bf7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:44:44 +0900 Subject: [PATCH 12/65] test(browser): configure three-engine evidence projects --- tests/browser/playwright.config.ts | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/browser/playwright.config.ts diff --git a/tests/browser/playwright.config.ts b/tests/browser/playwright.config.ts new file mode 100644 index 00000000..8b49b262 --- /dev/null +++ b/tests/browser/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from '@playwright/test'; + +const HARNESS_URL = 'http://127.0.0.1:4173/tests/browser/harness.html'; + +export default defineConfig({ + testDir: './specs', + outputDir: './test-results', + fullyParallel: false, + workers: 3, + retries: 0, + timeout: 20_000, + expect: { timeout: 5_000 }, + reporter: [['line']], + webServer: { + command: + 'pnpm --dir ../.. exec vite --host 127.0.0.1 --port 4173 --strictPort', + url: HARNESS_URL, + reuseExistingServer: false, + timeout: 120_000, + }, + projects: [ + { + name: 'chromium', + testMatch: /clipboard\.browser\.spec\.ts/u, + use: { ...devices['Desktop Chrome'], browserName: 'chromium' }, + }, + { + name: 'firefox', + testMatch: /clipboard\.browser\.spec\.ts/u, + use: { ...devices['Desktop Firefox'], browserName: 'firefox' }, + }, + { + name: 'webkit', + testMatch: /clipboard\.browser\.spec\.ts/u, + use: { ...devices['Desktop Safari'], browserName: 'webkit' }, + }, + { + name: 'consensus', + testMatch: /clipboard\.consensus\.spec\.ts/u, + dependencies: ['chromium', 'firefox', 'webkit'], + }, + ], +}); From 4a14e5fdde49730dc3cc8f2d29506f140830c222 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:44:54 +0900 Subject: [PATCH 13/65] test(browser): isolate browser workspace --- tests/browser/pnpm-workspace.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/browser/pnpm-workspace.yaml diff --git a/tests/browser/pnpm-workspace.yaml b/tests/browser/pnpm-workspace.yaml new file mode 100644 index 00000000..fcbac674 --- /dev/null +++ b/tests/browser/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - . + +onlyBuiltDependencies: [] From 6534e750bbf895e04e883cdd817b4383daee776b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:45:06 +0900 Subject: [PATCH 14/65] test(browser): lock Playwright browser dependency --- tests/browser/pnpm-lock.yaml | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/browser/pnpm-lock.yaml diff --git a/tests/browser/pnpm-lock.yaml b/tests/browser/pnpm-lock.yaml new file mode 100644 index 00000000..67f19004 --- /dev/null +++ b/tests/browser/pnpm-lock.yaml @@ -0,0 +1,49 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + .: + devDependencies: + '@playwright/test': + specifier: 1.62.0 + version: 1.62.0 + +packages: + '@playwright/test@1.62.0': + resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} + engines: {node: '>=20'} + hasBin: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + playwright-core@1.62.0: + resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} + hasBin: true + +snapshots: + '@playwright/test@1.62.0': + dependencies: + playwright: 1.62.0 + + fsevents@2.3.2: + optional: true + + playwright-core@1.62.0: {} + + playwright@1.62.0: + dependencies: + playwright-core: 1.62.0 + optionalDependencies: + fsevents: 2.3.2 From ffc208af6881043fb395dd325942789e2a172167 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:45:29 +0900 Subject: [PATCH 15/65] test(browser): add real-engine clipboard corpus --- tests/browser/specs/clipboard.browser.spec.ts | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 tests/browser/specs/clipboard.browser.spec.ts diff --git a/tests/browser/specs/clipboard.browser.spec.ts b/tests/browser/specs/clipboard.browser.spec.ts new file mode 100644 index 00000000..47fb2d6b --- /dev/null +++ b/tests/browser/specs/clipboard.browser.spec.ts @@ -0,0 +1,172 @@ +import { createHash } from 'node:crypto'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { expect, test } from '@playwright/test'; +import { + SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS, + SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION, + type CrossEngineClipboardEngine, + type CrossEngineClipboardObservation, +} from '../../../src/crossEngineClipboardEvidence.js'; + +type BrowserProbe = (request: { + sourceHtml: string; + clipboardConfig?: unknown; +}) => { + sanitizedHtml: string; + documentJson: unknown | null; + errorCode: string | null; +}; + +type HostileDocumentProbe = (sourceHtml: string) => { + errorCode: string | null; + message: string; +}; + +const evidenceDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '../.browser-evidence'); +const lockfilePath = resolve(dirname(fileURLToPath(import.meta.url)), '../pnpm-lock.yaml'); +const packagePath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'); +const observations: CrossEngineClipboardObservation[] = []; +let representativeWordMillis: number | null = null; + +const allowHarnessRequest = (requestUrl: string): boolean => { + const url = new URL(requestUrl); + return url.hostname === '127.0.0.1' && url.port === '4173'; +}; + +test.describe.configure({ mode: 'serial' }); + +test.beforeEach(async ({ page }) => { + const rejectedExternalRequests: string[] = []; + await page.route('**/*', async (route) => { + if (allowHarnessRequest(route.request().url())) { + await route.continue(); + return; + } + rejectedExternalRequests.push(new URL(route.request().url()).origin); + await route.abort('blockedbyclient'); + }); + await page.goto('http://127.0.0.1:4173/tests/browser/harness.html'); + expect(rejectedExternalRequests).toEqual([]); +}); + +for (const testCase of SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS) { + test(`sanitizes corpus case ${testCase.id}`, async ({ page, browserName }) => { + const rejectedExternalRequests: string[] = []; + page.on('request', (request) => { + if (!allowHarnessRequest(request.url())) { + rejectedExternalRequests.push(new URL(request.url()).origin); + } + }); + + const result = await page.evaluate( + ({ sourceHtml, clipboardConfig }) => + ( + window as unknown as { + runInkspanClipboardProbe: BrowserProbe; + } + ).runInkspanClipboardProbe({ sourceHtml, clipboardConfig }), + { + sourceHtml: testCase.sourceHtml, + clipboardConfig: testCase.clipboardConfig, + }, + ); + + expect(result.sanitizedHtml).toBe(testCase.expectedSanitizedHtml); + expect(result.errorCode).toBe(testCase.expectedErrorCode); + if (testCase.expectedErrorCode === null) { + expect(result.documentJson).not.toBeNull(); + } else { + expect(result.documentJson).toBeNull(); + } + expect(rejectedExternalRequests).toEqual([]); + + observations.push({ + caseId: testCase.id, + engine: browserName as CrossEngineClipboardEngine, + sanitizedHtml: result.sanitizedHtml, + documentJson: result.documentJson, + errorCode: result.errorCode as CrossEngineClipboardObservation['errorCode'], + }); + }); +} + +test('redacts hostile document capability failures without source disclosure', async ({ + page, +}) => { + const privateSource = '

private source must not escape

'; + const result = await page.evaluate( + (sourceHtml) => + ( + window as unknown as { + runInkspanHostileDocumentProbe: HostileDocumentProbe; + } + ).runInkspanHostileDocumentProbe(sourceHtml), + privateSource, + ); + + expect(result.errorCode).toBe('invalid_html'); + expect(result.message).toBe('Rich clipboard HTML could not be sanitized.'); + expect(result.message).not.toContain('private source'); +}); + +test('keeps representative Word-like sanitization within the release alarm budget', async ({ + page, +}) => { + const sourceHtml = `
${Array.from( + { length: 800 }, + (_, index) => + `

paragraph-${index}

`, + ).join('')}
`; + + const measurement = await page.evaluate((html) => { + const started = performance.now(); + const result = ( + window as unknown as { runInkspanClipboardProbe: BrowserProbe } + ).runInkspanClipboardProbe({ sourceHtml: html }); + return { elapsedMillis: performance.now() - started, result }; + }, sourceHtml); + + expect(measurement.result.errorCode).toBeNull(); + expect(measurement.elapsedMillis).toBeLessThan(8_000); + representativeWordMillis = Math.round(measurement.elapsedMillis * 100) / 100; +}); + +test.afterAll(async ({ browser, browserName }) => { + const lockfile = await readFile(lockfilePath); + const browserPackage = JSON.parse(await readFile(packagePath, 'utf8')) as { + devDependencies?: Record; + }; + const playwrightVersion = browserPackage.devDependencies?.['@playwright/test']; + if (playwrightVersion !== '1.62.0') { + throw new Error('Cross-engine browser evidence requires pinned @playwright/test 1.62.0.'); + } + if (observations.length !== SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length) { + throw new Error('Cross-engine browser evidence is incomplete for the shared corpus.'); + } + if (representativeWordMillis === null) { + throw new Error('Cross-engine browser performance evidence is missing.'); + } + + await mkdir(evidenceDirectory, { recursive: true }); + const evidence = Object.freeze({ + schemaVersion: 1, + corpusVersion: SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION, + engine: browserName, + playwrightVersion, + browserVersion: browser.version(), + osPlatform: process.platform, + runnerImage: process.env.ImageOS ?? null, + headSha: + process.env.INKSPAN_EXPECTED_HEAD_SHA ?? process.env.GITHUB_SHA ?? null, + lockSha256: createHash('sha256').update(lockfile).digest('hex'), + representativeWordMillis, + observations, + }); + await writeFile( + resolve(evidenceDirectory, `${browserName}.json`), + `${JSON.stringify(evidence)}\n`, + 'utf8', + ); +}); From 840ae0a18d31df63c4ecfa2d1264b0e4acf9adbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:45:49 +0900 Subject: [PATCH 16/65] test(browser): require exact three-engine consensus --- .../browser/specs/clipboard.consensus.spec.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/browser/specs/clipboard.consensus.spec.ts diff --git a/tests/browser/specs/clipboard.consensus.spec.ts b/tests/browser/specs/clipboard.consensus.spec.ts new file mode 100644 index 00000000..6b4a73ef --- /dev/null +++ b/tests/browser/specs/clipboard.consensus.spec.ts @@ -0,0 +1,94 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { expect, test } from '@playwright/test'; +import { + SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS, + SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION, + assertCrossEngineClipboardConsensus, + type CrossEngineClipboardEngine, + type CrossEngineClipboardObservation, +} from '../../../src/crossEngineClipboardEvidence.js'; + +interface BrowserEvidence { + readonly schemaVersion: number; + readonly corpusVersion: number; + readonly engine: CrossEngineClipboardEngine; + readonly playwrightVersion: string; + readonly browserVersion: string; + readonly osPlatform: string; + readonly runnerImage: string | null; + readonly headSha: string | null; + readonly lockSha256: string; + readonly representativeWordMillis: number; + readonly observations: readonly CrossEngineClipboardObservation[]; +} + +const evidenceDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '../.browser-evidence'); +const engines: readonly CrossEngineClipboardEngine[] = [ + 'chromium', + 'firefox', + 'webkit', +]; + +const readEvidence = async ( + engine: CrossEngineClipboardEngine, +): Promise => + JSON.parse( + await readFile(resolve(evidenceDirectory, `${engine}.json`), 'utf8'), + ) as BrowserEvidence; + +test('requires complete exact-head browser evidence and exact corpus consensus', async () => { + const evidence = await Promise.all(engines.map(readEvidence)); + const [reference] = evidence; + if (!reference) throw new Error('Cross-engine browser evidence is missing.'); + + for (const [index, item] of evidence.entries()) { + expect(item.schemaVersion).toBe(1); + expect(item.corpusVersion).toBe(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION); + expect(item.engine).toBe(engines[index]); + expect(item.playwrightVersion).toBe('1.62.0'); + expect(item.browserVersion.length).toBeGreaterThan(0); + expect(item.lockSha256).toBe(reference.lockSha256); + expect(item.headSha).toBe(reference.headSha); + expect(item.observations).toHaveLength(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length); + expect(item.representativeWordMillis).toBeGreaterThanOrEqual(0); + expect(item.representativeWordMillis).toBeLessThan(8_000); + } + + if (process.env.GITHUB_ACTIONS === 'true') { + expect(reference.headSha).toBe(process.env.INKSPAN_EXPECTED_HEAD_SHA); + expect(reference.runnerImage).not.toBeNull(); + } + + for (const testCase of SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS) { + const observations = evidence.map((item) => { + const observation = item.observations.find( + (candidate) => candidate.caseId === testCase.id, + ); + if (!observation) { + throw new Error( + `Cross-engine browser evidence is missing corpus case ${testCase.id}.`, + ); + } + return observation; + }); + assertCrossEngineClipboardConsensus(observations); + } + + const summary = { + schemaVersion: 1, + corpusVersion: SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION, + headSha: reference.headSha, + lockSha256: reference.lockSha256, + playwrightVersion: reference.playwrightVersion, + engines: evidence.map((item) => ({ + engine: item.engine, + browserVersion: item.browserVersion, + osPlatform: item.osPlatform, + runnerImage: item.runnerImage, + representativeWordMillis: item.representativeWordMillis, + })), + }; + console.log(`[inkspan-cross-engine-evidence] ${JSON.stringify(summary)}`); +}); From 3224a3c539634084db68037b496632fe3316f023 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:46:25 +0900 Subject: [PATCH 17/65] docs(browser): define cross-engine release evidence --- docs/TEST_STRATEGY.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 1b036ab0..cf1e59ea 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -22,7 +22,11 @@ Install or consume the packed artifacts rather than source-tree aliases. Verify ### Browser differential tests -Where browser fragment parsing or serialization participates in a security boundary, use dependency-locked Playwright coverage across Chromium, Firefox, and WebKit. The same adversarial corpus must run in all required engines. Differences are not normalized away merely to produce parity; every reviewed allowlisted difference requires a standards basis and threat analysis. Missing/skipped browsers are not successful release evidence. +Where browser fragment parsing or serialization participates in a security boundary, use dependency-locked Playwright coverage across Chromium, Firefox, and WebKit. SafeClipboard is already implemented on protected `main`; the cross-engine publication gate is implemented on the active browser-assurance PR and is not shipped until protected integration. + +The active gate pins **Playwright 1.62.0** in an isolated immutable browser-test lock and runs the same versioned synthetic corpus through the supported TipTap/ProseMirror `transformPastedHTML` path in named Chromium, Firefox, and WebKit projects on one **exact source head**. Evidence binds the corpus version, browser-test lock digest, Playwright version, actual browser versions, operating-system identity, and exact source head. The corpus covers active/resource/form content, hidden/Office/popover semantics, safe and unsafe links, malformed fragments, tables/lists, SVG/MathML, interactive/native fallback, byte/node/depth ceilings, hostile DOM capability failures, and a representative Word-like performance alarm. + +Differences are not normalized away merely to produce parity. The default gate has no generic normalization or broad engine allowlist; a permitted difference requires a focused regression fixture, authoritative standards basis, threat analysis, exact affected engine/version evidence, canonical interpretation, compatibility impact, and rollback. Missing, skipped, cancelled, incomplete, or divergent required browser evidence must fail closed rather than becoming successful release evidence. Passing the active PR does not become protected-main release authority until that exact implementation is reviewed and integrated. ### Office artifact tests @@ -47,7 +51,7 @@ Office Python uses a distinct language/tool contract. Across every advertised su At minimum, maintain regressions for: - duplicate JSON object names, negative zero, malformed JSON, malformed UTF-8, BOM, depth/value/string/byte limits, sparse/decorated/non-plain objects, symbols, accessors, proxies, reflection failures, and detached/cross-realm byte views; -- rich clipboard scripts, embeds, resources, forms, metadata, SVG/MathML, images, hidden subtrees, `dialog`, `details`, `popover`, Office `mso-hide`, CSS comments/escapes/case/whitespace, malformed fragments, tables/lists/formatting elements, unsafe links, and resource ceilings; +- rich clipboard scripts, embeds, resources, forms, metadata, SVG/MathML, images, hidden subtrees, `dialog`, `details`, `popover`, Office `mso-hide`, CSS comments/escapes/case/whitespace, malformed fragments, tables/lists/formatting elements, unsafe links, resource ceilings, real-engine parser/serializer differences, and hostile DOM capabilities; - SSR client-controlled form values, escaping, hydration continuity, reset behavior, and absence of server editor construction; - autosave stale validators, conflict/failure recovery, ambiguous transport outcomes, duplicate/no-op lifecycle transitions, callback exceptions, queue bounds, flush/close behavior, and durable-validator coherence; - selection/revision races and document movement during asynchronous hashing; @@ -66,7 +70,7 @@ A release candidate requires the exact integrated protected head to satisfy appl The release workflow must also satisfy the normative `docs/CONTRACTS.md` draft inventory contract: exactly one npm tarball, exactly one Office wheel, and `SHA256SUMS`; no other top-level entry; remote uploaded asset names exactly equal local names; and every GitHub-reported `sha256:` digest equals the exact transferred local file digest. Missing, stale, unexpected, non-regular, incomplete, or digest-mismatched assets are failures, not cleanup opportunities. -The 0.6.0 rich-clipboard release line specifically requires the Chromium, Firefox, and WebKit differential gate before publication. Deterministic jsdom coverage remains useful but is not a substitute for browser-engine acceptance. +The 0.6.0 rich-clipboard release line specifically requires the dependency-locked **Playwright 1.62.0** Chromium, Firefox, and WebKit differential gate on the exact integrated protected source head before publication. Deterministic jsdom coverage remains useful but is not a substitute for browser-engine acceptance; active-PR browser evidence remains proposed evidence until protected integration. ## Documentation verification @@ -74,4 +78,4 @@ Documentation tests must compare canonical PRD/TRD/Architecture/ADR/UML/data-mod ## Rollback of a test gate -A gate may be changed only because its product contract changed or the gate itself is technically invalid. The replacement begins with a regression that demonstrates the mismatch. Do not disable, skip, broaden allowlists, or lower coverage/security thresholds merely to make a branch green. +A gate may be changed only because its product contract changed or the gate itself is technically invalid. The replacement begins with a regression that demonstrates the mismatch. Do not disable, skip, broaden allowlists, or lower coverage/security thresholds merely to make a branch green. Browser-gate rollback must leave the rich-clipboard publication claim unaccepted unless equivalent or stronger real-engine evidence replaces it. From 93133d5d4a7e29dcd5be176bc64367efb650d36b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:47:19 +0900 Subject: [PATCH 18/65] docs(browser): make real-engine clipboard boundary explicit --- docs/clipboard-security.md | 39 ++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/clipboard-security.md b/docs/clipboard-security.md index a7c841bb..7e72f6d9 100644 --- a/docs/clipboard-security.md +++ b/docs/clipboard-security.md @@ -261,18 +261,35 @@ test-first evidence, standards interpretation, residual risk, and rollback. ## Browser evidence boundary -The current deterministic corpus runs in jsdom and proves the repository's -allowlist, bounds, error redaction, integration wiring, transform ordering, and -known Office/Google fixtures. It does not by itself prove parser, CSS, or -serialization parity across Chromium, Firefox, and WebKit. The doctoring record -therefore treats cross-engine differential execution as a release-acceptance -gate for the future 0.6.0 publication rather than claiming browser conformance -from jsdom evidence. +SafeClipboard itself is implemented on protected `main`. Deterministic jsdom +coverage remains the fast structural/security regression layer, but real browser +fragment parsing and serialization are a separate release-assurance authority. +The active cross-engine assurance PR pins **Playwright 1.62.0** and executes one +versioned synthetic **corpus version** through the supported TipTap/ProseMirror +paste pipeline in Chromium, Firefox, and WebKit on one **exact source head**. + +The browser gate records the exact source head, corpus version, browser-test lock +digest, Playwright version, actual engine versions, runner identity, and bounded +synthetic observations. It compares sanitized HTML, resulting ProseMirror JSON, +and rejection behavior and must fail closed when a required engine is missing, +skipped, cancelled, incomplete, or divergent. It uses no generic normalization; +a permitted difference requires a focused regression plus standards basis, +threat analysis, compatibility consequence, and rollback. The committed browser +fixtures are synthetic and the evidence contains no tenant document or +production clipboard payload. The detailed implementation and claim limits are +recorded in `docs/doctoring/cross-engine-rich-clipboard-assurance.md` and +`docs/TEST_STRATEGY.md`. + +Until the active browser-assurance PR reaches protected integration, its results +are `implemented_on_active_pr` evidence rather than shipped release authority. +The 0.6.0 rich-clipboard line must not be published without equivalent or stronger +exact-protected-head Chromium, Firefox, and WebKit acceptance. ## Ownership boundary Inkspan owns clipboard HTML validation, semantic reconstruction, shared editor -integration, bounded errors, and deterministic tests. The host still owns: +integration, bounded errors, deterministic tests, and its protected release +assurance contract. The host still owns: - clipboard permissions or custom clipboard APIs; - user notification and recovery UX; @@ -283,5 +300,7 @@ integration, bounded errors, and deterministic tests. The host still owns: - model or AI use of pasted content; and - legal, privacy, and information-governance policy. -The feature introduces no network request, storage adapter, credential, -database object, model call, or provider dependency. +The feature introduces no application network request, storage adapter, +credential, database object, model call, or provider dependency. Browser binary +provisioning belongs to CI/build evidence and does not grant runtime egress to the +sanitizer. From e7d809bdc4c918b5ad700a511a0dd7aa13148798 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:48:07 +0900 Subject: [PATCH 19/65] docs(fitness): bind active browser assurance to protected routing --- docs/DOCUMENTATION_FITNESS.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index f639d2d5..1b1824e8 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -38,23 +38,23 @@ Document fitness and implementation maturity are independent. A `present_current | TRD | `docs/TRD.md` | `present_current` | Same mixed maturity discipline; protected `main` remains runtime authority | Technical invariants, failure semantics, package boundaries and release evidence are reconstructable. | | ARCHITECTURE | `ARCHITECTURE.md` | `present_current` | `implemented_on_protected_main` for the bounded standalone/modular architecture | Host-vs-Inkspan authority and modular CWL composition are explicit. | | Public/API/schema/plugin contracts | `docs/CONTRACTS.md` | `present_current` | Protected-main contracts plus explicitly proposed extensions | Integration authority and degraded behavior are not hidden in implementation details. | -| ADR | `docs/adr/README.md` and detailed ADRs | `present_current` | Decisions distinguish current, proposed and future work; ADR 0015 governs active identity routing and ADR 0017 records the protected security-disclosure lifecycle | Alternatives, consequences, recovery, migration, verification and supersession are reviewable. | +| ADR | `docs/adr/README.md` and detailed ADRs | `present_current` | Decisions distinguish current, proposed and future work; ADR 0015 governs protected identity routing, ADR 0016 governs active browser-semantic assurance, and ADR 0017 records the protected security-disclosure lifecycle | Alternatives, consequences, recovery, migration, verification and supersession are reviewable. | | UML | `docs/UML.md` | `present_current` | Diagrams include protected-main and clearly proposed flows | Component, sequence, state, deployment, degraded-mode and authority flows are visible as diagram-as-code. | | DATA_MODEL / ERD | `docs/DATA_MODEL.md` | `present_current` | Current logical evidence/domain model; host persistence remains outside Inkspan | The model distinguishes document/evidence/conversion/release values from host-owned entities. | | physical relational ERD | none by design | `not_applicable` | `out_of_scope` while Inkspan owns no application database | No fake database is invented merely to satisfy an ERD checklist; a physical ERD becomes mandatory if persistence authority moves into Inkspan. | | SECURITY disclosure policy | root `SECURITY.md` plus ADR 0017 | `present_current` | `implemented_on_protected_main`; the root policy is protected authority and ADR 0017 records its durable architecture/process decision | Private reporting, evidence minimization, supported release-line binding, ownership limits, coordinated disclosure, and explicit no-SLA/no-certification claim boundaries are reconstructable. | -| Safe rich clipboard | PRD, TRD, clipboard/security doctoring and protected SafeClipboard source | `present_current` | `implemented_on_protected_main`; protected main includes bounded fail-closed semantic reconstruction in the actual TipTap/ProseMirror paste pipeline | Buyers can reconstruct the sanitizer trust boundary without confusing jsdom integration evidence with cross-engine browser assurance. | +| Safe rich clipboard | PRD, TRD, `docs/clipboard-security.md`, ADR 0003/0016 and protected SafeClipboard source | `present_current` | `implemented_on_protected_main`; protected main contains the real TipTap/ProseMirror SafeClipboard paste boundary | Buyers can reconstruct bounded active/hidden/resource rejection, transform ordering, error redaction and host ownership without treating the sanitizer as active-only work. | | Autosave lifecycle observation | PRD, TRD, `docs/document-autosave.md`, lifecycle doctoring and protected autosave package/session source | `present_current` | `implemented_on_protected_main`; protected main exposes the bounded construction-time observer contract | Buyers can reconstruct saving/blocked/recovery/idle/shutdown observation, document-free snapshots, observer-failure isolation, and durable-validator coherence without treating it as an active-PR promise. | | SSR/native-form serialization | PRD, TRD, `docs/server-rendering.md`, SSR doctoring and protected editor/form source | `present_current` | `implemented_on_protected_main`; protected main includes the explicit server-value handoff and synchronous hydrated mirror | Buyers can reconstruct opt-in server serialization, hydration continuity, client-controlled submission semantics, reset behavior and host-owned auth/CSRF/persistence boundaries. | | Toolbar shortcut accessibility metadata | PRD/TRD accessibility requirements, accessibility guide/doctoring and protected toolbar source | `present_current` | `implemented_on_protected_main`; shipped bold/italic/link/undo/redo shortcuts expose truthful `aria-keyshortcuts` metadata | Accessibility metadata is tied to actual repository-level keyboard behavior rather than extension-local assumptions. | | Revision-scoped selection evidence | selection lifecycle guide, doctoring and protected public handle/type contract | `present_current` | `implemented_on_protected_main`; protected main atomically binds structural selection coordinates to the exact revision | Atomic selection+revision evidence, privacy minimization and host-owned re-anchoring are reconstructable as shipped behavior without overstating cross-revision authority. | | Document-transition evidence | transition doctoring, public framework-independent contract and protected revision-evidence package | `present_current` | `implemented_on_protected_main`; protected main exposes object/JSON and strict UTF-8 transition evidence | Previous/resulting revision lineage and privacy/provenance boundaries are reconstructable as shipped local evidence without implying actor/time/durable-write provenance. | | THREAT_MODEL | `docs/THREAT_MODEL.md` | `present_current` | Covers current and explicitly proposed trust boundaries | Clipboard, Office, SSR/form, Yjs, model, host-authority and supply-chain threats are reconstructable. | -| TEST_STRATEGY | `docs/TEST_STRATEGY.md` | `present_current` | Current deterministic evidence plus `planned` cross-engine acceptance | Test authority and claim limits are explicit rather than inferred from CI badges. | -| OPERABILITY | `docs/OPERABILITY.md` | `present_current` | Current local/product responsibilities plus host-owned recovery boundaries | Conflict, collaboration, conversion and release recovery/rollback ownership are explicit. | +| TEST_STRATEGY | `docs/TEST_STRATEGY.md` | `present_current` | Protected deterministic evidence plus `implemented_on_active_pr` Playwright 1.62.0 cross-engine assurance | Test authority, exact source-head browser evidence and claim limits are explicit rather than inferred from CI badges. | +| OPERABILITY | `docs/OPERABILITY.md` | `present_current` | Current product responsibilities plus active browser-assurance recovery boundaries and host-owned recovery boundaries | Conflict, collaboration, conversion, browser divergence and release recovery/rollback ownership are explicit. | | Release / rollback / provenance | TRD, OPERABILITY and release ADRs | `present_current` | Mix of `implemented_on_protected_main` and active hardening | Exact-source release authority, stale-evidence rejection and rollback are reconstructable. | -| Envelope schema identity / migration routing | ADR 0015, PRD, TRD, DATA_MODEL, envelope guide/doctoring and PR #84 | `present_current` | Identity-only routing capability is `implemented_on_active_pr`; strict current-schema parsing and host migration ownership remain authoritative | The architecture distinguishes bounded schema identification from host-owned migration execution without calling the active PR shipped. | -| Cross-engine browser-semantic release assurance | ADR 0016, UML, TEST_STRATEGY, TRACEABILITY and Issue #66 | `present_current` | Differential Chromium/Firefox/WebKit release gate is `planned` and now unblocked by SafeClipboard protected integration | Browser-realistic security assurance is a durable release decision even though its implementation remains future work. | +| Envelope schema identity / migration routing | ADR 0015, PRD, TRD, DATA_MODEL, envelope guide/doctoring and protected identity-routing source | `present_current` | `implemented_on_protected_main`; strict current-schema parsing and host migration ownership remain authoritative | The architecture distinguishes bounded schema identification from host-owned migration execution without expanding Inkspan persistence authority. | +| Cross-engine browser-semantic release assurance | ADR 0016, `docs/doctoring/cross-engine-rich-clipboard-assurance.md`, TEST_STRATEGY, OPERABILITY, TRACEABILITY and the active cross-engine assurance PR | `present_current` | `implemented_on_active_pr`; SafeClipboard itself is `implemented_on_protected_main` | Browser-realistic Chromium/Firefox/WebKit release assurance is implemented and reviewable without promoting active-PR evidence to protected release authority. | | TRACEABILITY | `docs/TRACEABILITY.md` | `present_current` | Links standards/research/requirements to decisions and evidence with scoped claims | Acquisition reviewers can distinguish evidence from aspiration. | | Contributor/agent authority | `AGENTS.md`, `CLAUDE.md`, `docs/README.md` | `present_current` | Protected-main-first decision discipline | Agents are directed back to the same canonical graph rather than parallel private memory. | | Autonomous maintenance governance | `AGENTS.md`, `CLAUDE.md` plus the external scheduler | `present_current` | `out_of_scope` as Inkspan runtime behavior; the external scheduler owns cadence/continuation | Work-conserving execution, lane-local waiting, no-report-as-completion, and the scheduler-vs-product authority boundary are reconstructable without pretending automation is an Inkspan API. | @@ -77,8 +77,8 @@ The canonical graph must retain durable product decisions from the project conve - accessibility, keyboard, print/export and document-fidelity evidence boundaries; - host ownership of transport, authentication, authorization, tenant isolation, persistence, credentials, migration, retention, deployment, durable audit and model policy; - protected-main private vulnerability reporting and coordinated disclosure with explicit evidence-minimization and no-SLA/no-certification boundaries; -- strict current-schema parsing plus `implemented_on_active_pr` identity-only envelope routing in PR #84, while migration execution remains host-owned; -- real Chromium/Firefox/WebKit differential evidence as a planned release gate for browser-semantic clipboard security rather than a jsdom conformance claim; and +- protected-main identity-only envelope routing with strict current-schema parsing and host-owned migration execution; +- active dependency-locked Playwright 1.62.0 Chromium/Firefox/WebKit differential evidence as a release gate for browser-semantic clipboard security rather than a jsdom conformance claim; and - exact-head/package/security/provenance/release evidence as separate authorities from comments, model verdicts and historical checks. Autonomous commercial-maintenance scheduling and the no-early-stop execution discipline are **control-plane governance, not a shipped Inkspan product capability**. The external scheduler is the execution authority for cadence and continuation; repository guidance records writer leases, work-conserving queue behavior, lane-local waiting, evidence hierarchy and protected-main authority without pretending the automation prompt is a runtime API or architectural feature. @@ -89,14 +89,13 @@ Where an older conversation, PR body, or plan conflicts with protected `main`, i The documentation pack itself is substantially complete for acquisition review, but **repository closure is not documentation closure**. The remaining gaps are intentionally represented rather than hidden: -1. PR #84 implements Issue #74 on an active branch: identity-only migration routing must still pass exact-head CI/security/package/review/repository-policy gates and protected integration before its maturity changes to `implemented_on_protected_main`. -2. Issue #66 remains `planned`, but SafeClipboard is now integrated: the dependency-locked Chromium/Firefox/WebKit differential suite can start from fresh protected `main` and remains required before the rich-clipboard release line. -3. SafeClipboard, autosave lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are `implemented_on_protected_main` and must not be described as active-only work. -4. The canonical documentation graph is already integrated on protected `main`; future reconciliation is required when protected source, accepted decisions, or implementation maturity materially changes. -5. Documentation becoming mergeable or protected-merged is not a reason for the commercial loop to stop; the next safe product, release, security, accessibility or interoperability lane must continue. +1. The active cross-engine assurance PR implements Issue #66: the dependency-locked Playwright 1.62.0 Chromium/Firefox/WebKit differential suite must prove exact-head browser evidence, complete its reviews/checks, and reach protected integration before becoming release authority. +2. Envelope identity routing, SafeClipboard, autosave lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are `implemented_on_protected_main` and must not be described as active-only work. +3. The canonical documentation graph is integrated on protected `main`; future reconciliation is required when protected source, accepted decisions, active implementation maturity, or release evidence materially changes. +4. Documentation becoming mergeable or protected-merged is not a reason for the commercial loop to stop; the next safe product, release, security, accessibility or interoperability lane must continue. ## Sufficiency decision -PRD, TRD, Architecture, ADR, UML, conceptual ERD/data model, contracts, threat model, test strategy, operability, security disclosure, and traceability are `present_current` for the durable product and accepted/planned architecture decisions reconstructed from the conversation and live repository. SafeClipboard, autosave lifecycle observation, the security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are `implemented_on_protected_main`. The pre-existing canonical documentation graph is integrated on protected `main`; PR #84's envelope-routing changes remain `implemented_on_active_pr` evidence until protected integration. Cross-engine browser assurance remains `planned`. A physical relational ERD is `not_applicable` because Inkspan deliberately owns no application persistence. +PRD, TRD, Architecture, ADR, UML, conceptual ERD/data model, contracts, threat model, test strategy, operability, security disclosure, and traceability are `present_current` for the durable product and accepted/planned architecture decisions reconstructed from the conversation and live repository. Envelope identity routing, SafeClipboard, autosave lifecycle observation, the security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are `implemented_on_protected_main`. Cross-engine browser assurance is `implemented_on_active_pr` and remains non-authoritative until protected integration. A physical relational ERD is `not_applicable` because Inkspan deliberately owns no application persistence. -No material product architecture decision identified by this review remains only in chat or issue prose. Accordingly, the **pre-existing documentation graph is a protected-main canonical baseline** and is sufficient for acquisition reconstruction under the current product boundary; active-PR deltas do not become protected-main authority merely because this branch documents them. Product/release readiness must continue to be evaluated independently of documentation completeness. +No material product architecture decision identified by this review remains only in chat or issue prose. Accordingly, the **documentation graph is a protected-main canonical baseline** and is sufficient for acquisition reconstruction under the current product boundary. Product/release readiness must continue to be evaluated independently of documentation completeness. From 95a2d62206dd3a96955e92c6b8ae3c7996409200 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:49:01 +0900 Subject: [PATCH 20/65] docs(changelog): record cross-engine release assurance --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f74c34..7a9ad7e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,10 +32,12 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - Added deterministic regression and documentation contracts plus APA 7th doctoring for exact `Control`/`Meta` shortcut alternatives, the descriptive-only accessibility boundary, repository-level shortcut verification, and omission of unsupported shortcut claims ### Tests +- Added a dependency-locked Chromium/Firefox/WebKit **cross-engine rich-clipboard release gate** using Playwright 1.62.0, one versioned synthetic adversarial corpus, the actual TipTap/ProseMirror paste path, exact-source-head/lock/browser evidence, hostile-DOM and resource-ceiling cases, bounded performance alarm evidence, and fail-closed three-engine consensus without generic normalization - Added test-first Node `renderToString` evidence for the missing SSR native value, controlled-over-default selection, escaping, external form ownership, no ProseMirror server construction, and opt-out non-disclosure - Added browser-DOM handoff tests proving the field retains and updates the selected value before TipTap exists while reset-only unnamed fields remain empty ### Documentation +- Added browser-assurance doctoring, operability, test-strategy, clipboard-security, and documentation-fitness coverage for the **dependency-locked Chromium/Firefox/WebKit** release boundary, including Playwright 1.62.0 provenance, exact-head/corpus identity, standards-backed difference policy, evidence minimization, fail-closed behavior, and rollback - Added a canonical acquisition documentation spine covering product requirements, technical requirements, public interface/integration contracts, Mermaid UML, a conceptual data/evidence model, a threat model, test strategy, operability/recovery, standards/evidence traceability, and seventeen linked architecture decision records without inventing Inkspan-owned persistence or host authority; the newest decisions make envelope schema identity/host-owned migration routing, cross-engine browser-semantic release assurance, and the protected security-disclosure lifecycle first-class while keeping unimplemented capabilities explicitly planned - Added machine-checkable canonical-documentation decision coverage that keeps required files, ADR index links and completeness, migration-routing and browser-assurance UML/data-model/traceability evidence, physical-ERD non-applicability, browser-security evidence, offline font provenance/no-runtime-font-egress, standards references, rollback sections, host-vs-Inkspan authority boundaries, implemented-vs-active-PR status, and work-conserving autonomous-maintenance guidance synchronized - Documented work-conserving autonomous-maintenance governance in `AGENTS.md` and `CLAUDE.md`: a blocked PR blocks only its lane, status/report/prompt/documentation milestones are intermediate while safe work remains, and the external scheduler owns cadence rather than becoming an Inkspan runtime capability From 2bae63d4004582279a80b6fcaf1f806d00d19fd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:51:38 +0900 Subject: [PATCH 21/65] test(release): require tagged cross-engine browser evidence --- src/releaseBrowserEvidence.test.ts | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/releaseBrowserEvidence.test.ts diff --git a/src/releaseBrowserEvidence.test.ts b/src/releaseBrowserEvidence.test.ts new file mode 100644 index 00000000..535490e4 --- /dev/null +++ b/src/releaseBrowserEvidence.test.ts @@ -0,0 +1,45 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +/** Read one authoritative repository file as UTF-8 text. */ +function repositoryFile(path: string): string { + return readFileSync(resolve(process.cwd(), path), 'utf8'); +} + +const workflow = repositoryFile('.github/workflows/release.yml'); + +describe('release cross-engine browser evidence contract', () => { + it('re-runs the pinned three-engine clipboard gate on the exact tagged source', () => { + const browserJobIndex = workflow.indexOf(' browser-release-evidence:'); + const buildJobIndex = workflow.indexOf(' build-release-artifacts:'); + + expect(browserJobIndex).toBeGreaterThan(-1); + expect(buildJobIndex).toBeGreaterThan(browserJobIndex); + + const browserJob = workflow.slice(browserJobIndex, buildJobIndex); + expect(browserJob).toContain('Cross-engine Clipboard / Playwright 1.62.0'); + expect(browserJob).toContain('runs-on: ubuntu-24.04'); + expect(browserJob).toContain('permissions:\n contents: read'); + expect(browserJob).toContain('persist-credentials: false'); + expect(browserJob).toContain('pnpm --dir tests/browser install --frozen-lockfile'); + expect(browserJob).toContain( + 'pnpm --dir tests/browser exec playwright install --with-deps chromium firefox webkit', + ); + expect(browserJob).toContain('INKSPAN_EXPECTED_HEAD_SHA: ${{ github.sha }}'); + expect(browserJob).toContain( + 'pnpm --dir tests/browser exec playwright test --config playwright.config.ts', + ); + }); + + it('makes immutable publication depend on both artifacts and tagged browser evidence', () => { + const publishJobIndex = workflow.indexOf(' publish-release:'); + expect(publishJobIndex).toBeGreaterThan(-1); + + const publishJob = workflow.slice(publishJobIndex); + expect(publishJob).toContain( + 'needs: [build-release-artifacts, browser-release-evidence]', + ); + }); +}); From 3cc2f974226c814802f1f1309391969ad1b78245 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:52:38 +0900 Subject: [PATCH 22/65] ci(release): gate publication on tagged browser evidence --- .github/workflows/release.yml | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afc2d612..d0feb0e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,39 @@ env: PYTHON_VERSION: '3.14' jobs: + browser-release-evidence: + name: Cross-engine Clipboard / Playwright 1.62.0 + if: github.repository == 'ContextualWisdomLab/inkspan' + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + env: + PLAYWRIGHT_BROWSERS_PATH: /tmp/inkspan-playwright-browsers + steps: + - name: Check out the exact tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + - name: Set up pnpm + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install root dependencies from the immutable lock + run: pnpm install --frozen-lockfile + - name: Install browser-test dependencies from the isolated immutable lock + run: pnpm --dir tests/browser install --frozen-lockfile + - name: Install Playwright browser revisions pinned by 1.62.0 + run: pnpm --dir tests/browser exec playwright install --with-deps chromium firefox webkit + - name: Verify real-engine rich clipboard release evidence on the tagged source + env: + INKSPAN_EXPECTED_HEAD_SHA: ${{ github.sha }} + run: pnpm --dir tests/browser exec playwright test --config playwright.config.ts + build-release-artifacts: if: github.repository == 'ContextualWisdomLab/inkspan' runs-on: ubuntu-24.04 @@ -136,7 +169,7 @@ jobs: publish-release: if: github.repository == 'ContextualWisdomLab/inkspan' - needs: build-release-artifacts + needs: [build-release-artifacts, browser-release-evidence] runs-on: ubuntu-24.04 permissions: contents: write From 82681e8e24ef4bda4d14ee2e22dc59af5f0be41f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:04:21 +0900 Subject: [PATCH 23/65] test(docs): bind routing maturity to protected main --- src/canonicalDocumentationDecisionCoverage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canonicalDocumentationDecisionCoverage.test.ts b/src/canonicalDocumentationDecisionCoverage.test.ts index 79dcd3b9..849435aa 100644 --- a/src/canonicalDocumentationDecisionCoverage.test.ts +++ b/src/canonicalDocumentationDecisionCoverage.test.ts @@ -61,7 +61,7 @@ describe('canonical architecture decision coverage', () => { expect(fitness).toContain('implemented_on_active_pr'); expect(fitness).toContain('planned'); expect(fitness).toMatch( - /Envelope schema identity \/ migration routing[^\n]*implemented_on_active_pr/u, + /Envelope schema identity \/ migration routing[^\n]*implemented_on_protected_main/u, ); expect(fitness).toContain('Issue #66'); }); From b06bba148037030066e432c1ecec8a8a0d0ce893 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:05:03 +0900 Subject: [PATCH 24/65] test(docs): accept protected routing fitness state --- src/canonicalProductDocumentation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canonicalProductDocumentation.test.ts b/src/canonicalProductDocumentation.test.ts index f2a98bae..ecaf991f 100644 --- a/src/canonicalProductDocumentation.test.ts +++ b/src/canonicalProductDocumentation.test.ts @@ -112,7 +112,7 @@ describe('canonical product documentation graph', () => { expect(trd).toMatch(/Envelope identity routing is `implemented_on_active_pr`/u); expect(fitness).toContain('Autosave lifecycle observation'); expect(fitness).toMatch(/Autosave lifecycle observation[^\n]*implemented_on_protected_main/u); - expect(fitness).toMatch(/Envelope schema identity \/ migration routing[^\n]*implemented_on_active_pr/u); + expect(fitness).toMatch(/Envelope schema identity \/ migration routing[^\n]*implemented_on_protected_main/u); }); it('records host ownership and deterministic Inkspan authority consistently', () => { From b44d7b0f713b79b097b40803adb6b4ed637975fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:05:24 +0900 Subject: [PATCH 25/65] test(browser): honor DOM capability error contract --- tests/browser/specs/clipboard.browser.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/browser/specs/clipboard.browser.spec.ts b/tests/browser/specs/clipboard.browser.spec.ts index 47fb2d6b..e523957f 100644 --- a/tests/browser/specs/clipboard.browser.spec.ts +++ b/tests/browser/specs/clipboard.browser.spec.ts @@ -106,8 +106,10 @@ test('redacts hostile document capability failures without source disclosure', a privateSource, ); - expect(result.errorCode).toBe('invalid_html'); - expect(result.message).toBe('Rich clipboard HTML could not be sanitized.'); + expect(result.errorCode).toBe('dom_unavailable'); + expect(result.message).toBe( + 'Rich clipboard sanitization requires a DOM-capable document.', + ); expect(result.message).not.toContain('private source'); }); From 433ae44e1e269a10d7c145a5a5e2ee86d6183ec5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:11:32 +0900 Subject: [PATCH 26/65] test(docs): reject stale envelope routing maturity --- src/canonicalProductDocumentation.test.ts | 28 +++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/canonicalProductDocumentation.test.ts b/src/canonicalProductDocumentation.test.ts index ecaf991f..328d577f 100644 --- a/src/canonicalProductDocumentation.test.ts +++ b/src/canonicalProductDocumentation.test.ts @@ -100,16 +100,40 @@ describe('canonical product documentation graph', () => { it('tracks protected and active implementation maturity semantically', () => { const prd = repositoryFile('docs/PRD.md'); const trd = repositoryFile('docs/TRD.md'); + const contracts = repositoryFile('docs/CONTRACTS.md'); + const dataModel = repositoryFile('docs/DATA_MODEL.md'); + const traceability = repositoryFile('docs/TRACEABILITY.md'); + const routingDoctoring = repositoryFile( + 'docs/doctoring/envelope-identity-routing.md', + ); const fitness = repositoryFile('docs/DOCUMENTATION_FITNESS.md'); const currentScope = prd.slice(prd.indexOf('## Current, proposed, and planned scope')); expect(currentScope).toMatch(/lifecycle observation[^.]*implemented on protected `main`/iu); expect(currentScope).toMatch(/SafeClipboard[^.]*implemented on protected `main`/u); - expect(currentScope).toMatch(/Envelope identity-only migration routing[^.]*implemented_on_active_pr/u); + expect(currentScope).toMatch(/Envelope identity(?:-only)? migration routing[^.]*implemented on protected `main`/iu); + expect(currentScope).not.toContain('PR #84'); + expect(currentScope).not.toMatch(/Envelope identity[^.]*implemented_on_active_pr/iu); expect(currentScope).not.toMatch(/open development lines include[^.]*lifecycle observation/u); + expect(trd).toContain('Autosave lifecycle observation is implemented on protected `main`'); expect(trd).toContain('SafeClipboard is implemented on protected `main`'); - expect(trd).toMatch(/Envelope identity routing is `implemented_on_active_pr`/u); + expect(trd).toContain('Envelope identity routing is implemented on protected `main`'); + expect(trd).not.toContain('PR #84 implements'); + expect(trd).not.toMatch(/Envelope identity routing is `implemented_on_active_pr`/u); + + expect(contracts).toContain('Envelope identity routing is implemented on protected `main`'); + expect(contracts).not.toContain('PR #84 adds an active-PR identity-routing contract'); + expect(contracts).not.toContain('implemented_on_active_pr'); + + expect(dataModel).toContain('`document_schema_identity`: `implemented_on_protected_main`'); + expect(dataModel).not.toContain('`document_schema_identity`: `implemented_on_active_pr`'); + expect(traceability).toContain('Envelope version routing'); + expect(traceability).toContain('protected-main evidence'); + expect(traceability).not.toContain('PR #84 is active implementation evidence'); + expect(routingDoctoring).toContain('Status: Implemented on protected main'); + expect(routingDoctoring).not.toContain('Status: Implemented on active PR'); + expect(fitness).toContain('Autosave lifecycle observation'); expect(fitness).toMatch(/Autosave lifecycle observation[^\n]*implemented_on_protected_main/u); expect(fitness).toMatch(/Envelope schema identity \/ migration routing[^\n]*implemented_on_protected_main/u); From 71e4228d0e9e0dc4643e54e0995efe562a099665 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:12:59 +0900 Subject: [PATCH 27/65] docs: reconcile envelope routing with protected main --- docs/PRD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index dac363f2..88d8e540 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -4,7 +4,7 @@ Status: Protected-main canonical baseline ## Product definition -Inkspan is a standalone Markdown/HTML rich-text authoring and deterministic document-conversion product that can also be embedded as a modular CWL component. Protected `main` is the implementation authority for what Inkspan actually provides today. Safe rich clipboard handling, lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. This canonical PRD also records Active PR / Proposed requirements such as bounded envelope identity routing; such requirements are not shipped claims until protected integration. Across current and proposed scope, Inkspan keeps host transport, authentication, authorization, tenant isolation, durable persistence, credentials, migrations, retention, deployment, audit storage, and model-use policy outside the product boundary unless a future accepted versioned contract explicitly changes that division. +Inkspan is a standalone Markdown/HTML rich-text authoring and deterministic document-conversion product that can also be embedded as a modular CWL component. Protected `main` is the implementation authority for what Inkspan actually provides today. Safe rich clipboard handling, lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, document-transition evidence, and bounded envelope identity routing are implemented on protected `main`. This canonical PRD also records Active PR / Proposed requirements such as cross-engine browser-semantic release assurance; such requirements are not shipped claims until protected integration. Across current and proposed scope, Inkspan keeps host transport, authentication, authorization, tenant isolation, durable persistence, credentials, migrations, retention, deployment, audit storage, and model-use policy outside the product boundary unless a future accepted versioned contract explicitly changes that division. The product promise is: **author, convert, collaborate, and prove document changes without hiding authority inside the editor.** @@ -136,4 +136,4 @@ Shareable acquisition evidence excludes production tenant content and credential Protected `main` is the sole implemented baseline. Open PRs may describe Proposed or Active work but are not shipped contracts until protected integration. Canonical documentation must state when a requirement is target architecture rather than current implementation. -SafeClipboard, lifecycle observation, the root security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. Envelope identity-only migration routing is `implemented_on_active_pr` in PR #84 and remains unshipped until protected integration. Cross-engine browser assurance remains `planned`, now unblocked by SafeClipboard integration, and must start from fresh protected `main` without weakening the sanitizer or treating jsdom evidence as browser conformance. +SafeClipboard, lifecycle observation, the root security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, document-transition evidence, and envelope identity migration routing are implemented on protected `main`. Cross-engine browser assurance is `implemented_on_active_pr` on the current release-assurance line and remains non-authoritative until protected integration; it must preserve the sanitizer's protected behavior and must not treat jsdom evidence as browser conformance. From dbef92c8b62657f15cebc8c2fdac5e27935c9d74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:13:43 +0900 Subject: [PATCH 28/65] docs: reconcile routing technical authority --- docs/TRD.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index 4164a440..0f501dea 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -12,7 +12,7 @@ Protected `main` is the implementation authority. Open PRs may provide Proposed The root product may compose TipTap/ProseMirror, React-facing integration, deterministic conversion, revision evidence, autosave, collaboration adapters, email/base64 utilities, and Office rendering. Framework-independent subpaths must not accidentally require React, DOM globals, TipTap UI, ProseMirror view, Yjs, network, credentials, database clients, or model SDKs when their public contract excludes those dependencies. -The active envelope-routing slice adds `@contextualwisdomlab/cwl-editor/envelope-identity` as a framework-independent ESM/CommonJS/strict-TypeScript surface. It must remain usable without the interactive editor graph and expose only routing metadata plus bounded input options. +The protected envelope-routing contract includes `@contextualwisdomlab/cwl-editor/envelope-identity` as a framework-independent ESM/CommonJS/strict-TypeScript surface. It remains usable without the interactive editor graph and exposes only routing metadata plus bounded input options. Standalone use must not require naruon or contextual-orchestrator. A CWL host can compose Inkspan additively through stable interfaces. @@ -20,7 +20,7 @@ Standalone use must not require naruon or contextual-orchestrator. A CWL host ca A versioned `document_envelope` is validated before canonicalization or hashing. Strict current-schema JSON text/byte handling rejects malformed input, duplicate object names, malformed UTF-8, unsupported schemas, hostile JavaScript object descriptors, and resource-limit violations according to the active contract. -PR #84 implements the ADR 0015 identity-only routing boundary. `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` require complete bounded envelope input and return only a frozen string `schemaId` plus positive safe-integer `schemaVersion`. They preserve duplicate-name, strict UTF-8/BOM, JSON-compatibility, resource-limit, descriptor/accessor/proxy-reflection, and redacted-error boundaries while deliberately not treating an unsupported `documentJson` as current TipTap/ProseMirror semantics. +Envelope identity routing is implemented on protected `main` under ADR 0015. `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` require complete bounded envelope input and return only a frozen string `schemaId` plus positive safe-integer `schemaVersion`. They preserve duplicate-name, strict UTF-8/BOM, JSON-compatibility, resource-limit, descriptor/accessor/proxy-reflection, and redacted-error boundaries while deliberately not treating an unsupported `documentJson` as current TipTap/ProseMirror semantics. `parseDocumentEnvelope()` remains strict and current-schema-only. Schema registry, migration selection/execution, authorization, tenant isolation, persistence, durable audit, retention, rollback, and recovery remain host-owned. A migrated value must pass the strict current parser before it becomes canonical Inkspan state. @@ -34,7 +34,7 @@ TipTap/ProseMirror is the deterministic editing authority for supported Markdown SafeClipboard is implemented on protected `main`. Rich clipboard HTML is untrusted. Sanitization is installed in the actual TipTap/ProseMirror paste pipeline, uses bounded fail-closed semantic reconstruction, performs no external fetch or active execution, and preserves only supported semantic content. Pure sanitizer APIs and editor integration remain behaviorally consistent. -Where browser fragment parsing or serialization can change security semantics, a release must run the same adversarial corpus through dependency-locked Playwright Chromium, Firefox, and WebKit projects. Differences require an explicit standards/threat rationale rather than an unconditional parity normalization. Issue #66 remains the separate browser-realistic release-assurance implementation lane and is no longer blocked by SafeClipboard integration. +Where browser fragment parsing or serialization can change security semantics, a release must run the same adversarial corpus through dependency-locked Playwright Chromium, Firefox, and WebKit projects. Differences require an explicit standards/threat rationale rather than an unconditional parity normalization. The active cross-engine assurance PR implements Issue #66 on the protected SafeClipboard baseline and remains non-authoritative until protected integration. ## SSR and native forms @@ -101,7 +101,7 @@ Toolbar shortcut metadata is implemented on protected `main`. Shipped keyboard b ## Packaging, compatibility, and release evidence -The root package and framework-independent subpaths are verified from packed artifacts under ESM, CommonJS, and strict TypeScript consumers. PR #84 adds equivalent packed-artifact verification for `envelope-identity` without framework dependencies. Office Python surfaces are verified under the documented supported Python matrix, exact production statement/branch/function/line coverage, complete public docstrings, built wheel/package inspection, and license/dependency consistency. +The root package and framework-independent subpaths are verified from packed artifacts under ESM, CommonJS, and strict TypeScript consumers. The protected `envelope-identity` subpath has equivalent packed-artifact verification without framework dependencies. Office Python surfaces are verified under the documented supported Python matrix, exact production statement/branch/function/line coverage, complete public docstrings, built wheel/package inspection, and license/dependency consistency. Release publication verifies one exact integrated protected source head, expected artifact inventory and digests, package/runtime compatibility, security/coverage/accessibility/document-fidelity gates, SBOM/provenance/reproducibility where configured, zero valid unresolved findings, formal review/branch-protection requirements, and post-publication artifact smoke evidence. @@ -113,4 +113,4 @@ Queued, cancelled, skipped-required, absent, stale-head, predecessor-head, statu ## Implemented versus proposed -Protected `main` is the sole implemented baseline. SafeClipboard, the security disclosure lifecycle, autosave lifecycle observation, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. Envelope identity routing is `implemented_on_active_pr` in PR #84 and remains unshipped until protected integration. Cross-engine browser assurance remains `planned`, now unblocked and required before the SafeClipboard behavior enters the verified release line. Canonical docs distinguish target architecture from shipped behavior and must be updated when the protected implementation changes. +Protected `main` is the sole implemented baseline. SafeClipboard, the security disclosure lifecycle, autosave lifecycle observation, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, document-transition evidence, and envelope identity routing are implemented on protected `main`. Cross-engine browser assurance is `implemented_on_active_pr` on the current release-assurance line and remains unshipped until protected integration. Canonical docs distinguish target architecture from shipped behavior and must be updated when the protected implementation changes. From ca8511abfa354512852e50489591a8ce9ec0f18c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:14:16 +0900 Subject: [PATCH 29/65] docs: reconcile routing interface contract --- docs/CONTRACTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index e402a024..7715705e 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -20,7 +20,7 @@ Package exports, declaration files, runtime support, and packed ESM/CommonJS/str The **document envelope** is a versioned boundary carrying schema identity and supported document content. Current-schema parsing validates strict JSON/UTF-8 and active schema requirements before canonicalization or hashing. Unknown schema migration remains host-owned. -PR #84 adds an active-PR identity-routing contract through `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()`, exported by the root package and the framework-independent `./envelope-identity` subpath. The inspectors validate a complete bounded envelope and return only frozen `{ schemaId, schemaVersion }` routing metadata. They do not return `documentJson`, accept unknown document semantics as current, execute a migration, authorize access, or claim durable persistence. Until protected integration this is `implemented_on_active_pr` evidence rather than a shipped contract. +Envelope identity routing is implemented on protected `main` through `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()`, exported by the root package and the framework-independent `./envelope-identity` subpath. The inspectors validate a complete bounded envelope and return only frozen `{ schemaId, schemaVersion }` routing metadata. They do not return `documentJson`, accept unknown document semantics as current, execute a migration, authorize access, or claim durable persistence. Canonical revision evidence is content-equality metadata only. It is not an actor identity, tenant identity, authorization grant, signature, timestamp, durable write receipt, or bearer credential. Identity routing does not weaken the strict current-schema parser or transfer schema-registry, migration, persistence, rollback, or recovery authority from the host. @@ -119,7 +119,7 @@ Rollback must preserve readable canonical documents and must not require silentl | Contract surface | Inkspan authority | Host authority | | --- | --- | --- | | Markdown/HTML editing | deterministic editor state and supported import/export semantics | application workflow, document ownership, authorization | -| document envelope/revision | schema validation, canonical bytes, local equality evidence | migration orchestration, durable storage, signatures, tenant binding | +| document envelope/revision | schema validation, identity routing, canonical bytes, local equality evidence | migration orchestration, durable storage, signatures, tenant binding | | autosave | local ordering/state, callback contract, validator validation | transport, durable CAS, retry/offline policy, persistence | | collaboration | provider-neutral editor/Yjs binding | provider lifecycle, rooms, identity, authorization, persistence, awareness privacy | | Office rendering | deterministic bounded JSON→artifact conversion | file destination policy, downstream distribution, tenant authorization | From 90b40e92d1167a83e0805050f3f7a59d278770ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:14:54 +0900 Subject: [PATCH 30/65] test(docs): bind ADR maturity to protected authority --- ...nicalDocumentationDecisionCoverage.test.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/canonicalDocumentationDecisionCoverage.test.ts b/src/canonicalDocumentationDecisionCoverage.test.ts index 849435aa..6576cb1e 100644 --- a/src/canonicalDocumentationDecisionCoverage.test.ts +++ b/src/canonicalDocumentationDecisionCoverage.test.ts @@ -30,10 +30,20 @@ describe('canonical architecture decision coverage', () => { expect(index).toContain('Security disclosure lifecycle and coordinated vulnerability handling'); }); - it('keeps the new decisions complete enough for acquisition review', () => { - for (const path of [migrationAdr, browserAssuranceAdr, securityDisclosureAdr]) { - const adr = repositoryFile(path); - expect(adr).toContain('Status: Proposed'); + it('keeps decision status synchronized with protected and active implementation maturity', () => { + const migration = repositoryFile(migrationAdr); + const browser = repositoryFile(browserAssuranceAdr); + const security = repositoryFile(securityDisclosureAdr); + const index = repositoryFile('docs/adr/README.md'); + + expect(migration).toContain('Status: Accepted'); + expect(security).toContain('Status: Accepted'); + expect(browser).toContain('Status: Proposed'); + expect(index).toMatch(/0015[^\n]*\| Accepted \|/u); + expect(index).toMatch(/0016[^\n]*\| Proposed \|/u); + expect(index).toMatch(/0017[^\n]*\| Accepted \|/u); + + for (const adr of [migration, browser, security]) { for (const heading of [ '## Context', '## Alternatives considered', @@ -63,10 +73,12 @@ describe('canonical architecture decision coverage', () => { expect(fitness).toMatch( /Envelope schema identity \/ migration routing[^\n]*implemented_on_protected_main/u, ); - expect(fitness).toContain('Issue #66'); + expect(fitness).toMatch( + /Cross-engine browser-semantic release assurance[^\n]*implemented_on_active_pr/u, + ); }); - it('makes active and planned decision paths reviewable as diagrams and standards traceability', () => { + it('makes protected and active decision paths reviewable as diagrams and standards traceability', () => { const uml = repositoryFile('docs/UML.md'); const traceability = repositoryFile('docs/TRACEABILITY.md'); @@ -76,8 +88,9 @@ describe('canonical architecture decision coverage', () => { expect(traceability).toContain('Cross-engine release assurance'); expect(traceability).toContain('RFC 7493'); expect(traceability).toMatch( - /Envelope version routing[^\n]*Active-PR evidence is not shipped authority/u, + /Envelope version routing[^\n]*protected-main evidence/iu, ); + expect(traceability).not.toContain('PR #84 is active implementation evidence'); }); it('keeps the conceptual ERD explicit without inventing Inkspan persistence', () => { @@ -90,8 +103,9 @@ describe('canonical architecture decision coverage', () => { ]) { expect(dataModel).toContain(marker); } - expect(dataModel).toContain('`document_schema_identity`: `implemented_on_active_pr`'); - expect(dataModel).toContain('planned evidence objects'); + expect(dataModel).toContain('`document_schema_identity`: `implemented_on_protected_main`'); + expect(dataModel).toContain('`browser_assurance_evidence`: `implemented_on_active_pr`'); + expect(dataModel).toContain('active release-assurance evidence objects'); expect(dataModel).toContain('does **not** own an application database'); expect(dataModel).toContain('physical database ERD'); }); From e88a955d0cc4c5b3b28945051d511ed4231dfec0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:15:31 +0900 Subject: [PATCH 31/65] docs: reconcile evidence-model maturity --- docs/DATA_MODEL.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 7be7e870..6963059e 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -40,7 +40,7 @@ erDiagram ## Inkspan-owned in-memory or package value objects - `document_envelope`: versioned, strictly validated, canonicalizable complete document value. It can contain the complete document body. Inkspan constructs/validates it; a host may persist it under its own policy. -- `document_schema_identity`: `implemented_on_active_pr` in PR #84 as a frozen identity-only routing value containing the complete envelope's bounded `schemaId` and positive safe-integer `schemaVersion`. It contains no document body and does not validate unsupported-version document semantics. The host owns migration selection/execution and persistence. +- `document_schema_identity`: `implemented_on_protected_main` as a frozen identity-only routing value containing the complete envelope's bounded `schemaId` and positive safe-integer `schemaVersion`. It contains no document body and does not validate unsupported-version document semantics. The host owns migration selection/execution and persistence. - `document_revision`: SHA-256 equality evidence derived from one exact canonical envelope. It is not authorization, tenant identity, actor identity, timestamp, signature, or proof of a durable write. - `editor_session`: conceptual local editor/runtime lifetime. It binds one mounted editor state to local evidence and host callbacks. It is not a durable account/session record and has no authentication authority. - `document_transition`: previous/resulting revision pair plus changed classification. It deliberately omits the document body from ordinary evidence. @@ -54,12 +54,12 @@ erDiagram - `release_artifact`: package/wheel/checksum or other expected artifact considered for release. - `release_evidence`: exact-source artifact inventory/digest/provenance/verification evidence used before publication. -## Planned evidence objects +## Active release-assurance evidence objects -The following **planned evidence objects** make an accepted future release-assurance boundary visible in the conceptual ERD without claiming that its APIs or persistence exist on protected `main`: +The following **active release-assurance evidence objects** make the current browser-assurance implementation visible in the conceptual ERD without claiming that an unmerged PR is protected-main authority or that either value requires an application database: -- `browser_assurance_evidence`: proposed exact-head release evidence from the same committed rich-clipboard corpus executed in required Chromium, Firefox, and WebKit projects. It records bounded public fixture/corpus, browser-revision, package-lock, platform and source identity rather than tenant clipboard data. -- `browser_difference_allowance`: proposed reviewed explanation for one standards-permitted browser serialization difference. It is attached only to focused evidence with threat analysis and rollback; it never acts as a generic normalization rule or approval substitute. +- `browser_assurance_evidence`: `implemented_on_active_pr` as exact-head release evidence from the same committed rich-clipboard corpus executed in required Chromium, Firefox, and WebKit projects. It records bounded public fixture/corpus, browser-revision, package-lock, platform and source identity rather than tenant clipboard data. +- `browser_difference_allowance`: `planned` unless and until a real standards-permitted browser serialization difference requires one focused reviewed explanation. It must be attached only to focused evidence with a standards basis, threat analysis, compatibility consequence, and rollback; it never acts as a generic normalization rule or approval substitute. These values may remain ephemeral or release-artifact metadata. Their presence in the logical model does not create an Inkspan application database or transfer host authority. @@ -77,7 +77,7 @@ These values may remain ephemeral or release-artifact metadata. Their presence i | Entity | Current physical persistence owner | Typical lifecycle | Contains complete document body? | Authority claim | |---|---|---|---|---| | `document_envelope` | host if persisted | document revision | yes | deterministic document value only | -| `document_schema_identity` | none required; `implemented_on_active_pr` | one routing inspection | no | schema-route metadata only | +| `document_schema_identity` | none required; `implemented_on_protected_main` | one routing inspection | no | schema-route metadata only | | `document_revision` | local or host metadata by policy | derived per exact content | no | equality only | | `editor_session` | none required | mounted editor runtime | may reference local state | no auth/session authority | | `document_transition` | none required; host may store | change evidence | no | content-lineage evidence only | @@ -92,7 +92,7 @@ These values may remain ephemeral or release-artifact metadata. Their presence i | `conversion_request` | none required | one deterministic conversion | may reference/contain requested source content | conversion intent only | | `conversion_artifact` | caller/host filesystem or artifact store | successful render/export | yes, rendered form | successful deterministic output only | | `render_warning` | none required; host may log under policy | conversion result | no by default | warning/limitation only | -| `browser_assurance_evidence` | release system if retained; planned | one exact-head browser gate | public fixture metadata only | release assurance evidence only | +| `browser_assurance_evidence` | release system if retained; `implemented_on_active_pr` | one exact-head browser gate | public fixture metadata only | release assurance evidence only | | `browser_difference_allowance` | release system if retained; planned | focused engine difference | no tenant content | reviewed safe-difference rationale only | | `audit_event` | host/release system | durable operational history | should avoid complete body unless policy requires | authenticated host/release evidence | | `release_artifact` | release system | build/release | package content | candidate artifact only | @@ -103,11 +103,11 @@ These values may remain ephemeral or release-artifact metadata. Their presence i The current Inkspan runtime does not create a tenant database, but products embedding it depend on temporal/version provenance boundaries: - `document_envelope` carries an explicit schema/version contract; unknown versions require host-owned migration routing rather than permissive parsing. -- active-PR `document_schema_identity` exposes only bounded routing metadata; it never validates or migrates the unsupported document body and never creates a durable version claim. +- protected-main `document_schema_identity` exposes only bounded routing metadata; it never validates or migrates the unsupported document body and never creates a durable version claim. - `document_revision`, selection, transition, and autosave evidence bind to one exact content state; they do not add actor/time/tenant claims not present in the source contract. - `durable_validator` is temporally ordered by the host's atomic persistence service and must advance only after validated durable success. - `collaboration_document`, `provider_binding`, `awareness_state`, `host_capability`, and `audit_event` can be tenant-scoped in a host, but Inkspan does not define or infer that tenant key. -- `browser_assurance_evidence` and any focused `browser_difference_allowance` bind to one exact source, corpus, package lock and browser revision set; predecessor or different-browser evidence does not transfer silently. +- active-PR `browser_assurance_evidence` and any future focused `browser_difference_allowance` bind to one exact source, corpus, package lock and browser revision set; predecessor or different-browser evidence does not transfer silently. - `release_evidence` binds package artifacts to one exact protected source generation; predecessor evidence does not transfer after source movement. ## Privacy and minimum-disclosure rules @@ -116,7 +116,7 @@ Ordinary lifecycle/selection/transition/schema-identity evidence should remain d ## Persistence non-applicability and future change -No Inkspan-owned relational schema is required by the current architecture, so no physical database ERD or migration set is invented here merely to satisfy documentation completeness. Active identity-routing and planned browser-assurance values are logical API/evidence objects, not database tables. If Inkspan later introduces durable persistence, that is a material architecture change requiring: +No Inkspan-owned relational schema is required by the current architecture, so no physical database ERD or migration set is invented here merely to satisfy documentation completeness. Protected identity-routing and active browser-assurance values are logical API/evidence objects, not database tables. If Inkspan later introduces durable persistence, that is a material architecture change requiring: 1. an Accepted ADR defining why persistence moved into Inkspan; 2. a physical database ERD with descriptive multiword `snake_case` object names; From cd898f8de23fe26f588d4a1b9f4d1f01758bbb4b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:16:03 +0900 Subject: [PATCH 32/65] docs: reconcile routing and browser traceability --- docs/TRACEABILITY.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 391acfd6..13728487 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -12,12 +12,12 @@ This record maps durable Inkspan product decisions to authoritative standards, p |---|---|---|---|---| | HTTP optimistic concurrency | Durable saves use a host/server-selected strong entity tag; local content digests do not substitute for durable `If-Match` authority | RFC 9110, HTTP Semantics | autosave session tests, `docs/document-autosave.md`, architecture concurrency sequence | Inkspan validates/coordinates local semantics; host owns atomic persistence and authorization | | JSON envelope grammar | Versioned envelopes use strict JSON handling, duplicate-name defenses, bounded parsing, and explicit schema identity | RFC 8259; RFC 7493 where interoperable JSON constraints apply | envelope parser/resource-limit tests and package consumers | Current-schema parsing does not imply migration authority for unknown schemas | -| Envelope version routing | A bounded identity-only inspector identifies `schemaId`/`schemaVersion` for dispatch while the current parser stays strict and the host owns migration execution | RFC 8259; RFC 7493; RFC 8785 for canonical current-schema bytes | ADR 0015, PR #84, `documentEnvelopeIdentity` tests, envelope guide/doctoring and framework-independent packed consumers | Active-PR evidence is not shipped authority; identifying a schema generation does not validate that generation's document semantics, authorize migration, or prove durable persistence | +| Envelope version routing | A bounded identity-only inspector identifies `schemaId`/`schemaVersion` for dispatch while the current parser stays strict and the host owns migration execution | RFC 8259; RFC 7493; RFC 8785 for canonical current-schema bytes | ADR 0015, protected-main `documentEnvelopeIdentity` implementation/tests, envelope guide/doctoring and framework-independent packed consumers | Protected-main evidence proves only bounded routing metadata; identifying a schema generation does not validate that generation's document semantics, authorize migration, or prove durable persistence | | Canonical document bytes | Deterministic revision evidence is derived from canonicalized validated document content | RFC 8785, JSON Canonicalization Scheme | revision-evidence, transition-evidence, restore tests | A content digest proves equality only, not actor/time/authorization/durable write | | Provenance semantics | Local transition/release evidence keeps content lineage separate from actor/authorization/durable claims | W3C PROV family | transition evidence, release evidence, canonical data model | Inkspan does not claim complete PROV conformance or host audit provenance | | Accessibility | Native controls, keyboard semantics, shortcut metadata, and host-facing status state support accessible embedding | W3C WCAG 2.2; WAI-ARIA where used | toolbar/accessibility tests, SSR tests, autosave lifecycle data | Component evidence alone is not a full host WCAG conformance claim | | Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | protected-main rich-clipboard unit/integration corpus and SafeClipboard ADR | Protected jsdom/TipTap integration success is not universal browser-engine conformance | -| Cross-engine release assurance | The same committed adversarial corpus must run under required Chromium, Firefox, and WebKit projects; only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright browser/project documentation | ADR 0016, Issue #66, TEST_STRATEGY, UML and planned exact-head browser evidence | The gate is planned and now unblocked by SafeClipboard integration; it does not claim byte-identical browser serialization or branded enterprise-policy coverage | +| Cross-engine release assurance | The same committed adversarial corpus runs under required Chromium, Firefox, and WebKit projects; only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright browser/project documentation | ADR 0016, Issue #66, active-PR exact-head browser job/evidence, TEST_STRATEGY and UML | The active implementation remains non-authoritative until protected integration; it does not claim byte-identical browser serialization or branded enterprise-policy coverage | | Editor integration | Public behavior must exercise the actual TipTap/ProseMirror integration path, not an inert extension field or test-only hook | official TipTap and ProseMirror documentation for the locked dependency line | integration tests and package consumers | Inkspan does not claim compatibility with untested major-version integration semantics | | Collaboration | Inkspan provides provider-neutral editor/Yjs bindings; host owns provider lifecycle, room authorization, awareness privacy, persistence and audit | official Yjs/provider documentation plus Inkspan public contract | collaboration tests and architecture ownership matrix | No network-provider or tenant-authorization authority is implied | | Secure development | Security controls are developed test-first, with exact-head scanning/review/package evidence and root-cause regression | NIST SP 800-218 SSDF 1.1 | CI/security/SAST/package/provenance gates, doctoring and regression history | Repository evidence is not a claim of complete SSDF organizational conformance | @@ -72,7 +72,7 @@ Lower levels may explain intent or history but cannot override a contradictory h - **Planned** means an accepted future direction without a protected implementation. - **Superseded** means retained for history but replaced by a later explicit decision. -PR #84 is active implementation evidence for envelope identity routing, not protected-main/shipped authority. SafeClipboard is now implemented on protected `main`; Issue #66 remains planned browser-realistic assurance rather than a statement about current browser conformance. +Envelope identity routing and SafeClipboard are implemented on protected `main`. Cross-engine browser assurance is implemented on the active release-assurance PR and remains Proposed/non-authoritative until protected integration. Documentation must not promote Proposed or Planned capabilities to Implemented merely because a PR, issue, or design document is detailed. From bd61ef2a8bb5f7f461abc5783dd711dcafe7cfc1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:16:37 +0900 Subject: [PATCH 33/65] docs(adr): accept protected envelope routing decision --- .../0015-envelope-schema-migration-routing.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/adr/0015-envelope-schema-migration-routing.md b/docs/adr/0015-envelope-schema-migration-routing.md index 5995c1e4..6a33f4e0 100644 --- a/docs/adr/0015-envelope-schema-migration-routing.md +++ b/docs/adr/0015-envelope-schema-migration-routing.md @@ -1,12 +1,12 @@ # ADR 0015: Envelope schema identity and host-owned migration routing -Status: Proposed +Status: Accepted ## Context Inkspan's protected implementation uses a strict versioned document envelope so canonical document bytes, revisions, restore behavior, and conversion contracts are not silently reinterpreted. The current parser is intentionally current-schema-only. That protects document semantics, but it also means a host that receives a structurally valid legacy or future envelope cannot use the strict parser merely to learn which migration route should handle it. -Issue #74 tracks a bounded identity-only inspection surface for that gap. The architectural question is whether Inkspan should become a migration engine, relax the current parser, require every host to reimplement hostile-input parsing, or expose only enough validated schema identity to let the host select its own migration. +Issue #74 tracked a bounded identity-only inspection surface for that gap. The implementation is now integrated on protected `main`. The architectural question remains whether Inkspan should become a migration engine, relax the current parser, require every host to reimplement hostile-input parsing, or expose only enough validated schema identity to let the host select its own migration. ## Alternatives considered @@ -17,15 +17,15 @@ Issue #74 tracks a bounded identity-only inspection surface for that gap. The ar ## Decision -Inkspan may expose a framework-independent identity inspector that returns only a frozen `schemaId` and positive safe-integer `schemaVersion` after bounded structural validation. The inspector is a dispatcher aid, not a document parser or migration engine. +Inkspan exposes a framework-independent identity inspector that returns only a frozen `schemaId` and positive safe-integer `schemaVersion` after bounded structural validation. The inspector is a dispatcher aid, not a document parser or migration engine. -The identity path must share the envelope's existing byte, string, depth, value-count, JSON, duplicate-name, strict UTF-8, BOM, plain-object, descriptor, accessor, proxy/reflection, and redacted-error boundaries where applicable. It may require the complete envelope and the presence of `documentJson` while deliberately avoiding current TipTap/ProseMirror semantic validation of that member before returning routing metadata. It may ignore future fields only when doing so requires no accessor execution and does not weaken complete-input validation. +The identity path shares the envelope's existing byte, string, depth, value-count, JSON, duplicate-name, strict UTF-8, BOM, plain-object, descriptor, accessor, proxy/reflection, and redacted-error boundaries where applicable. It requires the complete envelope and the presence of `documentJson` while deliberately avoiding current TipTap/ProseMirror semantic validation of that member before returning routing metadata. It may ignore future fields only when doing so requires no accessor execution and does not weaken complete-input validation. `parseDocumentEnvelope()` remains strict and current-schema-only. After identity inspection, the host selects and executes any migration, authorizes the operation, chooses schema-registry policy, performs durable persistence, records audit evidence, applies tenant isolation and retention, and decides rollback. A migrated result must re-enter the ordinary strict current-schema validation path before it can become canonical Inkspan document state. ## Consequences -A host can route old or future envelopes without duplicating a second untrusted JSON parser, while Inkspan avoids accepting unknown document semantics. The public API gains one additional versioned evidence/value surface, so package consumers and acquisition reviewers can distinguish schema identification from migration success. +A host can route old or future envelopes without duplicating a second untrusted JSON parser, while Inkspan avoids accepting unknown document semantics. The public API has one additional versioned evidence/value surface, so package consumers and acquisition reviewers can distinguish schema identification from migration success. The identity result is intentionally too small to support editing, authorization, durable persistence, or provenance claims. A host that needs richer legacy metadata must obtain it from the version-specific migration component rather than widening this generic inspector. @@ -43,19 +43,19 @@ Schema identity can still be application metadata, so hosts may classify it as t ## Compatibility and migration -Existing current-schema create/parse/encode/revision/restore behavior remains unchanged. The identity inspector is additive and must be available from a framework-independent package surface without pulling in React, TipTap UI, ProseMirror view, Yjs, DOM globals, network clients, or model SDKs. +Existing current-schema create/parse/encode/revision/restore behavior remains unchanged. The identity inspector is additive and is available from a framework-independent package surface without pulling in React, TipTap UI, ProseMirror view, Yjs, DOM globals, network clients, or model SDKs. Any future change to envelope identity fields, canonicalization, or digest meaning requires an explicit new versioned contract and corresponding ADR update or supersession. Migration implementations remain host-owned and version-specific; this ADR does not define a universal transformation between arbitrary schema generations. ## Verification -Issue #74 defines the test-first implementation acceptance. Verification must include legacy/current/future identity, JSON text and strict UTF-8 bytes, duplicate names, malformed input, hostile JavaScript descriptors/proxies, resource limits, recursive absence of document-bearing output, deterministic repeated inspection, packed ESM/CommonJS/strict-TypeScript consumers, and proof that the strict current parser still rejects unsupported schemas. +Issue #74 defined the test-first implementation acceptance. Protected-main verification includes legacy/current/future identity, JSON text and strict UTF-8 bytes, duplicate names, malformed input, hostile JavaScript descriptors/proxies, resource limits, recursive absence of document-bearing output, deterministic repeated inspection, packed ESM/CommonJS/strict-TypeScript consumers, and proof that the strict current parser still rejects unsupported schemas. -Repository-wide exact owned production statement/branch/function/line coverage and public docstring gates remain unchanged. Exact-head CI, security, package, provenance, review, and branch-protection evidence are required before the implementation can become protected-main authority. +Repository-wide exact owned production statement/branch/function/line coverage and public docstring gates remain required. Future changes to this boundary require exact-head CI, security, package, provenance, review, and branch-protection evidence before they can replace the protected implementation. ## Rollback or supersession -Before protected integration, rollback removes the proposed inspector and leaves the strict parser plus host-owned migration guidance unchanged. After integration, removal or semantic broadening is a compatibility change requiring package-version review and migration guidance. +Because the inspector is now protected-main API, removal or semantic broadening is a compatibility change requiring package-version review, migration guidance, and explicit rollback/recovery evidence. An emergency revert must preserve the strict current parser and host-owned migration boundary and must not silently reinterpret persisted documents. Supersession is acceptable only if a later design preserves or strengthens the separation between bounded schema identification, strict current-schema validation, and host-owned migration/persistence authority. From 704ec36c805f402e162fa9797ffc8c761e1870aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:17:06 +0900 Subject: [PATCH 34/65] docs(adr): accept protected security disclosure decision --- docs/adr/0017-security-disclosure-lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0017-security-disclosure-lifecycle.md b/docs/adr/0017-security-disclosure-lifecycle.md index 385f20d8..6967f032 100644 --- a/docs/adr/0017-security-disclosure-lifecycle.md +++ b/docs/adr/0017-security-disclosure-lifecycle.md @@ -1,6 +1,6 @@ # ADR 0017: Security disclosure lifecycle and coordinated vulnerability handling -Status: Proposed +Status: Accepted ## Context From 09ca45ae7f2c8caa2bcea3d959850df3f82f1579 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:17:30 +0900 Subject: [PATCH 35/65] docs: mark envelope routing doctoring protected --- docs/doctoring/envelope-identity-routing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/envelope-identity-routing.md b/docs/doctoring/envelope-identity-routing.md index 9b6420ed..bb71eadb 100644 --- a/docs/doctoring/envelope-identity-routing.md +++ b/docs/doctoring/envelope-identity-routing.md @@ -1,6 +1,6 @@ # Envelope identity-only migration routing -Status: Implemented on active PR +Status: Implemented on protected main Decision: ADR 0015 Tracking: Issue #74 @@ -10,7 +10,7 @@ Inkspan's strict `parseDocumentEnvelope()` correctly rejects unknown schema iden ## Implemented decision -The active implementation adds `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` plus the framework-independent `@contextualwisdomlab/cwl-editor/envelope-identity` package subpath. Successful inspection returns only a frozen `schemaId` string and positive safe-integer `schemaVersion`. +The protected implementation provides `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` plus the framework-independent `@contextualwisdomlab/cwl-editor/envelope-identity` package subpath. Successful inspection returns only a frozen `schemaId` string and positive safe-integer `schemaVersion`. Inspection requires a complete syntactically valid envelope with `schemaId`, `schemaVersion`, and `documentJson`. It applies the established byte, text, value-count, string-length, nesting, duplicate-name, strict UTF-8, BOM, plain-object, dense-array, descriptor, accessor, proxy/reflection, and public-error boundaries where applicable. It deliberately does **not** require an unsupported `documentJson` to satisfy the current TipTap/ProseMirror schema before returning routing metadata. @@ -32,11 +32,11 @@ If the host has no matching migration, it preserves the original source unchange The API is additive. Existing create/parse/encode/revision/restore behavior is unchanged. The dedicated `envelope-identity` subpath is built and consumed through ESM, CommonJS, and strict TypeScript without React, TipTap UI, ProseMirror view, Yjs, DOM globals, network clients, database clients, credentials, or model SDKs. -Rollback before protected integration removes the new entry points/subpath and restores the prior host migration guidance. Any future widening of identity semantics or movement of migration/persistence authority into Inkspan requires explicit versioning and an ADR update or supersession. +Because this API is now protected-main behavior, removal or semantic broadening is a compatibility change requiring version review, migration guidance, and explicit rollback/recovery evidence. Any future widening of identity semantics or movement of migration/persistence authority into Inkspan requires explicit versioning and an ADR update or supersession. ## Verification -Acceptance is test-first and requires: current/legacy/future identity routing; proof that the strict parser still rejects unsupported schemas; JSON text and strict UTF-8 bytes including Buffer; duplicate/malformed/BOM input; invalid scalar versions; accessors and hostile proxies; resource ceilings; absence of document-bearing output; root and framework-independent public exports; packed ESM/CommonJS/strict-TypeScript consumers; exact owned production coverage; public docstrings; security scans; zero valid unresolved findings; and live repository policy on the unchanged exact head. +Protected-main acceptance was test-first and covers: current/legacy/future identity routing; proof that the strict parser still rejects unsupported schemas; JSON text and strict UTF-8 bytes including Buffer; duplicate/malformed/BOM input; invalid scalar versions; accessors and hostile proxies; resource ceilings; absence of document-bearing output; root and framework-independent public exports; packed ESM/CommonJS/strict-TypeScript consumers; exact owned production coverage; public docstrings; security scans; zero valid unresolved findings; and repository policy on the integrated source generation. ## References From d469dac11f34badf9a25722c82a329a2d2b79691 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:18:11 +0900 Subject: [PATCH 36/65] docs(adr): reconcile accepted protected decisions --- docs/adr/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/adr/README.md b/docs/adr/README.md index 2f984d91..642f7814 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -20,17 +20,19 @@ This index records durable architectural decisions. Protected-main implementatio | [0012](0012-spreadsheet-formula-injection.md) | Proposed | Spreadsheet formula-injection handling | | [0013](0013-atomic-file-publication.md) | Proposed | Atomic file publication and explicit overwrite semantics | | [0014](0014-local-assets-font-licensing.md) | Proposed | Local assets and font-licensing boundary | -| [0015](0015-envelope-schema-migration-routing.md) | Proposed | Envelope schema identity and host-owned migration routing | +| [0015](0015-envelope-schema-migration-routing.md) | Accepted | Envelope schema identity and host-owned migration routing | | [0016](0016-cross-engine-browser-assurance.md) | Proposed | Cross-engine browser-semantic release assurance | -| [0017](0017-security-disclosure-lifecycle.md) | Proposed | Security disclosure lifecycle and coordinated vulnerability handling | +| [0017](0017-security-disclosure-lifecycle.md) | Accepted | Security disclosure lifecycle and coordinated vulnerability handling | ## Decision discipline -- **Proposed**: documented or implemented on an unmerged branch; not protected-main authority. -- **Accepted**: integrated into protected `main` with the applicable verification evidence. +- **Proposed**: documented decision whose acceptance evidence is incomplete or whose governing implementation/operational boundary is still being validated; not protected-main implementation authority by status alone. +- **Accepted**: decision has protected-main implementation or process authority with the applicable verification evidence and its canonical ADR status has been reconciled to that authority. - **Superseded**: retained for historical traceability but explicitly replaced by a later ADR. -An ADR cannot promote an unmerged feature to shipped behavior. When a decision changes materially, add or supersede an ADR rather than silently rewriting history. A Proposed ADR may document a decision whose underlying implementation already exists on protected `main`; its own status remains Proposed until this ADR file is integrated, while implementation maturity is recorded separately in the canonical fitness/traceability records. +ADR decision status and implementation maturity are related but distinct. An ADR file may be present on protected `main` while its decision remains Proposed; conversely, an implemented capability can expose stale documentation until the ADR is reconciled. Canonical fitness and traceability records state implementation maturity explicitly, and an ADR cannot promote an unmerged feature to shipped behavior. + +When a decision changes materially, add or supersede an ADR rather than silently rewriting history. ## ADR quality requirements From 5dbebea692f85baaf81e95375ca82d53390691d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:18:56 +0900 Subject: [PATCH 37/65] docs(adr): reconcile active cross-engine assurance --- docs/adr/0016-cross-engine-browser-assurance.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/adr/0016-cross-engine-browser-assurance.md b/docs/adr/0016-cross-engine-browser-assurance.md index aaac3cfa..e84b827a 100644 --- a/docs/adr/0016-cross-engine-browser-assurance.md +++ b/docs/adr/0016-cross-engine-browser-assurance.md @@ -6,7 +6,7 @@ Status: Proposed Inkspan's safe rich-clipboard boundary depends on browser HTML fragment parsing, DOM construction, serialization, hidden-content interpretation, and TipTap/ProseMirror integration. Unit and jsdom evidence is valuable but cannot prove that Chromium, Firefox, and WebKit expose identical security-relevant behavior. Earlier development already found a CSSOM mismatch around proprietary Office style handling, demonstrating that one simulated DOM is not a sufficient publication boundary. -PR #65 owns the sanitizer implementation. Issue #66 separately owns the browser-realistic differential release gate after PR #65 reaches protected `main`. The architectural decision is how browser-semantic differences are admitted into a release without turning engine disagreement into either an unbounded compatibility promise or an excuse to normalize away a security defect. +SafeClipboard is now integrated on protected `main`. Issue #66 owns the browser-realistic differential release gate and is implemented on the active cross-engine assurance PR. The architectural decision is how browser-semantic differences are admitted into a release without turning engine disagreement into either an unbounded compatibility promise or an excuse to normalize away a security defect. Until that active implementation reaches protected `main`, its evidence remains non-authoritative for release. ## Alternatives considered @@ -47,13 +47,13 @@ The design reduces parser-confusion, hidden-content, active-resource, serializat Each accepted browser/Playwright revision is part of release evidence rather than a forever-supported browser guarantee. Upgrading Playwright or its browser revisions requires the complete corpus to rerun before the new evidence becomes authoritative. A future browser difference that is safe only behind a narrower supported construct must be reflected in the public compatibility contract rather than silently normalized. -The gate is ordered behind the safe-clipboard implementation because there is no useful release assurance for a trust boundary that is not yet integrated. It does not change existing document-envelope or persistence migration semantics. +The active gate starts from the already integrated SafeClipboard boundary and does not change existing document-envelope or persistence migration semantics. Until protected integration, it remains active-PR release evidence only. ## Verification -Issue #66 defines the test-first implementation acceptance. Required proof includes RED evidence that the differential harness detects an intentionally introduced divergence or unsafe reconstruction, GREEN evidence after removing the fault, all three named browser projects, deterministic corpus/allowlist/evidence-generation tests, representative performance bounds, exact-head CI/security/package evidence, and a qualifying independent review before the release gate is accepted. +Issue #66 defines the test-first implementation acceptance. The active implementation includes a permanent differential oracle, an intentionally failing historical RED lineage from the superseded predecessor branch, real Playwright Chromium/Firefox/WebKit execution through the supported paste pipeline, deterministic corpus/evidence-generation tests, representative performance bounds, and exact-head CI/security/package evidence. Any head movement invalidates predecessor evidence and requires the exact current head to re-prove the gate. -The canonical documentation and test strategy must continue to state that jsdom-only evidence is not real-engine conformance and that differences are never normalized merely to make engines agree. +Protected acceptance additionally requires zero valid unresolved findings, any actually required qualifying independent review, repository policy, and protected integration. The canonical documentation and test strategy continue to state that jsdom-only evidence is not real-engine conformance and that differences are never normalized merely to make engines agree. ## Rollback or supersession From 7e256cd3adf121dd809c4cf3d1aa17ba36b55ed9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:19:51 +0900 Subject: [PATCH 38/65] docs: reconcile routing and browser UML maturity --- docs/UML.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/UML.md b/docs/UML.md index 10f83a23..eb236f2f 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -93,7 +93,7 @@ flowchart TB Difference -->|unsafe, unexplained, missing browser, skipped or failed| Repair --> Blocked ``` -A queued, pending, skipped, cancelled, absent or failed required browser is not passing evidence. Differences are never normalized merely to make engines agree; any admitted difference is a reviewed compatibility artifact. ADR 0016 and Issue #66 own this planned release-assurance decision behind the SafeClipboard integration dependency. +A queued, pending, skipped, cancelled, absent or failed required browser is not passing evidence. Differences are never normalized merely to make engines agree; any admitted difference is a reviewed compatibility artifact. ADR 0016 governs this decision, SafeClipboard is already protected-main authority, and Issue #66 is implemented on the active cross-engine assurance PR until protected integration. ## Author-to-model proposal sequence @@ -147,7 +147,7 @@ Unsupported or lossy constructs are surfaced by the relevant contract instead of ```mermaid sequenceDiagram participant Host - participant Inspector as Planned bounded identity inspector + participant Inspector as Protected bounded identity inspector participant Registry as Host migration registry participant Migration as Host-owned version migration participant StrictParser as Current-schema strict parser @@ -173,7 +173,7 @@ sequenceDiagram end ``` -The identity result does not contain the document body and does not prove migration, authorization, persistence or durable success. ADR 0015 and Issue #74 define this planned routing aid; the host continues to own schema registry, migration execution, persistence, audit and rollback. +The identity result does not contain the document body and does not prove migration, authorization, persistence or durable success. ADR 0015 governs this protected-main routing aid; Issue #74 is historical implementation tracking. The host continues to own schema registry, migration execution, persistence, audit and rollback. ## Office render and file publication sequence From fe93a3786ba240510c69558769fbd2db752975e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:22:29 +0900 Subject: [PATCH 39/65] test(docs): accept protected envelope routing doctoring --- src/documentEnvelopeIdentityDocumentation.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/documentEnvelopeIdentityDocumentation.test.ts b/src/documentEnvelopeIdentityDocumentation.test.ts index 594c377d..4f81b2ad 100644 --- a/src/documentEnvelopeIdentityDocumentation.test.ts +++ b/src/documentEnvelopeIdentityDocumentation.test.ts @@ -29,9 +29,11 @@ describe('document envelope identity documentation', () => { expect(adr).toContain('`parseDocumentEnvelope()` remains strict'); expect(guide).toContain('@contextualwisdomlab/cwl-editor/envelope-identity'); expect(guide).toMatch(/host[- ]owned|Migration execution remains host-owned/i); - expect(doctoring).toContain('Status: Implemented on active PR'); + expect(doctoring).toContain('Status: Implemented on protected main'); + expect(doctoring).not.toContain('Status: Implemented on active PR'); expect(doctoring).toContain('Issue #74'); expect(adr).toContain('Issue #74'); + expect(adr).toContain('Status: Accepted'); }); it('keeps the identity result document-free and migration authority outside Inkspan', () => { From d2191f32ff6d9558ffa261968f81e04acf4e2c5c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:23:02 +0900 Subject: [PATCH 40/65] test(docs): match protected routing traceability --- src/canonicalProductDocumentation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canonicalProductDocumentation.test.ts b/src/canonicalProductDocumentation.test.ts index 328d577f..1df46438 100644 --- a/src/canonicalProductDocumentation.test.ts +++ b/src/canonicalProductDocumentation.test.ts @@ -129,7 +129,7 @@ describe('canonical product documentation graph', () => { expect(dataModel).toContain('`document_schema_identity`: `implemented_on_protected_main`'); expect(dataModel).not.toContain('`document_schema_identity`: `implemented_on_active_pr`'); expect(traceability).toContain('Envelope version routing'); - expect(traceability).toContain('protected-main evidence'); + expect(traceability).toMatch(/protected-main evidence/iu); expect(traceability).not.toContain('PR #84 is active implementation evidence'); expect(routingDoctoring).toContain('Status: Implemented on protected main'); expect(routingDoctoring).not.toContain('Status: Implemented on active PR'); From 35a40d60b7d53f891d362a6d3914013be81876c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:32:34 +0900 Subject: [PATCH 41/65] test(release): require packed browser artifact evidence --- src/releaseBrowserEvidence.test.ts | 72 +++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/src/releaseBrowserEvidence.test.ts b/src/releaseBrowserEvidence.test.ts index 535490e4..69784921 100644 --- a/src/releaseBrowserEvidence.test.ts +++ b/src/releaseBrowserEvidence.test.ts @@ -8,21 +8,54 @@ function repositoryFile(path: string): string { return readFileSync(resolve(process.cwd(), path), 'utf8'); } +/** Extract one top-level workflow job without allowing another job to satisfy it. */ +function workflowJob(source: string, jobName: string, nextJobName: string): string { + const start = source.indexOf(` ${jobName}:`); + const end = source.indexOf(` ${nextJobName}:`, start + 1); + expect(start).toBeGreaterThan(-1); + expect(end).toBeGreaterThan(start); + return source.slice(start, end); +} + const workflow = repositoryFile('.github/workflows/release.yml'); +const playwrightConfig = repositoryFile('tests/browser/playwright.config.ts'); +const browserHarness = repositoryFile('tests/browser/harness.ts'); +const browserSpec = repositoryFile('tests/browser/specs/clipboard.browser.spec.ts'); +const consensusSpec = repositoryFile('tests/browser/specs/clipboard.consensus.spec.ts'); describe('release cross-engine browser evidence contract', () => { - it('re-runs the pinned three-engine clipboard gate on the exact tagged source', () => { - const browserJobIndex = workflow.indexOf(' browser-release-evidence:'); + it('tests the packed npm artifact built from the exact tagged source', () => { const buildJobIndex = workflow.indexOf(' build-release-artifacts:'); + const browserJobIndex = workflow.indexOf(' browser-release-evidence:'); + const publishJobIndex = workflow.indexOf(' publish-release:'); - expect(browserJobIndex).toBeGreaterThan(-1); - expect(buildJobIndex).toBeGreaterThan(browserJobIndex); + expect(buildJobIndex).toBeGreaterThan(-1); + expect(browserJobIndex).toBeGreaterThan(buildJobIndex); + expect(publishJobIndex).toBeGreaterThan(browserJobIndex); - const browserJob = workflow.slice(browserJobIndex, buildJobIndex); + const browserJob = workflowJob( + workflow, + 'browser-release-evidence', + 'publish-release', + ); + expect(browserJob).toContain('needs: build-release-artifacts'); expect(browserJob).toContain('Cross-engine Clipboard / Playwright 1.62.0'); expect(browserJob).toContain('runs-on: ubuntu-24.04'); expect(browserJob).toContain('permissions:\n contents: read'); + expect(browserJob).toContain( + 'uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1', + ); + expect(browserJob).toContain('ref: ${{ github.sha }}'); expect(browserJob).toContain('persist-credentials: false'); + expect(browserJob).toContain( + 'uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0', + ); + expect(browserJob).toContain('name: inkspan-release-${{ github.ref_name }}'); + expect(browserJob).toContain('path: release'); + expect(browserJob).toContain('sha256sum --check SHA256SUMS'); + expect(browserJob).toContain('Install exact packed editor artifact for browser verification'); + expect(browserJob).toContain('tests/browser/node_modules/@contextualwisdomlab/cwl-editor'); + expect(browserJob).toContain('INKSPAN_EXPECTED_PACKAGE_SHA256'); expect(browserJob).toContain('pnpm --dir tests/browser install --frozen-lockfile'); expect(browserJob).toContain( 'pnpm --dir tests/browser exec playwright install --with-deps chromium firefox webkit', @@ -33,6 +66,35 @@ describe('release cross-engine browser evidence contract', () => { ); }); + it('retains only bounded browser evidence and fails if it was not produced', () => { + const browserJob = workflowJob( + workflow, + 'browser-release-evidence', + 'publish-release', + ); + expect(browserJob).toContain( + 'uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1', + ); + expect(browserJob).toContain('path: tests/browser/.browser-evidence/'); + expect(browserJob).toContain('include-hidden-files: true'); + expect(browserJob).toContain('if-no-files-found: error'); + expect(browserJob).not.toContain('path: tests/browser/test-results'); + }); + + it('binds browser evidence to one fresh run, current lock, and packed package', () => { + expect(playwrightConfig).toContain("globalSetup: './globalSetup.ts'"); + expect(browserHarness).toContain("from 'inkspan-browser-under-test'"); + expect(browserSpec).toContain('runId'); + expect(browserSpec).toContain('packageSha256'); + expect(consensusSpec).toContain("createHash('sha256')"); + expect(consensusSpec).toContain("'../pnpm-lock.yaml'"); + expect(consensusSpec).toContain('item.runId'); + expect(consensusSpec).toContain('reference.runId'); + expect(consensusSpec).toContain('item.lockSha256'); + expect(consensusSpec).toContain('currentLockSha256'); + expect(consensusSpec).toContain('INKSPAN_EXPECTED_PACKAGE_SHA256'); + }); + it('makes immutable publication depend on both artifacts and tagged browser evidence', () => { const publishJobIndex = workflow.indexOf(' publish-release:'); expect(publishJobIndex).toBeGreaterThan(-1); From 884660b0f03abc43686a64f9ca5b818f7f956e9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:33:05 +0900 Subject: [PATCH 42/65] test(docs): bind browser maturity to exact entries --- src/crossEngineClipboardDocumentation.test.ts | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/crossEngineClipboardDocumentation.test.ts b/src/crossEngineClipboardDocumentation.test.ts index 3b1c8622..d51ab8dc 100644 --- a/src/crossEngineClipboardDocumentation.test.ts +++ b/src/crossEngineClipboardDocumentation.test.ts @@ -2,8 +2,29 @@ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; +const repositoryFile = (path: string): string => + readFileSync(resolve(process.cwd(), path), 'utf8'); + const documentText = (path: string): string => - readFileSync(resolve(process.cwd(), path), 'utf8').replace(/\s+/gu, ' ').trim(); + repositoryFile(path).replace(/\s+/gu, ' ').trim(); + +/** Return the unique Markdown table row containing the requested cell text. */ +function tableRow(document: string, cellText: string): string { + const matches = document + .split(/\r?\n/u) + .filter((line) => line.startsWith('|') && line.includes(cellText)); + expect(matches).toHaveLength(1); + return matches[0] ?? ''; +} + +/** Return the unique changelog bullet containing the requested feature phrase. */ +function changelogBullet(document: string, phrase: string): string { + const matches = document + .split(/\r?\n/u) + .filter((line) => line.startsWith('- ') && line.includes(phrase)); + expect(matches).toHaveLength(1); + return matches[0] ?? ''; +} describe('cross-engine clipboard documentation contract', () => { it('documents the exact browser release gate and evidence identity', () => { @@ -43,17 +64,22 @@ describe('cross-engine clipboard documentation contract', () => { expect(doctoring).toContain('no tenant document'); }); - it('records the current implementation maturity without calling the active PR shipped', () => { - const fitness = documentText('docs/DOCUMENTATION_FITNESS.md'); - const changelog = documentText('CHANGELOG.md'); - - expect(fitness).toContain( + it('records browser maturity in the exact canonical and changelog entries', () => { + const fitness = repositoryFile('docs/DOCUMENTATION_FITNESS.md'); + const changelog = repositoryFile('CHANGELOG.md'); + const assuranceRow = tableRow( + fitness, 'Cross-engine browser-semantic release assurance', ); - expect(fitness).toContain('`implemented_on_active_pr`'); - expect(fitness).toContain('SafeClipboard'); - expect(fitness).toContain('`implemented_on_protected_main`'); - expect(changelog).toContain('dependency-locked Chromium/Firefox/WebKit'); - expect(changelog).toContain('cross-engine rich-clipboard release gate'); + const releaseBullet = changelogBullet( + changelog, + 'cross-engine rich-clipboard release gate', + ); + + expect(assuranceRow).toContain('`implemented_on_active_pr`'); + expect(assuranceRow).toContain('SafeClipboard itself is `implemented_on_protected_main`'); + expect(assuranceRow).not.toContain('`implemented_on_protected_main`; SafeClipboard itself'); + expect(releaseBullet).toContain('dependency-locked Chromium/Firefox/WebKit'); + expect(releaseBullet).toContain('Playwright 1.62.0'); }); }); From 0ad463ba5095ca271fb0b94b340d10973fa63e5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:33:23 +0900 Subject: [PATCH 43/65] test(browser): reset exact-run evidence before projects --- tests/browser/globalSetup.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/browser/globalSetup.ts diff --git a/tests/browser/globalSetup.ts b/tests/browser/globalSetup.ts new file mode 100644 index 00000000..1483e201 --- /dev/null +++ b/tests/browser/globalSetup.ts @@ -0,0 +1,23 @@ +import { randomUUID } from 'node:crypto'; +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const browserDirectory = dirname(fileURLToPath(import.meta.url)); +const evidenceDirectory = resolve(browserDirectory, '.browser-evidence'); + +/** Build one opaque run identity shared by all browser projects in this invocation. */ +function browserEvidenceRunId(): string { + const githubRunId = process.env.GITHUB_RUN_ID?.trim(); + if (!githubRunId) return randomUUID(); + const attempt = process.env.GITHUB_RUN_ATTEMPT?.trim() || '1'; + const head = process.env.INKSPAN_EXPECTED_HEAD_SHA?.trim() || process.env.GITHUB_SHA?.trim() || 'unknown-head'; + return `${githubRunId}:${attempt}:${head}`; +} + +/** Clear predecessor-run evidence before any browser project can produce current evidence. */ +export default async function globalSetup(): Promise { + await rm(evidenceDirectory, { recursive: true, force: true }); + await mkdir(evidenceDirectory, { recursive: true }); + await writeFile(resolve(evidenceDirectory, '.run-id'), `${browserEvidenceRunId()}\n`, 'utf8'); +} From fa9885cffb39611ae46a8cb55863b517052a71ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:33:41 +0900 Subject: [PATCH 44/65] test(browser): route harness through selected package entry --- tests/browser/vite.config.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/browser/vite.config.ts diff --git a/tests/browser/vite.config.ts b/tests/browser/vite.config.ts new file mode 100644 index 00000000..50c49dd1 --- /dev/null +++ b/tests/browser/vite.config.ts @@ -0,0 +1,18 @@ +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +const browserDirectory = dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = resolve(browserDirectory, '../..'); +const configuredPackageEntry = process.env.INKSPAN_BROWSER_PACKAGE_ENTRY?.trim(); +const packageEntry = configuredPackageEntry + ? resolve(configuredPackageEntry) + : resolve(repositoryRoot, 'src/index.ts'); + +export default defineConfig({ + resolve: { + alias: { + 'inkspan-browser-under-test': packageEntry, + }, + }, +}); From 5edfd10566feeecaec82781010c41ad57e8fb2ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:34:10 +0900 Subject: [PATCH 45/65] test(browser): exercise selected public package entry --- tests/browser/harness.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/browser/harness.ts b/tests/browser/harness.ts index 459917e6..c00b47a7 100644 --- a/tests/browser/harness.ts +++ b/tests/browser/harness.ts @@ -1,11 +1,11 @@ import { Editor } from '@tiptap/core'; import { ClipboardSanitizationError, + buildExtensions, sanitizeRichClipboardHtml, type ClipboardConfig, type ClipboardSanitizationErrorCode, -} from '../../src/extensions/SafeClipboard.js'; -import { buildExtensions } from '../../src/extensions/kit.js'; +} from 'inkspan-browser-under-test'; interface BrowserClipboardProbeRequest { readonly sourceHtml: string; @@ -14,7 +14,7 @@ interface BrowserClipboardProbeRequest { interface BrowserClipboardProbeResult { readonly sanitizedHtml: string; - readonly documentJson: unknown | null; + readonly documentJson: unknown; readonly errorCode: ClipboardSanitizationErrorCode | null; } From cb70dbf30df6a7957eecd661a4e38c3d52b7c35d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:34:42 +0900 Subject: [PATCH 46/65] test(browser): bind projects to fresh evidence setup --- tests/browser/playwright.config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/browser/playwright.config.ts b/tests/browser/playwright.config.ts index 8b49b262..826b13d2 100644 --- a/tests/browser/playwright.config.ts +++ b/tests/browser/playwright.config.ts @@ -1,19 +1,22 @@ import { defineConfig, devices } from '@playwright/test'; -const HARNESS_URL = 'http://127.0.0.1:4173/tests/browser/harness.html'; +const HARNESS_ORIGIN = 'http://127.0.0.1:4173'; +const HARNESS_URL = `${HARNESS_ORIGIN}/tests/browser/harness.html`; export default defineConfig({ testDir: './specs', outputDir: './test-results', + globalSetup: './globalSetup.ts', fullyParallel: false, workers: 3, retries: 0, timeout: 20_000, expect: { timeout: 5_000 }, reporter: [['line']], + use: { baseURL: HARNESS_ORIGIN }, webServer: { command: - 'pnpm --dir ../.. exec vite --host 127.0.0.1 --port 4173 --strictPort', + 'pnpm --dir ../.. exec vite --config tests/browser/vite.config.ts --host 127.0.0.1 --port 4173 --strictPort', url: HARNESS_URL, reuseExistingServer: false, timeout: 120_000, From c341c56c0a12c10e426ce4c49b2f90832434121f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:35:05 +0900 Subject: [PATCH 47/65] test(browser): centralize release evidence budget --- tests/browser/evidenceContract.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/browser/evidenceContract.ts diff --git a/tests/browser/evidenceContract.ts b/tests/browser/evidenceContract.ts new file mode 100644 index 00000000..0bf24b08 --- /dev/null +++ b/tests/browser/evidenceContract.ts @@ -0,0 +1,5 @@ +/** Maximum accepted sanitizer time for the representative Word-like browser fixture. */ +export const BROWSER_PERFORMANCE_BUDGET_MILLIS = 8_000; + +/** Evidence schema version for one browser-engine release observation file. */ +export const BROWSER_EVIDENCE_SCHEMA_VERSION = 2; From e14f627dc4ec4887177b578871171a0bd048039a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:35:47 +0900 Subject: [PATCH 48/65] test(browser): bind evidence to one fresh packed run --- tests/browser/specs/clipboard.browser.spec.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/tests/browser/specs/clipboard.browser.spec.ts b/tests/browser/specs/clipboard.browser.spec.ts index e523957f..7390aa1c 100644 --- a/tests/browser/specs/clipboard.browser.spec.ts +++ b/tests/browser/specs/clipboard.browser.spec.ts @@ -2,20 +2,24 @@ import { createHash } from 'node:crypto'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; -import { expect, test } from '@playwright/test'; +import { expect, test, type Page } from '@playwright/test'; import { SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS, SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION, type CrossEngineClipboardEngine, type CrossEngineClipboardObservation, } from '../../../src/crossEngineClipboardEvidence.js'; +import { + BROWSER_EVIDENCE_SCHEMA_VERSION, + BROWSER_PERFORMANCE_BUDGET_MILLIS, +} from '../evidenceContract.js'; type BrowserProbe = (request: { sourceHtml: string; clipboardConfig?: unknown; }) => { sanitizedHtml: string; - documentJson: unknown | null; + documentJson: unknown; errorCode: string | null; }; @@ -24,9 +28,11 @@ type HostileDocumentProbe = (sourceHtml: string) => { message: string; }; -const evidenceDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '../.browser-evidence'); -const lockfilePath = resolve(dirname(fileURLToPath(import.meta.url)), '../pnpm-lock.yaml'); -const packagePath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'); +const specDirectory = dirname(fileURLToPath(import.meta.url)); +const evidenceDirectory = resolve(specDirectory, '../.browser-evidence'); +const lockfilePath = resolve(specDirectory, '../pnpm-lock.yaml'); +const packagePath = resolve(specDirectory, '../package.json'); +const rejectedRequestsByPage = new WeakMap(); const observations: CrossEngineClipboardObservation[] = []; let representativeWordMillis: number | null = null; @@ -39,6 +45,7 @@ test.describe.configure({ mode: 'serial' }); test.beforeEach(async ({ page }) => { const rejectedExternalRequests: string[] = []; + rejectedRequestsByPage.set(page, rejectedExternalRequests); await page.route('**/*', async (route) => { if (allowHarnessRequest(route.request().url())) { await route.continue(); @@ -47,19 +54,16 @@ test.beforeEach(async ({ page }) => { rejectedExternalRequests.push(new URL(route.request().url()).origin); await route.abort('blockedbyclient'); }); - await page.goto('http://127.0.0.1:4173/tests/browser/harness.html'); - expect(rejectedExternalRequests).toEqual([]); + await page.goto('/tests/browser/harness.html'); +}); + +test.afterEach(async ({ page }) => { + await page.waitForLoadState('networkidle'); + expect(rejectedRequestsByPage.get(page) ?? []).toEqual([]); }); for (const testCase of SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS) { test(`sanitizes corpus case ${testCase.id}`, async ({ page, browserName }) => { - const rejectedExternalRequests: string[] = []; - page.on('request', (request) => { - if (!allowHarnessRequest(request.url())) { - rejectedExternalRequests.push(new URL(request.url()).origin); - } - }); - const result = await page.evaluate( ({ sourceHtml, clipboardConfig }) => ( @@ -80,7 +84,6 @@ for (const testCase of SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS) { } else { expect(result.documentJson).toBeNull(); } - expect(rejectedExternalRequests).toEqual([]); observations.push({ caseId: testCase.id, @@ -116,6 +119,7 @@ test('redacts hostile document capability failures without source disclosure', a test('keeps representative Word-like sanitization within the release alarm budget', async ({ page, }) => { + test.setTimeout(35_000); const sourceHtml = `
${Array.from( { length: 800 }, (_, index) => @@ -131,12 +135,16 @@ test('keeps representative Word-like sanitization within the release alarm budge }, sourceHtml); expect(measurement.result.errorCode).toBeNull(); - expect(measurement.elapsedMillis).toBeLessThan(8_000); + expect(measurement.elapsedMillis).toBeLessThan(BROWSER_PERFORMANCE_BUDGET_MILLIS); representativeWordMillis = Math.round(measurement.elapsedMillis * 100) / 100; }); test.afterAll(async ({ browser, browserName }) => { const lockfile = await readFile(lockfilePath); + const runId = (await readFile(resolve(evidenceDirectory, '.run-id'), 'utf8')).trim(); + if (!runId) { + throw new Error('Cross-engine browser evidence run identity is missing.'); + } const browserPackage = JSON.parse(await readFile(packagePath, 'utf8')) as { devDependencies?: Record; }; @@ -153,8 +161,9 @@ test.afterAll(async ({ browser, browserName }) => { await mkdir(evidenceDirectory, { recursive: true }); const evidence = Object.freeze({ - schemaVersion: 1, + schemaVersion: BROWSER_EVIDENCE_SCHEMA_VERSION, corpusVersion: SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION, + runId, engine: browserName, playwrightVersion, browserVersion: browser.version(), @@ -162,6 +171,7 @@ test.afterAll(async ({ browser, browserName }) => { runnerImage: process.env.ImageOS ?? null, headSha: process.env.INKSPAN_EXPECTED_HEAD_SHA ?? process.env.GITHUB_SHA ?? null, + packageSha256: process.env.INKSPAN_EXPECTED_PACKAGE_SHA256?.trim() || null, lockSha256: createHash('sha256').update(lockfile).digest('hex'), representativeWordMillis, observations, From afd837a041da9ed2a35715d78074da3992b64278 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:36:29 +0900 Subject: [PATCH 49/65] test(browser): reject stale or mismatched evidence --- .../browser/specs/clipboard.consensus.spec.ts | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/browser/specs/clipboard.consensus.spec.ts b/tests/browser/specs/clipboard.consensus.spec.ts index 6b4a73ef..8005405b 100644 --- a/tests/browser/specs/clipboard.consensus.spec.ts +++ b/tests/browser/specs/clipboard.consensus.spec.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; @@ -9,22 +10,30 @@ import { type CrossEngineClipboardEngine, type CrossEngineClipboardObservation, } from '../../../src/crossEngineClipboardEvidence.js'; +import { + BROWSER_EVIDENCE_SCHEMA_VERSION, + BROWSER_PERFORMANCE_BUDGET_MILLIS, +} from '../evidenceContract.js'; interface BrowserEvidence { readonly schemaVersion: number; readonly corpusVersion: number; + readonly runId: string; readonly engine: CrossEngineClipboardEngine; readonly playwrightVersion: string; readonly browserVersion: string; readonly osPlatform: string; readonly runnerImage: string | null; readonly headSha: string | null; + readonly packageSha256: string | null; readonly lockSha256: string; readonly representativeWordMillis: number; readonly observations: readonly CrossEngineClipboardObservation[]; } -const evidenceDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '../.browser-evidence'); +const specDirectory = dirname(fileURLToPath(import.meta.url)); +const evidenceDirectory = resolve(specDirectory, '../.browser-evidence'); +const lockfilePath = resolve(specDirectory, '../pnpm-lock.yaml'); const engines: readonly CrossEngineClipboardEngine[] = [ 'chromium', 'firefox', @@ -39,21 +48,37 @@ const readEvidence = async ( ) as BrowserEvidence; test('requires complete exact-head browser evidence and exact corpus consensus', async () => { + const currentRunId = ( + await readFile(resolve(evidenceDirectory, '.run-id'), 'utf8') + ).trim(); + expect(currentRunId.length).toBeGreaterThan(0); + const currentLockSha256 = createHash('sha256') + .update(await readFile(lockfilePath)) + .digest('hex'); + const expectedPackageSha256 = + process.env.INKSPAN_EXPECTED_PACKAGE_SHA256?.trim() || null; const evidence = await Promise.all(engines.map(readEvidence)); const [reference] = evidence; if (!reference) throw new Error('Cross-engine browser evidence is missing.'); + expect(reference.runId).toBe(currentRunId); for (const [index, item] of evidence.entries()) { - expect(item.schemaVersion).toBe(1); + expect(item.schemaVersion).toBe(BROWSER_EVIDENCE_SCHEMA_VERSION); expect(item.corpusVersion).toBe(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION); + expect(item.runId).toBe(reference.runId); + expect(item.runId).toBe(currentRunId); expect(item.engine).toBe(engines[index]); expect(item.playwrightVersion).toBe('1.62.0'); expect(item.browserVersion.length).toBeGreaterThan(0); expect(item.lockSha256).toBe(reference.lockSha256); + expect(item.lockSha256).toBe(currentLockSha256); + expect(item.packageSha256).toBe(expectedPackageSha256); expect(item.headSha).toBe(reference.headSha); expect(item.observations).toHaveLength(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length); expect(item.representativeWordMillis).toBeGreaterThanOrEqual(0); - expect(item.representativeWordMillis).toBeLessThan(8_000); + expect(item.representativeWordMillis).toBeLessThan( + BROWSER_PERFORMANCE_BUDGET_MILLIS, + ); } if (process.env.GITHUB_ACTIONS === 'true') { @@ -77,9 +102,11 @@ test('requires complete exact-head browser evidence and exact corpus consensus', } const summary = { - schemaVersion: 1, + schemaVersion: BROWSER_EVIDENCE_SCHEMA_VERSION, corpusVersion: SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS_VERSION, + runId: reference.runId, headSha: reference.headSha, + packageSha256: reference.packageSha256, lockSha256: reference.lockSha256, playwrightVersion: reference.playwrightVersion, engines: evidence.map((item) => ({ From cb5aba65a6363f06ccc0040a7628b0876cff1f10 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:37:43 +0900 Subject: [PATCH 50/65] ci(release): verify packed browser artifact evidence --- .github/workflows/release.yml | 113 ++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0feb0e0..d2513efd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,39 +17,6 @@ env: PYTHON_VERSION: '3.14' jobs: - browser-release-evidence: - name: Cross-engine Clipboard / Playwright 1.62.0 - if: github.repository == 'ContextualWisdomLab/inkspan' - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - env: - PLAYWRIGHT_BROWSERS_PATH: /tmp/inkspan-playwright-browsers - steps: - - name: Check out the exact tagged source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: false - - name: Set up pnpm - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - cache: pnpm - - name: Install root dependencies from the immutable lock - run: pnpm install --frozen-lockfile - - name: Install browser-test dependencies from the isolated immutable lock - run: pnpm --dir tests/browser install --frozen-lockfile - - name: Install Playwright browser revisions pinned by 1.62.0 - run: pnpm --dir tests/browser exec playwright install --with-deps chromium firefox webkit - - name: Verify real-engine rich clipboard release evidence on the tagged source - env: - INKSPAN_EXPECTED_HEAD_SHA: ${{ github.sha }} - run: pnpm --dir tests/browser exec playwright test --config playwright.config.ts - build-release-artifacts: if: github.repository == 'ContextualWisdomLab/inkspan' runs-on: ubuntu-24.04 @@ -167,6 +134,86 @@ jobs: retention-days: 1 compression-level: 0 + browser-release-evidence: + name: Cross-engine Clipboard / Playwright 1.62.0 + if: github.repository == 'ContextualWisdomLab/inkspan' + needs: build-release-artifacts + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + env: + PLAYWRIGHT_BROWSERS_PATH: /tmp/inkspan-playwright-browsers + steps: + - name: Check out the exact tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + - name: Set up pnpm + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - name: Install root dependencies from the immutable lock + run: pnpm install --frozen-lockfile + - name: Install browser-test dependencies from the isolated immutable lock + run: pnpm --dir tests/browser install --frozen-lockfile + - name: Download validated release artifacts + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: inkspan-release-${{ github.ref_name }} + path: release + - name: Verify transferred release checksums + run: | + set -euo pipefail + cd release + sha256sum --check SHA256SUMS + - name: Install exact packed editor artifact for browser verification + run: | + set -euo pipefail + mapfile -t npm_assets < <( + find release -maxdepth 1 -type f -name '*.tgz' -printf '%f\n' | LC_ALL=C sort + ) + if [[ ${#npm_assets[@]} -ne 1 ]]; then + echo "::error::Browser release evidence requires exactly one packed npm artifact." + exit 1 + fi + package_file="release/${npm_assets[0]}" + package_sha="$(sha256sum -- "$package_file" | awk '{print $1}')" + package_target="$GITHUB_WORKSPACE/tests/browser/node_modules/@contextualwisdomlab/cwl-editor" + rm -rf "$package_target" + mkdir -p "$package_target" + tar -xzf "$package_file" --strip-components=1 -C "$package_target" + test -f "$package_target/package.json" + test -f "$package_target/dist/cwl-editor.js" + package_name="$(node -e "const fs=require('node:fs'); const p=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.stdout.write(p.name || '')" "$package_target/package.json")" + if [[ "$package_name" != '@contextualwisdomlab/cwl-editor' ]]; then + echo "::error::Packed browser artifact has an unexpected package identity." + exit 1 + fi + echo "INKSPAN_BROWSER_PACKAGE_ENTRY=$package_target/dist/cwl-editor.js" >> "$GITHUB_ENV" + echo "INKSPAN_EXPECTED_PACKAGE_SHA256=$package_sha" >> "$GITHUB_ENV" + - name: Install Playwright browser revisions pinned by 1.62.0 + run: pnpm --dir tests/browser exec playwright install --with-deps chromium firefox webkit + - name: Verify real-engine rich clipboard release evidence on the tagged source + env: + INKSPAN_EXPECTED_HEAD_SHA: ${{ github.sha }} + INKSPAN_EXPECTED_PACKAGE_SHA256: ${{ env.INKSPAN_EXPECTED_PACKAGE_SHA256 }} + run: pnpm --dir tests/browser exec playwright test --config playwright.config.ts + - name: Upload bounded browser release evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: inkspan-browser-evidence-${{ github.sha }} + path: tests/browser/.browser-evidence/ + include-hidden-files: true + if-no-files-found: error + retention-days: 14 + compression-level: 0 + publish-release: if: github.repository == 'ContextualWisdomLab/inkspan' needs: [build-release-artifacts, browser-release-evidence] From 6f9e14864ddbd637f19e60b8161362e30eb24640 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:39:15 +0900 Subject: [PATCH 51/65] docs(browser): bind release assurance to packed artifact --- .../cross-engine-rich-clipboard-assurance.md | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/cross-engine-rich-clipboard-assurance.md b/docs/doctoring/cross-engine-rich-clipboard-assurance.md index 91a22b64..d8262468 100644 --- a/docs/doctoring/cross-engine-rich-clipboard-assurance.md +++ b/docs/doctoring/cross-engine-rich-clipboard-assurance.md @@ -6,35 +6,45 @@ Status: Implemented on active PR Inkspan's SafeClipboard runtime is already integrated on protected `main`, but HTML fragment parsing, DOM reconstruction, CSS interpretation, serialization, and ProseMirror parsing are browser semantics. jsdom remains useful deterministic unit evidence; it is not real-engine conformance. The 0.6.0 rich-clipboard publication boundary therefore requires the same committed synthetic fixtures to execute through the supported TipTap/ProseMirror paste path in real Chromium, Firefox, and WebKit on one exact source head. -This active implementation uses **Playwright 1.62.0** from the isolated `tests/browser/pnpm-lock.yaml`. That pinned Playwright release identifies Chromium 151.0.7922.34, Firefox 153, and WebKit 26.5 as its bundled browser versions. The CI evidence records the actual `browser.version()` value for each engine, the Playwright version, operating-system identity, corpus version, SHA-256 of the browser-test `pnpm-lock.yaml`, and the exact source head. Those observed values, rather than this prose, are the release evidence when a browser revision changes. +This active implementation uses **Playwright 1.62.0** from the isolated `tests/browser/pnpm-lock.yaml`. That pinned Playwright release identifies Chromium 151.0.7922.34, Firefox 153, and WebKit 26.5 as its bundled browser versions. The CI evidence records the actual `browser.version()` value for each engine, the Playwright version, operating-system identity, corpus version, SHA-256 of the browser-test `pnpm-lock.yaml`, the exact source head, one fresh browser-run identity, and—on the release path—the SHA-256 of the exact packed npm artifact under test. Those observed values, rather than this prose, are the release evidence when a browser revision changes. ## Test-first evidence RED commit `cd6945b642a5d48449e1a59581e728ce6b440ff6` added the permanent release-oracle contract before its implementation existed. Hosted CI failed at TypeScript resolution because `crossEngineClipboardEvidence` was deliberately absent. The contract also injects deliberate sanitized-HTML, ProseMirror-structure, rejection-behavior, missing-engine, duplicate-engine, and mixed-case divergences. The production oracle then implemented fail-closed three-engine consensus, and a later exact coverage run exposed and removed one unreachable fallback instead of excluding it from coverage. +A later exact-head review found three release-evidence integrity gaps that direct source-tree browser success could not prove: the tag workflow did not exercise the packed `.tgz`, completed browser evidence disappeared with the runner, and predecessor-run evidence could survive in the local evidence directory. Permanent regression contracts now require the release browser job to consume the npm artifact produced by `build-release-artifacts`, verify its transferred checksum, expose only that artifact's public bundle to the browser harness, retain the bounded `.browser-evidence` directory as a GitHub Actions artifact, clear predecessor evidence once before projects start, bind every engine record to one run identifier, and recompute the current browser lock digest at consensus time. + The browser corpus covers active content, external-resource and form subtrees, hidden CSS/ARIA/Office/popover content, safe and unsafe links, malformed formatting and paragraph reconstruction, table/list parser repair, SVG/MathML, interactive and native-widget fallback, semantic inline-style reconstruction, byte limits, node limits, and depth limits. A separate real-browser probe uses a revoked `Document` proxy to require stable redacted DOM-capability failure without reflecting the private source string. A representative Word-like fixture provides a generous release alarm rather than a universal performance benchmark. +## Packed-artifact and source authority + +Pull-request CI may use the exact checked-out source entry through the browser harness alias to prove the current implementation before packaging. The release path is stricter: `build-release-artifacts` first builds, tests, packs, and transfers the npm `.tgz`; the browser job then downloads that artifact, verifies `SHA256SUMS`, unpacks the exact package into the isolated browser consumer, and points the harness alias at the packaged `dist/cwl-editor.js`. The browser release result therefore cannot be satisfied solely by an uninstalled source-tree module that differs from what will be published. + +The packed npm SHA-256 is carried into every engine evidence record and must agree with the expected release artifact digest. This package digest is release-integrity metadata, not a tenant, actor, authorization, or durable-document identifier. + ## Hermeticity and evidence minimization -The browser scenario permits requests only to the loopback Vite harness and aborts any external request. SafeClipboard itself performs no network fetch. The workflow installs the exact browser revisions selected by pinned Playwright before the test scenario begins; browser provisioning is a build prerequisite, not application egress. +The browser scenario permits requests only to the loopback Vite harness and aborts any external request. The external-request assertion is evaluated after each scenario has reached network idle so a delayed request cannot escape an early assertion. SafeClipboard itself performs no network fetch. The workflow installs the exact browser revisions selected by pinned Playwright before the test scenario begins; browser provisioning is a build prerequisite, not application egress. + +Global browser setup removes predecessor `.browser-evidence` once before Chromium, Firefox, WebKit, and consensus projects begin and creates one opaque run identity shared by the current invocation. Consensus requires every engine record to match that run identity and recomputes the current `tests/browser/pnpm-lock.yaml` SHA-256 rather than merely trusting equality among evidence files. -The evidence files contain only public synthetic fixture identifiers, sanitized output, ProseMirror JSON produced from those synthetic fixtures, stable rejection codes, engine/runtime versions, lock digest, corpus version, source SHA, runner identity, and representative timing. They contain **no tenant document**, production clipboard payload, credential, model prompt/output, authorization context, user identity, or private local path. The committed corpus uses synthetic fixtures only. +The evidence files contain only public synthetic fixture identifiers, sanitized output, ProseMirror JSON produced from those synthetic fixtures, stable rejection codes, engine/runtime versions, lock digest, optional packed-package digest, corpus version, source SHA, run identity, runner identity, and representative timing. They contain **no tenant document**, production clipboard payload, credential, model prompt/output, authorization context, user identity, or private local path. The committed corpus uses synthetic fixtures only. Release execution uploads only the bounded `.browser-evidence` directory with hidden run-identity metadata included; Playwright screenshots/traces and the broader `test-results` directory are not retained by this evidence artifact. ## Difference policy Security-relevant results must agree across Chromium, Firefox, and WebKit. The default comparator uses **no generic normalization** and no broad engine allowlist. A difference may be admitted only through a focused regression fixture and a reviewed rule that records the authoritative standards basis, exact affected engine/version, threat analysis, canonical interpretation, compatibility consequence, and rollback. An unexplained parser, sanitizer, error, or ProseMirror-structure difference must **fail closed**. -The same rule applies when one project is missing, skipped, cancelled, unable to provision, or unable to emit exact-head evidence: the rich-clipboard release lane remains blocked. Other Inkspan work may continue; the browser gate itself does not become optional. +The same rule applies when one project is missing, skipped, cancelled, unable to provision, unable to emit current-run evidence, has a stale lock digest, or cannot prove the expected packed package: the rich-clipboard release lane remains blocked. Other Inkspan work may continue; the browser gate itself does not become optional. ## Compatibility and rollback -Playwright/browser upgrades are compatibility events. Update the immutable browser-test lock, rerun every engine and the complete corpus, review any difference against current standards, and accept the new evidence only on the unchanged exact head. Do not transfer browser evidence from a predecessor commit. +Playwright/browser upgrades are compatibility events. Update the immutable browser-test lock, rerun every engine and the complete corpus, review any difference against current standards, and accept the new evidence only on the unchanged exact head. Do not transfer browser evidence from a predecessor commit or prior workflow attempt. -If the browser gate itself is faulty, rollback may revert the gate change while explicitly leaving the 0.6.0 rich-clipboard publication claim unaccepted. After protected integration, removing a required engine, weakening the corpus, broadening normalization, or replacing the exact-head evidence contract requires a superseding ADR and new threat analysis. A sanitizer defect discovered by the gate is fixed at the runtime boundary test-first rather than hidden in an engine-specific expectation. +If the browser gate itself is faulty, rollback may revert the gate change while explicitly leaving the 0.6.0 rich-clipboard publication claim unaccepted. After protected integration, removing a required engine, weakening the corpus, broadening normalization, dropping packed-artifact binding/current-run identity/lock revalidation, or replacing the exact-head evidence contract requires a superseding ADR and new threat analysis. A sanitizer defect discovered by the gate is fixed at the runtime boundary test-first rather than hidden in an engine-specific expectation. ## Claim limits -Passing these projects proves the committed SafeClipboard corpus and supported paste integration under the pinned Playwright engine builds on the recorded runner. It does not claim byte-identical behavior for every browser build, enterprise browser policy, extension environment, branded channel, downstream renderer, or arbitrary HTML. Hosts continue to own authorization, tenancy, persistence, CSP, application egress, deployment, model-use policy, and legal/privacy policy. +Passing these projects proves the committed SafeClipboard corpus and supported paste integration under the pinned Playwright engine builds on the recorded runner, and on the release path proves that the exact packed npm artifact exercised by the harness matches the recorded package digest. It does not claim byte-identical behavior for every browser build, enterprise browser policy, extension environment, branded channel, downstream renderer, or arbitrary HTML. Hosts continue to own authorization, tenancy, persistence, CSP, application egress, deployment, model-use policy, and legal/privacy policy. ## References From f9d8d3d66d3872547d2d5a5b8111f2d6560b46d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:42:21 +0900 Subject: [PATCH 52/65] test(browser): derive packed digest from release artifact --- tests/browser/evidenceContract.ts | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/browser/evidenceContract.ts b/tests/browser/evidenceContract.ts index 0bf24b08..3b575e22 100644 --- a/tests/browser/evidenceContract.ts +++ b/tests/browser/evidenceContract.ts @@ -1,5 +1,50 @@ +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { readdir } from 'node:fs/promises'; +import { resolve } from 'node:path'; + /** Maximum accepted sanitizer time for the representative Word-like browser fixture. */ export const BROWSER_PERFORMANCE_BUDGET_MILLIS = 8_000; /** Evidence schema version for one browser-engine release observation file. */ export const BROWSER_EVIDENCE_SCHEMA_VERSION = 2; + +/** + * Resolve the exact packed npm artifact digest when a release artifact is present. + * + * Ordinary pull-request browser CI has no `release/` directory and therefore + * returns `null`. The tag release lane must expose exactly one `.tgz`; its digest + * is recomputed from bytes rather than trusting a propagated environment value. + */ +export async function packedPackageSha256(repositoryRoot: string): Promise { + const releaseDirectory = resolve(repositoryRoot, 'release'); + let entries; + try { + entries = await readdir(releaseDirectory, { withFileTypes: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; + } + + const packageFiles = entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.tgz')) + .map((entry) => entry.name) + .sort(); + if (packageFiles.length !== 1) { + throw new Error('Cross-engine release evidence requires exactly one packed npm artifact.'); + } + + const digest = createHash('sha256'); + await new Promise((resolveStream, rejectStream) => { + const stream = createReadStream(resolve(releaseDirectory, packageFiles[0] ?? '')); + stream.on('data', (chunk) => digest.update(chunk)); + stream.once('error', rejectStream); + stream.once('end', resolveStream); + }); + const actualDigest = digest.digest('hex'); + const propagatedDigest = process.env.INKSPAN_EXPECTED_PACKAGE_SHA256?.trim(); + if (propagatedDigest && propagatedDigest !== actualDigest) { + throw new Error('Packed npm artifact digest does not match propagated release evidence.'); + } + return actualDigest; +} From 6fb9803a4441a42e3e7e52cbaa4d8652d52b5bf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:42:52 +0900 Subject: [PATCH 53/65] test(browser): hash exact packed bytes in evidence --- tests/browser/specs/clipboard.browser.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/browser/specs/clipboard.browser.spec.ts b/tests/browser/specs/clipboard.browser.spec.ts index 7390aa1c..b9446384 100644 --- a/tests/browser/specs/clipboard.browser.spec.ts +++ b/tests/browser/specs/clipboard.browser.spec.ts @@ -12,6 +12,7 @@ import { import { BROWSER_EVIDENCE_SCHEMA_VERSION, BROWSER_PERFORMANCE_BUDGET_MILLIS, + packedPackageSha256, } from '../evidenceContract.js'; type BrowserProbe = (request: { @@ -29,6 +30,7 @@ type HostileDocumentProbe = (sourceHtml: string) => { }; const specDirectory = dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = resolve(specDirectory, '../../..'); const evidenceDirectory = resolve(specDirectory, '../.browser-evidence'); const lockfilePath = resolve(specDirectory, '../pnpm-lock.yaml'); const packagePath = resolve(specDirectory, '../package.json'); @@ -171,7 +173,7 @@ test.afterAll(async ({ browser, browserName }) => { runnerImage: process.env.ImageOS ?? null, headSha: process.env.INKSPAN_EXPECTED_HEAD_SHA ?? process.env.GITHUB_SHA ?? null, - packageSha256: process.env.INKSPAN_EXPECTED_PACKAGE_SHA256?.trim() || null, + packageSha256: await packedPackageSha256(repositoryRoot), lockSha256: createHash('sha256').update(lockfile).digest('hex'), representativeWordMillis, observations, From 4c067a80dc394ae2ee41a765779f039c81328b5c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:43:18 +0900 Subject: [PATCH 54/65] test(browser): rederive packed digest in consensus --- tests/browser/specs/clipboard.consensus.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/browser/specs/clipboard.consensus.spec.ts b/tests/browser/specs/clipboard.consensus.spec.ts index 8005405b..71b7283d 100644 --- a/tests/browser/specs/clipboard.consensus.spec.ts +++ b/tests/browser/specs/clipboard.consensus.spec.ts @@ -13,6 +13,7 @@ import { import { BROWSER_EVIDENCE_SCHEMA_VERSION, BROWSER_PERFORMANCE_BUDGET_MILLIS, + packedPackageSha256, } from '../evidenceContract.js'; interface BrowserEvidence { @@ -32,6 +33,7 @@ interface BrowserEvidence { } const specDirectory = dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = resolve(specDirectory, '../../..'); const evidenceDirectory = resolve(specDirectory, '../.browser-evidence'); const lockfilePath = resolve(specDirectory, '../pnpm-lock.yaml'); const engines: readonly CrossEngineClipboardEngine[] = [ @@ -55,8 +57,7 @@ test('requires complete exact-head browser evidence and exact corpus consensus', const currentLockSha256 = createHash('sha256') .update(await readFile(lockfilePath)) .digest('hex'); - const expectedPackageSha256 = - process.env.INKSPAN_EXPECTED_PACKAGE_SHA256?.trim() || null; + const currentPackageSha256 = await packedPackageSha256(repositoryRoot); const evidence = await Promise.all(engines.map(readEvidence)); const [reference] = evidence; if (!reference) throw new Error('Cross-engine browser evidence is missing.'); @@ -72,7 +73,7 @@ test('requires complete exact-head browser evidence and exact corpus consensus', expect(item.browserVersion.length).toBeGreaterThan(0); expect(item.lockSha256).toBe(reference.lockSha256); expect(item.lockSha256).toBe(currentLockSha256); - expect(item.packageSha256).toBe(expectedPackageSha256); + expect(item.packageSha256).toBe(currentPackageSha256); expect(item.headSha).toBe(reference.headSha); expect(item.observations).toHaveLength(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length); expect(item.representativeWordMillis).toBeGreaterThanOrEqual(0); From 6eca78ae2a028edf11bd5f5dc6ea7ec1c37d6249 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:45:50 +0900 Subject: [PATCH 55/65] test(browser): require semantic JSON object equality --- src/crossEngineClipboardEvidence.test.ts | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/crossEngineClipboardEvidence.test.ts b/src/crossEngineClipboardEvidence.test.ts index a8430cf8..d6729bab 100644 --- a/src/crossEngineClipboardEvidence.test.ts +++ b/src/crossEngineClipboardEvidence.test.ts @@ -56,6 +56,34 @@ describe('cross-engine rich clipboard release oracle', () => { ).not.toThrow(); }); + it('accepts semantically identical JSON objects with different key insertion order', () => { + expect(() => + assertCrossEngineClipboardConsensus([ + observation('chromium', { + documentJson: { + type: 'doc', + attrs: { zeta: 1, alpha: 2 }, + content: [{ type: 'paragraph', attrs: { beta: true, alpha: false } }], + }, + }), + observation('firefox', { + documentJson: { + content: [{ attrs: { alpha: false, beta: true }, type: 'paragraph' }], + attrs: { alpha: 2, zeta: 1 }, + type: 'doc', + }, + }), + observation('webkit', { + documentJson: { + attrs: { zeta: 1, alpha: 2 }, + content: [{ attrs: { beta: true, alpha: false }, type: 'paragraph' }], + type: 'doc', + }, + }), + ]), + ).not.toThrow(); + }); + it('fails closed when one engine reconstructs unsafe or divergent HTML', () => { expect(() => assertCrossEngineClipboardConsensus([ From 6bc1be708b721ea97e243c313b694c1e7443f1b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:46:37 +0900 Subject: [PATCH 56/65] test(browser): cover null JSON attributes in consensus --- src/crossEngineClipboardEvidence.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crossEngineClipboardEvidence.test.ts b/src/crossEngineClipboardEvidence.test.ts index d6729bab..fb84bc5f 100644 --- a/src/crossEngineClipboardEvidence.test.ts +++ b/src/crossEngineClipboardEvidence.test.ts @@ -62,20 +62,20 @@ describe('cross-engine rich clipboard release oracle', () => { observation('chromium', { documentJson: { type: 'doc', - attrs: { zeta: 1, alpha: 2 }, + attrs: { zeta: 1, optional: null, alpha: 2 }, content: [{ type: 'paragraph', attrs: { beta: true, alpha: false } }], }, }), observation('firefox', { documentJson: { content: [{ attrs: { alpha: false, beta: true }, type: 'paragraph' }], - attrs: { alpha: 2, zeta: 1 }, + attrs: { alpha: 2, optional: null, zeta: 1 }, type: 'doc', }, }), observation('webkit', { documentJson: { - attrs: { zeta: 1, alpha: 2 }, + attrs: { zeta: 1, alpha: 2, optional: null }, content: [{ attrs: { beta: true, alpha: false }, type: 'paragraph' }], type: 'doc', }, From 408750d567a26e43604717adfe66edf99450eaa4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:47:08 +0900 Subject: [PATCH 57/65] fix(browser): compare JSON structure independent of key order --- src/crossEngineClipboardEvidence.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/crossEngineClipboardEvidence.ts b/src/crossEngineClipboardEvidence.ts index 2767e666..e2275a0f 100644 --- a/src/crossEngineClipboardEvidence.ts +++ b/src/crossEngineClipboardEvidence.ts @@ -35,7 +35,7 @@ export interface CrossEngineClipboardObservation { readonly caseId: string; readonly engine: CrossEngineClipboardEngine; readonly sanitizedHtml: string | null; - readonly documentJson: unknown | null; + readonly documentJson: unknown; readonly errorCode: ClipboardSanitizationErrorCode | null; } @@ -193,10 +193,12 @@ const REQUIRED_ENGINES: readonly CrossEngineClipboardEngine[] = Object.freeze([ /** * Require exact rich-clipboard parity across one observation from every engine. * - * The default gate intentionally contains no broad normalization or difference - * allowlist. A future standards-permitted engine exception must first add a - * focused corpus case, threat rationale, explicit comparison rule, and rollback - * note rather than being silently normalized here. + * The gate compares JSON object semantics independent of member insertion order, + * because JSON object member order carries no document-structure authority. It + * otherwise contains no broad normalization or engine difference allowlist. A + * future standards-permitted engine exception must first add a focused corpus + * case, threat rationale, explicit comparison rule, and rollback note rather + * than being silently normalized here. */ export function assertCrossEngineClipboardConsensus( observations: readonly CrossEngineClipboardObservation[], @@ -242,6 +244,20 @@ export function assertCrossEngineClipboardConsensus( } } +/** Serialize JSON values with recursively sorted object member names. */ function canonicalJson(value: unknown): string { - return JSON.stringify(value); + return JSON.stringify(canonicalizeJson(value)); +} + +/** Preserve array order and values while normalizing unordered JSON object members. */ +function canonicalizeJson(value: unknown): unknown { + if (Array.isArray(value)) return value.map(canonicalizeJson); + if (value === null || typeof value !== 'object') return value; + + const record = value as Record; + return Object.fromEntries( + Object.keys(record) + .sort() + .map((key) => [key, canonicalizeJson(record[key])]), + ); } From 23372c922c97fdb6c3ef03514718633627514347 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:47:45 +0900 Subject: [PATCH 58/65] docs(browser): define semantic JSON comparison boundary --- docs/doctoring/cross-engine-rich-clipboard-assurance.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/cross-engine-rich-clipboard-assurance.md b/docs/doctoring/cross-engine-rich-clipboard-assurance.md index d8262468..a5475a94 100644 --- a/docs/doctoring/cross-engine-rich-clipboard-assurance.md +++ b/docs/doctoring/cross-engine-rich-clipboard-assurance.md @@ -20,7 +20,7 @@ The browser corpus covers active content, external-resource and form subtrees, h Pull-request CI may use the exact checked-out source entry through the browser harness alias to prove the current implementation before packaging. The release path is stricter: `build-release-artifacts` first builds, tests, packs, and transfers the npm `.tgz`; the browser job then downloads that artifact, verifies `SHA256SUMS`, unpacks the exact package into the isolated browser consumer, and points the harness alias at the packaged `dist/cwl-editor.js`. The browser release result therefore cannot be satisfied solely by an uninstalled source-tree module that differs from what will be published. -The packed npm SHA-256 is carried into every engine evidence record and must agree with the expected release artifact digest. This package digest is release-integrity metadata, not a tenant, actor, authorization, or durable-document identifier. +The packed npm SHA-256 is carried into every engine evidence record and is recomputed from the exact transferred `.tgz` bytes at evidence creation and consensus time. A propagated digest, when available, must match those bytes rather than replacing that calculation. This package digest is release-integrity metadata, not a tenant, actor, authorization, or durable-document identifier. ## Hermeticity and evidence minimization @@ -32,7 +32,9 @@ The evidence files contain only public synthetic fixture identifiers, sanitized ## Difference policy -Security-relevant results must agree across Chromium, Firefox, and WebKit. The default comparator uses **no generic normalization** and no broad engine allowlist. A difference may be admitted only through a focused regression fixture and a reviewed rule that records the authoritative standards basis, exact affected engine/version, threat analysis, canonical interpretation, compatibility consequence, and rollback. An unexplained parser, sanitizer, error, or ProseMirror-structure difference must **fail closed**. +Security-relevant results must agree across Chromium, Firefox, and WebKit. The comparator uses **no generic normalization** and no broad engine allowlist. The one structural canonicalization is limited to recursively sorting JSON object member names before comparing ProseMirror JSON, because JSON object member ordering is not part of the document structure; array order, primitive values, nulls, sanitized HTML, and rejection codes remain exact. This rule prevents a semantically irrelevant object insertion-order difference from becoming an engine exception while preserving all ordered document structure. + +Any other difference may be admitted only through a focused regression fixture and a reviewed rule that records the authoritative standards basis, exact affected engine/version, threat analysis, canonical interpretation, compatibility consequence, and rollback. An unexplained parser, sanitizer, error, ordered ProseMirror-structure, or value difference must **fail closed**. The same rule applies when one project is missing, skipped, cancelled, unable to provision, unable to emit current-run evidence, has a stale lock digest, or cannot prove the expected packed package: the rich-clipboard release lane remains blocked. Other Inkspan work may continue; the browser gate itself does not become optional. @@ -40,7 +42,7 @@ The same rule applies when one project is missing, skipped, cancelled, unable to Playwright/browser upgrades are compatibility events. Update the immutable browser-test lock, rerun every engine and the complete corpus, review any difference against current standards, and accept the new evidence only on the unchanged exact head. Do not transfer browser evidence from a predecessor commit or prior workflow attempt. -If the browser gate itself is faulty, rollback may revert the gate change while explicitly leaving the 0.6.0 rich-clipboard publication claim unaccepted. After protected integration, removing a required engine, weakening the corpus, broadening normalization, dropping packed-artifact binding/current-run identity/lock revalidation, or replacing the exact-head evidence contract requires a superseding ADR and new threat analysis. A sanitizer defect discovered by the gate is fixed at the runtime boundary test-first rather than hidden in an engine-specific expectation. +If the browser gate itself is faulty, rollback may revert the gate change while explicitly leaving the 0.6.0 rich-clipboard publication claim unaccepted. After protected integration, removing a required engine, weakening the corpus, broadening normalization beyond unordered JSON object-member canonicalization, dropping packed-artifact binding/current-run identity/lock revalidation, or replacing the exact-head evidence contract requires a superseding ADR and new threat analysis. A sanitizer defect discovered by the gate is fixed at the runtime boundary test-first rather than hidden in an engine-specific expectation. ## Claim limits From 40cff625bce3743e89295f8a64a31cada0f53bb6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:52:02 +0900 Subject: [PATCH 59/65] test(release): assert byte-derived package evidence --- src/releaseBrowserEvidence.test.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/releaseBrowserEvidence.test.ts b/src/releaseBrowserEvidence.test.ts index 69784921..fd1f0eb9 100644 --- a/src/releaseBrowserEvidence.test.ts +++ b/src/releaseBrowserEvidence.test.ts @@ -20,6 +20,7 @@ function workflowJob(source: string, jobName: string, nextJobName: string): stri const workflow = repositoryFile('.github/workflows/release.yml'); const playwrightConfig = repositoryFile('tests/browser/playwright.config.ts'); const browserHarness = repositoryFile('tests/browser/harness.ts'); +const browserEvidenceContract = repositoryFile('tests/browser/evidenceContract.ts'); const browserSpec = repositoryFile('tests/browser/specs/clipboard.browser.spec.ts'); const consensusSpec = repositoryFile('tests/browser/specs/clipboard.consensus.spec.ts'); @@ -81,18 +82,29 @@ describe('release cross-engine browser evidence contract', () => { expect(browserJob).not.toContain('path: tests/browser/test-results'); }); - it('binds browser evidence to one fresh run, current lock, and packed package', () => { + it('binds browser evidence to one fresh run, current lock, and packed package bytes', () => { expect(playwrightConfig).toContain("globalSetup: './globalSetup.ts'"); expect(browserHarness).toContain("from 'inkspan-browser-under-test'"); + expect(browserEvidenceContract).toContain("createReadStream(resolve(releaseDirectory"); + expect(browserEvidenceContract).toContain("entry.name.endsWith('.tgz')"); + expect(browserEvidenceContract).toContain("createHash('sha256')"); + expect(browserEvidenceContract).toContain( + 'Cross-engine release evidence requires exactly one packed npm artifact.', + ); + expect(browserEvidenceContract).toContain( + 'Packed npm artifact digest does not match propagated release evidence.', + ); expect(browserSpec).toContain('runId'); - expect(browserSpec).toContain('packageSha256'); + expect(browserSpec).toContain('packageSha256: await packedPackageSha256(repositoryRoot)'); expect(consensusSpec).toContain("createHash('sha256')"); expect(consensusSpec).toContain("'../pnpm-lock.yaml'"); expect(consensusSpec).toContain('item.runId'); expect(consensusSpec).toContain('reference.runId'); expect(consensusSpec).toContain('item.lockSha256'); expect(consensusSpec).toContain('currentLockSha256'); - expect(consensusSpec).toContain('INKSPAN_EXPECTED_PACKAGE_SHA256'); + expect(consensusSpec).toContain('currentPackageSha256'); + expect(consensusSpec).toContain('item.packageSha256'); + expect(consensusSpec).toContain('packedPackageSha256(repositoryRoot)'); }); it('makes immutable publication depend on both artifacts and tagged browser evidence', () => { From 484c8e4af968b17d1e68990b9bb6b1c3f7112d74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:08:02 +0900 Subject: [PATCH 60/65] test(docs): require packed browser operability guidance --- src/crossEngineClipboardDocumentation.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/crossEngineClipboardDocumentation.test.ts b/src/crossEngineClipboardDocumentation.test.ts index d51ab8dc..8de6d32a 100644 --- a/src/crossEngineClipboardDocumentation.test.ts +++ b/src/crossEngineClipboardDocumentation.test.ts @@ -64,6 +64,17 @@ describe('cross-engine clipboard documentation contract', () => { expect(doctoring).toContain('no tenant document'); }); + it('documents operator-verifiable packed artifact and retained evidence semantics', () => { + const operability = documentText('docs/OPERABILITY.md'); + + expect(operability).toContain('packed npm artifact'); + expect(operability).toContain('SHA-256'); + expect(operability).toContain('.browser-evidence'); + expect(operability).toContain('fresh run identity'); + expect(operability).toContain('Playwright screenshots'); + expect(operability).toContain('test-results'); + }); + it('records browser maturity in the exact canonical and changelog entries', () => { const fitness = repositoryFile('docs/DOCUMENTATION_FITNESS.md'); const changelog = repositoryFile('CHANGELOG.md'); From 659f30968221148e83237aec48dad0534461a05b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:08:51 +0900 Subject: [PATCH 61/65] docs(browser): document retained packed evidence operations --- docs/OPERABILITY.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/OPERABILITY.md b/docs/OPERABILITY.md index 61e01b9f..5cee59fb 100644 --- a/docs/OPERABILITY.md +++ b/docs/OPERABILITY.md @@ -41,11 +41,11 @@ File publication must follow the documented atomic/non-overwrite behavior. A cal ## Cross-engine clipboard assurance operations -SafeClipboard is shipped on protected `main`; the browser-realistic release assurance is implemented on the active browser-assurance PR and remains non-authoritative until protected integration. That active gate uses dependency-locked **Playwright 1.62.0** Chromium, Firefox, and WebKit projects and binds every result to one **exact source head**, one browser-test lock digest, and one corpus version. +SafeClipboard is shipped on protected `main`; the browser-realistic release assurance is implemented on the active browser-assurance PR and remains non-authoritative until protected integration. That active gate uses dependency-locked **Playwright 1.62.0** Chromium, Firefox, and WebKit projects and binds every result to one **exact source head**, one browser-test lock digest, one corpus version, and one **fresh run identity**. On the tag release path it additionally tests the exact **packed npm artifact** produced by the release build and records the artifact's byte-derived **SHA-256**. -Treat missing, skipped, cancelled, provisioning-failed, incomplete, or semantically divergent browser evidence as a **fail closed** release condition. Do not silently drop one engine or substitute predecessor-head results. The first response to divergence is to determine whether the difference is a sanitizer/integration defect, a standards-permitted serialization difference, or a test/environment defect. Unsafe behavior is repaired at the runtime boundary test-first. A safe difference is admitted only with focused regression evidence, current authoritative **standards** basis, threat analysis, exact affected engine/version evidence, canonical interpretation, compatibility impact, and explicit **rollback**. +Treat missing, skipped, cancelled, provisioning-failed, incomplete, stale-run, stale-lock, package-mismatched, or semantically divergent browser evidence as a **fail closed** release condition. Do not silently drop one engine or substitute predecessor-head results. The first response to divergence is to determine whether the difference is a sanitizer/integration defect, a standards-permitted serialization difference, or a test/environment defect. Unsafe behavior is repaired at the runtime boundary test-first. A safe difference is admitted only with focused regression evidence, current authoritative **standards** basis, threat analysis, exact affected engine/version evidence, canonical interpretation, compatibility impact, and explicit **rollback**. -Browser evidence contains only committed synthetic fixtures and bounded version/hash/timing metadata; no tenant document, credential, model data, authorization context, or production clipboard payload belongs in the evidence bundle. The test scenario permits only loopback harness requests; browser installation happens before the scenario as a pinned build prerequisite. +Browser evidence contains only committed synthetic fixtures and bounded version/hash/timing metadata; no tenant document, credential, model data, authorization context, or production clipboard payload belongs in the evidence bundle. The test scenario permits only loopback harness requests; browser installation happens before the scenario as a pinned build prerequisite. Release execution retains only the bounded `tests/browser/.browser-evidence/` directory, including its hidden run identity, as the reviewable browser evidence artifact. **Playwright screenshots**, traces, and the broader `test-results` directory are deliberately not retained by that release-evidence upload. Operators reviewing a release candidate must confirm that all three engine records share the current run identity, current lock SHA-256, exact source head, and exact packed npm artifact digest before treating the browser gate as satisfied. A Playwright/browser revision upgrade is an operational compatibility event. Rebuild the browser evidence from the new immutable lock on one exact source head and rerun the complete corpus. If browser provisioning is unavailable, only the rich-clipboard release lane is blocked; unrelated Inkspan work continues. Rolling back the browser gate leaves the 0.6.0 rich-clipboard publication claim unaccepted unless equivalent or stronger real-engine assurance replaces it. From 5a52c702fe88db07c3bbfe77f1744d7e37cfb3cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:10:54 +0900 Subject: [PATCH 62/65] test(docs): require primary Playwright version source --- src/crossEngineClipboardDocumentation.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/crossEngineClipboardDocumentation.test.ts b/src/crossEngineClipboardDocumentation.test.ts index 8de6d32a..50bf8d59 100644 --- a/src/crossEngineClipboardDocumentation.test.ts +++ b/src/crossEngineClipboardDocumentation.test.ts @@ -75,6 +75,18 @@ describe('cross-engine clipboard documentation contract', () => { expect(operability).toContain('test-results'); }); + it('grounds browser-version claims in the official Playwright release notes', () => { + const doctoring = documentText( + 'docs/doctoring/cross-engine-rich-clipboard-assurance.md', + ); + const traceability = documentText('docs/TRACEABILITY.md'); + + for (const text of [doctoring, traceability]) { + expect(text).toContain('Release notes: Version 1.62'); + expect(text).toContain('https://playwright.dev/docs/release-notes'); + } + }); + it('records browser maturity in the exact canonical and changelog entries', () => { const fitness = repositoryFile('docs/DOCUMENTATION_FITNESS.md'); const changelog = repositoryFile('CHANGELOG.md'); From 219f51b1388e21e6807390c8dde711d1443d8860 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:11:42 +0900 Subject: [PATCH 63/65] docs(browser): cite official Playwright 1.62 release notes --- docs/doctoring/cross-engine-rich-clipboard-assurance.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/cross-engine-rich-clipboard-assurance.md b/docs/doctoring/cross-engine-rich-clipboard-assurance.md index a5475a94..f4469a00 100644 --- a/docs/doctoring/cross-engine-rich-clipboard-assurance.md +++ b/docs/doctoring/cross-engine-rich-clipboard-assurance.md @@ -6,7 +6,7 @@ Status: Implemented on active PR Inkspan's SafeClipboard runtime is already integrated on protected `main`, but HTML fragment parsing, DOM reconstruction, CSS interpretation, serialization, and ProseMirror parsing are browser semantics. jsdom remains useful deterministic unit evidence; it is not real-engine conformance. The 0.6.0 rich-clipboard publication boundary therefore requires the same committed synthetic fixtures to execute through the supported TipTap/ProseMirror paste path in real Chromium, Firefox, and WebKit on one exact source head. -This active implementation uses **Playwright 1.62.0** from the isolated `tests/browser/pnpm-lock.yaml`. That pinned Playwright release identifies Chromium 151.0.7922.34, Firefox 153, and WebKit 26.5 as its bundled browser versions. The CI evidence records the actual `browser.version()` value for each engine, the Playwright version, operating-system identity, corpus version, SHA-256 of the browser-test `pnpm-lock.yaml`, the exact source head, one fresh browser-run identity, and—on the release path—the SHA-256 of the exact packed npm artifact under test. Those observed values, rather than this prose, are the release evidence when a browser revision changes. +This active implementation uses **Playwright 1.62.0** from the isolated `tests/browser/pnpm-lock.yaml`. The official Playwright **Release notes: Version 1.62** identify Chromium 151.0.7922.34, Mozilla Firefox 153.0, and WebKit 26.5 as the bundled browser versions for that release. The CI evidence records the actual `browser.version()` value for each engine, the Playwright version, operating-system identity, corpus version, SHA-256 of the browser-test `pnpm-lock.yaml`, the exact source head, one fresh browser-run identity, and—on the release path—the SHA-256 of the exact packed npm artifact under test. Those observed values, rather than this prose, are the release evidence when a browser revision changes. ## Test-first evidence @@ -52,6 +52,8 @@ Passing these projects proves the committed SafeClipboard corpus and supported p Microsoft. (2026). *Playwright Test 1.62.0*. npm. https://www.npmjs.com/package/@playwright/test/v/1.62.0 +Microsoft. (2026). *Release notes: Version 1.62*. Playwright. Retrieved August 10, 2026, from https://playwright.dev/docs/release-notes + Microsoft. (n.d.-a). *Browsers*. Playwright documentation. Retrieved August 10, 2026, from https://playwright.dev/docs/browsers Microsoft. (n.d.-b). *Projects*. Playwright documentation. Retrieved August 10, 2026, from https://playwright.dev/docs/test-projects From 6806aa40b523d00bb4d8268b622971d86088d306 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:12:25 +0900 Subject: [PATCH 64/65] docs(traceability): pin browser claims to Playwright release notes --- docs/TRACEABILITY.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 13728487..47d3c8e6 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -17,12 +17,12 @@ This record maps durable Inkspan product decisions to authoritative standards, p | Provenance semantics | Local transition/release evidence keeps content lineage separate from actor/authorization/durable claims | W3C PROV family | transition evidence, release evidence, canonical data model | Inkspan does not claim complete PROV conformance or host audit provenance | | Accessibility | Native controls, keyboard semantics, shortcut metadata, and host-facing status state support accessible embedding | W3C WCAG 2.2; WAI-ARIA where used | toolbar/accessibility tests, SSR tests, autosave lifecycle data | Component evidence alone is not a full host WCAG conformance claim | | Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | protected-main rich-clipboard unit/integration corpus and SafeClipboard ADR | Protected jsdom/TipTap integration success is not universal browser-engine conformance | -| Cross-engine release assurance | The same committed adversarial corpus runs under required Chromium, Firefox, and WebKit projects; only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright browser/project documentation | ADR 0016, Issue #66, active-PR exact-head browser job/evidence, TEST_STRATEGY and UML | The active implementation remains non-authoritative until protected integration; it does not claim byte-identical browser serialization or branded enterprise-policy coverage | +| Cross-engine release assurance | The same committed adversarial corpus runs under required Chromium, Firefox, and WebKit projects; only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright 1.62 release notes and browser/project documentation | ADR 0016, Issue #66, active-PR exact-head browser job/evidence, TEST_STRATEGY, OPERABILITY and UML | The active implementation remains non-authoritative until protected integration; the release path additionally binds browser evidence to the exact packed npm artifact and does not claim byte-identical browser serialization or branded enterprise-policy coverage | | Editor integration | Public behavior must exercise the actual TipTap/ProseMirror integration path, not an inert extension field or test-only hook | official TipTap and ProseMirror documentation for the locked dependency line | integration tests and package consumers | Inkspan does not claim compatibility with untested major-version integration semantics | | Collaboration | Inkspan provides provider-neutral editor/Yjs bindings; host owns provider lifecycle, room authorization, awareness privacy, persistence and audit | official Yjs/provider documentation plus Inkspan public contract | collaboration tests and architecture ownership matrix | No network-provider or tenant-authorization authority is implied | | Secure development | Security controls are developed test-first, with exact-head scanning/review/package evidence and root-cause regression | NIST SP 800-218 SSDF 1.1 | CI/security/SAST/package/provenance gates, doctoring and regression history | Repository evidence is not a claim of complete SSDF organizational conformance | | Office rendering | JSON→DOCX/XLSX/PPTX is deterministic, bounded, network-free, macro-free, injection-aware and package-inspected | Office Open XML specifications and relevant Python package contracts | Office renderer tests, Python coverage/docstring/package gates | Format fidelity is limited to explicitly tested supported constructs | -| Release authority | Source movement invalidates exact-head evidence; stale assets/digest ambiguity fail closed; formal approval remains distinct from status/comments | GitHub protected-branch/review/release/attestation behavior and repository policy | release workflow tests, package checksums, SBOM/provenance, formal reviews | Local success or automated prose does not authorize protected merge/release | +| Release authority | Source movement invalidates exact-head evidence; stale assets/digest ambiguity fail closed; formal approval remains distinct from status/comments | GitHub protected-branch/review/release/attestation behavior and repository policy | release workflow tests, package checksums, browser evidence, SBOM/provenance, formal reviews | Local success or automated prose does not authorize protected merge/release | ## Current primary references @@ -36,7 +36,7 @@ Rundgren, A., Jordan, B., & Erdtman, S. (2020). *JSON Canonicalization Scheme (J Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities* (NIST SP 800-218). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 -Web Hypertext Application Technology Working Group. (2026). *HTML Standard: Parsing HTML documents* (Living Standard). Retrieved August 9, 2026, from https://html.spec.whatwg.org/multipage/parsing.html +Web Hypertext Application Technology Working Group. (2026). *HTML Standard: Parsing HTML documents* (Living Standard). Retrieved August 10, 2026, from https://html.spec.whatwg.org/multipage/parsing.html World Wide Web Consortium. (2026, June 24). *Clipboard API and events* (W3C Working Draft). https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/ @@ -44,9 +44,11 @@ World Wide Web Consortium. (2024, December 12). *Web Content Accessibility Guide World Wide Web Consortium. (2013). *PROV-DM: The PROV Data Model*. https://www.w3.org/TR/prov-dm/ -Microsoft. (n.d.). *Browsers*. Playwright documentation. Retrieved August 9, 2026, from https://playwright.dev/docs/browsers +Microsoft. (2026). *Release notes: Version 1.62*. Playwright. Retrieved August 10, 2026, from https://playwright.dev/docs/release-notes -Microsoft. (n.d.). *Projects*. Playwright documentation. Retrieved August 9, 2026, from https://playwright.dev/docs/test-projects +Microsoft. (n.d.-a). *Browsers*. Playwright documentation. Retrieved August 10, 2026, from https://playwright.dev/docs/browsers + +Microsoft. (n.d.-b). *Projects*. Playwright documentation. Retrieved August 10, 2026, from https://playwright.dev/docs/test-projects ## Research-backed concurrency rationale From 9e2fe05dbe87b7f3ec6fa3022610c42d49a8f4ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 08:18:10 +0900 Subject: [PATCH 65/65] test(release): bind browser evidence retention contract --- src/releaseBrowserEvidence.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/releaseBrowserEvidence.test.ts b/src/releaseBrowserEvidence.test.ts index fd1f0eb9..86e8c99a 100644 --- a/src/releaseBrowserEvidence.test.ts +++ b/src/releaseBrowserEvidence.test.ts @@ -79,6 +79,8 @@ describe('release cross-engine browser evidence contract', () => { expect(browserJob).toContain('path: tests/browser/.browser-evidence/'); expect(browserJob).toContain('include-hidden-files: true'); expect(browserJob).toContain('if-no-files-found: error'); + expect(browserJob).toContain('if: always()'); + expect(browserJob).toContain('retention-days: 14'); expect(browserJob).not.toContain('path: tests/browser/test-results'); });