From cd6945b642a5d48449e1a59581e728ce6b440ff6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:34:59 +0900 Subject: [PATCH 01/31] test(browser): define cross-engine clipboard release oracle --- src/crossEngineClipboardEvidence.test.ts | 117 +++++++++++++++++++++++ 1 file changed, 117 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..d994f7d2 --- /dev/null +++ b/src/crossEngineClipboardEvidence.test.ts @@ -0,0 +1,117 @@ +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', + ]), + ); + expect(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length).toBeGreaterThanOrEqual(14); + 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 14bb2aec0da46f5085b4c642492aa5f9f4ad2656 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:36:54 +0900 Subject: [PATCH 02/31] feat(browser): implement cross-engine clipboard release oracle --- src/crossEngineClipboardEvidence.ts | 200 ++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 src/crossEngineClipboardEvidence.ts diff --git a/src/crossEngineClipboardEvidence.ts b/src/crossEngineClipboardEvidence.ts new file mode 100644 index 00000000..67e2ec84 --- /dev/null +++ b/src/crossEngineClipboardEvidence.ts @@ -0,0 +1,200 @@ +import type { ClipboardSanitizationErrorCode } from './extensions/SafeClipboard.js'; + +/** 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'; + +/** 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; +} + +/** 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

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

visibledisplayend

', + expectedSanitizedHtml: '

visibleend

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

beforesecretafter

', + expectedSanitizedHtml: '

beforeafter

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

one

two', + expectedSanitizedHtml: '

one

two

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

axbyc

', + expectedSanitizedHtml: '

abc

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

secret

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

open

end

', + expectedSanitizedHtml: '

open

end

', + }, + { + id: 'semantic-style-reconstruction', + riskFamily: 'parser-edge', + sourceHtml: + 'styled', + expectedSanitizedHtml: 'styled', + }, +] 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; + if (!reference) { + throw new Error( + 'Cross-engine clipboard evidence requires exactly one observation from chromium, firefox, and webkit.', + ); + } + 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 40bbeb61b4cf99fc55d96e1d6c9bfd388e8549f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:42:06 +0900 Subject: [PATCH 03/31] fix(browser): remove unreachable consensus fallback --- src/crossEngineClipboardEvidence.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/crossEngineClipboardEvidence.ts b/src/crossEngineClipboardEvidence.ts index 67e2ec84..1764e104 100644 --- a/src/crossEngineClipboardEvidence.ts +++ b/src/crossEngineClipboardEvidence.ts @@ -165,12 +165,11 @@ export function assertCrossEngineClipboardConsensus( ); } - const [reference, ...others] = observations; - if (!reference) { - 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.', From 525a6dcc7a069b7da3511af0ee0ba60219e72274 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:42:51 +0900 Subject: [PATCH 04/31] build(browser): pin Playwright release harness --- 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 25a6d7451c356d622dfad5200c7fab77dde3dba1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:42:59 +0900 Subject: [PATCH 05/31] build(browser): isolate browser-test dependency lock --- 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 acabbcf22684bf847831bdd4d1b8765515a39670 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:43:17 +0900 Subject: [PATCH 06/31] build(browser): lock Playwright 1.62.0 supply chain --- 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 2771eb8150f5c2fa335b6681eb138814e7e140e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:45:02 +0900 Subject: [PATCH 07/31] test(browser): cover clipboard resource ceilings --- src/crossEngineClipboardEvidence.ts | 52 +++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/crossEngineClipboardEvidence.ts b/src/crossEngineClipboardEvidence.ts index 1764e104..1e99562a 100644 --- a/src/crossEngineClipboardEvidence.ts +++ b/src/crossEngineClipboardEvidence.ts @@ -1,4 +1,10 @@ -import type { ClipboardSanitizationErrorCode } from './extensions/SafeClipboard.js'; +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'; @@ -11,7 +17,8 @@ export type CrossEngineClipboardRiskFamily = | 'malformed-markup' | 'table-list' | 'svg-mathml' - | 'parser-edge'; + | 'parser-edge' + | 'resource-limit'; /** One immutable public fixture in the cross-engine rich-clipboard corpus. */ export interface CrossEngineClipboardCase { @@ -19,6 +26,8 @@ export interface CrossEngineClipboardCase { 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. */ @@ -36,6 +45,7 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ riskFamily: 'active-content', sourceHtml: '

safe

', expectedSanitizedHtml: '

safe

', + expectedErrorCode: null, }, { id: 'active-resource-and-form', @@ -43,6 +53,7 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ sourceHtml: '
before
after
', expectedSanitizedHtml: '
beforeafter
', + expectedErrorCode: null, }, { id: 'hidden-display-and-aria', @@ -50,6 +61,7 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ sourceHtml: '

visibledisplayend

', expectedSanitizedHtml: '

visibleend

', + expectedErrorCode: null, }, { id: 'hidden-office-eof-comment', @@ -57,6 +69,7 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ sourceHtml: '

beforesecretafter

', expectedSanitizedHtml: '

beforeafter

', + expectedErrorCode: null, }, { id: 'hidden-content-visibility-popover', @@ -64,12 +77,14 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ sourceHtml: '
onetwothreefour
', expectedSanitizedHtml: '
onefour
', + expectedErrorCode: null, }, { id: 'unsafe-javascript-link', riskFamily: 'unsafe-link', sourceHtml: 'click', expectedSanitizedHtml: 'click', + expectedErrorCode: null, }, { id: 'safe-https-link', @@ -77,18 +92,21 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ 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', @@ -96,12 +114,14 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ 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', @@ -109,12 +129,14 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ 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', @@ -122,6 +144,7 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ sourceHtml: 'closed

open

end

', expectedSanitizedHtml: '

open

end

', + expectedErrorCode: null, }, { id: 'semantic-style-reconstruction', @@ -129,6 +152,31 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ 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; From 7d719a3e4b6ce7fa2bef26f1e3153ff7f6932855 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:45:34 +0900 Subject: [PATCH 08/31] test(browser): require resource-limit corpus coverage --- src/crossEngineClipboardEvidence.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crossEngineClipboardEvidence.test.ts b/src/crossEngineClipboardEvidence.test.ts index d994f7d2..a8430cf8 100644 --- a/src/crossEngineClipboardEvidence.test.ts +++ b/src/crossEngineClipboardEvidence.test.ts @@ -32,9 +32,10 @@ describe('cross-engine rich clipboard release oracle', () => { 'table-list', 'svg-mathml', 'parser-edge', + 'resource-limit', ]), ); - expect(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length).toBeGreaterThanOrEqual(14); + expect(SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.length).toBeGreaterThanOrEqual(18); expect( SAFE_CLIPBOARD_CROSS_ENGINE_CORPUS.every( (testCase) => From e2daadb4c4e722743901718b7d29868e6b20a89f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:45:54 +0900 Subject: [PATCH 09/31] test(browser): add real-engine clipboard 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 84795fe1a10364d888818f69a32d8b4465007736 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:46:15 +0900 Subject: [PATCH 10/31] test(browser): exercise supported paste pipeline in engines --- 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 355383b09e05c53748c5968125fa2e08a8f0aa98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:46:43 +0900 Subject: [PATCH 11/31] test(browser): define three-engine release 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 d6e4a2991faf633d89eda92a93e404073368c783 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:47:26 +0900 Subject: [PATCH 12/31] test(browser): execute SafeClipboard corpus in real engines --- tests/browser/specs/clipboard.browser.spec.ts | 171 ++++++++++++++++++ 1 file changed, 171 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..5950dde7 --- /dev/null +++ b/tests/browser/specs/clipboard.browser.spec.ts @@ -0,0 +1,171 @@ +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.GITHUB_SHA ?? null, + lockSha256: createHash('sha256').update(lockfile).digest('hex'), + representativeWordMillis, + observations, + }); + await writeFile( + resolve(evidenceDirectory, `${browserName}.json`), + `${JSON.stringify(evidence)}\n`, + 'utf8', + ); +}); From 124d44190dcf41ed11f748f9206090592956ddd7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:47:55 +0900 Subject: [PATCH 13/31] test(browser): fail closed on cross-engine divergence --- .../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..225f756d --- /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.GITHUB_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 1d4e12d95f3b0f8046cf1071f2c18adb1bea2b5c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:48:23 +0900 Subject: [PATCH 14/31] ci(browser): require Chromium Firefox WebKit clipboard evidence --- .github/workflows/cross-engine-clipboard.yml | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/cross-engine-clipboard.yml diff --git a/.github/workflows/cross-engine-clipboard.yml b/.github/workflows/cross-engine-clipboard.yml new file mode 100644 index 00000000..cda77883 --- /dev/null +++ b/.github/workflows/cross-engine-clipboard.yml @@ -0,0 +1,44 @@ +name: Cross-engine Clipboard + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PLAYWRIGHT_BROWSERS_PATH: ${{ runner.temp }}/inkspan-playwright-browsers + +jobs: + browser-release-evidence: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + 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 From b3b61e494b8a82cacc618e0c3f9c7bc91c16eae1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:49:14 +0900 Subject: [PATCH 15/31] fix(browser): bind evidence to exact source head --- tests/browser/specs/clipboard.browser.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/browser/specs/clipboard.browser.spec.ts b/tests/browser/specs/clipboard.browser.spec.ts index 5950dde7..47fb2d6b 100644 --- a/tests/browser/specs/clipboard.browser.spec.ts +++ b/tests/browser/specs/clipboard.browser.spec.ts @@ -158,7 +158,8 @@ test.afterAll(async ({ browser, browserName }) => { browserVersion: browser.version(), osPlatform: process.platform, runnerImage: process.env.ImageOS ?? null, - headSha: process.env.GITHUB_SHA ?? null, + headSha: + process.env.INKSPAN_EXPECTED_HEAD_SHA ?? process.env.GITHUB_SHA ?? null, lockSha256: createHash('sha256').update(lockfile).digest('hex'), representativeWordMillis, observations, From f9f8b81994b10029362c0407c5d663f608ddfb76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:49:47 +0900 Subject: [PATCH 16/31] fix(browser): verify exact PR head evidence --- tests/browser/specs/clipboard.consensus.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/specs/clipboard.consensus.spec.ts b/tests/browser/specs/clipboard.consensus.spec.ts index 225f756d..6b4a73ef 100644 --- a/tests/browser/specs/clipboard.consensus.spec.ts +++ b/tests/browser/specs/clipboard.consensus.spec.ts @@ -57,7 +57,7 @@ test('requires complete exact-head browser evidence and exact corpus consensus', } if (process.env.GITHUB_ACTIONS === 'true') { - expect(reference.headSha).toBe(process.env.GITHUB_SHA); + expect(reference.headSha).toBe(process.env.INKSPAN_EXPECTED_HEAD_SHA); expect(reference.runnerImage).not.toBeNull(); } From fb031ed28a23909c03be198f954b5c540056412d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:50:02 +0900 Subject: [PATCH 17/31] chore(browser): ignore local browser evidence artifacts --- .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 69f2253ed15d73f8c14a4e1746134b8d6b5a42dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:52:07 +0900 Subject: [PATCH 18/31] test(docs): bind cross-engine release-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 024ffcd27df79e4d08c4a8aade782c382bcb619a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:52:32 +0900 Subject: [PATCH 19/31] docs(browser): record cross-engine release-assurance evidence --- .../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 5d457a0236aca85dc13008a1dbd4ca54c8997952 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:54:46 +0900 Subject: [PATCH 20/31] docs(browser): bind active three-engine release gate --- 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 58fc1bf94e52f3396445a25033b874fa081508c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:55:14 +0900 Subject: [PATCH 21/31] docs(browser): define divergence recovery operations --- 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 913bad2976d230bdfc891f369da65ec50e7400ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:55:49 +0900 Subject: [PATCH 22/31] docs(browser): reconcile protected and active assurance status --- docs/DOCUMENTATION_FITNESS.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index ef5f49f5..3940fddb 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -43,17 +43,18 @@ Document fitness and implementation maturity are independent. A `present_current | 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, `docs/clipboard-security.md`, ADR 0003/0016 and protected SafeClipboard source | `present_current` | `implemented_on_protected_main`; protected main now 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 where dependency order requires it | 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 and Issue #74 | `present_current` | Identity-only routing capability is `planned`; strict current-schema parsing and host migration ownership remain authoritative | The architecture distinguishes bounded schema identification from host-owned migration execution without calling the planned API 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` behind PR #65 | Browser-realistic security assurance is a durable release decision even though its implementation remains dependency-ordered future work. | +| Envelope schema identity / migration routing | ADR 0015, PRD, TRD, DATA_MODEL and PR #84 | `present_current` | `implemented_on_active_pr`; strict current-schema parsing and host migration ownership remain protected-main authority until #84 integrates | The architecture distinguishes bounded schema identification from host-owned migration execution without calling the active API shipped. | +| Cross-engine browser-semantic release assurance | ADR 0016, `docs/doctoring/cross-engine-rich-clipboard-assurance.md`, TEST_STRATEGY, OPERABILITY, TRACEABILITY and PR #85 | `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. | @@ -63,7 +64,7 @@ Document fitness and implementation maturity are independent. A `present_current The canonical graph must retain durable product decisions from the project conversation only when they agree with live implementation or are explicitly labeled as target architecture. The reviewed baseline currently covers: - Markdown/HTML WYSIWYG authoring and deterministic source/document authority; -- strict link, image, clipboard, envelope and revision/evidence boundaries; +- strict link, image, protected-main SafeClipboard, envelope and revision/evidence boundaries; - bundled local/offline font licensing and air-gapped asset behavior; - deterministic email/document conversion boundaries and independently reusable Office rendering; - provider-neutral collaboration with host-owned Yjs provider, room, persistence and awareness authority; @@ -76,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 planned identity-only envelope routing, while migration execution remains host-owned; -- real Chromium/Firefox/WebKit differential evidence as a release gate for browser-semantic clipboard security rather than a jsdom conformance claim; and +- strict current-schema parsing plus active-PR identity-only envelope routing, while migration execution remains host-owned; +- 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. @@ -88,14 +89,14 @@ 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. Issue #74 remains `planned`: the identity-only migration-routing API must still be implemented test-first while the current parser remains strict. Its architectural decision is now present rather than hidden in issue prose. -2. Issue #66 remains `planned` behind PR #65: the dependency-locked Chromium/Firefox/WebKit differential suite must still be implemented before the rich-clipboard release line. Its release-assurance decision is now present rather than hidden in issue prose. -3. SafeClipboard (PR #65) remains `implemented_on_active_pr` until protected integration. Autosave lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are now `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 only when protected source, accepted decisions, or implementation maturity materially changes. +1. PR #84 implements Issue #74 on an active branch: the identity-only migration-routing API must still receive exact-head review and protected integration while the current parser remains strict. Until then it is `implemented_on_active_pr`, not shipped. +2. PR #85 implements Issue #66 on an active branch: 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. +3. SafeClipboard is now `implemented_on_protected_main`, as are autosave lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence. Canonical documents must not regress those capabilities to active-only wording. +4. The canonical documentation graph is already integrated on protected `main`; future reconciliation is required when protected source, accepted decisions, active implementation maturity, or release evidence 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. ## 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. 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`. SafeClipboard remains active-PR work. Envelope migration routing and cross-engine browser assurance are explicit Proposed ADR decisions while their implementations remain `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. 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`. Envelope identity routing and cross-engine browser assurance are `implemented_on_active_pr` and remain 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 **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 6dc6b8bccef12e6cd95a361611540155d6cd5d6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:57:27 +0900 Subject: [PATCH 23/31] ci(browser): self-validate three-engine clipboard gate --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2444529a..baa5d07a 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: ${{ runner.temp }}/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 2aeb86bf96e74e16297a4cbda960042a042c4782 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:57:43 +0900 Subject: [PATCH 24/31] ci(browser): keep one self-validating browser gate --- .github/workflows/cross-engine-clipboard.yml | 44 -------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/cross-engine-clipboard.yml diff --git a/.github/workflows/cross-engine-clipboard.yml b/.github/workflows/cross-engine-clipboard.yml deleted file mode 100644 index cda77883..00000000 --- a/.github/workflows/cross-engine-clipboard.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Cross-engine Clipboard - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - PLAYWRIGHT_BROWSERS_PATH: ${{ runner.temp }}/inkspan-playwright-browsers - -jobs: - browser-release-evidence: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - 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 From 161a1939183a11209c8490785b6ae46a8cf19fe1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:58:22 +0900 Subject: [PATCH 25/31] fix(ci): use runner-safe browser cache path --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index baa5d07a..01e77d2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 30 env: - PLAYWRIGHT_BROWSERS_PATH: ${{ runner.temp }}/inkspan-playwright-browsers + PLAYWRIGHT_BROWSERS_PATH: /tmp/inkspan-playwright-browsers steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: From 5b0c0afa52453523d3b41d4a327660c18f0a3a01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:59:28 +0900 Subject: [PATCH 26/31] docs(browser): reconcile SafeClipboard engine assurance --- 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 4e4a1a4adee55c03858035cd2b337f257358936d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:00:43 +0900 Subject: [PATCH 27/31] docs(browser): record cross-engine release gate --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1c20d0..d52e8070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,10 +31,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 d855a79cd9fc57c21e58fd3177a20f59d1bbcd4d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:33:03 +0900 Subject: [PATCH 28/31] test(browser): use a standards-valid CSS escape fixture --- src/crossEngineClipboardEvidence.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crossEngineClipboardEvidence.ts b/src/crossEngineClipboardEvidence.ts index 1e99562a..2767e666 100644 --- a/src/crossEngineClipboardEvidence.ts +++ b/src/crossEngineClipboardEvidence.ts @@ -75,7 +75,7 @@ const CORPUS: readonly CrossEngineClipboardCase[] = [ id: 'hidden-content-visibility-popover', riskFamily: 'hidden-content', sourceHtml: - '
onetwothreefour
', + '
onetwothreefour
', expectedSanitizedHtml: '
onefour
', expectedErrorCode: null, }, From 4a3d7fe026c219f02c4a62c4d7a0d163d3709651 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:33:19 +0900 Subject: [PATCH 29/31] 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 411840a33a1e5130cd95e10ff93608a21cc8545c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:36:15 +0900 Subject: [PATCH 30/31] docs(fitness): reconcile protected envelope routing --- docs/DOCUMENTATION_FITNESS.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index 3940fddb..fd86715b 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -38,12 +38,12 @@ 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 0017 records the protected security-disclosure lifecycle without duplicating its root policy | 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, `docs/clipboard-security.md`, ADR 0003/0016 and protected SafeClipboard source | `present_current` | `implemented_on_protected_main`; protected main now 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. | +| 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. | @@ -53,7 +53,7 @@ Document fitness and implementation maturity are independent. A `present_current | 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 and PR #84 | `present_current` | `implemented_on_active_pr`; strict current-schema parsing and host migration ownership remain protected-main authority until #84 integrates | The architecture distinguishes bounded schema identification from host-owned migration execution without calling the active API shipped. | +| 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 PR #85 | `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. | @@ -77,7 +77,7 @@ 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 active-PR identity-only envelope routing, while migration execution remains host-owned; +- 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. @@ -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: the identity-only migration-routing API must still receive exact-head review and protected integration while the current parser remains strict. Until then it is `implemented_on_active_pr`, not shipped. -2. PR #85 implements Issue #66 on an active branch: 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. -3. SafeClipboard is now `implemented_on_protected_main`, as are autosave lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence. Canonical documents must not regress those capabilities to active-only wording. -4. The canonical documentation graph is already integrated on protected `main`; future reconciliation is required when protected source, accepted decisions, active implementation maturity, or release evidence 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. PR #85 implements Issue #66 on an active branch: 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`. Envelope identity routing and cross-engine browser assurance are `implemented_on_active_pr` and remain non-authoritative until protected integration. 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 **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 731dc2bfdae3a12aba176a7fdd5bc732e41df8bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:40:13 +0900 Subject: [PATCH 31/31] docs(changelog): reconcile protected envelope routing --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d52e8070..7a9ad7e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim - Kept collaborative Yjs document content out of server markup until the host-owned client collaboration lifecycle is bound ### Added +- Added bounded `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` routing metadata plus the framework-independent `envelope-identity` package subpath so hosts can select explicit schema migrations without exposing document bodies, weakening the strict current-schema parser, or moving migration/persistence authority into Inkspan - Added one optional construction-time `onSnapshotChange` callback to the framework-free autosave queue and durable autosave session so hosts can observe saving, pending, blocked, recovery, idle, and shutdown state without polling or introducing a subscriber collection - Added privacy-minimized revision-scoped selection evidence through `getSelectionRevisionEvidence()`, binding frozen ProseMirror coordinates to the SHA-256 strong revision of the exact same immutable editor state before asynchronous hashing begins - Added privacy-minimized document transition evidence for validated previous and resulting canonical revisions through the framework-independent `revision-evidence` subpath, with object/JSON and strict UTF-8 entry points, deterministic previous-then-resulting SHA-256 derivation, frozen revision-only results, and no document body, actor, tenant, time, authorization, signature, transport, model, or durable-write claim