From 8c5246ee3c80fdc29f17ee55d5d5cc9c823408fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:12:57 +0900 Subject: [PATCH 01/27] test(reliability): require autosave array preflight --- .../evidenceValidationArrayPreflight.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/autosave/evidenceValidationArrayPreflight.test.ts diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts new file mode 100644 index 00000000..76058fc2 --- /dev/null +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { isDeeplyFrozenDocumentJson } from './evidenceValidation.js'; + +describe('autosave detached evidence array resource preflight', () => { + it('rejects an impossible array length before explicit own-key enumeration', () => { + const oversizedArray = new Array(1_000_001); + Object.freeze(oversizedArray); + + const ownKeys = vi.spyOn(Reflect, 'ownKeys'); + try { + expect(isDeeplyFrozenDocumentJson(oversizedArray)).toBe(false); + expect(ownKeys).not.toHaveBeenCalledWith(oversizedArray); + } finally { + ownKeys.mockRestore(); + } + }); +}); From e0f4f54a4121daba7521ec2667274ea03bb9183e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:18:52 +0900 Subject: [PATCH 02/27] fix(reliability): preflight autosave array length --- src/autosave/evidenceValidation.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index 1fb5fd71..d8e3beec 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -138,6 +138,9 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { const childDepth = currentEntry.depth + 1; if (Array.isArray(currentValue)) { const length = currentValue.length; + const remainingValueCapacity = + MAX_AUTOSAVE_EVIDENCE_JSON_VALUES - inspectedValueCount; + if (length > remainingValueCapacity) return false; const ownKeys = Reflect.ownKeys(currentValue); if (ownKeys.length !== length + 1) return false; for (let index = 0; index < length; index += 1) { From d7d8d879941fb08ec0597f394cc4d3a6e629a7de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:08:50 +0900 Subject: [PATCH 03/27] test(reliability): preflight detached autosave digest length --- .../detachedDigestResourceBoundary.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/autosave/detachedDigestResourceBoundary.test.ts diff --git a/src/autosave/detachedDigestResourceBoundary.test.ts b/src/autosave/detachedDigestResourceBoundary.test.ts new file mode 100644 index 00000000..187eaf11 --- /dev/null +++ b/src/autosave/detachedDigestResourceBoundary.test.ts @@ -0,0 +1,36 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { createDetachedAutosaveRevisionEvidence } from './evidenceValidation.js'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +function createFrozenEvidence(digestHex: string): unknown { + const documentJson = Object.freeze({ + type: 'doc', + content: Object.freeze([]), + }); + const envelope = Object.freeze({ + schemaId: 'https://inkspan.io/schemas/document-envelope/v1', + schemaVersion: 1, + documentJson, + }); + const revision = Object.freeze({ + algorithm: 'SHA-256', + digestHex, + strongEntityTag: `"sha256-${digestHex}"`, + }); + return Object.freeze({ envelope, revision }); +} + +describe('detached autosave digest resource preflight', () => { + it('rejects an impossible SHA-256 digest length before regex scanning', () => { + const regexTest = vi.spyOn(RegExp.prototype, 'test'); + + expect( + createDetachedAutosaveRevisionEvidence(createFrozenEvidence('a'.repeat(65))), + ).toBeNull(); + expect(regexTest).not.toHaveBeenCalled(); + }); +}); From 6d75be1826442531952c51698bf19e5ab54cfa53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 23:14:36 +0900 Subject: [PATCH 04/27] fix(reliability): preflight detached autosave digest length --- src/autosave/evidenceValidation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index d8e3beec..bd7d78d8 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -237,6 +237,7 @@ export function createDetachedAutosaveRevisionEvidence( revisionRecord === null || revisionRecord.algorithm !== 'SHA-256' || typeof revisionRecord.digestHex !== 'string' || + revisionRecord.digestHex.length !== 64 || !LOWERCASE_SHA256_DIGEST.test(revisionRecord.digestHex) || typeof revisionRecord.strongEntityTag !== 'string' || revisionRecord.strongEntityTag !== From c55519c1e4aee9e4836e7530bcda3d9e02f183cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 08:01:46 +0900 Subject: [PATCH 05/27] test(reliability): expose autosave array length proxy trap --- .../evidenceValidationArrayPreflight.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 76058fc2..aea23a15 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -15,4 +15,18 @@ describe('autosave detached evidence array resource preflight', () => { ownKeys.mockRestore(); } }); + + it('does not execute array length get traps while validating frozen evidence', () => { + const target = Object.freeze([1]); + let lengthRead = false; + const proxiedArray = new Proxy(target, { + get(currentTarget, property, receiver) { + if (property === 'length') lengthRead = true; + return Reflect.get(currentTarget, property, receiver); + }, + }); + + expect(isDeeplyFrozenDocumentJson(proxiedArray)).toBe(true); + expect(lengthRead).toBe(false); + }); }); From ca8da4a18a81ae28e5b9a9e236dca1cc164a7794 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 09:33:16 +0900 Subject: [PATCH 06/27] fix(reliability): avoid array length getter execution --- src/autosave/evidenceValidation.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index bd7d78d8..3fde3abc 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -137,7 +137,19 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { const childDepth = currentEntry.depth + 1; if (Array.isArray(currentValue)) { - const length = currentValue.length; + const lengthDescriptor = Object.getOwnPropertyDescriptor( + currentValue, + 'length', + ); + if ( + lengthDescriptor === undefined || + lengthDescriptor.enumerable || + !Object.prototype.hasOwnProperty.call(lengthDescriptor, 'value') || + typeof lengthDescriptor.value !== 'number' + ) { + return false; + } + const length = lengthDescriptor.value; const remainingValueCapacity = MAX_AUTOSAVE_EVIDENCE_JSON_VALUES - inspectedValueCount; if (length > remainingValueCapacity) return false; From cba0ecdac7cc57e165f7793f284c213478a7f9de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 10:33:11 +0900 Subject: [PATCH 07/27] fix(autosave): remove unreachable array descriptor branches --- src/autosave/evidenceValidation.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index 3fde3abc..872181b3 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -137,19 +137,10 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { const childDepth = currentEntry.depth + 1; if (Array.isArray(currentValue)) { - const lengthDescriptor = Object.getOwnPropertyDescriptor( + const length = Object.getOwnPropertyDescriptor( currentValue, 'length', - ); - if ( - lengthDescriptor === undefined || - lengthDescriptor.enumerable || - !Object.prototype.hasOwnProperty.call(lengthDescriptor, 'value') || - typeof lengthDescriptor.value !== 'number' - ) { - return false; - } - const length = lengthDescriptor.value; + )!.value as number; const remainingValueCapacity = MAX_AUTOSAVE_EVIDENCE_JSON_VALUES - inspectedValueCount; if (length > remainingValueCapacity) return false; @@ -253,7 +244,7 @@ export function createDetachedAutosaveRevisionEvidence( !LOWERCASE_SHA256_DIGEST.test(revisionRecord.digestHex) || typeof revisionRecord.strongEntityTag !== 'string' || revisionRecord.strongEntityTag !== - `"sha256-${revisionRecord.digestHex}"` + `\"sha256-${revisionRecord.digestHex}\"` ) { return null; } From 83b7d7c30547aa43ab82aa8579fab3691b53de63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 10:34:05 +0900 Subject: [PATCH 08/27] fix(autosave): preserve entity-tag source spelling --- src/autosave/evidenceValidation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index 872181b3..dc8c7c1c 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -244,7 +244,7 @@ export function createDetachedAutosaveRevisionEvidence( !LOWERCASE_SHA256_DIGEST.test(revisionRecord.digestHex) || typeof revisionRecord.strongEntityTag !== 'string' || revisionRecord.strongEntityTag !== - `\"sha256-${revisionRecord.digestHex}\"` + `"sha256-${revisionRecord.digestHex}"` ) { return null; } From 286d41e4e0193461f9f8cadefd8181293fbf03f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 15:32:22 +0900 Subject: [PATCH 09/27] test(autosave): preflight over-depth array children --- .../evidenceValidationArrayPreflight.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index aea23a15..82ec40a9 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -29,4 +29,27 @@ describe('autosave detached evidence array resource preflight', () => { expect(isDeeplyFrozenDocumentJson(proxiedArray)).toBe(true); expect(lengthRead).toBe(false); }); + + it('rejects over-depth array children before reading their descriptors', () => { + const deepestArray: readonly unknown[] = Object.freeze([null]); + let root: readonly unknown[] = deepestArray; + for (let depth = 0; depth < 128; depth += 1) { + root = Object.freeze([root]); + } + + const getOwnPropertyDescriptor = vi.spyOn( + Object, + 'getOwnPropertyDescriptor', + ); + try { + expect(isDeeplyFrozenDocumentJson(root)).toBe(false); + expect( + getOwnPropertyDescriptor.mock.calls.some( + ([value, property]) => value === deepestArray && property === '0', + ), + ).toBe(false); + } finally { + getOwnPropertyDescriptor.mockRestore(); + } + }); }); From 38d7568a52be2d7564ecc7fe4fdc24b8ca302537 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 15:36:09 +0900 Subject: [PATCH 10/27] fix(autosave): preflight over-depth array children --- src/autosave/evidenceValidation.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index dc8c7c1c..36b7ada1 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -144,6 +144,12 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { const remainingValueCapacity = MAX_AUTOSAVE_EVIDENCE_JSON_VALUES - inspectedValueCount; if (length > remainingValueCapacity) return false; + if ( + length > 0 && + childDepth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH + ) { + return false; + } const ownKeys = Reflect.ownKeys(currentValue); if (ownKeys.length !== length + 1) return false; for (let index = 0; index < length; index += 1) { From d421cd20bce11e7a4db9bee97183b130dbd626a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 16:07:29 +0900 Subject: [PATCH 11/27] test(autosave): preflight over-depth object children --- .../evidenceValidationArrayPreflight.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 82ec40a9..8d7542cc 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -52,4 +52,27 @@ describe('autosave detached evidence array resource preflight', () => { getOwnPropertyDescriptor.mockRestore(); } }); + + it('rejects over-depth object children before reading their descriptors', () => { + const deepestObject = Object.freeze({ child: null }); + let root: Readonly<{ child: unknown }> = deepestObject; + for (let depth = 0; depth < 128; depth += 1) { + root = Object.freeze({ child: root }); + } + + const getOwnPropertyDescriptor = vi.spyOn( + Object, + 'getOwnPropertyDescriptor', + ); + try { + expect(isDeeplyFrozenDocumentJson(root)).toBe(false); + expect( + getOwnPropertyDescriptor.mock.calls.some( + ([value, property]) => value === deepestObject && property === 'child', + ), + ).toBe(false); + } finally { + getOwnPropertyDescriptor.mockRestore(); + } + }); }); From ccae833493bb95f9f37aebb370fc03404663ce61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 16:10:24 +0900 Subject: [PATCH 12/27] fix(autosave): preflight over-depth object children --- src/autosave/evidenceValidation.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index 36b7ada1..c9b2644d 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -174,7 +174,14 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { const prototype = Object.getPrototypeOf(currentValue); if (prototype !== Object.prototype && prototype !== null) return false; - for (const key of Reflect.ownKeys(currentValue)) { + const ownKeys = Reflect.ownKeys(currentValue); + if ( + ownKeys.length > 0 && + childDepth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH + ) { + return false; + } + for (const key of ownKeys) { if (typeof key !== 'string') return false; const descriptor = Object.getOwnPropertyDescriptor(currentValue, key); if ( From f18568af5e7af72ed76d8a3ad55a65854f994736 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 16:15:07 +0900 Subject: [PATCH 13/27] test(autosave): cover empty object at depth ceiling --- src/autosave/evidenceValidationArrayPreflight.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 8d7542cc..1cea69ec 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -75,4 +75,14 @@ describe('autosave detached evidence array resource preflight', () => { getOwnPropertyDescriptor.mockRestore(); } }); + + it('accepts an empty object exactly at the maximum nesting depth', () => { + const deepestObject = Object.freeze({}); + let root: Readonly> = deepestObject; + for (let depth = 0; depth < 128; depth += 1) { + root = Object.freeze({ child: root }); + } + + expect(isDeeplyFrozenDocumentJson(root)).toBe(true); + }); }); From 60ea3c12dbdf7abe677c69544ccf81bb8e2bc253 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 16:23:05 +0900 Subject: [PATCH 14/27] fix(autosave): avoid over-depth object descriptor reads --- src/autosave/evidenceValidation.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index c9b2644d..28c4f138 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -174,14 +174,8 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { const prototype = Object.getPrototypeOf(currentValue); if (prototype !== Object.prototype && prototype !== null) return false; - const ownKeys = Reflect.ownKeys(currentValue); - if ( - ownKeys.length > 0 && - childDepth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH - ) { - return false; - } - for (const key of ownKeys) { + for (const key of Reflect.ownKeys(currentValue)) { + if (childDepth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH) return false; if (typeof key !== 'string') return false; const descriptor = Object.getOwnPropertyDescriptor(currentValue, key); if ( From 7138816b6f8bea3c12da6c280e0278ade6206b70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 07:52:22 +0900 Subject: [PATCH 15/27] fix(autosave): enforce traversal budget before enqueue --- src/autosave/evidenceValidation.ts | 51 ++++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index 28c4f138..1840e98b 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -18,6 +18,10 @@ interface JsonTraversalEntry { readonly depth: number; } +type JsonContainerChildren = + | Readonly<{ kind: 'array'; length: number }> + | Readonly<{ kind: 'object'; keys: (string | symbol)[] }>; + /** Detached evidence shape returned to the public autosave queue. */ export interface DetachedDocumentAutosaveRevisionEvidence { /** Detached active-schema document envelope. */ @@ -105,12 +109,6 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { while (pendingEntries.length > 0) { const currentEntry = pendingEntries.pop() as JsonTraversalEntry; inspectedValueCount += 1; - if ( - inspectedValueCount > MAX_AUTOSAVE_EVIDENCE_JSON_VALUES || - currentEntry.depth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH - ) { - return false; - } const currentValue = currentEntry.value; if ( @@ -136,23 +134,37 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { visitedContainers.add(currentValue); const childDepth = currentEntry.depth + 1; + let children: JsonContainerChildren; if (Array.isArray(currentValue)) { const length = Object.getOwnPropertyDescriptor( currentValue, 'length', )!.value as number; - const remainingValueCapacity = - MAX_AUTOSAVE_EVIDENCE_JSON_VALUES - inspectedValueCount; - if (length > remainingValueCapacity) return false; - if ( - length > 0 && - childDepth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH - ) { - return false; - } + children = { kind: 'array', length }; + } else { + const prototype = Object.getPrototypeOf(currentValue); + if (prototype !== Object.prototype && prototype !== null) return false; + children = { kind: 'object', keys: Reflect.ownKeys(currentValue) }; + } + + const childCount = + children.kind === 'array' ? children.length : children.keys.length; + const remainingValueCapacity = + MAX_AUTOSAVE_EVIDENCE_JSON_VALUES - + inspectedValueCount - + pendingEntries.length; + if (childCount > remainingValueCapacity) return false; + if ( + childCount > 0 && + childDepth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH + ) { + return false; + } + + if (children.kind === 'array') { const ownKeys = Reflect.ownKeys(currentValue); - if (ownKeys.length !== length + 1) return false; - for (let index = 0; index < length; index += 1) { + if (ownKeys.length !== children.length + 1) return false; + for (let index = 0; index < children.length; index += 1) { const descriptor = Object.getOwnPropertyDescriptor( currentValue, String(index), @@ -172,10 +184,7 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { continue; } - const prototype = Object.getPrototypeOf(currentValue); - if (prototype !== Object.prototype && prototype !== null) return false; - for (const key of Reflect.ownKeys(currentValue)) { - if (childDepth > MAX_AUTOSAVE_EVIDENCE_NESTING_DEPTH) return false; + for (const key of children.keys) { if (typeof key !== 'string') return false; const descriptor = Object.getOwnPropertyDescriptor(currentValue, key); if ( From 245b7c6059ac5b7140f97374c51d57826cedee4b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 20:53:49 -0700 Subject: [PATCH 16/27] test(reliability): preflight missing autosave evidence fields --- .../evidenceValidationArrayPreflight.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 1cea69ec..8bb5f2f5 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; -import { isDeeplyFrozenDocumentJson } from './evidenceValidation.js'; +import { + createDetachedAutosaveRevisionEvidence, + isDeeplyFrozenDocumentJson, +} from './evidenceValidation.js'; describe('autosave detached evidence array resource preflight', () => { it('rejects an impossible array length before explicit own-key enumeration', () => { @@ -85,4 +88,18 @@ describe('autosave detached evidence array resource preflight', () => { expect(isDeeplyFrozenDocumentJson(root)).toBe(true); }); + + it('rejects missing record members before whole-record key enumeration', () => { + const incompleteEvidence = Object.freeze({ envelope: null }); + let ownKeysCalls = 0; + const proxiedEvidence = new Proxy(incompleteEvidence, { + ownKeys(target) { + ownKeysCalls += 1; + return Reflect.ownKeys(target); + }, + }); + + expect(createDetachedAutosaveRevisionEvidence(proxiedEvidence)).toBeNull(); + expect(ownKeysCalls).toBe(0); + }); }); From 57baa55e8e74acd6f0cf23e26ce9f850578deeab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 20:57:58 -0700 Subject: [PATCH 17/27] fix(reliability): preflight required autosave evidence fields --- src/autosave/evidenceValidation.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index 1840e98b..235948fe 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -50,13 +50,16 @@ function readExactFrozenDataRecord( expectedKeys: readonly string[], ): ExactDataRecord | null { try { - if ( - typeof value !== 'object' || - value === null || - !Object.isFrozen(value) - ) { - return null; + if (typeof value !== 'object' || value === null) return null; + + for (const expectedKey of expectedKeys) { + if (Object.getOwnPropertyDescriptor(value, expectedKey) === undefined) { + return null; + } } + + if (!Object.isFrozen(value)) return null; + const ownKeys = Reflect.ownKeys(value); if ( ownKeys.length !== expectedKeys.length || From 385f4f031ae16fc5c4ca338195cc46d8f0130069 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:00:31 -0700 Subject: [PATCH 18/27] test(reliability): preserve exact autosave evidence record shape --- .../evidenceValidationArrayPreflight.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 8bb5f2f5..2c324694 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -102,4 +102,16 @@ describe('autosave detached evidence array resource preflight', () => { expect(createDetachedAutosaveRevisionEvidence(proxiedEvidence)).toBeNull(); expect(ownKeysCalls).toBe(0); }); + + it('rejects unexpected record members after required-key preflight', () => { + const evidenceWithExtraMember = Object.freeze({ + envelope: null, + revision: null, + unexpected: null, + }); + + expect( + createDetachedAutosaveRevisionEvidence(evidenceWithExtraMember), + ).toBeNull(); + }); }); From fbecfa050946641eeeef16de01e6442ca8a1e25b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:01:01 -0700 Subject: [PATCH 19/27] fix(reliability): remove unreachable exact-key rescan --- src/autosave/evidenceValidation.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index 235948fe..cd64e058 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -61,15 +61,8 @@ function readExactFrozenDataRecord( if (!Object.isFrozen(value)) return null; const ownKeys = Reflect.ownKeys(value); - if ( - ownKeys.length !== expectedKeys.length || - ownKeys.some( - (key) => - typeof key !== 'string' || !expectedKeys.includes(key), - ) - ) { - return null; - } + if (ownKeys.length !== expectedKeys.length) return null; + const record: ExactDataRecord = {}; for (const expectedKey of expectedKeys) { const descriptor = Object.getOwnPropertyDescriptor(value, expectedKey); From 62f406c2321492ce3161d2f72f45794bdfde9034 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:04:16 -0700 Subject: [PATCH 20/27] test(reliability): cover unfrozen autosave evidence preflight --- src/autosave/evidenceValidationArrayPreflight.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 2c324694..44077d86 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -103,6 +103,17 @@ describe('autosave detached evidence array resource preflight', () => { expect(ownKeysCalls).toBe(0); }); + it('rejects a complete unfrozen record before whole-record key enumeration', () => { + const unfrozenEvidence = { envelope: null, revision: null }; + const ownKeys = vi.spyOn(Reflect, 'ownKeys'); + try { + expect(createDetachedAutosaveRevisionEvidence(unfrozenEvidence)).toBeNull(); + expect(ownKeys).not.toHaveBeenCalledWith(unfrozenEvidence); + } finally { + ownKeys.mockRestore(); + } + }); + it('rejects unexpected record members after required-key preflight', () => { const evidenceWithExtraMember = Object.freeze({ envelope: null, From 5d915003b93759e1527debecf74dd0504204d1f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:26:47 +0900 Subject: [PATCH 21/27] test(ci): cover event-specific Python matrix Signed-off-by: Seongho Bae --- office/tests/test_python_support_contract.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/office/tests/test_python_support_contract.py b/office/tests/test_python_support_contract.py index 7104fd66..209f4845 100644 --- a/office/tests/test_python_support_contract.py +++ b/office/tests/test_python_support_contract.py @@ -50,10 +50,14 @@ def test_python_support_range_matches_classifiers_and_ci_matrix() -> None: office_job = _workflow_job_block(workflow, "office") assert "runs-on: ubuntu-24.04" in office_job assert "runs-on: ubuntu-latest" not in office_job - matrix_match = re.search(r'python-version:\s*\[([^\]]+)\]', office_job) + matrix_match = re.search(r"python-version:\s*(.+)", office_job) assert matrix_match is not None - matrix_versions = tuple(re.findall(r'"(3\.\d+)"', matrix_match.group(1))) - assert matrix_versions == SUPPORTED_PYTHON_VERSIONS + pull_request_versions, push_versions = ( + tuple(re.findall(r'"(3\.\d+)"', versions)) + for versions in re.findall(r"fromJSON\('(\[[^']+\])'\)", matrix_match.group(1)) + ) + assert pull_request_versions == (SUPPORTED_PYTHON_VERSIONS[-1],) + assert push_versions == SUPPORTED_PYTHON_VERSIONS def test_python_support_documentation_matches_the_fixed_ci_environment() -> None: From 6b41807557e6bdb71f08aac9878e8733bc66513f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:32:54 +0900 Subject: [PATCH 22/27] test(ci): bind Python matrix to event Signed-off-by: Seongho Bae --- office/tests/test_python_support_contract.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/office/tests/test_python_support_contract.py b/office/tests/test_python_support_contract.py index 209f4845..a52ddec3 100644 --- a/office/tests/test_python_support_contract.py +++ b/office/tests/test_python_support_contract.py @@ -50,11 +50,16 @@ def test_python_support_range_matches_classifiers_and_ci_matrix() -> None: office_job = _workflow_job_block(workflow, "office") assert "runs-on: ubuntu-24.04" in office_job assert "runs-on: ubuntu-latest" not in office_job - matrix_match = re.search(r"python-version:\s*(.+)", office_job) + matrix_match = re.search( + r"python-version:\s*\$\{\{\s*github\.event_name\s*==\s*'pull_request'" + r"\s*&&\s*fromJSON\('(\[[^']+\])'\)\s*\|\|\s*" + r"fromJSON\('(\[[^']+\])'\)\s*\}\}", + office_job, + ) assert matrix_match is not None pull_request_versions, push_versions = ( tuple(re.findall(r'"(3\.\d+)"', versions)) - for versions in re.findall(r"fromJSON\('(\[[^']+\])'\)", matrix_match.group(1)) + for versions in matrix_match.groups() ) assert pull_request_versions == (SUPPORTED_PYTHON_VERSIONS[-1],) assert push_versions == SUPPORTED_PYTHON_VERSIONS From e146a47f6a0fac976d719096d03b01ce21062991 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:20:53 +0900 Subject: [PATCH 23/27] revert(ci): restore Office contract owner Remove the duplicated Python support contract changes from this autosave branch. PR #405 remains the single writer while this branch keeps its array-key preflight delta. Signed-off-by: Seongho Bae Commit-Message-Assisted-by: Claude (via Claude Code) --- office/tests/test_python_support_contract.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/office/tests/test_python_support_contract.py b/office/tests/test_python_support_contract.py index a52ddec3..7104fd66 100644 --- a/office/tests/test_python_support_contract.py +++ b/office/tests/test_python_support_contract.py @@ -50,19 +50,10 @@ def test_python_support_range_matches_classifiers_and_ci_matrix() -> None: office_job = _workflow_job_block(workflow, "office") assert "runs-on: ubuntu-24.04" in office_job assert "runs-on: ubuntu-latest" not in office_job - matrix_match = re.search( - r"python-version:\s*\$\{\{\s*github\.event_name\s*==\s*'pull_request'" - r"\s*&&\s*fromJSON\('(\[[^']+\])'\)\s*\|\|\s*" - r"fromJSON\('(\[[^']+\])'\)\s*\}\}", - office_job, - ) + matrix_match = re.search(r'python-version:\s*\[([^\]]+)\]', office_job) assert matrix_match is not None - pull_request_versions, push_versions = ( - tuple(re.findall(r'"(3\.\d+)"', versions)) - for versions in matrix_match.groups() - ) - assert pull_request_versions == (SUPPORTED_PYTHON_VERSIONS[-1],) - assert push_versions == SUPPORTED_PYTHON_VERSIONS + matrix_versions = tuple(re.findall(r'"(3\.\d+)"', matrix_match.group(1))) + assert matrix_versions == SUPPORTED_PYTHON_VERSIONS def test_python_support_documentation_matches_the_fixed_ci_environment() -> None: From 4fa8d65c47b6ece20e96288cdf8df366d33ef942 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:27:55 +0900 Subject: [PATCH 24/27] test(autosave): expose frozen-state enumeration before array limits Signed-off-by: Seongho Bae --- .../evidenceValidationArrayPreflight.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 44077d86..6281e4c2 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -6,6 +6,33 @@ import { } from './evidenceValidation.js'; describe('autosave detached evidence array resource preflight', () => { + it('rejects an impossible array length before frozen-state key enumeration', () => { + let ownKeysCalls = 0; + const oversizedArray = new Proxy(Object.freeze(new Array(1_000_001)), { + ownKeys(target) { + ownKeysCalls += 1; + return Reflect.ownKeys(target); + }, + }); + + expect(isDeeplyFrozenDocumentJson(oversizedArray)).toBe(false); + expect(ownKeysCalls).toBe(0); + }); + + it('rejects over-depth array children before frozen-state key enumeration', () => { + let ownKeysCalls = 0; + let root: readonly unknown[] = new Proxy(Object.freeze([null]), { + ownKeys(target) { + ownKeysCalls += 1; + return Reflect.ownKeys(target); + }, + }); + for (let depth = 0; depth < 128; depth += 1) root = Object.freeze([root]); + + expect(isDeeplyFrozenDocumentJson(root)).toBe(false); + expect(ownKeysCalls).toBe(0); + }); + it('rejects an impossible array length before explicit own-key enumeration', () => { const oversizedArray = new Array(1_000_001); Object.freeze(oversizedArray); From 31227038fb3608abf8e1b1fe132da530a662cbe8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:28:32 +0900 Subject: [PATCH 25/27] fix(autosave): preflight arrays before frozen-state enumeration Signed-off-by: Seongho Bae --- src/autosave/evidenceValidation.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/autosave/evidenceValidation.ts b/src/autosave/evidenceValidation.ts index cd64e058..d3d91328 100644 --- a/src/autosave/evidenceValidation.ts +++ b/src/autosave/evidenceValidation.ts @@ -122,8 +122,7 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { } if ( typeof currentValue !== 'object' || - visitedContainers.has(currentValue) || - !Object.isFrozen(currentValue) + visitedContainers.has(currentValue) ) { return false; } @@ -138,6 +137,7 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { )!.value as number; children = { kind: 'array', length }; } else { + if (!Object.isFrozen(currentValue)) return false; const prototype = Object.getPrototypeOf(currentValue); if (prototype !== Object.prototype && prototype !== null) return false; children = { kind: 'object', keys: Reflect.ownKeys(currentValue) }; @@ -158,6 +158,8 @@ export function isDeeplyFrozenDocumentJson(rootValue: unknown): boolean { } if (children.kind === 'array') { + // Frozen-state inspection also enumerates Proxy keys; preflight first. + if (!Object.isFrozen(currentValue)) return false; const ownKeys = Reflect.ownKeys(currentValue); if (ownKeys.length !== children.length + 1) return false; for (let index = 0; index < children.length; index += 1) { From f7f511d8891509e0877ff480b23a7a4132e2fa02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:29:07 +0900 Subject: [PATCH 26/27] test(autosave): retain bounded array immutability rejection Signed-off-by: Seongho Bae --- src/autosave/evidenceValidationArrayPreflight.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/autosave/evidenceValidationArrayPreflight.test.ts b/src/autosave/evidenceValidationArrayPreflight.test.ts index 6281e4c2..45383874 100644 --- a/src/autosave/evidenceValidationArrayPreflight.test.ts +++ b/src/autosave/evidenceValidationArrayPreflight.test.ts @@ -6,6 +6,10 @@ import { } from './evidenceValidation.js'; describe('autosave detached evidence array resource preflight', () => { + it('still rejects a bounded unfrozen array', () => { + expect(isDeeplyFrozenDocumentJson([null])).toBe(false); + }); + it('rejects an impossible array length before frozen-state key enumeration', () => { let ownKeysCalls = 0; const oversizedArray = new Proxy(Object.freeze(new Array(1_000_001)), { From 903b317fc7a7b0d247b6994796340a7bad33163c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 11:30:12 +0900 Subject: [PATCH 27/27] docs(autosave): record intrinsic array enumeration preflight Signed-off-by: Seongho Bae --- docs/doctoring/document-autosave-queue.md | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/doctoring/document-autosave-queue.md b/docs/doctoring/document-autosave-queue.md index 94526282..1de64a32 100644 --- a/docs/doctoring/document-autosave-queue.md +++ b/docs/doctoring/document-autosave-queue.md @@ -133,6 +133,32 @@ hashing boundary. Deep immutability proves that the submitted graph cannot be mutated after validation; it does not prove that a caller-supplied digest was honestly derived from that graph. +## Array preflight finding — 2026-09-07 + +Status: Active PR / Proposed (#184), not protected-main implementation. + +The earlier array preflight avoided explicit key enumeration but first checked +whether each container was frozen. Frozen-state inspection also enumerates a +Proxy's keys. A packed public queue probe with a frozen, oversized sparse array +was correctly rejected without calling the host save function, yet its key trap +ran once before rejection. A costly trap could therefore run even when the +array's length already made acceptance impossible. + +The candidate keeps early frozen-state validation for objects and checks array +length, remaining value capacity, and child depth before array frozen-state +inspection. It still rejects bounded mutable arrays and preserves dense-array, +descriptor, prototype, cycle, schema, digest, and detached-snapshot checks. +No API, limit, durable-save authority, or error category changes. This avoids +unnecessary enumeration; it does not place a time limit on arbitrary Proxy code +that runs during otherwise necessary reflection. + +The two regressions in `evidenceValidationArrayPreflight.test.ts` first failed +with one key-trap invocation instead of zero for oversized and over-depth +arrays. A separate bounded-unfrozen-array case preserves the immutability gate. +Both public queue and durable session reuse this validation boundary. New-head +packed consumer verification remains required; predecessor archive verification +does not prove the candidate implementation. + ## Security and privacy considerations Revision tags are equality validators, not authorization tokens, signatures,