From 4338a9a99535eb2f2e41c442bfbc3bafe79bebdd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:01:50 +0900 Subject: [PATCH 01/43] test(envelope): define identity-only migration routing contract --- src/documentEnvelopeIdentity.test.ts | 176 +++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/documentEnvelopeIdentity.test.ts diff --git a/src/documentEnvelopeIdentity.test.ts b/src/documentEnvelopeIdentity.test.ts new file mode 100644 index 00000000..329e8a49 --- /dev/null +++ b/src/documentEnvelopeIdentity.test.ts @@ -0,0 +1,176 @@ +import { describe, expect, it } from 'vitest'; +import { + DOCUMENT_ENVELOPE_SCHEMA_ID, + DocumentEnvelopeError, + inspectDocumentEnvelopeIdentity, + inspectDocumentEnvelopeIdentityBytes, + parseDocumentEnvelope, +} from './documentEnvelope.js'; + +const legacyEnvelope = { + schemaId: 'https://inkspan.io/schemas/document-envelope/v0', + schemaVersion: 7, + documentJson: { legacyShape: ['tenant-secret-text'] }, +}; + +describe('document envelope identity inspection', () => { + it('identifies unsupported envelopes without weakening strict parsing', () => { + const identity = inspectDocumentEnvelopeIdentity(legacyEnvelope); + + expect(identity).toEqual({ + schemaId: legacyEnvelope.schemaId, + schemaVersion: legacyEnvelope.schemaVersion, + }); + expect(Object.isFrozen(identity)).toBe(true); + expect(() => parseDocumentEnvelope(legacyEnvelope)).toThrow( + 'schema is unsupported', + ); + expect(JSON.stringify(identity)).not.toContain('tenant-secret-text'); + }); + + it('accepts complete JSON text with future top-level data but returns routing metadata only', () => { + const source = JSON.stringify({ + ...legacyEnvelope, + futureRoutingHint: { ignored: true }, + }); + + expect(inspectDocumentEnvelopeIdentity(source)).toEqual({ + schemaId: legacyEnvelope.schemaId, + schemaVersion: 7, + }); + }); + + it('accepts strict UTF-8 bytes including Buffer subclasses', () => { + const bytes = Buffer.from(JSON.stringify(legacyEnvelope), 'utf8'); + + expect(inspectDocumentEnvelopeIdentityBytes(bytes)).toEqual({ + schemaId: legacyEnvelope.schemaId, + schemaVersion: 7, + }); + }); + + it.each([false, true, 1.5, 0, -1, Number.MAX_SAFE_INTEGER + 1])( + 'rejects invalid schemaVersion %s', + (schemaVersion) => { + expect(() => + inspectDocumentEnvelopeIdentity({ + ...legacyEnvelope, + schemaVersion, + }), + ).toThrow(DocumentEnvelopeError); + }, + ); + + it('requires schemaId and documentJson but does not require the current document schema', () => { + expect(() => + inspectDocumentEnvelopeIdentity({ + schemaVersion: 1, + documentJson: {}, + }), + ).toThrow(DocumentEnvelopeError); + expect(() => + inspectDocumentEnvelopeIdentity({ + schemaId: DOCUMENT_ENVELOPE_SCHEMA_ID, + schemaVersion: 1, + }), + ).toThrow(DocumentEnvelopeError); + + expect( + inspectDocumentEnvelopeIdentity({ + schemaId: DOCUMENT_ENVELOPE_SCHEMA_ID, + schemaVersion: 1, + documentJson: { not: 'a ProseMirror doc root' }, + }), + ).toEqual({ + schemaId: DOCUMENT_ENVELOPE_SCHEMA_ID, + schemaVersion: 1, + }); + }); + + it('rejects duplicate names and malformed JSON without reflecting source content', () => { + const duplicate = + '{"schemaId":"first-secret","schemaId":"second-secret","schemaVersion":1,"documentJson":{}}'; + const malformed = '{"schemaId":"tenant-secret"'; + + for (const source of [duplicate, malformed]) { + try { + inspectDocumentEnvelopeIdentity(source); + throw new Error('expected identity inspection to fail'); + } catch (error) { + expect(error).toBeInstanceOf(DocumentEnvelopeError); + expect(String(error)).not.toContain('first-secret'); + expect(String(error)).not.toContain('second-secret'); + expect(String(error)).not.toContain('tenant-secret'); + } + } + }); + + it('does not invoke envelope accessors and redacts hostile reflection failures', () => { + let getterCalled = false; + const accessorEnvelope: Record = { + schemaId: 'legacy', + schemaVersion: 2, + documentJson: {}, + }; + Object.defineProperty(accessorEnvelope, 'futureField', { + enumerable: true, + get() { + getterCalled = true; + throw new Error('tenant-secret'); + }, + }); + + expect(() => inspectDocumentEnvelopeIdentity(accessorEnvelope)).toThrow( + DocumentEnvelopeError, + ); + expect(getterCalled).toBe(false); + + const hostile = new Proxy(legacyEnvelope, { + ownKeys() { + throw new Error('tenant-secret'); + }, + }); + try { + inspectDocumentEnvelopeIdentity(hostile); + throw new Error('expected hostile proxy to fail'); + } catch (error) { + expect(error).toBeInstanceOf(DocumentEnvelopeError); + expect(String(error)).not.toContain('tenant-secret'); + } + }); + + it('reuses document resource ceilings without interpreting current-schema semantics', () => { + expect(() => + inspectDocumentEnvelopeIdentity(legacyEnvelope, { + maxJsonValues: 1, + }), + ).toThrow('JSON value count'); + + expect(() => + inspectDocumentEnvelopeIdentity(legacyEnvelope, { + maxStringCodeUnits: 5, + }), + ).toThrow('strings exceed'); + + expect(() => + inspectDocumentEnvelopeIdentity( + { + ...legacyEnvelope, + documentJson: { nested: { too: { deep: true } } }, + }, + { maxNestingDepth: 1 }, + ), + ).toThrow('nesting'); + }); + + it('rejects BOM and invalid UTF-8 bytes', () => { + expect(() => + inspectDocumentEnvelopeIdentityBytes( + new Uint8Array([0xef, 0xbb, 0xbf, 0x7b, 0x7d]), + ), + ).toThrow('byte-order mark'); + expect(() => + inspectDocumentEnvelopeIdentityBytes(new Uint8Array([0xff])), + ).toThrow('valid UTF-8'); + }); +}); From 0b73319577e8c7884815684db6f889d2dd152715 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:02:55 +0900 Subject: [PATCH 02/43] feat(envelope): add bounded identity-only migration routing --- src/documentEnvelopeIdentity.ts | 439 ++++++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 src/documentEnvelopeIdentity.ts diff --git a/src/documentEnvelopeIdentity.ts b/src/documentEnvelopeIdentity.ts new file mode 100644 index 00000000..ff6c7b47 --- /dev/null +++ b/src/documentEnvelopeIdentity.ts @@ -0,0 +1,439 @@ +import { inspectJsonText } from './jsonObjectNameScanner.js'; +import { + DEFAULT_DOCUMENT_ENVELOPE_LIMITS, + DocumentEnvelopeError, + type DocumentEnvelopeLimits, +} from './documentEnvelope.js'; + +interface ResolvedIdentityLimits { + readonly maxUtf8Bytes: number; + readonly maxJsonTextCodeUnits: number; + readonly maxJsonValues: number; + readonly maxStringCodeUnits: number; + readonly maxNestingDepth: number; +} + +const LIMIT_FIELDS = new Set([ + 'maxUtf8Bytes', + 'maxJsonTextCodeUnits', + 'maxJsonValues', + 'maxStringCodeUnits', + 'maxNestingDepth', +]); +const UTF8_BYTE_ORDER_MARK = 0xefbbbf; +const TYPED_ARRAY_TAG_GETTER = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(Uint8Array.prototype), + Symbol.toStringTag, +)!.get!; +const REDACTED_INSPECTION_ERROR = + 'Document envelope identity could not be inspected safely'; + +/** + * Routing metadata extracted from a complete versioned document envelope. + * + * This value intentionally contains no document body or durable-write claim. + */ +export interface CwlEditorDocumentEnvelopeIdentity { + /** Schema identifier supplied by the envelope for host-owned migration routing. */ + readonly schemaId: string; + /** Positive safe-integer schema version supplied by the envelope. */ + readonly schemaVersion: number; +} + +/** + * Inspect only the schema identity of a complete document envelope. + * + * The inspector preserves the envelope resource ceilings and hostile-object + * defenses but deliberately does not require the current TipTap/ProseMirror + * document schema. Hosts may therefore select an explicit migration function + * without weakening {@link parseDocumentEnvelope} into a permissive parser. + */ +export function inspectDocumentEnvelopeIdentity( + source: unknown, + limits?: DocumentEnvelopeLimits, +): Readonly { + return withRedactedIdentityErrors(() => + inspectIdentityWithLimits(source, resolveIdentityLimits(limits)), + ); +} + +/** + * Strictly decode UTF-8 bytes and inspect only document-envelope schema identity. + * + * Byte input is copied, bounded before decoding, rejects a UTF-8 BOM, and uses + * fatal UTF-8 decoding before delegating to the complete-input identity parser. + */ +export function inspectDocumentEnvelopeIdentityBytes( + source: unknown, + limits?: DocumentEnvelopeLimits, +): Readonly { + return withRedactedIdentityErrors(() => { + const resolvedLimits = resolveIdentityLimits(limits); + if (!hasUint8ArrayBrand(source)) { + throw new DocumentEnvelopeError( + 'Document envelope identity bytes must be a Uint8Array', + ); + } + if (source.byteLength > resolvedLimits.maxUtf8Bytes) { + throw new DocumentEnvelopeError( + 'Document envelope identity UTF-8 bytes exceed the supported length', + ); + } + + const bytes = new Uint8Array(source.byteLength); + bytes.set(source); + const prefix = + bytes.length < 3 + ? -1 + : (bytes[0] << 16) | (bytes[1] << 8) | bytes[2]; + if (prefix === UTF8_BYTE_ORDER_MARK) { + throw new DocumentEnvelopeError( + 'Document envelope identity UTF-8 bytes must not include a byte-order mark', + ); + } + + let decoded: string; + try { + decoded = new TextDecoder('utf-8', { + fatal: true, + ignoreBOM: true, + }).decode(bytes); + } catch { + throw new DocumentEnvelopeError( + 'Document envelope identity bytes must contain valid UTF-8', + ); + } + + return inspectIdentityWithLimits(decoded, resolvedLimits); + }); +} + +function inspectIdentityWithLimits( + source: unknown, + limits: ResolvedIdentityLimits, +): Readonly { + let value: unknown = source; + if (typeof source === 'string') { + if (source.length > limits.maxJsonTextCodeUnits) { + throw new DocumentEnvelopeError( + 'Document envelope identity JSON text exceeds the supported length', + ); + } + const inspection = inspectJsonText(source, { + maxValues: Number.MAX_SAFE_INTEGER, + maxDepth: Number.MAX_SAFE_INTEGER, + }); + if (inspection === 'duplicate-object-name') { + throw new DocumentEnvelopeError( + 'Document envelope identity JSON must not contain duplicate object names', + ); + } + if ( + inspection === 'value-count-limit' || + inspection === 'nesting-depth-limit' + ) { + throw new DocumentEnvelopeError( + 'Document envelope identity JSON could not be inspected within limits', + ); + } + try { + value = JSON.parse(source) as unknown; + } catch { + throw new DocumentEnvelopeError( + 'Document envelope identity must contain valid JSON', + ); + } + } + + if (!isPlainObject(value)) { + throw new DocumentEnvelopeError( + 'Document envelope identity must be an object', + ); + } + + const entries = readDataEntries( + value, + Math.min(Number.MAX_SAFE_INTEGER, limits.maxJsonValues + 3), + limits.maxStringCodeUnits, + 'Document envelope identity contains too many top-level fields', + ); + const fields = new Map(entries); + if ( + !fields.has('schemaId') || + !fields.has('schemaVersion') || + !fields.has('documentJson') + ) { + throw new DocumentEnvelopeError( + 'Document envelope identity requires schemaId, schemaVersion, and documentJson', + ); + } + + const schemaId = fields.get('schemaId'); + if (typeof schemaId !== 'string') { + throw new DocumentEnvelopeError( + 'Document envelope schemaId must be a string', + ); + } + assertStringWithinLimit(schemaId, limits.maxStringCodeUnits); + + const schemaVersion = fields.get('schemaVersion'); + if ( + typeof schemaVersion !== 'number' || + !Number.isSafeInteger(schemaVersion) || + schemaVersion < 1 + ) { + throw new DocumentEnvelopeError( + 'Document envelope schemaVersion must be a positive safe integer', + ); + } + + const state: ValidationState = { + valueCount: 0, + limits, + active: new WeakSet(), + }; + validateJsonValue(fields.get('documentJson'), 0, state); + + return Object.freeze({ schemaId, schemaVersion }); +} + +interface ValidationState { + valueCount: number; + readonly limits: ResolvedIdentityLimits; + readonly active: WeakSet; +} + +function validateJsonValue( + value: unknown, + depth: number, + state: ValidationState, +): void { + state.valueCount += 1; + if (state.valueCount > state.limits.maxJsonValues) { + throw new DocumentEnvelopeError( + 'Document envelope exceeds the supported JSON value count', + ); + } + if (depth > state.limits.maxNestingDepth) { + throw new DocumentEnvelopeError( + 'Document envelope exceeds the supported document nesting depth', + ); + } + if (value === null || typeof value === 'boolean') return; + if (typeof value === 'string') { + assertStringWithinLimit(value, state.limits.maxStringCodeUnits); + return; + } + if (typeof value === 'number') { + if (!Number.isFinite(value)) { + throw new DocumentEnvelopeError( + 'Document envelope must contain finite numbers', + ); + } + return; + } + if (typeof value !== 'object') { + throw new DocumentEnvelopeError( + 'Document envelope must contain JSON-compatible values', + ); + } + if (state.active.has(value)) { + throw new DocumentEnvelopeError( + 'Document envelope contains a cyclic reference', + ); + } + + state.active.add(value); + try { + if (Array.isArray(value)) { + validateJsonArray(value, depth, state); + return; + } + if (!isPlainObject(value)) { + throw new DocumentEnvelopeError( + 'Document envelope must contain JSON-compatible values', + ); + } + const entries = readDataEntries( + value, + state.limits.maxJsonValues, + state.limits.maxStringCodeUnits, + 'Document envelope exceeds the supported JSON value count', + ); + for (const [, child] of entries) { + validateJsonValue(child, depth + 1, state); + } + } finally { + state.active.delete(value); + } +} + +function validateJsonArray( + value: unknown[], + depth: number, + state: ValidationState, +): void { + const lengthDescriptor = Object.getOwnPropertyDescriptor( + value, + 'length', + ) as PropertyDescriptor; + const length = lengthDescriptor.value as number; + const keys = Reflect.ownKeys(value); + if (keys.length !== length + 1) { + throw new DocumentEnvelopeError( + 'Document envelope arrays must contain only dense JSON elements', + ); + } + for (let index = 0; index < length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); + if ( + descriptor === undefined || + !descriptor.enumerable || + !('value' in descriptor) + ) { + throw new DocumentEnvelopeError( + 'Document envelope arrays must contain only dense JSON elements', + ); + } + validateJsonValue(descriptor.value, depth + 1, state); + } +} + +function resolveIdentityLimits( + overrides: DocumentEnvelopeLimits | undefined, +): ResolvedIdentityLimits { + if (overrides === undefined) { + return DEFAULT_DOCUMENT_ENVELOPE_LIMITS; + } + if (!isPlainObject(overrides)) { + throw new DocumentEnvelopeError( + 'Document envelope limits must be a plain configuration object', + ); + } + const entries = readDataEntries( + overrides, + LIMIT_FIELDS.size, + Number.MAX_SAFE_INTEGER, + 'Document envelope limits contain unsupported fields', + ); + const values = new Map(entries); + for (const [field] of entries) { + if (!LIMIT_FIELDS.has(field)) { + throw new DocumentEnvelopeError( + 'Document envelope limits contain unsupported fields', + ); + } + } + + return Object.freeze({ + maxUtf8Bytes: readPositiveSafeIntegerLimit( + values, + 'maxUtf8Bytes', + DEFAULT_DOCUMENT_ENVELOPE_LIMITS.maxUtf8Bytes, + ), + maxJsonTextCodeUnits: readPositiveSafeIntegerLimit( + values, + 'maxJsonTextCodeUnits', + DEFAULT_DOCUMENT_ENVELOPE_LIMITS.maxJsonTextCodeUnits, + ), + maxJsonValues: readPositiveSafeIntegerLimit( + values, + 'maxJsonValues', + DEFAULT_DOCUMENT_ENVELOPE_LIMITS.maxJsonValues, + ), + maxStringCodeUnits: readPositiveSafeIntegerLimit( + values, + 'maxStringCodeUnits', + DEFAULT_DOCUMENT_ENVELOPE_LIMITS.maxStringCodeUnits, + ), + maxNestingDepth: readPositiveSafeIntegerLimit( + values, + 'maxNestingDepth', + DEFAULT_DOCUMENT_ENVELOPE_LIMITS.maxNestingDepth, + ), + }); +} + +function readPositiveSafeIntegerLimit( + values: Map, + field: string, + fallback: number, +): number { + if (!values.has(field) || values.get(field) === undefined) return fallback; + const value = values.get(field); + if ( + typeof value !== 'number' || + !Number.isSafeInteger(value) || + value < 1 + ) { + throw new DocumentEnvelopeError( + 'Document envelope limits must be positive safe integers', + ); + } + return value; +} + +function readDataEntries( + value: Record, + maximumEntries: number, + maximumStringCodeUnits: number, + limitErrorMessage: string, +): Array<[string, unknown]> { + const keys = Reflect.ownKeys(value); + if (keys.length > maximumEntries) { + throw new DocumentEnvelopeError(limitErrorMessage); + } + const entries: Array<[string, unknown]> = []; + for (const key of keys) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if ( + typeof key !== 'string' || + descriptor === undefined || + !descriptor.enumerable || + !('value' in descriptor) + ) { + throw new DocumentEnvelopeError( + 'Document envelope objects must contain enumerable JSON data fields', + ); + } + assertStringWithinLimit(key, maximumStringCodeUnits); + entries.push([key, descriptor.value]); + } + return entries; +} + +function assertStringWithinLimit( + value: string, + maximumStringCodeUnits: number, +): void { + if (value.length > maximumStringCodeUnits) { + throw new DocumentEnvelopeError( + 'Document envelope strings exceed the supported length', + ); + } +} + +function hasUint8ArrayBrand(value: unknown): value is Uint8Array { + try { + return TYPED_ARRAY_TAG_GETTER.call(value) === 'Uint8Array'; + } catch { + return false; + } +} + +function isPlainObject(value: unknown): value is Record { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function withRedactedIdentityErrors(operation: () => T): T { + try { + return operation(); + } catch (error) { + if (error instanceof DocumentEnvelopeError) throw error; + throw new DocumentEnvelopeError(REDACTED_INSPECTION_ERROR); + } +} From cecc7feb76fc80674cba3ad22346f3b478200060 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:03:18 +0900 Subject: [PATCH 03/43] feat(envelope): export identity-only routing API --- src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.ts b/src/index.ts index 4a863c1e..85db7240 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,6 +50,11 @@ export type { CwlEditorDocumentEnvelope, DocumentEnvelopeLimits, } from './documentEnvelope.js'; +export { + inspectDocumentEnvelopeIdentity, + inspectDocumentEnvelopeIdentityBytes, +} from './documentEnvelopeIdentity.js'; +export type { CwlEditorDocumentEnvelopeIdentity } from './documentEnvelopeIdentity.js'; export { encodeDocumentEnvelope, serializeDocumentEnvelope, From 598db72f258e5e14a97450a9a56205e41db5a10b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:03:45 +0900 Subject: [PATCH 04/43] test(envelope): bind identity tests to public routing module --- src/documentEnvelopeIdentity.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/documentEnvelopeIdentity.test.ts b/src/documentEnvelopeIdentity.test.ts index 329e8a49..25d50239 100644 --- a/src/documentEnvelopeIdentity.test.ts +++ b/src/documentEnvelopeIdentity.test.ts @@ -2,10 +2,12 @@ import { describe, expect, it } from 'vitest'; import { DOCUMENT_ENVELOPE_SCHEMA_ID, DocumentEnvelopeError, - inspectDocumentEnvelopeIdentity, - inspectDocumentEnvelopeIdentityBytes, parseDocumentEnvelope, } from './documentEnvelope.js'; +import { + inspectDocumentEnvelopeIdentity, + inspectDocumentEnvelopeIdentityBytes, +} from './documentEnvelopeIdentity.js'; const legacyEnvelope = { schemaId: 'https://inkspan.io/schemas/document-envelope/v0', From df7efa19be54830765d6747eb061551d8706080b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:04:44 +0900 Subject: [PATCH 05/43] test(envelope): verify identity routing public exports --- src/index.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 96efa6dc..506096ad 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -18,6 +18,11 @@ describe('package entry point', () => { expect(typeof api.Toolbar).toBe('function'); }); + it('re-exports the persistence routing surface', () => { + expect(typeof api.inspectDocumentEnvelopeIdentity).toBe('function'); + expect(typeof api.inspectDocumentEnvelopeIdentityBytes).toBe('function'); + }); + it('re-exports the headless extensions', () => { expect(api.Base64Image).toBeTruthy(); expect(api.base64ImagePluginKey).toBeTruthy(); From e9c6405f5168cf1e704e82dcc31b7d928c147dbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:05:24 +0900 Subject: [PATCH 06/43] feat(envelope): add framework-independent identity subpath --- src/envelope-identity/index.ts | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/envelope-identity/index.ts diff --git a/src/envelope-identity/index.ts b/src/envelope-identity/index.ts new file mode 100644 index 00000000..6a812928 --- /dev/null +++ b/src/envelope-identity/index.ts @@ -0,0 +1,56 @@ +import { + inspectDocumentEnvelopeIdentity as inspectIdentityInternal, + inspectDocumentEnvelopeIdentityBytes as inspectIdentityBytesInternal, +} from '../documentEnvelopeIdentity.js'; + +/** + * Framework-independent document-envelope identity routing entrypoint. + * + * This subpath intentionally excludes React, TipTap UI, ProseMirror view, Yjs, + * network clients, persistence adapters, credentials, and model SDKs. It returns + * only validated schema identity so a host can select its own migration path. + */ + +/** Optional resource ceilings shared with Inkspan's strict envelope boundary. */ +export interface DocumentEnvelopeLimits { + /** Maximum raw UTF-8 byte length before decoding. */ + readonly maxUtf8Bytes?: number; + /** Maximum raw JSON-text length measured in JavaScript UTF-16 code units. */ + readonly maxJsonTextCodeUnits?: number; + /** Maximum total scalar and container values inside `documentJson`. */ + readonly maxJsonValues?: number; + /** Maximum decoded string value or object-name length. */ + readonly maxStringCodeUnits?: number; + /** Maximum nested object/array depth below `documentJson`. */ + readonly maxNestingDepth?: number; +} + +/** Frozen schema identity used only for host-owned migration routing. */ +export interface CwlEditorDocumentEnvelopeIdentity { + /** Schema identifier supplied by the complete envelope. */ + readonly schemaId: string; + /** Positive safe-integer schema version supplied by the complete envelope. */ + readonly schemaVersion: number; +} + +/** Inspect an envelope object or complete JSON text without validating current document semantics. */ +export function inspectDocumentEnvelopeIdentity( + source: unknown, + limits?: DocumentEnvelopeLimits, +): Readonly { + return inspectIdentityInternal( + source, + limits as Parameters[1], + ) as Readonly; +} + +/** Inspect strict UTF-8 envelope bytes without validating current document semantics. */ +export function inspectDocumentEnvelopeIdentityBytes( + source: unknown, + limits?: DocumentEnvelopeLimits, +): Readonly { + return inspectIdentityBytesInternal( + source, + limits as Parameters[1], + ) as Readonly; +} From 3c0015daf01eb52c5dd35e2871f82f08c459fae2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:05:32 +0900 Subject: [PATCH 07/43] build(envelope): bundle framework-independent identity routing --- vite.envelope-identity.config.ts | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 vite.envelope-identity.config.ts diff --git a/vite.envelope-identity.config.ts b/vite.envelope-identity.config.ts new file mode 100644 index 00000000..884c3d31 --- /dev/null +++ b/vite.envelope-identity.config.ts @@ -0,0 +1,34 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +// Framework-independent envelope-identity build: ZERO React, TipTap UI, +// ProseMirror view, Yjs, network, credential, persistence, or model imports. +export default defineConfig({ + plugins: [ + dts({ + include: [ + 'src/envelope-identity', + 'src/documentEnvelopeIdentity.ts', + 'src/documentEnvelope.ts', + 'src/jsonObjectNameScanner.ts', + ], + exclude: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'src/**/*.spec.ts'], + rollupTypes: false, + entryRoot: 'src', + }), + ], + build: { + emptyOutDir: false, + lib: { + entry: resolve(__dirname, 'src/envelope-identity/index.ts'), + name: 'InkspanEnvelopeIdentity', + fileName: (format) => + format === 'es' + ? 'cwl-envelope-identity.js' + : 'cwl-envelope-identity.cjs', + formats: ['es', 'cjs'], + }, + sourcemap: true, + }, +}); From 4b48b6c3e89f8eb962d0a3c205982fbe74443d0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:06:13 +0900 Subject: [PATCH 08/43] build(envelope): publish framework-independent identity subpath --- package.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 269c5e83..f0287f2e 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,11 @@ "import": "./dist/cwl-converter.js", "require": "./dist/cwl-converter.cjs" }, + "./envelope-identity": { + "types": "./dist/envelope-identity/index.d.ts", + "import": "./dist/cwl-envelope-identity.js", + "require": "./dist/cwl-envelope-identity.cjs" + }, "./revision-evidence": { "types": "./dist/revision-evidence/index.d.ts", "import": "./dist/cwl-revision-evidence.js", @@ -84,7 +89,7 @@ ], "scripts": { "dev": "vite", - "build": "tsc --noEmit && vite build && vite build --config vite.collaboration.config.ts && vite build --config vite.converter.config.ts && vite build --config vite.revision-evidence.config.ts && vite build --config vite.autosave.config.ts && node ./scripts/copy-styles.mjs", + "build": "tsc --noEmit && vite build && vite build --config vite.collaboration.config.ts && vite build --config vite.converter.config.ts && vite build --config vite.envelope-identity.config.ts && vite build --config vite.revision-evidence.config.ts && vite build --config vite.autosave.config.ts && node ./scripts/copy-styles.mjs", "build:demo": "vite build --config vite.demo.config.ts", "fonts": "node ./scripts/fetch-fonts.mjs", "preview": "vite preview", @@ -93,7 +98,7 @@ "test:watch": "vitest", "coverage": "vitest run --coverage", "test:package-config": "node --test ./scripts/revision-evidence-consumer-config.test.mjs ./scripts/release-metadata.test.mjs", - "verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs" + "verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", From adad24ae566d1a998a3c705bfd37142ecd78faa8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:06:34 +0900 Subject: [PATCH 09/43] test(envelope): verify framework-independent packed consumers --- ...amework-free-envelope-identity-package.mjs | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 scripts/verify-framework-free-envelope-identity-package.mjs diff --git a/scripts/verify-framework-free-envelope-identity-package.mjs b/scripts/verify-framework-free-envelope-identity-package.mjs new file mode 100644 index 00000000..928a3252 --- /dev/null +++ b/scripts/verify-framework-free-envelope-identity-package.mjs @@ -0,0 +1,178 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const packageJson = JSON.parse( + readFileSync(join(repositoryRoot, 'package.json'), 'utf8'), +); +const verificationRoot = mkdtempSync( + join(tmpdir(), 'inkspan-envelope-identity-'), +); +const extractionDirectory = join(verificationRoot, 'extracted'); +const consumerDirectory = join(verificationRoot, 'consumer'); +const packageDirectory = join( + consumerDirectory, + 'node_modules', + ...packageJson.name.split('/'), +); + +function run(command, argumentsList, cwd = repositoryRoot) { + return execFileSync(command, argumentsList, { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); +} + +function preparePackage() { + mkdirSync(extractionDirectory, { recursive: true }); + mkdirSync(dirname(packageDirectory), { recursive: true }); + const packOutput = run('npm', [ + 'pack', + '--json', + '--ignore-scripts', + '--pack-destination', + verificationRoot, + ]); + const packResult = JSON.parse(packOutput)[0]; + assert.equal(packResult.name, packageJson.name); + assert.equal(packResult.version, packageJson.version); + const tarballPath = join(verificationRoot, packResult.filename); + assert.ok(existsSync(tarballPath)); + run('tar', ['-xzf', tarballPath, '-C', extractionDirectory]); + renameSync(join(extractionDirectory, 'package'), packageDirectory); + writeFileSync( + join(consumerDirectory, 'package.json'), + '{"name":"inkspan-envelope-identity-consumer","private":true,"type":"module"}\n', + 'utf8', + ); +} + +function verifyRuntimeConsumers() { + const esmPath = join(consumerDirectory, 'consumer.mjs'); + writeFileSync( + esmPath, + `import assert from 'node:assert/strict'; +import { fileURLToPath } from 'node:url'; +import { + inspectDocumentEnvelopeIdentity, + inspectDocumentEnvelopeIdentityBytes, +} from '${packageJson.name}/envelope-identity'; +const resolved = fileURLToPath(import.meta.resolve('${packageJson.name}/envelope-identity')); +assert.ok(resolved.endsWith('/dist/cwl-envelope-identity.js')); +const source = { + schemaId: 'https://inkspan.io/schemas/document-envelope/v9', + schemaVersion: 9, + documentJson: { legacy: 'private-body' }, +}; +const objectIdentity = inspectDocumentEnvelopeIdentity(source); +const byteIdentity = inspectDocumentEnvelopeIdentityBytes( + new TextEncoder().encode(JSON.stringify(source)), +); +assert.deepEqual(objectIdentity, { schemaId: source.schemaId, schemaVersion: 9 }); +assert.deepEqual(byteIdentity, objectIdentity); +assert.equal(Object.isFrozen(objectIdentity), true); +assert.equal(JSON.stringify(objectIdentity).includes('private-body'), false); +`, + 'utf8', + ); + + const cjsPath = join(consumerDirectory, 'consumer.cjs'); + writeFileSync( + cjsPath, + `const assert = require('node:assert/strict'); +const identity = require('${packageJson.name}/envelope-identity'); +assert.ok(require.resolve('${packageJson.name}/envelope-identity').endsWith('/dist/cwl-envelope-identity.cjs')); +const result = identity.inspectDocumentEnvelopeIdentity({ + schemaId: 'legacy-schema', + schemaVersion: 2, + documentJson: { future: true }, +}); +assert.deepEqual(result, { schemaId: 'legacy-schema', schemaVersion: 2 }); +`, + 'utf8', + ); + + run(process.execPath, [esmPath], consumerDirectory); + run(process.execPath, [cjsPath], consumerDirectory); +} + +function verifyDeclarationConsumer() { + const sourcePath = join(consumerDirectory, 'consumer.ts'); + const configurationPath = join(consumerDirectory, 'tsconfig.json'); + writeFileSync( + sourcePath, + `import { + inspectDocumentEnvelopeIdentity, + inspectDocumentEnvelopeIdentityBytes, + type CwlEditorDocumentEnvelopeIdentity, + type DocumentEnvelopeLimits, +} from '${packageJson.name}/envelope-identity'; +const limits: DocumentEnvelopeLimits = { maxJsonValues: 100 }; +const objectIdentity: Readonly = + inspectDocumentEnvelopeIdentity({ + schemaId: 'legacy', + schemaVersion: 3, + documentJson: {}, + }, limits); +const byteIdentity: Readonly = + inspectDocumentEnvelopeIdentityBytes(new Uint8Array(), limits); +void objectIdentity; +void byteIdentity; +`, + 'utf8', + ); + writeFileSync( + configurationPath, + `${JSON.stringify( + { + compilerOptions: { + noEmit: true, + strict: true, + skipLibCheck: false, + module: 'NodeNext', + moduleResolution: 'NodeNext', + target: 'ES2022', + lib: ['ES2022'], + types: [], + }, + files: ['./consumer.ts'], + }, + null, + 2, + )}\n`, + 'utf8', + ); + const compilerPath = join( + repositoryRoot, + 'node_modules', + 'typescript', + 'bin', + 'tsc', + ); + assert.ok(existsSync(compilerPath)); + run(process.execPath, [compilerPath, '--project', configurationPath]); +} + +try { + preparePackage(); + verifyRuntimeConsumers(); + verifyDeclarationConsumer(); + console.log( + `Verified framework-free packed ${packageJson.name}/envelope-identity through ESM, CommonJS, and strict TypeScript.`, + ); +} finally { + rmSync(verificationRoot, { recursive: true, force: true }); +} From 3ddde83beff9c696320277e8a49afe647d265d79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:08:11 +0900 Subject: [PATCH 10/43] docs(envelope): doctor identity-only migration routing --- docs/doctoring/envelope-identity-routing.md | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/doctoring/envelope-identity-routing.md diff --git a/docs/doctoring/envelope-identity-routing.md b/docs/doctoring/envelope-identity-routing.md new file mode 100644 index 00000000..4c43ddbf --- /dev/null +++ b/docs/doctoring/envelope-identity-routing.md @@ -0,0 +1,47 @@ +# Envelope identity-only migration routing + +Status: Implemented on active PR +Decision: ADR 0015 +Tracking: Issue #74 + +## Problem boundary + +Inkspan's strict `parseDocumentEnvelope()` correctly rejects unknown schema identifiers and versions. That protects current document semantics, but a host still needs a safe way to determine which version-specific migration should receive a complete legacy or future envelope. Asking every host to parse untrusted envelope JSON independently would duplicate duplicate-name, UTF-8, resource-limit, accessor, proxy/reflection, and error-redaction policy at the compatibility boundary. + +## Implemented decision + +The active implementation adds `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` plus the framework-independent `@contextualwisdomlab/cwl-editor/envelope-identity` package subpath. Successful inspection returns only a frozen `schemaId` string and positive safe-integer `schemaVersion`. + +Inspection requires a complete syntactically valid envelope with `schemaId`, `schemaVersion`, and `documentJson`. It applies the established byte, text, value-count, string-length, nesting, duplicate-name, strict UTF-8, BOM, plain-object, dense-array, descriptor, accessor, proxy/reflection, and public-error boundaries where applicable. It deliberately does **not** require an unsupported `documentJson` to satisfy the current TipTap/ProseMirror schema before returning routing metadata. + +`parseDocumentEnvelope()` remains strict and current-schema-only. The host owns migration selection and execution, schema registry, authorization, tenant isolation, persistence transaction, encryption, retention, durable audit, rollback, and recovery. A migrated value must pass the ordinary current-schema parser before it becomes canonical Inkspan document state. + +## Security and privacy + +The identity result contains no document body, selected text, links, inline image data, source-controlled future fields, actor, tenant, credential, transport state, signature, authorization decision, migration-success claim, or durable-write claim. Schema identity can still be application metadata and should remain purpose-bound rather than a public high-cardinality telemetry label. + +Direct JavaScript objects are inspected through descriptors without invoking getters. Hostile proxy/reflection failures and unexpected internal inspection failures are converted to stable `DocumentEnvelopeError` messages that do not echo source strings or private exception causes. Byte inspection copies bounded `Uint8Array` input, rejects a leading UTF-8 BOM, and uses fatal UTF-8 decoding. + +## Failure and recovery + +Malformed JSON/UTF-8, duplicate object names, missing required envelope members, invalid identity scalar types, non-positive/unsafe versions, non-JSON body values, cycles, sparse/decorated/accessor arrays, accessors or symbols, hostile reflection, and resource-limit violations fail closed without partial identity output. An unknown but structurally valid identity is a routing result rather than proof that Inkspan understands or can migrate that document version. + +If the host has no matching migration, it preserves the original source unchanged and reports an unsupported version. Migration failure does not authorize retry through the strict current parser or mutate the original source. + +## Compatibility and packaging + +The API is additive. Existing create/parse/encode/revision/restore behavior is unchanged. The dedicated `envelope-identity` subpath is built and consumed through ESM, CommonJS, and strict TypeScript without React, TipTap UI, ProseMirror view, Yjs, DOM globals, network clients, database clients, credentials, or model SDKs. + +Rollback before protected integration removes the new entry points/subpath and restores the prior host migration guidance. Any future widening of identity semantics or movement of migration/persistence authority into Inkspan requires explicit versioning and an ADR update or supersession. + +## Verification + +Acceptance is test-first and requires: current/legacy/future identity routing; proof that the strict parser still rejects unsupported schemas; JSON text and strict UTF-8 bytes including Buffer; duplicate/malformed/BOM input; invalid scalar versions; accessors and hostile proxies; resource ceilings; absence of document-bearing output; root and framework-independent public exports; packed ESM/CommonJS/strict-TypeScript consumers; exact owned production coverage; public docstrings; security scans; zero valid unresolved findings; and live repository policy on the unchanged exact head. + +## References + +Bray, T. (Ed.). (2015). *The I-JSON Message Format* (RFC 7493). RFC Editor. https://doi.org/10.17487/RFC7493 + +Bray, T. (Ed.). (2017). *The JavaScript Object Notation (JSON) Data Interchange Format* (RFC 8259; STD 90). RFC Editor. https://doi.org/10.17487/RFC8259 + +Rundgren, A., Jordan, B., & Erdtman, S. (2020). *JSON Canonicalization Scheme (JCS)* (RFC 8785). RFC Editor. https://doi.org/10.17487/RFC8785 From e54eba2418c83cfb1ba1e83e6f2aacde799f6e61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:09:00 +0900 Subject: [PATCH 11/43] docs(envelope): document identity-only migration routing --- docs/document-envelope.md | 135 +++++++++++++++++++++++++++++--------- 1 file changed, 104 insertions(+), 31 deletions(-) diff --git a/docs/document-envelope.md b/docs/document-envelope.md index 13b2b92e..2c043b50 100644 --- a/docs/document-envelope.md +++ b/docs/document-envelope.md @@ -34,9 +34,74 @@ Version 1 contains exactly three fields: } ``` -Unknown fields, schema identifiers, and versions fail closed. Hosts must route -older or newer versions through an explicit migration function rather than -silently asking the current editor schema to interpret them. +Unknown fields, schema identifiers, and versions fail closed in the strict +current-schema parser. Hosts route older or newer versions through an explicit +migration function rather than silently asking the current editor schema to +interpret them. + +## Identity-only migration routing + +When a host needs to select a migration for a complete legacy or future +envelope, use the bounded identity-only inspector rather than weakening the +strict parser or implementing a second untrusted JSON parser in the host: + +```ts +import { + inspectDocumentEnvelopeIdentity, + parseDocumentEnvelope, +} from '@contextualwisdomlab/cwl-editor/envelope-identity'; + +const identity = inspectDocumentEnvelopeIdentity(persisted); +const migrated = await migrationRegistry.migrate(identity, persisted); +const currentEnvelope = parseDocumentEnvelope(migrated); +``` + +The same functions are also exported by the root package. The dedicated +`envelope-identity` subpath is framework-independent for servers, workers, +migration jobs, storage adapters, and other non-editor processes. + +`inspectDocumentEnvelopeIdentity()` and +`inspectDocumentEnvelopeIdentityBytes()` return only a deeply frozen routing +value: + +```ts +interface CwlEditorDocumentEnvelopeIdentity { + readonly schemaId: string; + readonly schemaVersion: number; +} +``` + +Inspection requires a complete syntactically valid envelope with `schemaId`, +`schemaVersion`, and `documentJson`. The identity must use a string schema ID and +a positive safe-integer schema version. JSON text retains duplicate-name and +resource-limit defenses; byte input retains bounded copying, BOM rejection, and +fatal UTF-8 decoding. Direct JavaScript objects are inspected through data +property descriptors without invoking getters, and hostile proxy/reflection +failures become bounded redacted `DocumentEnvelopeError` values. + +The inspector may ignore additional future top-level data fields only when they +are ordinary enumerable JSON data fields and can be ignored without evaluating +accessors. It validates that `documentJson` is bounded JSON-compatible data, but +it deliberately does **not** require an unsupported generation to satisfy the +current TipTap/ProseMirror `doc` schema before returning routing metadata. That +is the key distinction between safe schema identification and current-schema +acceptance. + +The inspector never returns `documentJson`, document text, links, inline image +data, future source fields, actor or tenant identity, credentials, transport +state, signatures, authorization decisions, migration-success claims, or +durable-write claims. Identifying an unsupported schema is therefore a routing +result, not proof that Inkspan understands, authorizes, or migrated that +representation. + +Migration execution remains host-owned. The host chooses the schema registry, +authorizes the operation, applies tenant isolation and encryption, executes the +version-specific migration, performs durable persistence/audit/retention, and +owns rollback and recovery. The migrated result must re-enter +`parseDocumentEnvelope()` and pass the ordinary strict current-schema boundary +before it becomes canonical Inkspan document state. If no migration exists or a +migration fails, preserve the original source unchanged and report that bounded +host outcome. ## Canonical UTF-8 byte round trip @@ -87,8 +152,9 @@ fail-closed defaults before and during envelope inspection: | `maxNestingDepth` | 128 | Object/array nesting below the document root | The root package exports `DEFAULT_DOCUMENT_ENVELOPE_LIMITS` and the -`DocumentEnvelopeLimits` type. A host can tighten one or more ceilings without -forking the parser: +`DocumentEnvelopeLimits` type. The identity-only subpath exposes the same limits +contract without importing the interactive editor dependency graph. A host can +tighten one or more ceilings without forking either boundary: ```ts const envelope = parseDocumentEnvelope(rawEnvelopeJson, { @@ -108,12 +174,18 @@ message, compressed-body, and object-store limits before the library boundary. OWASP likewise recommends total request-size limits and avoiding unbounded input-driven resource allocation. -For JSON text and decoded UTF-8 bytes, the iterative duplicate-name scanner also -counts every scalar and container and tracks value depth before `JSON.parse()`. -The fixed three-value and one-depth envelope wrapper allowance keeps configured -limits scoped to `documentJson`. Oversized raw structures therefore fail before -the native parser allocates their complete object graph. Syntax-invalid input -that remains inside the ceilings is still reported by the canonical parser. +For strict current-schema JSON text and decoded UTF-8 bytes, the iterative +duplicate-name scanner counts every scalar and container and tracks value depth +before `JSON.parse()`. The fixed three-value and one-depth envelope wrapper +allowance keeps configured limits scoped to `documentJson`. Oversized raw +structures therefore fail before the native parser allocates their complete +object graph. Syntax-invalid input that remains inside the ceilings is still +reported by the canonical parser. + +The identity-only path preserves the same configured limits and validates the +complete decoded document as JSON-compatible bounded data after parsing; it +does not treat an unknown version as current-schema document semantics merely +because the source is inside resource ceilings. The post-parse value and array-width checks remain as defense in depth before recursively cloning child values. Object reflection reads descriptors without @@ -122,43 +194,44 @@ source strings, object names, inline image bytes, or tenant identifiers. ## Validation and safety -Both creation and parsing return a detached, deeply frozen envelope. The -validator rejects cycles, non-JSON values, non-finite numbers, non-plain -objects, a non-`doc` root, sparse or decorated arrays, accessor properties, -symbol or non-enumerable fields, and nesting beyond the configured ceiling. -`__proto__` is retained only as an inert own data field and cannot mutate the -prototype of the cloned document. +Both current-schema creation and parsing return a detached, deeply frozen +envelope. The validator rejects cycles, non-JSON values, non-finite numbers, +non-plain objects, a non-`doc` root, sparse or decorated arrays, accessor +properties, symbol or non-enumerable fields, and nesting beyond the configured +ceiling. `__proto__` is retained only as an inert own data field and cannot +mutate the prototype of the cloned document. -Raw JSON parsing rejects duplicate object names before native `JSON.parse()` -can discard earlier values. The public functions also treat direct JavaScript +Raw JSON parsing rejects duplicate object names before native `JSON.parse()` can +discard earlier values. The public functions also treat direct JavaScript objects as hostile. Proxy traps and reflection failures are converted to a -bounded `DocumentEnvelopeError`; getters are never executed during document -validation. Error messages do not include source values, source-defined -property names, URLs, inline image bytes, or tenant data. +bounded `DocumentEnvelopeError`; getters are never executed during document or +identity inspection. Error messages do not include source values, +source-defined property names, URLs, inline image bytes, or tenant data. -This validation establishes a portable storage boundary; it does not sanitize -or authorize the document body. The JSON may still contain client-controlled -text, links, inline image data, alternative text, and extension attributes. -CWL and naruon hosts remain responsible for: +This validation establishes portable storage and routing boundaries; it does not +sanitize or authorize the document body. The JSON may still contain +client-controlled text, links, inline image data, alternative text, and +extension attributes. CWL and naruon hosts remain responsible for: - document and tenant authorization; - transport-level byte limits, decompression limits, timeouts, rate limits, and concurrency limits; - selecting tighter envelope ceilings for each product tier and use case; - encryption, retention, audit, and backup policy; -- schema migration before restoration; +- schema registry, migration selection/execution, persistence, and rollback before restoration; - validating descriptive nonnumeric document and revision identifiers; - authorizing collaborative mutations before `setDocumentJson`; - deciding whether document content may enter indexing or model workflows. -The envelope has no transport, database, credential, environment-variable, or -provider dependency. It can be stored by a standalone application or carried -through CWL/naruon service boundaries as an ordinary versioned data contract. +The envelope and identity inspector have no transport, database, credential, +environment-variable, or provider dependency. They can be used by a standalone +application or carried through CWL/naruon service boundaries as ordinary +versioned data contracts. ## Standards and security references - RFC 8259, sections 8–10 — UTF-8 JSON interchange, BOM behavior, and implementation limits - WHATWG Encoding Standard — fatal UTF-8 decoding and BOM handling - RFC 7493 — I-JSON interoperability profile -- RFC 8785 — deterministic JSON canonicalization +- RFC 8785 — deterministic JSON canonicalization for accepted current-schema bytes, not unsupported-version semantic validation - OWASP Denial of Service Cheat Sheet — request-size and resource-allocation limits - OWASP REST Security Cheat Sheet — length, range, format, type, and request-size validation From 33143cd5901e460b466f482f90eb1904f9280a37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:09:11 +0900 Subject: [PATCH 12/43] test(envelope): bind migration-routing documentation contract --- ...umentEnvelopeIdentityDocumentation.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/documentEnvelopeIdentityDocumentation.test.ts diff --git a/src/documentEnvelopeIdentityDocumentation.test.ts b/src/documentEnvelopeIdentityDocumentation.test.ts new file mode 100644 index 00000000..83b51e53 --- /dev/null +++ b/src/documentEnvelopeIdentityDocumentation.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const repositoryRoot = process.cwd(); + +function read(path: string): string { + return readFileSync(resolve(repositoryRoot, path), 'utf8'); +} + +describe('document envelope identity documentation', () => { + it('keeps identity routing separate from strict current-schema parsing', () => { + const guide = read('docs/document-envelope.md'); + const doctoring = read('docs/doctoring/envelope-identity-routing.md'); + const adr = read('docs/adr/0015-envelope-schema-migration-routing.md'); + + for (const source of [guide, doctoring, adr]) { + expect(source).toContain('inspectDocumentEnvelopeIdentity'); + expect(source).toContain('parseDocumentEnvelope'); + } + expect(guide).toContain('@contextualwisdomlab/cwl-editor/envelope-identity'); + expect(guide).toMatch(/host[- ]owned|Migration execution remains host-owned/i); + expect(doctoring).toContain('Status: Implemented on active PR'); + expect(doctoring).toContain('Issue #74'); + expect(adr).toContain('Issue #74'); + }); + + it('keeps the identity result document-free and migration authority outside Inkspan', () => { + const guide = read('docs/document-envelope.md'); + const doctoring = read('docs/doctoring/envelope-identity-routing.md'); + + expect(guide).toContain('never returns `documentJson`'); + expect(doctoring).toContain('contains no document body'); + expect(doctoring).toMatch(/schema registry/i); + expect(doctoring).toMatch(/rollback/i); + expect(doctoring).toMatch(/RFC 8259/); + expect(doctoring).toMatch(/RFC 7493/); + expect(doctoring).toMatch(/RFC 8785/); + }); +}); From 7995049bb4875096d762dbc82af780e0883c4f12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:09:36 +0900 Subject: [PATCH 13/43] test(envelope): bind docs to decision semantics --- src/documentEnvelopeIdentityDocumentation.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/documentEnvelopeIdentityDocumentation.test.ts b/src/documentEnvelopeIdentityDocumentation.test.ts index 83b51e53..eca84a2a 100644 --- a/src/documentEnvelopeIdentityDocumentation.test.ts +++ b/src/documentEnvelopeIdentityDocumentation.test.ts @@ -14,10 +14,12 @@ describe('document envelope identity documentation', () => { const doctoring = read('docs/doctoring/envelope-identity-routing.md'); const adr = read('docs/adr/0015-envelope-schema-migration-routing.md'); - for (const source of [guide, doctoring, adr]) { + for (const source of [guide, doctoring]) { expect(source).toContain('inspectDocumentEnvelopeIdentity'); expect(source).toContain('parseDocumentEnvelope'); } + expect(adr).toMatch(/identity inspector/i); + expect(adr).toContain('`parseDocumentEnvelope()` remains strict'); expect(guide).toContain('@contextualwisdomlab/cwl-editor/envelope-identity'); expect(guide).toMatch(/host[- ]owned|Migration execution remains host-owned/i); expect(doctoring).toContain('Status: Implemented on active PR'); From bfcb3d151718bd9498bf4fcf167ccf49ba5fb2db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:11:19 +0900 Subject: [PATCH 14/43] docs(product): reconcile SafeClipboard and identity-routing maturity --- docs/PRD.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index cea011c2..b34ed5d7 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -4,7 +4,7 @@ Status: Protected-main canonical baseline ## Product definition -Inkspan is a standalone Markdown/HTML rich-text authoring and deterministic document-conversion product that can also be embedded as a modular CWL component. Protected `main` is the implementation authority for what Inkspan actually provides today. Lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. This canonical PRD also records Active PR / Proposed requirements such as safe rich clipboard handling; such requirements are not shipped claims until protected integration. Across current and proposed scope, Inkspan keeps host transport, authentication, authorization, tenant isolation, durable persistence, credentials, migrations, retention, deployment, audit storage, and model-use policy outside the product boundary unless a future accepted versioned contract explicitly changes that division. +Inkspan is a standalone Markdown/HTML rich-text authoring and deterministic document-conversion product that can also be embedded as a modular CWL component. Protected `main` is the implementation authority for what Inkspan actually provides today. Safe rich clipboard handling, lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. This canonical PRD also records Active PR / Proposed requirements such as bounded envelope identity routing; such requirements are not shipped claims until protected integration. Across current and proposed scope, Inkspan keeps host transport, authentication, authorization, tenant isolation, durable persistence, credentials, migrations, retention, deployment, audit storage, and model-use policy outside the product boundary unless a future accepted versioned contract explicitly changes that division. The product promise is: **author, convert, collaborate, and prove document changes without hiding authority inside the editor.** @@ -29,6 +29,7 @@ The product promise is: **author, convert, collaborate, and prove document chang 9. Offer model-assisted authoring only as host-approved untrusted proposals; deterministic editor/conversion validation remains authoritative. 10. Produce reviewable package, security, compatibility, accessibility, SBOM/provenance, and release evidence tied to one exact protected source head. 11. Give security researchers a discoverable private vulnerability-reporting and coordinated-disclosure path without promising unsupported SLAs, bounties, certification, or legal safe harbor. +12. Identify a complete unsupported document-envelope generation safely enough for the host to select its own migration without accepting that generation as current Inkspan document semantics. ## Required outcomes @@ -37,6 +38,8 @@ The product promise is: **author, convert, collaborate, and prove document chang - Markdown and HTML authoring use a deterministic TipTap/ProseMirror surface. - Supported import/export behavior is explicitly versioned and testable; unsupported or lossy constructs are not silently advertised as lossless. - Canonical document envelopes preserve exact schema/version identity and strict JSON/UTF-8 boundaries. +- A bounded identity-only routing surface may return only validated `schemaId` and positive safe-integer `schemaVersion` for a complete unsupported envelope; it must not return the document body, validate unknown-version semantics as current, execute a migration, or weaken the strict current-schema parser. +- Hosts retain schema registry, migration selection/execution, authorization, tenant isolation, persistence, audit, retention, rollback, and recovery; migrated output must pass the strict current parser before it becomes canonical Inkspan state. - Source and rendered/exported representations must have documented authority; generated output never silently becomes a new source of truth. ### Clipboard and external content @@ -102,12 +105,13 @@ Inkspan does not promise universal HTML/Office round-trip fidelity, arbitrary ex ## Security and privacy requirements -- Untrusted HTML, DOM capabilities, clipboard configuration, form values, Office structures, host callbacks, collaboration updates, and model proposals fail closed at documented boundaries. +- Untrusted HTML, DOM capabilities, clipboard configuration, form values, Office structures, host callbacks, collaboration updates, unsupported envelope generations, and model proposals fail closed at documented boundaries. - Active or hidden rich content must not bypass the supported semantic clipboard policy. +- Envelope identity output is routing metadata only and must not expose document-bearing source fields or become migration/authorization/durable-write evidence. - Spreadsheet formula-significant input must not silently become executable formulas unless a future explicit trusted formula contract says otherwise. - Document bodies, revision/entity tags, provider metadata, tenant identifiers, prompts, and model outputs must not enter generic public metrics or unauthenticated logs. - Vulnerability reporting must prefer private channels and minimized/synthetic evidence; public fallback must not disclose vulnerability details, secrets, proof-of-concept payloads, or customer data. -- Host applications remain responsible for authentication, authorization, CSRF, tenant isolation, persistence, encryption, retention, audit storage, provider admission, and external-model policy. +- Host applications remain responsible for authentication, authorization, CSRF, tenant isolation, persistence, encryption, retention, audit storage, provider admission, schema migration, and external-model policy. ## Reliability and operability requirements @@ -115,6 +119,7 @@ Inkspan does not promise universal HTML/Office round-trip fidelity, arbitrary ex - Observer failures cannot change save ordering, result classification, or durable-validator handoff. - Async revision/selection capture binds to one immutable editor state. - Conversion/publication never reports a partial artifact as successful output. +- Unsupported envelope identity inspection returns no partial routing result on malformed/unsafe input; a host migration failure leaves the original source unchanged. - Package/release operations fail closed on stale source, ambiguous artifact inventory, digest mismatch, missing required evidence, or stale review/check state. - Security policy support rows and reporting mechanics must remain synchronized with package manifests and current repository capabilities. - Boundary-specific rollback must preserve canonical document readability and host-owned durable state. @@ -129,4 +134,4 @@ Shareable acquisition evidence excludes production tenant content and credential Protected `main` is the sole implemented baseline. Open PRs may describe Proposed or Active work but are not shipped contracts until protected integration. Canonical documentation must state when a requirement is target architecture rather than current implementation. -Lifecycle observation is implemented on protected `main`. The root security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are also implemented on protected `main`. SafeClipboard remains the current active product line. Cross-engine browser assurance remains planned behind SafeClipboard, and a future envelope-identity migration-routing API is tracked separately and must preserve host migration ownership. +SafeClipboard, lifecycle observation, the root security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. Envelope identity-only migration routing is `implemented_on_active_pr` in PR #84 and remains unshipped until protected integration. Cross-engine browser assurance remains `planned`, now unblocked by SafeClipboard integration, and must start from fresh protected `main` without weakening the sanitizer or treating jsdom evidence as browser conformance. From 06adf10aa987baa5f6c1f22e166ac7b40d3c5e35 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:11:56 +0900 Subject: [PATCH 15/43] docs(technical): bind envelope identity active contract --- docs/TRD.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/TRD.md b/docs/TRD.md index 1e72c30e..4164a440 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -12,13 +12,19 @@ Protected `main` is the implementation authority. Open PRs may provide Proposed The root product may compose TipTap/ProseMirror, React-facing integration, deterministic conversion, revision evidence, autosave, collaboration adapters, email/base64 utilities, and Office rendering. Framework-independent subpaths must not accidentally require React, DOM globals, TipTap UI, ProseMirror view, Yjs, network, credentials, database clients, or model SDKs when their public contract excludes those dependencies. +The active envelope-routing slice adds `@contextualwisdomlab/cwl-editor/envelope-identity` as a framework-independent ESM/CommonJS/strict-TypeScript surface. It must remain usable without the interactive editor graph and expose only routing metadata plus bounded input options. + Standalone use must not require naruon or contextual-orchestrator. A CWL host can compose Inkspan additively through stable interfaces. ## Document identity and schema boundary -A versioned `document_envelope` is validated before canonicalization or hashing. Strict JSON text/byte handling rejects malformed input, duplicate object names, malformed UTF-8, unsupported schemas, hostile JavaScript object descriptors, and resource-limit violations according to the active contract. +A versioned `document_envelope` is validated before canonicalization or hashing. Strict current-schema JSON text/byte handling rejects malformed input, duplicate object names, malformed UTF-8, unsupported schemas, hostile JavaScript object descriptors, and resource-limit violations according to the active contract. + +PR #84 implements the ADR 0015 identity-only routing boundary. `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()` require complete bounded envelope input and return only a frozen string `schemaId` plus positive safe-integer `schemaVersion`. They preserve duplicate-name, strict UTF-8/BOM, JSON-compatibility, resource-limit, descriptor/accessor/proxy-reflection, and redacted-error boundaries while deliberately not treating an unsupported `documentJson` as current TipTap/ProseMirror semantics. + +`parseDocumentEnvelope()` remains strict and current-schema-only. Schema registry, migration selection/execution, authorization, tenant isolation, persistence, durable audit, retention, rollback, and recovery remain host-owned. A migrated value must pass the strict current parser before it becomes canonical Inkspan state. -SHA-256 revision evidence identifies exact canonical content. Local content digests are equality evidence only and never replace server-selected RFC 9110 strong validators for durable compare-and-swap. Unknown schema migration remains host-owned; a future identity-inspection helper may expose routing metadata without becoming a permissive current-schema parser or migration executor. +SHA-256 revision evidence identifies exact canonical current-schema content. Local content digests are equality evidence only and never replace server-selected RFC 9110 strong validators for durable compare-and-swap. ## Markdown/HTML editor boundary @@ -26,9 +32,9 @@ TipTap/ProseMirror is the deterministic editing authority for supported Markdown ## Rich clipboard boundary -Rich clipboard HTML is untrusted. Sanitization must be installed in the actual TipTap/ProseMirror paste pipeline, use bounded fail-closed validation, perform no external fetch or active execution, and preserve only supported semantic content. Pure sanitizer APIs and editor integration must remain behaviorally consistent. +SafeClipboard is implemented on protected `main`. Rich clipboard HTML is untrusted. Sanitization is installed in the actual TipTap/ProseMirror paste pipeline, uses bounded fail-closed semantic reconstruction, performs no external fetch or active execution, and preserves only supported semantic content. Pure sanitizer APIs and editor integration remain behaviorally consistent. -Where browser fragment parsing or serialization can change security semantics, a release must run the same adversarial corpus through dependency-locked Playwright Chromium, Firefox, and WebKit projects. Differences require an explicit standards/threat rationale rather than an unconditional parity normalization. +Where browser fragment parsing or serialization can change security semantics, a release must run the same adversarial corpus through dependency-locked Playwright Chromium, Firefox, and WebKit projects. Differences require an explicit standards/threat rationale rather than an unconditional parity normalization. Issue #66 remains the separate browser-realistic release-assurance implementation lane and is no longer blocked by SafeClipboard integration. ## SSR and native forms @@ -85,9 +91,9 @@ Central `.github` automation, contextual-orchestrator, and other CWL repositorie ## Failure and diagnostic semantics -Public failures are bounded, deterministic where practical, and redacted. Document bodies, credentials, tenant identifiers, complete provider metadata, callback values, private exception causes, prompts/model output, and durable validators are not reflected into generic public diagnostics unless a versioned authorized contract explicitly requires them. +Public failures are bounded, deterministic where practical, and redacted. Document bodies, credentials, tenant identifiers, complete provider metadata, callback values, private exception causes, prompts/model output, durable validators, and source-defined unsupported envelope fields are not reflected into generic public diagnostics unless a versioned authorized contract explicitly requires them. -Cancellation, retry/offline policy, network timeout budgets, durable reconciliation, and user-facing localized recovery remain host responsibilities when they involve host transport or persistence. +Identity inspection returns no partial routing object on malformed input. An unknown structurally valid identity is not proof of migration success or current-schema compatibility. Cancellation, retry/offline policy, migration retry/recovery, network timeout budgets, durable reconciliation, and user-facing localized recovery remain host responsibilities when they involve host transport or persistence. ## Accessibility and interaction semantics @@ -95,7 +101,7 @@ Toolbar shortcut metadata is implemented on protected `main`. Shipped keyboard b ## Packaging, compatibility, and release evidence -The root package and framework-independent subpaths are verified from packed artifacts under ESM, CommonJS, and strict TypeScript consumers. Office Python surfaces are verified under the documented supported Python matrix, exact production statement/branch/function/line coverage, complete public docstrings, built wheel/package inspection, and license/dependency consistency. +The root package and framework-independent subpaths are verified from packed artifacts under ESM, CommonJS, and strict TypeScript consumers. PR #84 adds equivalent packed-artifact verification for `envelope-identity` without framework dependencies. Office Python surfaces are verified under the documented supported Python matrix, exact production statement/branch/function/line coverage, complete public docstrings, built wheel/package inspection, and license/dependency consistency. Release publication verifies one exact integrated protected source head, expected artifact inventory and digests, package/runtime compatibility, security/coverage/accessibility/document-fidelity gates, SBOM/provenance/reproducibility where configured, zero valid unresolved findings, formal review/branch-protection requirements, and post-publication artifact smoke evidence. @@ -107,4 +113,4 @@ Queued, cancelled, skipped-required, absent, stale-head, predecessor-head, statu ## Implemented versus proposed -Protected `main` is the sole implemented baseline. The security disclosure lifecycle, autosave lifecycle observation, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. SafeClipboard remains Proposed until merged; migration routing and cross-engine browser assurance remain planned according to their dependency order. Canonical docs distinguish target architecture from shipped behavior and must be updated when the protected implementation changes. +Protected `main` is the sole implemented baseline. SafeClipboard, the security disclosure lifecycle, autosave lifecycle observation, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are implemented on protected `main`. Envelope identity routing is `implemented_on_active_pr` in PR #84 and remains unshipped until protected integration. Cross-engine browser assurance remains `planned`, now unblocked and required before the SafeClipboard behavior enters the verified release line. Canonical docs distinguish target architecture from shipped behavior and must be updated when the protected implementation changes. From 83908f18ca8ee74d453ee724bb869ed0949129db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:12:52 +0900 Subject: [PATCH 16/43] docs(fitness): reconcile protected SafeClipboard and active routing --- 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..8fc0112f 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -38,22 +38,23 @@ Document fitness and implementation maturity are independent. A `present_current | TRD | `docs/TRD.md` | `present_current` | Same mixed maturity discipline; protected `main` remains runtime authority | Technical invariants, failure semantics, package boundaries and release evidence are reconstructable. | | ARCHITECTURE | `ARCHITECTURE.md` | `present_current` | `implemented_on_protected_main` for the bounded standalone/modular architecture | Host-vs-Inkspan authority and modular CWL composition are explicit. | | Public/API/schema/plugin contracts | `docs/CONTRACTS.md` | `present_current` | Protected-main contracts plus explicitly proposed extensions | Integration authority and degraded behavior are not hidden in implementation details. | -| ADR | `docs/adr/README.md` and detailed ADRs | `present_current` | Decisions distinguish current, proposed and future work; ADR 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 active identity routing and ADR 0017 records the protected security-disclosure lifecycle | Alternatives, consequences, recovery, migration, verification and supersession are reviewable. | | UML | `docs/UML.md` | `present_current` | Diagrams include protected-main and clearly proposed flows | Component, sequence, state, deployment, degraded-mode and authority flows are visible as diagram-as-code. | | DATA_MODEL / ERD | `docs/DATA_MODEL.md` | `present_current` | Current logical evidence/domain model; host persistence remains outside Inkspan | The model distinguishes document/evidence/conversion/release values from host-owned entities. | | physical relational ERD | none by design | `not_applicable` | `out_of_scope` while Inkspan owns no application database | No fake database is invented merely to satisfy an ERD checklist; a physical ERD becomes mandatory if persistence authority moves into Inkspan. | | SECURITY disclosure policy | root `SECURITY.md` plus ADR 0017 | `present_current` | `implemented_on_protected_main`; the root policy is protected authority and ADR 0017 records its durable architecture/process decision | Private reporting, evidence minimization, supported release-line binding, ownership limits, coordinated disclosure, and explicit no-SLA/no-certification claim boundaries are reconstructable. | +| Safe rich clipboard | PRD, TRD, clipboard/security doctoring and protected SafeClipboard source | `present_current` | `implemented_on_protected_main`; protected main includes bounded fail-closed semantic reconstruction in the actual TipTap/ProseMirror paste pipeline | Buyers can reconstruct the sanitizer trust boundary without confusing jsdom integration evidence with cross-engine browser assurance. | | 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. | +| TEST_STRATEGY | `docs/TEST_STRATEGY.md` | `present_current` | Current deterministic evidence plus `planned` cross-engine acceptance | Test authority and claim limits are explicit rather than inferred from CI badges. | | OPERABILITY | `docs/OPERABILITY.md` | `present_current` | Current local/product responsibilities plus host-owned recovery boundaries | Conflict, collaboration, conversion and release recovery/rollback ownership are explicit. | | 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, envelope guide/doctoring and PR #84 | `present_current` | Identity-only routing capability is `implemented_on_active_pr`; strict current-schema parsing and host migration ownership remain authoritative | The architecture distinguishes bounded schema identification from host-owned migration execution without calling the active PR shipped. | +| Cross-engine browser-semantic release assurance | ADR 0016, UML, TEST_STRATEGY, TRACEABILITY and Issue #66 | `present_current` | Differential Chromium/Firefox/WebKit release gate is `planned` and now unblocked by SafeClipboard protected integration | Browser-realistic security assurance is a durable release decision even though its implementation remains future work. | | 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 `implemented_on_active_pr` identity-only envelope routing in PR #84, while migration execution remains host-owned; +- real Chromium/Firefox/WebKit differential evidence as a planned release gate for browser-semantic clipboard security rather than a jsdom conformance claim; and - 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: identity-only migration routing must still pass exact-head CI/security/package/review/repository-policy gates and protected integration before its maturity changes to `implemented_on_protected_main`. +2. Issue #66 remains `planned`, but SafeClipboard is now integrated: the dependency-locked Chromium/Firefox/WebKit differential suite can start from fresh protected `main` and remains required before the rich-clipboard release line. +3. SafeClipboard, autosave lifecycle observation, security disclosure, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are `implemented_on_protected_main` and must not be described as active-only work. +4. The canonical documentation graph is already integrated on protected `main`; future reconciliation is required when protected source, accepted decisions, or implementation maturity materially changes. 5. Documentation becoming mergeable or protected-merged is not a reason for the commercial loop to stop; the next safe product, release, security, accessibility or interoperability lane must continue. ## 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 migration routing is `implemented_on_active_pr` in PR #84. Cross-engine browser assurance remains `planned`. 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 03b4ea9a529b357a5bf1aeaf4614d7759be495a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:13:36 +0900 Subject: [PATCH 17/43] docs(data): mark envelope identity as active value object --- docs/DATA_MODEL.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 424ac459..7be7e870 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -40,6 +40,7 @@ erDiagram ## Inkspan-owned in-memory or package value objects - `document_envelope`: versioned, strictly validated, canonicalizable complete document value. It can contain the complete document body. Inkspan constructs/validates it; a host may persist it under its own policy. +- `document_schema_identity`: `implemented_on_active_pr` in PR #84 as a frozen identity-only routing value containing the complete envelope's bounded `schemaId` and positive safe-integer `schemaVersion`. It contains no document body and does not validate unsupported-version document semantics. The host owns migration selection/execution and persistence. - `document_revision`: SHA-256 equality evidence derived from one exact canonical envelope. It is not authorization, tenant identity, actor identity, timestamp, signature, or proof of a durable write. - `editor_session`: conceptual local editor/runtime lifetime. It binds one mounted editor state to local evidence and host callbacks. It is not a durable account/session record and has no authentication authority. - `document_transition`: previous/resulting revision pair plus changed classification. It deliberately omits the document body from ordinary evidence. @@ -53,11 +54,10 @@ erDiagram - `release_artifact`: package/wheel/checksum or other expected artifact considered for release. - `release_evidence`: exact-source artifact inventory/digest/provenance/verification evidence used before publication. -## Planned value and evidence objects +## Planned evidence objects -The following **planned value and evidence objects** make already accepted future boundaries visible in the conceptual ERD without claiming that their APIs or persistence exist on protected `main`: +The following **planned evidence objects** make an accepted future release-assurance boundary visible in the conceptual ERD without claiming that its APIs or persistence exist on protected `main`: -- `document_schema_identity`: proposed frozen identity-only routing metadata containing the bounded envelope `schemaId` and `schemaVersion`. It contains no document body and does not validate unknown-version document semantics. ADR 0015 and Issue #74 define the planned public routing aid; the host owns migration selection/execution and persistence. - `browser_assurance_evidence`: proposed exact-head release evidence from the same committed rich-clipboard corpus executed in required Chromium, Firefox, and WebKit projects. It records bounded public fixture/corpus, browser-revision, package-lock, platform and source identity rather than tenant clipboard data. - `browser_difference_allowance`: proposed reviewed explanation for one standards-permitted browser serialization difference. It is attached only to focused evidence with threat analysis and rollback; it never acts as a generic normalization rule or approval substitute. @@ -77,7 +77,7 @@ These values may remain ephemeral or release-artifact metadata. Their presence i | Entity | Current physical persistence owner | Typical lifecycle | Contains complete document body? | Authority claim | |---|---|---|---|---| | `document_envelope` | host if persisted | document revision | yes | deterministic document value only | -| `document_schema_identity` | none required; planned | one routing inspection | no | schema-route metadata only | +| `document_schema_identity` | none required; `implemented_on_active_pr` | one routing inspection | no | schema-route metadata only | | `document_revision` | local or host metadata by policy | derived per exact content | no | equality only | | `editor_session` | none required | mounted editor runtime | may reference local state | no auth/session authority | | `document_transition` | none required; host may store | change evidence | no | content-lineage evidence only | @@ -103,7 +103,7 @@ These values may remain ephemeral or release-artifact metadata. Their presence i The current Inkspan runtime does not create a tenant database, but products embedding it depend on temporal/version provenance boundaries: - `document_envelope` carries an explicit schema/version contract; unknown versions require host-owned migration routing rather than permissive parsing. -- the planned `document_schema_identity` may expose only bounded routing metadata; it never validates or migrates the unsupported document body and never creates a durable version claim. +- active-PR `document_schema_identity` exposes only bounded routing metadata; it never validates or migrates the unsupported document body and never creates a durable version claim. - `document_revision`, selection, transition, and autosave evidence bind to one exact content state; they do not add actor/time/tenant claims not present in the source contract. - `durable_validator` is temporally ordered by the host's atomic persistence service and must advance only after validated durable success. - `collaboration_document`, `provider_binding`, `awareness_state`, `host_capability`, and `audit_event` can be tenant-scoped in a host, but Inkspan does not define or infer that tenant key. @@ -116,7 +116,7 @@ Ordinary lifecycle/selection/transition/schema-identity evidence should remain d ## Persistence non-applicability and future change -No Inkspan-owned relational schema is required by the current architecture, so no physical database ERD or migration set is invented here merely to satisfy documentation completeness. Planned identity-routing and browser-assurance values are logical API/evidence objects, not database tables. If Inkspan later introduces durable persistence, that is a material architecture change requiring: +No Inkspan-owned relational schema is required by the current architecture, so no physical database ERD or migration set is invented here merely to satisfy documentation completeness. Active identity-routing and planned browser-assurance values are logical API/evidence objects, not database tables. If Inkspan later introduces durable persistence, that is a material architecture change requiring: 1. an Accepted ADR defining why persistence moved into Inkspan; 2. a physical database ERD with descriptive multiword `snake_case` object names; From 2979e9e44ba829464051a74a93f58e98d9125da6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:13:59 +0900 Subject: [PATCH 18/43] docs(traceability): bind active envelope identity evidence --- docs/TRACEABILITY.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index d8b0c08f..391acfd6 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -12,12 +12,12 @@ This record maps durable Inkspan product decisions to authoritative standards, p |---|---|---|---|---| | HTTP optimistic concurrency | Durable saves use a host/server-selected strong entity tag; local content digests do not substitute for durable `If-Match` authority | RFC 9110, HTTP Semantics | autosave session tests, `docs/document-autosave.md`, architecture concurrency sequence | Inkspan validates/coordinates local semantics; host owns atomic persistence and authorization | | JSON envelope grammar | Versioned envelopes use strict JSON handling, duplicate-name defenses, bounded parsing, and explicit schema identity | RFC 8259; RFC 7493 where interoperable JSON constraints apply | envelope parser/resource-limit tests and package consumers | Current-schema parsing does not imply migration authority for unknown schemas | -| Envelope version routing | A bounded identity-only inspector may identify `schemaId`/`schemaVersion` for dispatch while the current parser stays strict and the host owns migration execution | RFC 8259; RFC 7493; RFC 8785 for canonical current-schema bytes | ADR 0015, Issue #74, envelope tests and planned framework-independent consumer contract | Identifying a schema generation does not validate that generation's document semantics, authorize migration, or prove durable persistence | +| Envelope version routing | A bounded identity-only inspector identifies `schemaId`/`schemaVersion` for dispatch while the current parser stays strict and the host owns migration execution | RFC 8259; RFC 7493; RFC 8785 for canonical current-schema bytes | ADR 0015, PR #84, `documentEnvelopeIdentity` tests, envelope guide/doctoring and framework-independent packed consumers | Active-PR evidence is not shipped authority; identifying a schema generation does not validate that generation's document semantics, authorize migration, or prove durable persistence | | Canonical document bytes | Deterministic revision evidence is derived from canonicalized validated document content | RFC 8785, JSON Canonicalization Scheme | revision-evidence, transition-evidence, restore tests | A content digest proves equality only, not actor/time/authorization/durable write | | Provenance semantics | Local transition/release evidence keeps content lineage separate from actor/authorization/durable claims | W3C PROV family | transition evidence, release evidence, canonical data model | Inkspan does not claim complete PROV conformance or host audit provenance | | Accessibility | Native controls, keyboard semantics, shortcut metadata, and host-facing status state support accessible embedding | W3C WCAG 2.2; WAI-ARIA where used | toolbar/accessibility tests, SSR tests, autosave lifecycle data | Component evidence alone is not a full host WCAG conformance claim | -| Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | rich-clipboard unit/integration corpus and SafeClipboard ADR | jsdom-only success is not universal browser-engine conformance | -| Cross-engine release assurance | The same committed adversarial corpus must run under required Chromium, Firefox, and WebKit projects; only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright browser/project documentation | ADR 0016, Issue #66, TEST_STRATEGY, UML and planned exact-head browser evidence | The gate is planned behind SafeClipboard integration and does not claim byte-identical browser serialization or branded enterprise-policy coverage | +| Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | protected-main rich-clipboard unit/integration corpus and SafeClipboard ADR | Protected jsdom/TipTap integration success is not universal browser-engine conformance | +| Cross-engine release assurance | The same committed adversarial corpus must run under required Chromium, Firefox, and WebKit projects; only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright browser/project documentation | ADR 0016, Issue #66, TEST_STRATEGY, UML and planned exact-head browser evidence | The gate is planned and now unblocked by SafeClipboard integration; it does not claim byte-identical browser serialization or branded enterprise-policy coverage | | Editor integration | Public behavior must exercise the actual TipTap/ProseMirror integration path, not an inert extension field or test-only hook | official TipTap and ProseMirror documentation for the locked dependency line | integration tests and package consumers | Inkspan does not claim compatibility with untested major-version integration semantics | | Collaboration | Inkspan provides provider-neutral editor/Yjs bindings; host owns provider lifecycle, room authorization, awareness privacy, persistence and audit | official Yjs/provider documentation plus Inkspan public contract | collaboration tests and architecture ownership matrix | No network-provider or tenant-authorization authority is implied | | Secure development | Security controls are developed test-first, with exact-head scanning/review/package evidence and root-cause regression | NIST SP 800-218 SSDF 1.1 | CI/security/SAST/package/provenance gates, doctoring and regression history | Repository evidence is not a claim of complete SSDF organizational conformance | @@ -72,6 +72,8 @@ Lower levels may explain intent or history but cannot override a contradictory h - **Planned** means an accepted future direction without a protected implementation. - **Superseded** means retained for history but replaced by a later explicit decision. +PR #84 is active implementation evidence for envelope identity routing, not protected-main/shipped authority. SafeClipboard is now implemented on protected `main`; Issue #66 remains planned browser-realistic assurance rather than a statement about current browser conformance. + Documentation must not promote Proposed or Planned capabilities to Implemented merely because a PR, issue, or design document is detailed. ## Review cadence From 37b959691f564962e9c18d81c394a2894bcf1ea5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:16:09 +0900 Subject: [PATCH 19/43] test(docs): bind maturity checks to semantic statuses --- src/canonicalProductDocumentation.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/canonicalProductDocumentation.test.ts b/src/canonicalProductDocumentation.test.ts index 709c7ca7..7a839ffe 100644 --- a/src/canonicalProductDocumentation.test.ts +++ b/src/canonicalProductDocumentation.test.ts @@ -97,18 +97,22 @@ describe('canonical product documentation graph', () => { expect(productDefinition).toContain('not shipped claims'); }); - it('does not leave merged autosave lifecycle observation classified as active-PR work', () => { + it('tracks protected and active implementation maturity semantically', () => { const prd = repositoryFile('docs/PRD.md'); const trd = repositoryFile('docs/TRD.md'); const fitness = repositoryFile('docs/DOCUMENTATION_FITNESS.md'); const currentScope = prd.slice(prd.indexOf('## Current, proposed, and planned scope')); - expect(currentScope).toContain('Lifecycle observation is implemented on protected `main`'); + expect(currentScope).toMatch(/lifecycle observation[^.]*implemented on protected `main`/iu); + expect(currentScope).toMatch(/SafeClipboard[^.]*implemented on protected `main`/u); + expect(currentScope).toMatch(/Envelope identity-only migration routing[^.]*implemented_on_active_pr/u); expect(currentScope).not.toMatch(/open development lines include[^.]*lifecycle observation/u); expect(trd).toContain('Autosave lifecycle observation is implemented on protected `main`'); - expect(trd).not.toContain('open clipboard, autosave observation'); + expect(trd).toContain('SafeClipboard is implemented on protected `main`'); + expect(trd).toContain('Envelope identity routing is `implemented_on_active_pr` in PR #84'); expect(fitness).toContain('Autosave lifecycle observation'); expect(fitness).toMatch(/Autosave lifecycle observation[^\n]*implemented_on_protected_main/u); + expect(fitness).toMatch(/Envelope schema identity \/ migration routing[^\n]*implemented_on_active_pr/u); }); it('records host ownership and deterministic Inkspan authority consistently', () => { From 1cbf60990c86ec32d352b3e5cdd81effc3a859d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:16:26 +0900 Subject: [PATCH 20/43] test(docs): distinguish active routing from planned browser evidence --- src/canonicalDocumentationDecisionCoverage.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/canonicalDocumentationDecisionCoverage.test.ts b/src/canonicalDocumentationDecisionCoverage.test.ts index 27baa3c6..9ba8d833 100644 --- a/src/canonicalDocumentationDecisionCoverage.test.ts +++ b/src/canonicalDocumentationDecisionCoverage.test.ts @@ -58,12 +58,13 @@ describe('canonical architecture decision coverage', () => { expect(fitness).toContain('SECURITY disclosure policy'); expect(fitness).toContain('present_current'); expect(fitness).toContain('implemented_on_protected_main'); + expect(fitness).toContain('implemented_on_active_pr'); expect(fitness).toContain('planned'); - expect(fitness).toContain('Issue #74'); + expect(fitness).toContain('PR #84'); expect(fitness).toContain('Issue #66'); }); - it('makes both planned decision paths reviewable as diagrams and standards traceability', () => { + it('makes active and planned decision paths reviewable as diagrams and standards traceability', () => { const uml = repositoryFile('docs/UML.md'); const traceability = repositoryFile('docs/TRACEABILITY.md'); @@ -72,6 +73,7 @@ describe('canonical architecture decision coverage', () => { expect(traceability).toContain('Envelope version routing'); expect(traceability).toContain('Cross-engine release assurance'); expect(traceability).toContain('RFC 7493'); + expect(traceability).toContain('PR #84'); }); it('keeps the conceptual ERD explicit without inventing Inkspan persistence', () => { @@ -84,7 +86,8 @@ describe('canonical architecture decision coverage', () => { ]) { expect(dataModel).toContain(marker); } - expect(dataModel).toContain('planned value and evidence objects'); + expect(dataModel).toContain('`document_schema_identity`: `implemented_on_active_pr`'); + expect(dataModel).toContain('planned evidence objects'); expect(dataModel).toContain('does **not** own an application database'); expect(dataModel).toContain('physical database ERD'); }); From 27b0cbc9617f31c8bf253215581be05c6b19bb03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:19:28 +0900 Subject: [PATCH 21/43] test(envelope): cover identity-routing failure boundaries --- src/documentEnvelopeIdentity.coverage.test.ts | 386 ++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 src/documentEnvelopeIdentity.coverage.test.ts diff --git a/src/documentEnvelopeIdentity.coverage.test.ts b/src/documentEnvelopeIdentity.coverage.test.ts new file mode 100644 index 00000000..d7939745 --- /dev/null +++ b/src/documentEnvelopeIdentity.coverage.test.ts @@ -0,0 +1,386 @@ +import { describe, expect, it } from 'vitest'; +import { DocumentEnvelopeError } from './documentEnvelope.js'; +import { + inspectDocumentEnvelopeIdentity, + inspectDocumentEnvelopeIdentityBytes, +} from './documentEnvelopeIdentity.js'; +import { + inspectDocumentEnvelopeIdentity as inspectFrameworkFreeIdentity, + inspectDocumentEnvelopeIdentityBytes as inspectFrameworkFreeIdentityBytes, +} from './envelope-identity/index.js'; + +const validEnvelope = () => ({ + schemaId: 'https://inkspan.io/schemas/document-envelope/v9', + schemaVersion: 9, + documentJson: { type: 'legacy', content: [null, true, false, 'text', 42] }, +}); + +function expectDocumentEnvelopeError(operation: () => unknown, message?: RegExp | string) { + try { + operation(); + throw new Error('expected DocumentEnvelopeError'); + } catch (error) { + expect(error).toBeInstanceOf(DocumentEnvelopeError); + if (message !== undefined) { + if (typeof message === 'string') { + expect(String(error)).toContain(message); + } else { + expect(String(error)).toMatch(message); + } + } + } +} + +describe('document envelope identity branch coverage', () => { + it('exercises the framework-independent wrappers for object and byte input', () => { + const source = validEnvelope(); + const expected = { schemaId: source.schemaId, schemaVersion: 9 }; + + expect(inspectFrameworkFreeIdentity(source)).toEqual(expected); + expect( + inspectFrameworkFreeIdentityBytes( + new TextEncoder().encode(JSON.stringify(source)), + ), + ).toEqual(expected); + }); + + it('rejects non-byte input, oversized bytes, and short valid bytes', () => { + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentityBytes('not-bytes'), + 'must be a Uint8Array', + ); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentityBytes(new Uint8Array([0x7b, 0x7d]), { + maxUtf8Bytes: 1, + }), + 'exceed the supported length', + ); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentityBytes(new Uint8Array([0x7b, 0x7d])), + 'requires schemaId', + ); + }); + + it('rejects overlong JSON text and non-object decoded values', () => { + const source = JSON.stringify(validEnvelope()); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity(source, { + maxJsonTextCodeUnits: source.length - 1, + }), + 'JSON text exceeds', + ); + + for (const value of ['null', '[]', 'true', '1', '"string"']) { + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(value), + 'must be an object', + ); + } + }); + + it('rejects too many top-level fields under the configured value ceiling', () => { + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity( + { + ...validEnvelope(), + futureA: 1, + futureB: 2, + }, + { maxJsonValues: 1 }, + ), + 'too many top-level fields', + ); + }); + + it('rejects invalid schema identifiers and each invalid schema-version class', () => { + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity({ ...validEnvelope(), schemaId: 3 }), + 'schemaId must be a string', + ); + + for (const schemaVersion of [ + '9', + Number.NaN, + Number.POSITIVE_INFINITY, + 1.25, + Number.MAX_SAFE_INTEGER + 1, + 0, + -1, + ]) { + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + schemaVersion, + }), + 'positive safe integer', + ); + } + }); + + it('covers every JSON scalar class and rejects non-finite or non-JSON primitives', () => { + for (const documentJson of [null, true, false, 'text', 0, -2.5]) { + expect( + inspectDocumentEnvelopeIdentity({ ...validEnvelope(), documentJson }), + ).toEqual({ + schemaId: validEnvelope().schemaId, + schemaVersion: 9, + }); + } + + for (const documentJson of [ + Number.NaN, + Number.POSITIVE_INFINITY, + undefined, + 1n, + Symbol('x'), + () => undefined, + ]) { + expectDocumentEnvelopeError(() => + inspectDocumentEnvelopeIdentity({ ...validEnvelope(), documentJson }), + ); + } + }); + + it('rejects cycles, unsupported object prototypes, and oversized strings', () => { + const cyclic: Record = {}; + cyclic.self = cyclic; + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + documentJson: cyclic, + }), + 'cyclic reference', + ); + + for (const documentJson of [new Date(), new Map(), new Set()]) { + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity({ ...validEnvelope(), documentJson }), + 'JSON-compatible values', + ); + } + + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity( + { ...validEnvelope(), documentJson: 'abcdef' }, + { maxStringCodeUnits: 5 }, + ), + 'strings exceed', + ); + }); + + it('accepts null-prototype JSON objects', () => { + const body = Object.create(null) as Record; + body.value = 1; + expect(inspectDocumentEnvelopeIdentity({ ...validEnvelope(), documentJson: body })).toEqual({ + schemaId: validEnvelope().schemaId, + schemaVersion: 9, + }); + }); + + it('rejects sparse, decorated, non-enumerable, and accessor arrays', () => { + const sparse = new Array(1); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + documentJson: sparse, + }), + 'dense JSON elements', + ); + + const missingIndex = new Array(1) as unknown[] & { extra?: string }; + missingIndex.extra = 'x'; + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + documentJson: missingIndex, + }), + 'dense JSON elements', + ); + + const nonEnumerable = ['x']; + Object.defineProperty(nonEnumerable, '0', { + value: 'x', + enumerable: false, + configurable: true, + writable: true, + }); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + documentJson: nonEnumerable, + }), + 'dense JSON elements', + ); + + const accessor = ['x']; + Object.defineProperty(accessor, '0', { + enumerable: true, + configurable: true, + get() { + throw new Error('must-not-run'); + }, + }); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + documentJson: accessor, + }), + 'dense JSON elements', + ); + }); + + it('rejects top-level symbol, non-enumerable, accessor, and missing descriptors', () => { + const symbolEnvelope = validEnvelope() as Record; + symbolEnvelope[Symbol('private')] = 'value'; + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(symbolEnvelope), + 'enumerable JSON data fields', + ); + + const nonEnumerableEnvelope = validEnvelope(); + Object.defineProperty(nonEnumerableEnvelope, 'future', { + value: 'x', + enumerable: false, + }); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(nonEnumerableEnvelope), + 'enumerable JSON data fields', + ); + + const accessorEnvelope = validEnvelope(); + Object.defineProperty(accessorEnvelope, 'future', { + enumerable: true, + get() { + throw new Error('must-not-run'); + }, + }); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(accessorEnvelope), + 'enumerable JSON data fields', + ); + + const descriptorless = new Proxy(validEnvelope(), { + ownKeys() { + return ['schemaId', 'schemaVersion', 'documentJson', 'future']; + }, + getOwnPropertyDescriptor(target, property) { + if (property === 'future') return undefined; + return Object.getOwnPropertyDescriptor(target, property); + }, + }); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(descriptorless), + 'enumerable JSON data fields', + ); + }); + + it('rejects overlong object names', () => { + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity( + { + ...validEnvelope(), + abcdef: 1, + }, + { maxStringCodeUnits: 5 }, + ), + 'strings exceed', + ); + }); + + it('covers default, partial, undefined, unknown, excessive, and invalid limits', () => { + expect(inspectDocumentEnvelopeIdentity(validEnvelope())).toEqual({ + schemaId: validEnvelope().schemaId, + schemaVersion: 9, + }); + expect( + inspectDocumentEnvelopeIdentity(validEnvelope(), { + maxUtf8Bytes: undefined, + maxJsonTextCodeUnits: undefined, + maxJsonValues: undefined, + maxStringCodeUnits: undefined, + maxNestingDepth: undefined, + }), + ).toEqual({ schemaId: validEnvelope().schemaId, schemaVersion: 9 }); + + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(validEnvelope(), [] as never), + 'plain configuration object', + ); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity(validEnvelope(), { + maxUtf8Bytes: 1, + maxJsonTextCodeUnits: 1, + maxJsonValues: 1, + maxStringCodeUnits: 1, + maxNestingDepth: 1, + extra: 1, + } as never), + 'unsupported fields', + ); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity(validEnvelope(), { + maxUtf8Bytes: 1, + extra: 1, + } as never), + 'unsupported fields', + ); + + for (const invalid of ['1', 0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1]) { + for (const field of [ + 'maxUtf8Bytes', + 'maxJsonTextCodeUnits', + 'maxJsonValues', + 'maxStringCodeUnits', + 'maxNestingDepth', + ] as const) { + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity(validEnvelope(), { + [field]: invalid, + } as never), + 'positive safe integers', + ); + } + } + }); + + it('returns false from hostile Uint8Array brand inspection without leaking the trap', () => { + const { proxy, revoke } = Proxy.revocable(new Uint8Array([1]), {}); + revoke(); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentityBytes(proxy), + 'must be a Uint8Array', + ); + }); + + it('redacts unexpected reflection failures but preserves deliberate public errors', () => { + const hostile = new Proxy(validEnvelope(), { + getPrototypeOf() { + throw new Error('private-proxy-cause'); + }, + }); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(hostile), + 'could not be inspected safely', + ); + + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity({ schemaId: 'x' }), + 'requires schemaId, schemaVersion, and documentJson', + ); + }); +}); From 21cdd450aceb87fe211bcc3a51cc1cebf97af4c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:20:50 +0900 Subject: [PATCH 22/43] fix(envelope): bound complete identity inputs before routing --- src/documentEnvelopeIdentity.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/documentEnvelopeIdentity.ts b/src/documentEnvelopeIdentity.ts index ff6c7b47..4ee5b919 100644 --- a/src/documentEnvelopeIdentity.ts +++ b/src/documentEnvelopeIdentity.ts @@ -120,20 +120,28 @@ function inspectIdentityWithLimits( ); } const inspection = inspectJsonText(source, { - maxValues: Number.MAX_SAFE_INTEGER, - maxDepth: Number.MAX_SAFE_INTEGER, + maxValues: Math.min( + Number.MAX_SAFE_INTEGER, + limits.maxJsonValues + 3, + ), + maxDepth: Math.min( + Number.MAX_SAFE_INTEGER, + limits.maxNestingDepth + 1, + ), }); if (inspection === 'duplicate-object-name') { throw new DocumentEnvelopeError( 'Document envelope identity JSON must not contain duplicate object names', ); } - if ( - inspection === 'value-count-limit' || - inspection === 'nesting-depth-limit' - ) { + if (inspection === 'value-count-limit') { throw new DocumentEnvelopeError( - 'Document envelope identity JSON could not be inspected within limits', + 'Document envelope exceeds the supported JSON value count', + ); + } + if (inspection === 'nesting-depth-limit') { + throw new DocumentEnvelopeError( + 'Document envelope exceeds the supported document nesting depth', ); } try { @@ -192,7 +200,11 @@ function inspectIdentityWithLimits( limits, active: new WeakSet(), }; - validateJsonValue(fields.get('documentJson'), 0, state); + for (const [field, child] of entries) { + if (field !== 'schemaId' && field !== 'schemaVersion') { + validateJsonValue(child, 0, state); + } + } return Object.freeze({ schemaId, schemaVersion }); } From 0b3d3cf51312e630f8f43789bdbc24232d987a2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:21:50 +0900 Subject: [PATCH 23/43] test(envelope): cover preparse and future-field ceilings --- src/documentEnvelopeIdentity.coverage.test.ts | 69 +++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/documentEnvelopeIdentity.coverage.test.ts b/src/documentEnvelopeIdentity.coverage.test.ts index d7939745..4034c24b 100644 --- a/src/documentEnvelopeIdentity.coverage.test.ts +++ b/src/documentEnvelopeIdentity.coverage.test.ts @@ -80,6 +80,28 @@ describe('document envelope identity branch coverage', () => { } }); + it('enforces value-count and nesting ceilings before JSON.parse', () => { + const tooManyValues = JSON.stringify({ + schemaId: 'legacy', + schemaVersion: 1, + documentJson: { one: 1, two: 2 }, + }); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(tooManyValues, { maxJsonValues: 1 }), + 'JSON value count', + ); + + const tooDeep = JSON.stringify({ + schemaId: 'legacy', + schemaVersion: 1, + documentJson: { nested: { deeper: true } }, + }); + expectDocumentEnvelopeError( + () => inspectDocumentEnvelopeIdentity(tooDeep, { maxNestingDepth: 1 }), + 'nesting depth', + ); + }); + it('rejects too many top-level fields under the configured value ceiling', () => { expectDocumentEnvelopeError( () => @@ -95,12 +117,47 @@ describe('document envelope identity branch coverage', () => { ); }); + it('validates inert future fields without returning them', () => { + const identity = inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + futureRoutingHint: { nested: ['safe', true] }, + }); + expect(identity).toEqual({ + schemaId: validEnvelope().schemaId, + schemaVersion: 9, + }); + expect('futureRoutingHint' in identity).toBe(false); + + for (const futureRoutingHint of [ + () => undefined, + Symbol('future'), + Number.NaN, + new Date(), + ]) { + expectDocumentEnvelopeError(() => + inspectDocumentEnvelopeIdentity({ + ...validEnvelope(), + futureRoutingHint, + }), + ); + } + }); + it('rejects invalid schema identifiers and each invalid schema-version class', () => { expectDocumentEnvelopeError( () => inspectDocumentEnvelopeIdentity({ ...validEnvelope(), schemaId: 3 }), 'schemaId must be a string', ); + expectDocumentEnvelopeError( + () => + inspectDocumentEnvelopeIdentity( + { ...validEnvelope(), schemaId: 'x'.repeat(61) }, + { maxStringCodeUnits: 60 }, + ), + 'strings exceed', + ); + for (const schemaVersion of [ '9', Number.NaN, @@ -168,8 +225,8 @@ describe('document envelope identity branch coverage', () => { expectDocumentEnvelopeError( () => inspectDocumentEnvelopeIdentity( - { ...validEnvelope(), documentJson: 'abcdef' }, - { maxStringCodeUnits: 5 }, + { ...validEnvelope(), documentJson: 'x'.repeat(80) }, + { maxStringCodeUnits: 60 }, ), 'strings exceed', ); @@ -178,7 +235,9 @@ describe('document envelope identity branch coverage', () => { it('accepts null-prototype JSON objects', () => { const body = Object.create(null) as Record; body.value = 1; - expect(inspectDocumentEnvelopeIdentity({ ...validEnvelope(), documentJson: body })).toEqual({ + expect( + inspectDocumentEnvelopeIdentity({ ...validEnvelope(), documentJson: body }), + ).toEqual({ schemaId: validEnvelope().schemaId, schemaVersion: 9, }); @@ -291,9 +350,9 @@ describe('document envelope identity branch coverage', () => { inspectDocumentEnvelopeIdentity( { ...validEnvelope(), - abcdef: 1, + ['x'.repeat(61)]: 1, }, - { maxStringCodeUnits: 5 }, + { maxStringCodeUnits: 60 }, ), 'strings exceed', ); From 93c2ced15f6fa79b7a758fd5888c16c61a7d55e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:29:09 +0900 Subject: [PATCH 24/43] fix(envelope): remove unreachable typed-array brand branch --- src/documentEnvelopeIdentity.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/documentEnvelopeIdentity.ts b/src/documentEnvelopeIdentity.ts index 4ee5b919..b176a6b7 100644 --- a/src/documentEnvelopeIdentity.ts +++ b/src/documentEnvelopeIdentity.ts @@ -426,11 +426,7 @@ function assertStringWithinLimit( } function hasUint8ArrayBrand(value: unknown): value is Uint8Array { - try { - return TYPED_ARRAY_TAG_GETTER.call(value) === 'Uint8Array'; - } catch { - return false; - } + return TYPED_ARRAY_TAG_GETTER.call(value) === 'Uint8Array'; } function isPlainObject(value: unknown): value is Record { From 83fbc5f7201f4b16fdb66c5d49026e43d35d18f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 05:58:05 +0900 Subject: [PATCH 25/43] test(envelope): require identity contract documentation --- src/documentEnvelopeIdentityDocumentation.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/documentEnvelopeIdentityDocumentation.test.ts b/src/documentEnvelopeIdentityDocumentation.test.ts index eca84a2a..0606c40a 100644 --- a/src/documentEnvelopeIdentityDocumentation.test.ts +++ b/src/documentEnvelopeIdentityDocumentation.test.ts @@ -12,12 +12,16 @@ describe('document envelope identity documentation', () => { it('keeps identity routing separate from strict current-schema parsing', () => { const guide = read('docs/document-envelope.md'); const doctoring = read('docs/doctoring/envelope-identity-routing.md'); + const contracts = read('docs/CONTRACTS.md'); const adr = read('docs/adr/0015-envelope-schema-migration-routing.md'); for (const source of [guide, doctoring]) { expect(source).toContain('inspectDocumentEnvelopeIdentity'); expect(source).toContain('parseDocumentEnvelope'); } + expect(contracts).toContain('inspectDocumentEnvelopeIdentity()'); + expect(contracts).toContain('inspectDocumentEnvelopeIdentityBytes()'); + expect(contracts).toContain('./envelope-identity'); expect(adr).toMatch(/identity inspector/i); expect(adr).toContain('`parseDocumentEnvelope()` remains strict'); expect(guide).toContain('@contextualwisdomlab/cwl-editor/envelope-identity'); From 0180176477a210b13b0219474a14c80e9c928081 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:00:05 +0900 Subject: [PATCH 26/43] docs(envelope): remove duplicate wording From 70c12a525e5ccb8c2ccf255d396569f81d208fb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:00:29 +0900 Subject: [PATCH 27/43] docs(envelope): clarify duplicate-name policy --- docs/doctoring/envelope-identity-routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/envelope-identity-routing.md b/docs/doctoring/envelope-identity-routing.md index 4c43ddbf..9b6420ed 100644 --- a/docs/doctoring/envelope-identity-routing.md +++ b/docs/doctoring/envelope-identity-routing.md @@ -6,7 +6,7 @@ Tracking: Issue #74 ## Problem boundary -Inkspan's strict `parseDocumentEnvelope()` correctly rejects unknown schema identifiers and versions. That protects current document semantics, but a host still needs a safe way to determine which version-specific migration should receive a complete legacy or future envelope. Asking every host to parse untrusted envelope JSON independently would duplicate duplicate-name, UTF-8, resource-limit, accessor, proxy/reflection, and error-redaction policy at the compatibility boundary. +Inkspan's strict `parseDocumentEnvelope()` correctly rejects unknown schema identifiers and versions. That protects current document semantics, but a host still needs a safe way to determine which version-specific migration should receive a complete legacy or future envelope. Asking every host to parse untrusted envelope JSON independently would replicate duplicate-name, UTF-8, resource-limit, accessor, proxy/reflection, and error-redaction policy at the compatibility boundary. ## Implemented decision From 6def650ea5aa945b0158ef638ea57984095013be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:00:59 +0900 Subject: [PATCH 28/43] docs(envelope): correct identity subpath example --- docs/document-envelope.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/document-envelope.md b/docs/document-envelope.md index 2c043b50..edcb8613 100644 --- a/docs/document-envelope.md +++ b/docs/document-envelope.md @@ -46,17 +46,15 @@ envelope, use the bounded identity-only inspector rather than weakening the strict parser or implementing a second untrusted JSON parser in the host: ```ts -import { - inspectDocumentEnvelopeIdentity, - parseDocumentEnvelope, -} from '@contextualwisdomlab/cwl-editor/envelope-identity'; +import { parseDocumentEnvelope } from '@contextualwisdomlab/cwl-editor'; +import { inspectDocumentEnvelopeIdentity } from '@contextualwisdomlab/cwl-editor/envelope-identity'; const identity = inspectDocumentEnvelopeIdentity(persisted); const migrated = await migrationRegistry.migrate(identity, persisted); const currentEnvelope = parseDocumentEnvelope(migrated); ``` -The same functions are also exported by the root package. The dedicated +The identity functions are also exported by the root package. The dedicated `envelope-identity` subpath is framework-independent for servers, workers, migration jobs, storage adapters, and other non-editor processes. From d5bfe4f330282e5f97c26729f9772580d112a318 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:01:39 +0900 Subject: [PATCH 29/43] docs(prd): distinguish identity routing from rejection --- docs/PRD.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/PRD.md b/docs/PRD.md index b34ed5d7..dac363f2 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -105,7 +105,9 @@ Inkspan does not promise universal HTML/Office round-trip fidelity, arbitrary ex ## Security and privacy requirements -- Untrusted HTML, DOM capabilities, clipboard configuration, form values, Office structures, host callbacks, collaboration updates, unsupported envelope generations, and model proposals fail closed at documented boundaries. +- Untrusted HTML, DOM capabilities, clipboard configuration, form values, Office structures, host callbacks, collaboration updates, and model proposals fail closed at documented boundaries. +- Malformed or unsafe envelope inputs fail closed without partial routing metadata. +- Structurally valid unknown envelope identities return routing metadata only and do not prove current-schema compatibility or migration success. - Active or hidden rich content must not bypass the supported semantic clipboard policy. - Envelope identity output is routing metadata only and must not expose document-bearing source fields or become migration/authorization/durable-write evidence. - Spreadsheet formula-significant input must not silently become executable formulas unless a future explicit trusted formula contract says otherwise. From 1621fe63c5cebc4d9a8ecd77e8f711f6c14bd23f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:02:14 +0900 Subject: [PATCH 30/43] docs(fitness): separate protected baseline from active PR --- docs/DOCUMENTATION_FITNESS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DOCUMENTATION_FITNESS.md b/docs/DOCUMENTATION_FITNESS.md index 8fc0112f..f639d2d5 100644 --- a/docs/DOCUMENTATION_FITNESS.md +++ b/docs/DOCUMENTATION_FITNESS.md @@ -97,6 +97,6 @@ The documentation pack itself is substantially complete for acquisition review, ## 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 migration routing is `implemented_on_active_pr` in PR #84. Cross-engine browser assurance remains `planned`. A physical relational ERD is `not_applicable` because Inkspan deliberately owns no application persistence. +PRD, TRD, Architecture, ADR, UML, conceptual ERD/data model, contracts, threat model, test strategy, operability, security disclosure, and traceability are `present_current` for the durable product and accepted/planned architecture decisions reconstructed from the conversation and live repository. SafeClipboard, autosave lifecycle observation, the security disclosure lifecycle, toolbar shortcut accessibility metadata, SSR/native-form serialization, revision-scoped selection evidence, and document-transition evidence are `implemented_on_protected_main`. The pre-existing canonical documentation graph is integrated on protected `main`; PR #84's envelope-routing changes remain `implemented_on_active_pr` evidence until protected integration. Cross-engine browser assurance remains `planned`. A physical relational ERD is `not_applicable` because Inkspan deliberately owns no application persistence. -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. +No material product architecture decision identified by this review remains only in chat or issue prose. Accordingly, the **pre-existing documentation graph is a protected-main canonical baseline** and is sufficient for acquisition reconstruction under the current product boundary; active-PR deltas do not become protected-main authority merely because this branch documents them. Product/release readiness must continue to be evaluated independently of documentation completeness. From f242774f3748deae1e04482932b89f1fb34948e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:02:59 +0900 Subject: [PATCH 31/43] docs(contracts): bind envelope identity public API --- docs/CONTRACTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 34b48dd7..e402a024 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -20,7 +20,9 @@ Package exports, declaration files, runtime support, and packed ESM/CommonJS/str The **document envelope** is a versioned boundary carrying schema identity and supported document content. Current-schema parsing validates strict JSON/UTF-8 and active schema requirements before canonicalization or hashing. Unknown schema migration remains host-owned. -Canonical revision evidence is content-equality metadata only. It is not an actor identity, tenant identity, authorization grant, signature, timestamp, durable write receipt, or bearer credential. When a future identity-only migration-routing helper is implemented, it may expose bounded schema identity without becoming a permissive parser or migration executor. +PR #84 adds an active-PR identity-routing contract through `inspectDocumentEnvelopeIdentity()` and `inspectDocumentEnvelopeIdentityBytes()`, exported by the root package and the framework-independent `./envelope-identity` subpath. The inspectors validate a complete bounded envelope and return only frozen `{ schemaId, schemaVersion }` routing metadata. They do not return `documentJson`, accept unknown document semantics as current, execute a migration, authorize access, or claim durable persistence. Until protected integration this is `implemented_on_active_pr` evidence rather than a shipped contract. + +Canonical revision evidence is content-equality metadata only. It is not an actor identity, tenant identity, authorization grant, signature, timestamp, durable write receipt, or bearer credential. Identity routing does not weaken the strict current-schema parser or transfer schema-registry, migration, persistence, rollback, or recovery authority from the host. Compatibility rule: a new schema or incompatible interpretation requires an explicit new versioned contract and migration story; existing persisted semantics are not silently redefined. From 3bc63b1b1cdc9ac978c3a2323b4b350d72dac07b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:03:16 +0900 Subject: [PATCH 32/43] refactor(envelope): reuse canonical identity types --- src/envelope-identity/index.ts | 55 ++++------------------------------ 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/src/envelope-identity/index.ts b/src/envelope-identity/index.ts index 6a812928..9a32392d 100644 --- a/src/envelope-identity/index.ts +++ b/src/envelope-identity/index.ts @@ -1,8 +1,3 @@ -import { - inspectDocumentEnvelopeIdentity as inspectIdentityInternal, - inspectDocumentEnvelopeIdentityBytes as inspectIdentityBytesInternal, -} from '../documentEnvelopeIdentity.js'; - /** * Framework-independent document-envelope identity routing entrypoint. * @@ -10,47 +5,9 @@ import { * network clients, persistence adapters, credentials, and model SDKs. It returns * only validated schema identity so a host can select its own migration path. */ - -/** Optional resource ceilings shared with Inkspan's strict envelope boundary. */ -export interface DocumentEnvelopeLimits { - /** Maximum raw UTF-8 byte length before decoding. */ - readonly maxUtf8Bytes?: number; - /** Maximum raw JSON-text length measured in JavaScript UTF-16 code units. */ - readonly maxJsonTextCodeUnits?: number; - /** Maximum total scalar and container values inside `documentJson`. */ - readonly maxJsonValues?: number; - /** Maximum decoded string value or object-name length. */ - readonly maxStringCodeUnits?: number; - /** Maximum nested object/array depth below `documentJson`. */ - readonly maxNestingDepth?: number; -} - -/** Frozen schema identity used only for host-owned migration routing. */ -export interface CwlEditorDocumentEnvelopeIdentity { - /** Schema identifier supplied by the complete envelope. */ - readonly schemaId: string; - /** Positive safe-integer schema version supplied by the complete envelope. */ - readonly schemaVersion: number; -} - -/** Inspect an envelope object or complete JSON text without validating current document semantics. */ -export function inspectDocumentEnvelopeIdentity( - source: unknown, - limits?: DocumentEnvelopeLimits, -): Readonly { - return inspectIdentityInternal( - source, - limits as Parameters[1], - ) as Readonly; -} - -/** Inspect strict UTF-8 envelope bytes without validating current document semantics. */ -export function inspectDocumentEnvelopeIdentityBytes( - source: unknown, - limits?: DocumentEnvelopeLimits, -): Readonly { - return inspectIdentityBytesInternal( - source, - limits as Parameters[1], - ) as Readonly; -} +export { + inspectDocumentEnvelopeIdentity, + inspectDocumentEnvelopeIdentityBytes, +} from '../documentEnvelopeIdentity.js'; +export type { CwlEditorDocumentEnvelopeIdentity } from '../documentEnvelopeIdentity.js'; +export type { DocumentEnvelopeLimits } from '../documentEnvelope.js'; From bf030a65d49b324f07f07a0c050932669fc57b9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:03:59 +0900 Subject: [PATCH 33/43] refactor(envelope): centralize identity value ceiling --- src/documentEnvelopeIdentity.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/documentEnvelopeIdentity.ts b/src/documentEnvelopeIdentity.ts index b176a6b7..137d19e6 100644 --- a/src/documentEnvelopeIdentity.ts +++ b/src/documentEnvelopeIdentity.ts @@ -20,6 +20,7 @@ const LIMIT_FIELDS = new Set([ 'maxStringCodeUnits', 'maxNestingDepth', ]); +const IDENTITY_ENVELOPE_OVERHEAD_VALUES = 3; const UTF8_BYTE_ORDER_MARK = 0xefbbbf; const TYPED_ARRAY_TAG_GETTER = Object.getOwnPropertyDescriptor( Object.getPrototypeOf(Uint8Array.prototype), @@ -112,6 +113,10 @@ function inspectIdentityWithLimits( source: unknown, limits: ResolvedIdentityLimits, ): Readonly { + const envelopeValueCeiling = Math.min( + Number.MAX_SAFE_INTEGER, + limits.maxJsonValues + IDENTITY_ENVELOPE_OVERHEAD_VALUES, + ); let value: unknown = source; if (typeof source === 'string') { if (source.length > limits.maxJsonTextCodeUnits) { @@ -120,10 +125,7 @@ function inspectIdentityWithLimits( ); } const inspection = inspectJsonText(source, { - maxValues: Math.min( - Number.MAX_SAFE_INTEGER, - limits.maxJsonValues + 3, - ), + maxValues: envelopeValueCeiling, maxDepth: Math.min( Number.MAX_SAFE_INTEGER, limits.maxNestingDepth + 1, @@ -161,7 +163,7 @@ function inspectIdentityWithLimits( const entries = readDataEntries( value, - Math.min(Number.MAX_SAFE_INTEGER, limits.maxJsonValues + 3), + envelopeValueCeiling, limits.maxStringCodeUnits, 'Document envelope identity contains too many top-level fields', ); From 52cb623ed6ea87d8fb9a6e96342e014b0b31c62f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:04:28 +0900 Subject: [PATCH 34/43] test(package): enforce framework-free identity bundles --- ...ramework-free-envelope-identity-package.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/verify-framework-free-envelope-identity-package.mjs b/scripts/verify-framework-free-envelope-identity-package.mjs index 928a3252..aaa31d6c 100644 --- a/scripts/verify-framework-free-envelope-identity-package.mjs +++ b/scripts/verify-framework-free-envelope-identity-package.mjs @@ -27,6 +27,8 @@ const packageDirectory = join( 'node_modules', ...packageJson.name.split('/'), ); +const frameworkModulePattern = + /(?:['"](?:react(?:-dom)?(?:\/[^'"]*)?|@tiptap\/[^'"]+|prosemirror-[^'"]+|yjs(?:\/[^'"]*)?)['"])/u; function run(command, argumentsList, cwd = repositoryRoot) { return execFileSync(command, argumentsList, { @@ -60,6 +62,21 @@ function preparePackage() { ); } +function verifyFrameworkFreeBundles() { + for (const filename of [ + 'cwl-envelope-identity.js', + 'cwl-envelope-identity.cjs', + ]) { + const bundlePath = join(packageDirectory, 'dist', filename); + const bundleSource = readFileSync(bundlePath, 'utf8'); + assert.doesNotMatch( + bundleSource, + frameworkModulePattern, + `${filename} must not reference framework dependencies`, + ); + } +} + function verifyRuntimeConsumers() { const esmPath = join(consumerDirectory, 'consumer.mjs'); writeFileSync( @@ -168,6 +185,7 @@ void byteIdentity; try { preparePackage(); + verifyFrameworkFreeBundles(); verifyRuntimeConsumers(); verifyDeclarationConsumer(); console.log( From e109688f430742f7404888d745837ed343251151 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:05:11 +0900 Subject: [PATCH 35/43] test(docs): decouple lifecycle from PR number --- src/canonicalProductDocumentation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canonicalProductDocumentation.test.ts b/src/canonicalProductDocumentation.test.ts index 7a839ffe..f2a98bae 100644 --- a/src/canonicalProductDocumentation.test.ts +++ b/src/canonicalProductDocumentation.test.ts @@ -109,7 +109,7 @@ describe('canonical product documentation graph', () => { expect(currentScope).not.toMatch(/open development lines include[^.]*lifecycle observation/u); expect(trd).toContain('Autosave lifecycle observation is implemented on protected `main`'); expect(trd).toContain('SafeClipboard is implemented on protected `main`'); - expect(trd).toContain('Envelope identity routing is `implemented_on_active_pr` in PR #84'); + expect(trd).toMatch(/Envelope identity routing is `implemented_on_active_pr`/u); expect(fitness).toContain('Autosave lifecycle observation'); expect(fitness).toMatch(/Autosave lifecycle observation[^\n]*implemented_on_protected_main/u); expect(fitness).toMatch(/Envelope schema identity \/ migration routing[^\n]*implemented_on_active_pr/u); From fac77f94a94794eee00a64a62e825c1f297703a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:05:44 +0900 Subject: [PATCH 36/43] test(docs): assert semantic migration maturity --- src/canonicalDocumentationDecisionCoverage.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/canonicalDocumentationDecisionCoverage.test.ts b/src/canonicalDocumentationDecisionCoverage.test.ts index 9ba8d833..79dcd3b9 100644 --- a/src/canonicalDocumentationDecisionCoverage.test.ts +++ b/src/canonicalDocumentationDecisionCoverage.test.ts @@ -60,7 +60,9 @@ describe('canonical architecture decision coverage', () => { expect(fitness).toContain('implemented_on_protected_main'); expect(fitness).toContain('implemented_on_active_pr'); expect(fitness).toContain('planned'); - expect(fitness).toContain('PR #84'); + expect(fitness).toMatch( + /Envelope schema identity \/ migration routing[^\n]*implemented_on_active_pr/u, + ); expect(fitness).toContain('Issue #66'); }); @@ -73,7 +75,9 @@ describe('canonical architecture decision coverage', () => { expect(traceability).toContain('Envelope version routing'); expect(traceability).toContain('Cross-engine release assurance'); expect(traceability).toContain('RFC 7493'); - expect(traceability).toContain('PR #84'); + expect(traceability).toMatch( + /Envelope version routing[^\n]*Active-PR evidence is not shipped authority/u, + ); }); it('keeps the conceptual ERD explicit without inventing Inkspan persistence', () => { From f8241528525e7964abb141d1d62c82958d76f638 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:11:33 +0900 Subject: [PATCH 37/43] refactor(envelope): extract framework-free limits type --- src/documentEnvelopeLimits.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/documentEnvelopeLimits.ts diff --git a/src/documentEnvelopeLimits.ts b/src/documentEnvelopeLimits.ts new file mode 100644 index 00000000..4e6c0f73 --- /dev/null +++ b/src/documentEnvelopeLimits.ts @@ -0,0 +1,13 @@ +/** Optional resource ceilings applied while inspecting an Inkspan document envelope. */ +export interface DocumentEnvelopeLimits { + /** Maximum raw UTF-8 byte length before decoding. */ + readonly maxUtf8Bytes?: number; + /** Maximum raw JSON-text length measured in JavaScript UTF-16 code units. */ + readonly maxJsonTextCodeUnits?: number; + /** Maximum total number of scalar and container values in `documentJson`. */ + readonly maxJsonValues?: number; + /** Maximum length of any decoded string value or object name. */ + readonly maxStringCodeUnits?: number; + /** Maximum nested object/array depth below the document root. */ + readonly maxNestingDepth?: number; +} From 5308ecf04ca91932cfaa4f56eb53e0e1f3b052bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:12:30 +0900 Subject: [PATCH 38/43] fix(package): decouple identity limits declarations --- src/documentEnvelopeIdentity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documentEnvelopeIdentity.ts b/src/documentEnvelopeIdentity.ts index 137d19e6..74155a43 100644 --- a/src/documentEnvelopeIdentity.ts +++ b/src/documentEnvelopeIdentity.ts @@ -2,8 +2,8 @@ import { inspectJsonText } from './jsonObjectNameScanner.js'; import { DEFAULT_DOCUMENT_ENVELOPE_LIMITS, DocumentEnvelopeError, - type DocumentEnvelopeLimits, } from './documentEnvelope.js'; +import type { DocumentEnvelopeLimits } from './documentEnvelopeLimits.js'; interface ResolvedIdentityLimits { readonly maxUtf8Bytes: number; From 45617c9c02d71a1e463af76300cacace6b876698 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:12:46 +0900 Subject: [PATCH 39/43] fix(package): keep identity limits framework-free --- src/envelope-identity/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/envelope-identity/index.ts b/src/envelope-identity/index.ts index 9a32392d..98751f5f 100644 --- a/src/envelope-identity/index.ts +++ b/src/envelope-identity/index.ts @@ -10,4 +10,4 @@ export { inspectDocumentEnvelopeIdentityBytes, } from '../documentEnvelopeIdentity.js'; export type { CwlEditorDocumentEnvelopeIdentity } from '../documentEnvelopeIdentity.js'; -export type { DocumentEnvelopeLimits } from '../documentEnvelope.js'; +export type { DocumentEnvelopeLimits } from '../documentEnvelopeLimits.js'; From 77b47792b4a277cc81e4f6c4d9d23a0d719fa55a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:13:07 +0900 Subject: [PATCH 40/43] build(envelope): emit framework-free limits declaration --- vite.envelope-identity.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.envelope-identity.config.ts b/vite.envelope-identity.config.ts index 884c3d31..5a3045f4 100644 --- a/vite.envelope-identity.config.ts +++ b/vite.envelope-identity.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ include: [ 'src/envelope-identity', 'src/documentEnvelopeIdentity.ts', + 'src/documentEnvelopeLimits.ts', 'src/documentEnvelope.ts', 'src/jsonObjectNameScanner.ts', ], From 8dc4f19faab55f245440c0f9b39d98ac5e6cde52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:16:31 +0900 Subject: [PATCH 41/43] docs(changelog): record envelope identity routing --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1c20d0..c8f74c34 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 From cfc912759ed4dcb6255811a6cc1473e1f8c9eede Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:19:13 +0900 Subject: [PATCH 42/43] refactor(envelope): share limits type without framework coupling --- src/documentEnvelope.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/documentEnvelope.ts b/src/documentEnvelope.ts index b76311fc..9eceace5 100644 --- a/src/documentEnvelope.ts +++ b/src/documentEnvelope.ts @@ -1,6 +1,9 @@ import type { JSONContent } from '@tiptap/core'; +import type { DocumentEnvelopeLimits } from './documentEnvelopeLimits.js'; import { inspectJsonText } from './jsonObjectNameScanner.js'; +export type { DocumentEnvelopeLimits } from './documentEnvelopeLimits.js'; + /** Canonical identifier for Inkspan's first portable document envelope. */ export const DOCUMENT_ENVELOPE_SCHEMA_ID = 'https://inkspan.io/schemas/document-envelope/v1' as const; @@ -8,20 +11,6 @@ export const DOCUMENT_ENVELOPE_SCHEMA_ID = /** Current document-envelope schema version. */ export const DOCUMENT_ENVELOPE_SCHEMA_VERSION = 1 as const; -/** Optional resource ceilings applied while inspecting an envelope. */ -export interface DocumentEnvelopeLimits { - /** Maximum raw UTF-8 byte length before decoding. */ - readonly maxUtf8Bytes?: number; - /** Maximum raw JSON-text length measured in JavaScript UTF-16 code units. */ - readonly maxJsonTextCodeUnits?: number; - /** Maximum total number of scalar and container values in `documentJson`. */ - readonly maxJsonValues?: number; - /** Maximum length of any decoded string value or object name. */ - readonly maxStringCodeUnits?: number; - /** Maximum nested object/array depth below the document root. */ - readonly maxNestingDepth?: number; -} - interface ResolvedDocumentEnvelopeLimits { readonly maxUtf8Bytes: number; readonly maxJsonTextCodeUnits: number; From 18047c6850b55318791a4fbb728eafdbde436574 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 06:19:47 +0900 Subject: [PATCH 43/43] test(docs): bind envelope identity changelog --- src/documentEnvelopeIdentityDocumentation.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/documentEnvelopeIdentityDocumentation.test.ts b/src/documentEnvelopeIdentityDocumentation.test.ts index 0606c40a..594c377d 100644 --- a/src/documentEnvelopeIdentityDocumentation.test.ts +++ b/src/documentEnvelopeIdentityDocumentation.test.ts @@ -13,6 +13,7 @@ describe('document envelope identity documentation', () => { const guide = read('docs/document-envelope.md'); const doctoring = read('docs/doctoring/envelope-identity-routing.md'); const contracts = read('docs/CONTRACTS.md'); + const changelog = read('CHANGELOG.md'); const adr = read('docs/adr/0015-envelope-schema-migration-routing.md'); for (const source of [guide, doctoring]) { @@ -22,6 +23,8 @@ describe('document envelope identity documentation', () => { expect(contracts).toContain('inspectDocumentEnvelopeIdentity()'); expect(contracts).toContain('inspectDocumentEnvelopeIdentityBytes()'); expect(contracts).toContain('./envelope-identity'); + expect(changelog).toContain('inspectDocumentEnvelopeIdentity()'); + expect(changelog).toContain('framework-independent `envelope-identity`'); expect(adr).toMatch(/identity inspector/i); expect(adr).toContain('`parseDocumentEnvelope()` remains strict'); expect(guide).toContain('@contextualwisdomlab/cwl-editor/envelope-identity');